Skip to content

Instantly share code, notes, and snippets.

@bjeanes
Last active April 29, 2023 15:51
Show Gist options
  • Star 10 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save bjeanes/4310d30393a093bc2f1f2bd113fa820b to your computer and use it in GitHub Desktop.
Save bjeanes/4310d30393a093bc2f1f2bd113fa820b 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->gattc_if,
client->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
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

G4KCM commented Apr 20, 2023

Hi, @caliKev I haven’t really had the time to look into @bjeanes code in any depth, I will say this though that if you compile the code referenced in line 170 using the Arduino IDE and flash it into an ESP32 (https://git.faked.org/jan/ph-260bd/-/blob/master/src/main.cpp#L7) it decodes temperature. I have a feeling it’s something to do with the three hex numbers used to represent °C and will try the technique used in the aforementioned piece of code one day when I have time.

@bjeanes
Copy link
Author

bjeanes commented Apr 20, 2023 via email

@G4KCM
Copy link

G4KCM commented Apr 21, 2023

@caliKev @bjeanes
I started to do some checks and couldnt really find any changes in the byte stream, in the process of putting some more verbose logging I recompiled the code using the latest version of ESPhome in Home Assistant i.e. 2023.4.0 and it started working. I suspect it is something to do with the parse_number(val_str) statement as the parse_number() syntax changed a while ago methinks it may have been further 'tweaked'. Anyway it all really works 100% now!

@bjeanes
Copy link
Author

bjeanes commented Apr 21, 2023

Great! Thanks for the update :).

@G4KCM
Copy link

G4KCM commented Apr 21, 2023

Thank you for the original work!

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