-
-
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 |
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!
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!