Blue Connect pool measurements

Molinem, yes, it’d be very useful to have a few raw samples from your device and their corresponding data displayed by the App.

With the samples vampcp posted in this thread on Oct 18, 2021, I think the salinity calculation is straightforward:

 23ab093207ab0a7800240e
 "salinity":4.9,"conductivity":8742,"temperature":24.8,"ph":7.93,"orp":685

 23a6093007b70a78001e0e
 "salinity":4.9,"conductivity":8742,"temperature":24.7,"ph":7.94,"orp":688,

23ab093407980a7b00240e
"salinity":4.8,"conductivity":8528,"temperature":24.8,"ph":7.92,"orp":680

…you just divide the raw value by 25 and you get the value in g/l with 0.04 resolution, displayed by the App with just 1 decimal place.

23 ab 09 32 07 ab 0a 78 00 24 0e
                     ^^-^^---------- SAL            0x0078 =  120 / 25   = 4.80          <-- MOSTLY OK
                           ^^-^^---- COND           0x0e24 = 3620 / 8742 = K 0.4140 ?
23 a6 09 30 07 b7 0a 78 00 1e 0e
                     ^^-^^---------- SAL            0x0078 =  120 / 25   = 4.80          <-- MOSTLY OK
                           ^^-^^---- COND           0x0e1e = 3614 / 8742 = K 0.4134
23 ab 09 34 07 98 0a 7b 00 24 0e
                     ^^-^^---------- SAL            0x007b =  123 / 25   = 4.92          <-- MOSTLY OK
                           ^^-^^---- COND           0x0e24 = 3620 / 8528 = K 0.4244

For the conductivity, assuming a linear relationship, we can calculate a constant of approx K=0.4134, yielding a value in uS/cm with 2.4 resolution. The app seems to display it discarding the decimal part.

We need some more samples to try to calculate an accurate value for this constant. I wonder if some of the BLE characteristics of the device may provide the values for these constants (calibration data), but that would be another decoding nightmare.

I’m a little confused. According to some screenshots of the App found on Google, the Android one displays the conductivity in uS, and the iOS one displays the salinity in g/l, but none of them show both magnitudes at the same time:

Do recent versions of the App display both Salinity and Conductivity for the BlueConnect Plus?

Tomorrow I will try to get 5 samples, this is a screenshot of my blue connect plus.
The information that you write is very interesting.

By using the post of @JosePortillo i managed to get ESPHome32 to get the raw data back.
Last step is to get the data from ESPHome to HomeAssistent. Whenever I try to send the raw (hex) value the esp device boots or gets in some sort of error loop.

I use a button to send 0x01 to the BlueConnect and i receive a notification (10 sec later) on a text_sensor:

text_sensor:
  - platform: ble_client
    id: ${bleclient_id_prefix}_reading_data
    name: ${bleclient_name_prefix} reading data
    internal: true
    ble_client_id: ble_client_${bleclient_id_prefix}
    service_uuid: ${bleclient_recieve_service_uuid}
    characteristic_uuid: ${bleclient_recieve_characteristic_uuid}
    notify: true
    update_interval: never
    on_notify:
      then:
        lambda: |-
             ESP_LOGD("${bleclient_id_prefix}", "%s", format_hex_pretty((uint8_t *) x.c_str(), x.size()).c_str());

the response is like this:

[ble_text_sensor:096]: [BLE Client reading data] ESP_GATTC_NOTIFY_EVT: handle=0x14, value=0x33
[text_sensor:016]: 'BLE Client reading data': Received new state 3aea\xc6	
[text_sensor:067]: 'BLE Client reading data': Sending state '3aea\xc6
[ble-id:147]: 33.03.07.65.07.C6.09.00.00.25.0E.14 (12)

how can i transfer the hex value to Home Assistant?

Hi, I have managed to get the hex string back using ESPHome.
(see post 203 in this thread). I don’t know how to do the calculation in the lambda part.
Do you or @vampcp know how to do this?

ESPHome->HA does not support text at the moment, if the lampda function could do the calculation we can then send the output to HA sensors.

At the moment I am sending it to MQTT and want NodeRed do the math. That adds some unneeded part, for me the only option at the moment

HI Jose,

