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
@bjeanes
Copy link
Author

bjeanes commented Dec 20, 2021

I'm not sure if you are publishing µS/cm units directly from your code (it looks like you are using uS not µS), but for posterity: my YAML above incorrectly used the "mu" (Greek letter) instead of micro Unicode point. They render the same, but I had an annoying bug where I couldn't aggregate on my µS sensors because the units were distinct.

I've updated it in diff to use https://www.compart.com/en/unicode/U+00B5

@bjeanes
Copy link
Author

bjeanes commented Jan 2, 2022

@jangrewe how is your device going? For the second time now, after non-stop running for 4-5 days, it stopped emitting any BLE packets. I power-cycled it and when it came back on the temperature readings are about 2x what they should be. It is is now reporting my water temperature at 48-52°C, when it is more like 25°C. Power cycling doesn't resolve and the instructions don't have anything I can see about calibrating the temperature.

Have you seen anything like this?

@bjeanes
Copy link
Author

bjeanes commented Jan 3, 2022

It briefly went accurate again and then went back to bizarro readings. It shows the incorrect reading on screen too, so nothing to do with the BLE side.... Ugh very annoying.

Screen Shot 2022-01-04 at 8 05 51 am

@Emanuele-Spatola
Copy link

Hi! is this still working with the latest version of esphome? I tried and it seems the lambda is unable see any services/charcateristics even if I can read them as a sensor:

sensor:
  - platform: ble_client
  ...

@bjeanes
Copy link
Author

bjeanes commented Jan 19, 2022

@Emanuele-Spatola have you tried it exactly as I have it above first? The on_connect portion is critical for getting the device to start transmitting. It doesn't appear to transmit characteristics without the prompt.

I may not be on latest ESPHome so I'll try upgrading this weekend and let you know, but I see no reason that it shouldn't work.

@Emanuele-Spatola
Copy link

Yes, I've used the exact code you posted with the on_connect/then/wait_until (minus the address and the UUIDs as it's for a different device).
I even tried this:

sensor:
  - platform: ble_client
    ble_client_id: ble_bms_top
    name: "Bms Top Debug 0"
    service_uuid: 'ff00'
    characteristic_uuid: 'ff01'
    unit_of_measurement: '%'
    update_interval: 10s
    lambda: |-
      auto client = id(ble_bms_top);
      uint16_t service_uuid = 0xff00;

      auto service = client->get_service(service_uuid);
      if (service == nullptr) {
        ESP_LOGD("ble_client", "service not found");
      } else {
        ESP_LOGD("ble_client", "service found!");
      }
      return (float)x[0];

and the output is:

[10:21:25][D][ble_client:133]: service not found
[10:21:25][V][sensor:070]: 'Bms Top Debug 0': Received new state 221.000000
[10:21:25][D][sensor:121]: 'Bms Top Debug 0': Sending state 221.00000 % with 0 decimals of accuracy
[10:21:36][D][ble_client:133]: service not found
[10:21:36][V][sensor:070]: 'Bms Top Debug 0': Received new state 221.000000
[10:21:36][D][sensor:121]: 'Bms Top Debug 0': Sending state 221.00000 % with 0 decimals of accuracy
...and so on

so the service is definitely there, but it seems I cannot read it from the lambda

@bjeanes
Copy link
Author

bjeanes commented Jan 21, 2022

Hmm @Emanuele-Spatola to be honest I'm not sure I understand what you're trying to do. Where is the 221.000000 coming from if you aren't receiving values there? I'm not sure about the behaviour of get_service as I am using get_characteristic directly.

@Emanuele-Spatola
Copy link

