-
-
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 |
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!
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.
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!
Hi @bjeanes , I have just ordered a PH-260 and thought whilst waiting I would have a play with the code. I am using Esphome 2023.3.1 within Home Assistant 2023.3.6, Supervisor 2023.03.2, Operating System 9.5 and Frontend 20230309.1 i.e. all the latest.
I have to admit I'm about forty years past my coding prime so what I have here is mainly the result of Google suport as the depths of BLE are presently outside my grasp.
Anyway whilst compiling the code in our gist the following errors were thrown up (referenceing to line # in you gist)...
Firstly 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
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
);
it compiles :-)
As I mentioned I dont yet have a PH-260 to test and hence would really appreciate it if you could cast your eye over this and no doubt correct me as I'm likely barking completely up the wrong tree. But it looks as if something may have changed recently in esphome.
Many thanks for the work you have placed in the public domain it is truly appreciated.
Rgds
Clive