I managed to get ESPHome to get the raw data out of the sensor. With your calculations it’s translated.

33.08.07.B0.07.3B.08.00.00.23.0E.13
APP – raw
temp: 18.0 – 18.0
ph: 7.3 – 7.3
orp: 553 – 526.75

33.A4.06.AF.07.F1.08.00.00.21.0E.13
app - raw
temp: 17.1 – 17.0
ph: 7.3 – 7.3
orp 600 – 572.25

Values are pretty close

Based on raw-app readings, that were off by 72mv, I had to adjust the orp divider from 4 to 3.86.
I took 4 measurements across 2 days and used the python code from Jose to calculate the divider

Next days I will see if the result is closer to the app value. If so then I will upload my esphome config and the script to calculate/calibrate the orp divider to github and post the link here

And then wait for next year as the swimming time is over

1 Like

Hi,

thanks Jose for your code. I implemented it in my fhempy solution.

Furthermore I think the last 2 bytes might be the battery state.
e.g. 36 0e => 0x0e36 => 3638 => 3.638 volt
It’s just an assumption, I’ll check it for the next days.

My implementation can be found here:

1 Like

Can you help me please, I can’t do it

I don’t see anything coming in on my hass.
i installed ifttt on my hass

HI @Peerke
Would you mind sharing your esphome config input to have a look how do the conversion and send to MQTT.
Then I am confortable with NodeRed doing the rest or making template mqtt sensor…

Hi @Nordicfastware,

As the pool is stored I didn’t do anything with it. The code is fully working and the data looks good.
I added the config to github:

The calculation I use (with the work from Jose) are at the end of the config file.

1 Like

Thanks a lot @Peerke . Implemented it and worked right away. Added also BLE proxy function in the same ESP32 to read air quality data off an airthings Wave +.
Water quality readings are quite close to what quasi simultaneous app readings say. I will see and tweak formulas as needed.

I have to say, it is quite elegant with the internal button press and BLE client switch.

I would like to add a sensor to notify whether the measurement was successful, and a last mearsurement date (even if I will automate to make a query every x hours). How would you recommend to go about that ?

I fiddle with them and found that they where pretty close too, please let me know when you get them closer. I need to add the battery suggestion from fhempy

I use an automation to enable the ‘switch.prefix_sensor_enable’ every 2 hours. Once the reading is done BLE sends a notify that is picked up by the text_sensor. I would add a date function in the lamda part to that and add that to a new template sensor to show it. Besides that the last_updated info from the text_sensor can be shown as secondary info. I think this could be useful: Time Component — ESPHome

@JosePortillo amazing reverse engineering work. regarding the salt and conductivity doubts, I found this in the manual:


@Peerke Peter, great job implementing Jose findings in ESPHome. If I understand correctly (never used ESPHome before) once having the device configured with that code, it would automatically create the entities in HA, right? I hope it won’t be too difficult to add the conductivity/salt sensors once it will be clearer how to interpret them from raw data.

Thanks again to both of you.

I’m on Android, latest version of the app, and I only see Temp, pH, ORP and Salinity (g/l).

It automatically creates the (template) sensors and button when uploading the code to your ESPHome device. The esp part is specific for mine wt32eth01, if you have another device you should adjust that to match the hardware. I used a manual IP address to make sure it get the same address all the time. This is because I use it outside on a isolated network segment that causes the multicast-discovery of the unit to fail. That results in an off-line state. With the manual IP (bit more work) makes sure my device is reachable and the ethernet connection is safe guarded if someone would put the cable in a laptop

After the activation of your ESP32 device, you can create an automation to trigger the reading (or do that in the UI with the button)

If you can share your raw _hex (found in the console logs) and do a manual reading in the app just before or after the reading from HA we can see if that matches the findings earlier in the post.
( I don’t have such a device, I only have one without salt)
→ these 2 lines print the raw reading in the console:

std::string rawhex = format_hex_pretty((uint8_t *) x.c_str(), x.size()).c_str();
ESP_LOGD(“raw_hex”, “%s”, rawhex.c_str());

First of all, thanks for the great work in reverse engineering the BLE communications. After setting up the pool last week, I’ve been playing around with the ESPHome code of @Peerke. For some reason, I can’t get it to connect to the Blueconnect.

