Skip to content

Instantly share code, notes, and snippets.

@G4KCM
Forked from bjeanes/ph-260bd-relay.yml
Last active April 5, 2023 07:24
Show Gist options
  • Save G4KCM/f96342f1814f9e1a7d8a4ac857a4f09e to your computer and use it in GitHub Desktop.
Save G4KCM/f96342f1814f9e1a7d8a4ac857a4f09e to your computer and use it in GitHub Desktop.
ESPHome definition to pick up readings from the PH-260BD water PH/EC/TDS/Temp sensor - https://www.aliexpress.com/item/1005002707585119.html / https://www.aliexpress.com/item/4001143771176.html
esphome:
name: ph-260bd-relay
platform: ESP32
board: esp32dev
# Enable logging
logger:
logs:
esp32_ble_tracker: INFO
# Enable Home Assistant API
api:
password: !secret api_password
ota:
password: !secret ota_password
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
domain: !secret wifi_domain
fast_connect: true
captive_portal:
esp32_ble_tracker:
scan_parameters:
active: false
# https://amperkot.ru/static/3236/uploads/datasheets/JDY-08.pdf
# [13:44:48][I][ble_client:085]: Attempting BLE connection to 7c:01:0a:43:4e:9e
# [13:44:49][D][ble_client_lambda:035]: Connected to BLE device
# [13:44:49][I][ble_client:161]: Service UUID: 0xFFE0
# [13:44:49][I][ble_client:162]: start_handle: 0x1 end_handle: 0x9
# [13:44:49][I][ble_client:341]: characteristic 0xFFE1, handle 0x3, properties 0x1c
# [13:44:49][I][ble_client:341]: characteristic 0xFFE2, handle 0x7, properties 0x1c
# [13:44:49][I][ble_client:161]: Service UUID: 0x1800
# [13:44:49][I][ble_client:162]: start_handle: 0xa end_handle: 0x14
# [13:44:49][I][ble_client:341]: characteristic 0x2A00, handle 0xc, properties 0x2
# [13:44:49][I][ble_client:341]: characteristic 0x2A01, handle 0xe, properties 0x2
# [13:44:49][I][ble_client:341]: characteristic 0x2A02, handle 0x10, properties 0xa
# [13:44:49][I][ble_client:341]: characteristic 0x2A05, handle 0x17, properties 0x20
ble_client:
- mac_address: 'RE:PL:AC:EM:EE'
id: ph_260bd
on_connect: # see https://github.com/esphome/esphome/pull/2200#issuecomment-962559276
then:
- wait_until: # wait until characteristic is discovered
lambda: |-
esphome::ble_client::BLEClient* client = id(ph_260bd);
auto service_uuid = 0xFFE0; // can't get it from `sensor` because it is protected
auto char_uuid = 0xFFE1; // can't get it from `sensor` because it is protected
esphome::ble_client::BLECharacteristic* chr = client->get_characteristic(service_uuid, char_uuid);
return chr != nullptr;
- lambda: |-
ESP_LOGD("ble_client_lambda", "Connected to PH-260BD");
//esphome::ble_client::BLESensor* sensor = id(ph_260bd_sensor);
esphome::ble_client::BLEClient* client = id(ph_260bd);
auto service_uuid = 0xFFE0; // can't get it off `sensor` because it is protected
auto char_uuid = 0xFFE1; // can't get it off `sensor` because it is protected
esphome::ble_client::BLECharacteristic* chr = client->get_characteristic(service_uuid, char_uuid);
if (chr == nullptr) {
ESP_LOGW("ble_client", "[0xFFE1] Characteristic not found. State update can not be written.");
} else {
// 0x0003000000144414 puts it into "multi-value" mode where it streams constantly
// 0x01030000001445C5 requests a single value (for each sensor) to be emitted
unsigned char newVal[8] = {
0x00, 0x03, 0x00, 0x00,
0x00, 0x14, 0x44, 0x14
};
int status = esp_ble_gattc_write_char(
client->get_gattc_if(),
client->get_conn_id(),
chr->handle,
sizeof(newVal),
newVal,
ESP_GATT_WRITE_TYPE_NO_RSP,
ESP_GATT_AUTH_REQ_NONE
);
if (status) {
ESP_LOGW("ble_client", "Error sending write value to BLE gattc server, status=%d", status);
}
}
/*
Debug `some_var`'s type at compile time with:
decltype(some_var)::foo = 1;
*/
on_disconnect:
then:
- lambda: |-
ESP_LOGD("ble_client", "Disconnected from PH-260BD");
sensor:
- platform: template
name: "PH-260BD EC"
id: ph_260bd_ec_sensor
unit_of_measurement: "µS/cm"
accuracy_decimals: 0
state_class: measurement
icon: mdi:water-opacity
- platform: template
name: "PH-260BD Temperature"
id: ph_260bd_temperature_sensor
unit_of_measurement: "°C"
accuracy_decimals: 1
state_class: measurement
device_class: temperature
- platform: template
name: "PH-260BD pH"
id: ph_260bd_ph_sensor
unit_of_measurement: "pH"
accuracy_decimals: 2
state_class: measurement
icon: mdi:ph
filters:
- filter_out: nan
- or:
- throttle_average: 60s
- delta: 0.2
- platform: ble_client
type: characteristic
ble_client_id: ph_260bd
id: ph_260bd_sensor
internal: true
service_uuid: FFE0
characteristic_uuid: FFE1
notify: true
# on_notify:
# then:
# - lambda: |-
# // `x` is only a single byte here :(
# ESP_LOGD("ble_client_lambda", "got notify");
# The PH-260BD puts bytes onto the characteristic value which needs to be treated as text:
#
# [1] pry(main)> ['372e35312070480d0a32312e372020e284830d0a'].pack('H*')
# => "7.51 pH\r\n21.7 \xE2\x84\x83\r\n"
# [2] pry(main)> puts ['372e35312070480d0a32312e372020e284830d0a'].pack('H*')
# 7.51 pH
# 21.7 ℃
#
# It alternates between putting the EC/TDS value alone (as a string, with units) and the pH and
# temperature together. Perhaps it can't fit all three in a single buffer.
#
# All values follow: number(s)/dot, space(s), unit, carriage return, new line
#
# This lambda parses the string and publishes each value+unit to the appropriate template sensor on each newline.
lambda: |-
if (x.size() == 0) return NAN;
std::string val_str = "";
std::string val_unit = "";
ESP_LOGD("ble_client.receive", "value received with %d bytes: [%.*s]", x.size(), x.size(), &x[0]);
// https://git.faked.org/jan/ph-260bd/-/blob/master/src/main.cpp#L7
static int factorMsToPpm = 700; // US: 500, EU: 640, AU: 700 (= device default)
for (int i = 0; i < x.size(); i++) {
auto c = x[i];
switch(c) {
case '\x30': // "0"
case '\x31': // "1"
case '\x32': // "2"
case '\x33': // "3"
case '\x34': // "4"
case '\x35': // "5"
case '\x36': // "6"
case '\x37': // "7"
case '\x38': // "8"
case '\x39': // "9"
case '\x2E': // "."
val_str += c;
break;
case '\x20': // " "
break; // proceed until we hit units
case '\x0d': // '\r'
break; // ignore
case '\x0a': // '\n'
/* TODO:
* the ph-260bd is just publishing the display chars, and so the accuracy is not constant.
- account for the accuracy/resolution mentioned in the pamphlet
* the ph-260bd only pushes the EC unit which is displayed on the screen
- publish ESP sensors for all EC units by cross-calculating all of them from whichever we receive
*/
if (auto val = parse_number<float>(val_str)) {
auto ec = id(ph_260bd_ec_sensor);
if (val_unit == "pH") {
id(ph_260bd_ph_sensor).publish_state(*val);
} else if (val_unit == "\xE2\x84\x83") { // ℃ char
id(ph_260bd_temperature_sensor).publish_state(*val);
} else if (val_unit == "uS") { // microsiemens
ec->publish_state(*val);
} else if (val_unit == "mS") { // millisiemens
ec->publish_state(*val * 1000);
} else if (val_unit == "ppt") { // TDS parts per thousand
ec->publish_state(*val / factorMsToPpm * 1000 * 1000);
} else if (val_unit == "ppm") { // TDS parts per million
ec->publish_state(*val / factorMsToPpm * 1000);
} else {
ESP_LOGW("ble_client.receive", "value received with unknown unit: [%s]", val_unit.c_str());
}
} else {
ESP_LOGW("ble_client.receive", "value could not be parsed as float: [%s]", val_str.c_str());
}
val_unit = "";
val_str = "";
break;
default:
val_unit += c;
}
}
return 0.0; // this sensor isn't actually used other than to hook into raw value and publish to template sensors
@G4KCM
Copy link
Author

G4KCM commented Apr 5, 2023

Made a couple of changes to make it work with latest ESPhome…

(1) Line 134 wanted a 'type' I modified to the following and it was happy....

platform: ble_client
type: characteristic
ble_client_id: ph_260bd
id: ph_260bd_sensor
internal: true
service_uuid: FFE0
characteristic_uuid: FFE1
notify: true

(2) The next issue I had was at line 78 and the compilation failed as 'gattc_if' and 'conn_id' were not recognised. I found some similar code in the esphome repository (https://esphome.io/api/am43_8cpp_source.html) that used get_gattc_if() and get_conn_id(). I have no idea if this is correct!! But with this...

int status = esp_ble_gattc_write_char(
client->get_gattc_if(),
client->get_conn_id(),
chr->handle,
sizeof(newVal),
newVal,
ESP_GATT_WRITE_TYPE_NO_RSP,
ESP_GATT_AUTH_REQ_NONE

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment