-
-
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 |
@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?
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
...
@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.
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
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.
@bjeanes the 221 is coming from return (float)x[0];
as the sensor lambda is getting the raw values in the x
variable.
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
@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.
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.
@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...
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!
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.
@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...
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.
@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)
@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
@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!
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.
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
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!
I'm not sure if you are publishing
µS/cm
units directly from your code (it looks like you are usinguS
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