When using a raspberry pi, I can get the raw data using gatttool:

root@raspberrypi:~/ble-scanner# gatttool -b 00:A0:50:44:XX:XX --char-write-req --handle=0x0014 --value=0100 --char-write-req --handle=0x0012 --value=01 --listen
Characteristic value was written successfully
Notification handle = 0x0014 value: 33 15 09 35 07 58 09 00 00 15 0e 1a

I’m expecting the esphome to connect to the sensor when it starts, and update the values. However, I don’t see any values (in the very_verbose logging output). How should I trigger the esphome to connect and get values from the sensor?

Hi Nick,

What i found out during testing is the following:

  • The device can only connect to 1 app (either the app on your phone or the BLE hardware)
  • when connecting using ESPHOME it stays connected and no more reading can be done (at least I couldn’t get that to work. How it fixed that is by enabling and disabling BLE client on ESP32 (using the internal ESPHOME button) and the switch turns the button on/off from HA

When you need a reading:

  • enable BLE, wait 2 sec to settle the hardware
  • register with – register service_uuid: ‘F3300001-F0A2-9B06-0C59-1BC4763B5C00’ and char ‘F3300002-F0A2-9B06-0C59-1BC4763B5C00’
    This takes a few seconds to do anything, the device is slow

The device will return data (once the reading is complete) at:
service_uuid: ‘F3300001-F0A2-9B06-0C59-1BC4763B5C00’ at char ‘F3300003-F0A2-9B06-0C59-1BC4763B5C00’
I catch that using the text_sensor

You need to get the connect-disconnect sequence right and use the correct uuid’s (in esphome i had to string-ify them (put quotes around the uuid)
I tried to use the app after a reading and if that failed my esp32 was still connected… then i filled with the uuid’s and got the communication flowing.

I have HA on a RaspPi and use the HA ESPHome interface and console to debut thing. didn’t try the commandline.

Hopes this helps, keep me updates on your findings.
(ps took me a while to get this figured out)

And in your ESPhome secrets, there’s something like this?

blueriiot_mac: "00:A0:50:44:XX:XX"
blueriiot_nameprefix: "blueriiot"
blueriiot_idprefix: "blueriiot"

When I start my ESPhome, I get these loglines (related to BLE):

[13:13:57][C][esp32_ble:238]: ESP32 BLE:
[13:13:57][C][esp32_ble:240]:   MAC address: 40:22:D8:78:YY:YY
[13:13:57][C][esp32_ble:241]:   IO Capability: none
[13:13:57][C][esp32_ble_tracker:591]: BLE Tracker:
[13:13:57][C][esp32_ble_tracker:592]:   Scan Duration: 300 s
[13:13:57][C][esp32_ble_tracker:593]:   Scan Interval: 320.0 ms
[13:13:57][C][esp32_ble_tracker:594]:   Scan Window: 30.0 ms
[13:13:57][C][esp32_ble_tracker:595]:   Scan Type: ACTIVE
[13:13:57][C][ble_client:027]: BLE Client:
[13:13:57][C][ble_client:028]:   Address: 00:A0:50:44:XX:XX

In HA, the “Blueriiot enable” switch is set to on. I tried turning that on and off several times, but all I get in the logging of ESPhome is:

[13:15:00][D][switch:016]: 'blueriiot Enable' Turning OFF.
[13:15:00][D][switch:055]: 'blueriiot Enable': Sending state OFF

(And the same for ON).

I will play around with this a bit more. I’m sure it’s something small I missed.

In my secrets there is something simular.
nameprefix: BlueConnect Pool Sensor
for the idprefix I used the ID of the device.

Looks from your logs that the switch doesn’t press the internal button.
Just a thought that it might be because the id and name are the same?

This is my log (not at Debug level so its missing the conversion messages.
Checked the sensors and they were updated at the time of disconnect.

Enabled the switch in HA

[21:21:38][I][app:102]: ESPHome version 2023.5.5 compiled on Jun  2 2023, 12:07:37
[21:21:52][I][esp32_ble_client:064]: [0] [00:A0:50:XX:XX:XX] 0x00 Attempting BLE connection
[21:21:53][I][ble_text_sensor:034]: [BlueConnect Pool Sensor reading data] Connected successfully!
[21:21:54][I][esp32_ble_client:196]: [0] [00:A0:50::XX:XX:XX] Connected
--> took the reading and got the reply
[21:22:07][I][ble_client:041]: [00:A0:50::XX:XX:XX] Disabling BLE client.
[21:22:07][W][ble_text_sensor:040]: [BlueConnect Pool Sensor reading data] Disconnected!

Thanks for everyone contributing here. The reverse engineering work and ESPHome implementation is really useful and saves a lot of work!

I was wondering if I could use the code from @Peerke to poll two sensors by simply modifying the code a little bit:

substitutions:
  # 1st Blueconnect
  blueriiot1_mac: 'xx:yy'
  blueriiot1_name_prefix: 'pool'
  blueriiot1_id_prefix: 'pl'

  # 2nd Blueconnect
  blueriiot2_mac: 'zz:zz'
  blueriiot2_name_prefix: 'spa'
  blueriiot2_id_prefix: 'sp'
  
  # send true 0x01 to this service ID
  blueriiot_send_service_uuid: 'F3300001-F0A2-9B06-0C59-1BC4763B5C00'
  blueriiot_send_characteristic_uuid: 'F3300002-F0A2-9B06-0C59-1BC4763B5C00'
  
  # notification is recieved on this Service ID                  
  blueriiot_recieve_service_uuid: 'F3300001-F0A2-9B06-0C59-1BC4763B5C00'
  blueriiot_recieve_characteristic_uuid: 'F3300003-F0A2-9B06-0C59-1BC4763B5C00'

esphome:
  name: "poolsensors"
  friendly_name: Poolsensors

esp32:
  board: m5stack-atom
  framework:
    type: arduino

# Enable logging
logger:
  #level: VERY_VERBOSE
  level: INFO

# Enable Home Assistant API
api:
  password: "1234"
 
ota:
  password: "1234"

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password


esp32_ble_tracker:
  scan_parameters:
    active: false
    interval: 1000ms
    window: 700ms

  on_ble_advertise:
    - mac_address: ${blueriiot1_mac}
      then:
        - logger.log: 
            format: "Found 1st sensor"
            level: "INFO"

    - mac_address: ${blueriiot2_mac}
      then:
        - logger.log: 
            format: "Found 2nd sensor"
            level: "INFO"

ble_client:
  # 1st Sensor
  - mac_address: ${blueriiot1_mac}
    id: ble_client_${blueriiot1_id_prefix}
    on_connect: 
      then:
        - logger.log: 
            format: "Connected to 1st sensor"
            level: "INFO"
        - lambda: "id(binary_sensor_${blueriiot1_id_prefix}_connected).publish_state(true);"
        - delay: 2s
        - button.press: button_${blueriiot1_id_prefix}_doreading
    on_disconnect: [lambda: "id(binary_sensor_${blueriiot1_id_prefix}_connected).publish_state(false);"]

  # 2nd Sensor
  - mac_address: ${blueriiot2_mac}
    id: ble_client_${blueriiot2_id_prefix}
    on_connect: 
      then:
        - logger.log: 
            format: "Connected to 2nd sensor"
            level: "INFO"
        - lambda: "id(binary_sensor_${blueriiot2_id_prefix}_connected).publish_state(true);"
        - delay: 2s
        - button.press: button_${blueriiot2_id_prefix}_doreading
    on_disconnect: [lambda: "id(binary_sensor_${blueriiot2_id_prefix}_connected).publish_state(false);"]
    

binary_sensor:
# 1st Sensor  
  - platform: template
    id: binary_sensor_${blueriiot1_id_prefix}_connected
    name: ${blueriiot1_name_prefix} Status
    device_class: connectivity
    entity_category: diagnostic

# 2nd Sensor (optional)
  - platform: template
    id: binary_sensor_${blueriiot2_id_prefix}_connected
    name: ${blueriiot2_name_prefix} Status
    device_class: connectivity
    entity_category: diagnostic

switch:
  # 1st Sensor
  - platform: ble_client
    ble_client_id: ble_client_${blueriiot1_id_prefix}
    name: "${blueriiot1_name_prefix} Enable"
    id: switch_${blueriiot1_id_prefix}_enable
  
  # 2nd Sensor (optional)
  - platform: ble_client
    ble_client_id: ble_client_${blueriiot2_id_prefix}
    name: "${blueriiot2_name_prefix} Enable"
    id: switch_${blueriiot2_id_prefix}_enable

sensor:
  # 1st Sensor
  - platform: template 
    id: sensor_${blueriiot1_id_prefix}_temperature
    name: ${blueriiot1_name_prefix} Temperature
    # Optional variables:
    unit_of_measurement: "°C"
    icon: "mdi:water-percent"
    device_class: "temperature"
    state_class: "measurement"
    accuracy_decimals: 1
  
  - platform: template 
    id: sensor_${blueriiot1_id_prefix}_ph
    name: ${blueriiot1_name_prefix} PH
    # Optional variables:
    #unit_of_measurement: "°C"
    #icon: "mdi:water-percent"
    #device_class: "temperature"
    state_class: "measurement"
    accuracy_decimals: 1

  - platform: template 
    id: sensor_${blueriiot1_id_prefix}_orp
    name: ${blueriiot1_name_prefix} Chlor
    # Optional variables:
    #unit_of_measurement: "°C"
    #icon: "mdi:water-percent"
    device_class: "voltage"
    state_class: "measurement"
    accuracy_decimals: 1
    
  - platform: template 
    id: sensor_${blueriiot1_id_prefix}_bat
    name: ${blueriiot1_name_prefix} Battery
    # Optional variables:
    #unit_of_measurement: "°C"
    #icon: "mdi:water-percent"
    device_class: "battery"
    state_class: "measurement"
    accuracy_decimals: 1
  # END 1st Sensor

  # 2nd Sensor (optional)
  - platform: template 
    id: sensor_${blueriiot2_id_prefix}_temperature
    name: ${blueriiot2_name_prefix} Temperature
    # Optional variables:
    unit_of_measurement: "°C"
    icon: "mdi:water-percent"
    device_class: "temperature"
    state_class: "measurement"
    accuracy_decimals: 1
  
  - platform: template 
    id: sensor_${blueriiot2_id_prefix}_ph
    name: ${blueriiot2_name_prefix} PH
    # Optional variables:
    #unit_of_measurement: "°C"
    #icon: "mdi:water-percent"
    #device_class: "temperature"
    state_class: "measurement"
    accuracy_decimals: 1

  - platform: template 
    id: sensor_${blueriiot2_id_prefix}_orp
    name: ${blueriiot2_name_prefix} Chlor
    # Optional variables:
    #unit_of_measurement: "°C"
    #icon: "mdi:water-percent"
    device_class: "voltage"
    state_class: "measurement"
    accuracy_decimals: 1
    
  - platform: template 
    id: sensor_${blueriiot2_id_prefix}_bat
    name: ${blueriiot2_name_prefix} Battery
    # Optional variables:
    #unit_of_measurement: "°C"
    #icon: "mdi:water-percent"
    device_class: "battery"
    state_class: "measurement"
    accuracy_decimals: 1
  # ENDE 2nd

    
##
## Blue Connect
# 1st Sensor
button:
  - platform: template
    id: button_${blueriiot1_id_prefix}_doreading
    name: ${blueriiot1_name_prefix} do reading
    internal: true
    on_press:
      then:
        - logger.log: 
            format: "Writing to 1st sensor..."
            level: "INFO"
        - ble_client.ble_write:
            id: ble_client_${blueriiot1_id_prefix}
            service_uuid: ${blueriiot_send_service_uuid}
            characteristic_uuid: ${blueriiot_send_characteristic_uuid}
            # A lambda returning an std::vector<uint8_t>.
            value: !lambda |-
              return {0x01};

# 2nd Sensor
  - platform: template
    id: button_${blueriiot2_id_prefix}_doreading
    name: ${blueriiot2_name_prefix} do reading
    internal: true
    on_press:
      then:
        - logger.log: 
            format: "Writing to 2nd sensor..."
            level: "INFO"
        - ble_client.ble_write:
            id: ble_client_${blueriiot2_id_prefix}
            service_uuid: ${blueriiot_send_service_uuid}
            characteristic_uuid: ${blueriiot_send_characteristic_uuid}
            # A lambda returning an std::vector<uint8_t>.
            value: !lambda |-
              return {0x01};

text_sensor:
  # 1st Sensor
  - platform: ble_client
    id: ${blueriiot1_id_prefix}_reading_data
    name: ${blueriiot1_name_prefix} reading data
    internal: true
    ble_client_id: ble_client_${blueriiot1_id_prefix}
    service_uuid: ${blueriiot_recieve_service_uuid}
    characteristic_uuid: ${blueriiot_recieve_characteristic_uuid}
    notify: true
    update_interval: never
    on_notify:
      then:
       lambda: |-
          std::string rawhex = format_hex_pretty((uint8_t *) x.c_str(), x.size()).c_str();
          ESP_LOGD("raw_hex", "%s", rawhex.c_str());

          float temperature = (float)((int16_t)(x[2]<< 8) + x[1])/100;
          ESP_LOGD("temp", "%f", temperature);
          id(sensor_${blueriiot1_id_prefix}_temperature).publish_state(temperature);
              
          float raw_ph = (float)( (int16_t) (x[4]<< 8) + x[3]) ;
          float ph = (float)( (int16_t) (2048 - raw_ph)) / 232  + 7 ; 
          ESP_LOGD("ph", "%f", ph);
          id(sensor_${blueriiot1_id_prefix}_ph).publish_state(ph);
          
          float orp = (float)( (int16_t) (x[6]<< 8) + x[5]) / 3.86 ;
          ESP_LOGD("orp", "%f", orp);
          id(sensor_${blueriiot1_id_prefix}_orp).publish_state(orp);
          
          float bat = (float)( (int16_t) x[11]) ;
          ESP_LOGD("bat", "%f", bat);
          id(sensor_${blueriiot1_id_prefix}_bat).publish_state(bat);
          
          
          
          id(switch_${blueriiot1_id_prefix}_enable).turn_off();
    # END 1st Sensor

    # 2nd Sensor
  - platform: ble_client
    id: ${blueriiot2_id_prefix}_reading_data
    name: ${blueriiot2_name_prefix} reading data
    internal: true
    ble_client_id: ble_client_${blueriiot2_id_prefix}
    service_uuid: ${blueriiot_recieve_service_uuid}
    characteristic_uuid: ${blueriiot_recieve_characteristic_uuid}
    notify: true
    update_interval: never
    on_notify:
      then:
       lambda: |-
          std::string rawhex = format_hex_pretty((uint8_t *) x.c_str(), x.size()).c_str();
          ESP_LOGD("raw_hex", "%s", rawhex.c_str());

          float temperature = (float)((int16_t)(x[2]<< 8) + x[1])/100;
          ESP_LOGD("temp", "%f", temperature);
          id(sensor_${blueriiot2_id_prefix}_temperature).publish_state(temperature);
              
          float raw_ph = (float)( (int16_t) (x[4]<< 8) + x[3]) ;
          float ph = (float)( (int16_t) (2048 - raw_ph)) / 232  + 7 ; 
          ESP_LOGD("ph", "%f", ph);
          id(sensor_${blueriiot2_id_prefix}_ph).publish_state(ph);
          
          float orp = (float)( (int16_t) (x[6]<< 8) + x[5]) / 3.86 ;
          ESP_LOGD("orp", "%f", orp);
          id(sensor_${blueriiot2_id_prefix}_orp).publish_state(orp);
          
          float bat = (float)( (int16_t) x[11]) ;
          ESP_LOGD("bat", "%f", bat);
          id(sensor_${blueriiot2_id_prefix}_bat).publish_state(bat);
          
          
          
          id(switch_${blueriiot2_id_prefix}_enable).turn_off();
    # END 2nd Sensor

At the moment, it doesn’t work reliably for me because one of the sensors won’t give me any results. Not sure if it’s because of a problem in the code (my 1st yaml :wink: ) or maybe a bad reception.

Would you mind telling us how you’d find the bluetooth connection information / payload in the app?
I also use BlueConnect devices and due to the hard work in this thread, I’m able to read them cloudless now :wink:

But I also own a few other devices which only communicate with their vendor’s apps and I would really like to control them using ESPHome.

Which tools did you use to reverse the BlueConnect app? Or did you manage to “sniff” the outgoing BLE traffic from your tablet/phone?