-
-
Save bjeanes/4310d30393a093bc2f1f2bd113fa820b to your computer and use it in GitHub Desktop.
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 |
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!
Quick update - not quite 100% temperature isn’t returned. Can’t quite understand why at the moment…
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.
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.
@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!
Great! Thanks for the update :).
Thank you for the original work!
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.