Skip to content

Instantly share code, notes, and snippets.

@bjeanes
Last active April 29, 2023 15:51
Show Gist options
  • 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
@bjeanes
Copy link
Author

bjeanes commented Mar 26, 2023

Hey @G4KCM nice sleuthing. I don't know these APIs really at all and just fumbled my way through it undil I got something which worked. So, I can't really tell you if you're on the right track or not. I wrote this when the latest ESPHome was much older, so no doubt some of the APIs have changed and this example has bitrotted. Unfortunately I can't easily test this right now. Let me know if it works once your device arrives!

@G4KCM
Copy link

G4KCM commented Mar 26, 2023

Yep will do. I’m fairly certain now it will be Ok as I have since my last post seen another comment somewhere else (can’t remember where) with the same issue/fix. My unit is on the slow boat from China so it will likely be a few weeks before I can test.

@G4KCM
Copy link

G4KCM commented Apr 1, 2023

Dear @bjeanes. Just a quick update to say that my PH-260 arrived and it worked 100% first time so it might be worth incorporating the modifications I mentioned into your code for the latest versions of ESPhome. Cheers!

@G4KCM
Copy link

G4KCM commented Apr 15, 2023

Quick update - not quite 100% temperature isn’t returned. Can’t quite understand why at the moment…

@caliKev
Copy link

caliKev commented Apr 20, 2023

I am also getting an error with temperature.

This is the error from the ESPhome Logs:
[W][ble_client.receive:239]: value could not be parsed as float: []
EC and pH seem to work, but I don’t actually have the EC sensor connected.

@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