@bjeanes the 221 is coming from return (float)x[0]; as the sensor lambda is getting the raw values in the xvariable.
the get_characteristic you are using just does:

 BLECharacteristic *BLEClient::get_characteristic(espbt::ESPBTUUID service, espbt::ESPBTUUID chr) {
   auto svc = this->get_service(service);
   if (svc == nullptr)
     return nullptr;
   return svc->get_characteristic(chr);

What I wanted to prove is that that service/characteristic exists, as I can see values in x, but I cannot read it "manually".
I cannot even see the service as if the auto client = id(ble_bms_top); is returning an empty client with no sevices.
This is true in both the sensor lambda and in the on_connect lambda

@Emanuele-Spatola
Copy link

@bjeanes have you had a chance to try your code on the latest esphome?
I was able to write the characteristic with an horrible hack.
In the ble_sensor.cpp file, line 46, I added this:

        uint16_t service_uuid = 0xff00;
        uint16_t char_uuid = 0xff02;
        auto writeChr = this->parent()->get_characteristic(service_uuid, char_uuid);
        if (writeChr == nullptr) {
          ESP_LOGD(TAG, "No sensor characteristic found");
          break;
        } else {
          unsigned char newVal[8] = {
            0xdd, 0xa5,  0x03, 0x00,
            0xff, 0xfd, 0x77
          };
          writeChr->write_value(
            newVal,
            sizeof(newVal)
          );
        }

wondering if this kind of writes are something many users need.
In that case it might be worth to clean up the code , make it accessible with a yaml configuration and make a pull request.

@bjeanes
Copy link
Author

bjeanes commented Jan 25, 2022

Hi @Emanuele-Spatola I haven't tried anything yet. I got ill over the weekend and have just been playing Xbox and staying hydrated :|

I am a little bit confused about your situation. If you are getting the sensor's lambda called, I am surprised you need to write to the characteristic first. Is this BMS also using the JDY-08 chip perhaps? How did you find the {0xdd, 0xa5, 0x03, 0x00, 0xff, 0xfd, 0x77} that you write there?

It's interesting that your get_characteristic works there but get_service didn't. It may be that doing it within the lambda doesn't work due to internal state in the BLE code. I am only doing it in as a one-off on_connect in my case. You will note I am also doing a wait_until block which waits until the characterstic is found. It may be that is what works around what you were experiencing.

@bjeanes
Copy link
Author

bjeanes commented Feb 13, 2022

@Emanuele-Spatola any luck yet? I just upgraded to latest ESPHome and everything is still working fine. Whatever your issue is, it's not the ESPHome version...

@Emanuele-Spatola
Copy link

Unfortunately no luck :( I've spent way too much time on this. Giving up for now.
Thank you for taking the time to help me out!

@seeschloss
Copy link

Just want to add that I have the same problem (trying to write to another kind of device).

Basically, the BLEClient I get from id(ble_client_id) doesn't have services, it looks like it's some kind of uninitialized copy of the actual BLEClient that has received the list of services.

So sending data works if I add the code to ble_client.cpp, but I can't get to have a working BLEClient from within a lambda.

@bjeanes
Copy link
Author

bjeanes commented Apr 15, 2022

@seeschloss Are you doing the wait_until bit I am doing? The client exists before the connection is established and so the characteristics can be null until that happens. The wait_until is more or less a while loop until a characteristic is found...

@seeschloss
Copy link

Yes, that's actually in the wait_until that I found out that problem. The wait_until just keeps finding no services even after the BLEClient has actually printed the list of services in the debug log.

I have since moved to "simply" writing a new esphome component for my specific use case though so this doesn't matter much anymore for me, but I'm still puzzled at these different behaviours we seem to have experienced.

@bjeanes
Copy link
Author

bjeanes commented Apr 17, 2022

@seeschloss Right... well I am only looking for the characteristic, not the service. So perhaps there is a bug but I'm not seeing it because I am only polling for the characteristic. Do you have a stable characteristic UUID you can use directly instead? If so, try that and let me know if it's the same issue. My ESPHome and HA are both on the latest versions and I re-flash this regularly, with no breakage (so far)

@maxmib
Copy link

maxmib commented May 4, 2022

@bjeanes thanks for your code sharing ,it worked with 16bit uuid ,but I need to use 128bit uuid like 01000000-0000-0000-0000-000000000080 as service_uuid and char_uuid ,an error : invalid digit "8" in octal constant reported, so I modify it to 01000000-0000-0000-0000-0x000000000080 or 0x01000000-0x0000-0x0000-0x0000-0x000000000080 ,still can't make 'get_characteristic(service_uuid,char_uuid)' work ,any suggesition for this ? Thanks

@JayRama
Copy link

JayRama commented May 16, 2022

@bjeanes @jangrewe Thanks to you both for your work in here, I managed to get my ph260-bd set up without too much drama.

I was wondering if either of you could give us some pointers re this thread https://community.home-assistant.io/t/ac-infinity-controller-67-bluetooth-temp-humidity-fan-pwm/410673/6 - maybe some potential avenues we can explore to get these fans working too? At first glance it appears to be a similar type of process.

Thanks again!

@bjeanes
Copy link
Author

bjeanes commented May 16, 2022

Commented on the forums. Tl;DR decompile the Android app (where possible) to see if you can just see in the source code how this is done or follow https://www.youtube.com/watch?v=NIBmiPtCDdM to get HCI logs from an Android phone with the app installed.

@G4KCM
Copy link

G4KCM commented Mar 24, 2023

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

@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