Pool Monitor Device Yieryi BLE-YC01

I bought the same floating tester but it’s branded Yinmik. I found that data are exchanged with Bluetooth Low Energy (BLE) between the app and the device. With the Esphome BLE client, I can establish a connection to the device but, for now, I can’t decode the value returned by the device.

With the Bluetooth logging profile on my iPhone and the help of WireShark, I found these steps:

There are two types of values exchanged: real-time values and historical values. When the connection is established, the host (app) sends what seems to be a time synchronization packet to begin the conversation. The floater responds with real-time values. After few seconds, the app asks for the historical values. Following each historical value received, the app asks for another historical value with always the same command until the floater « saids » it was the last one. After, the floater posts the real-time values.

I will post further the commands involved as I’m away of my computer.

So, sorry for the delay.

Here are the steps sniffed with WireShark:

The host (phone) find the device (BLE-YC01). The host sends a time sync packet like “0b0216051511160b03” where 0b02 is the header, 16051511160b is the time (yy-mm-dd hh:mm:ss in hex → May 22th 2022 17:22:11) and 03 is the checksum

The device answer with a packet to confirm the time packet received:
“ff0016051511160bf566fef6fd4fff66faa937fc27fefeffffffff5d44”
ff00 is the header, 16051511160b is the real time, but I don’t understand what is following the time. I know there is a checksum at the end but what is between seems to be encrypted.

Following the time confirmation packet received, the host sends a request for real time data with this command: “010200000003”

The device sends a confirmation packet like : “fea1ffdaffee7fffb3dffaf55fffebfd75feae77fffaefff77ffef5d40”. This confirmation packet always begin with “fea1”

After, the host listens for data from the device. The device sends packet like these: “ffa1fe5af623ffccfc64fefdfd4eff66faa937fc25feffffffffff5745”
“ffa1fcfaf6feff30fe7dfe88fd75ffe0fab835fe2dfeaaffffffff5f51”
“ffa1fcfaf6fcff33fe7dfe89fd75ffe0fab931fe2dfeabffffffff5f55”

“ffa1” header is for real time datas.

If the option is enable in the app, the host will ask for historical datas after few seconds. The host sends the command “02040006”. The device answers with one data in his memory like “fda176fa02f6f0ff60fcfdfe4cfdecf5bfe3d1f1ffefffffffffffdd51”. This packet should contain a timestamp but again, I think it’s encrypted.

The host will ask for historical data with the “02040006” again and again until the device will answer with a packet starting with “f7a1” like : “f7a15ffa23fc3cfc06ffe2fe76fdead5d7d6d9f4feefffffffffff7d01”

After the end of historical datas, the host listens for real time datas until the comm stop.

The above steps are what is involved when the device talks with a host where the Yinmik app is running.

Also, I found that I can connect to the device with any other BLE manager app (ex: nRF Connect) or with the BLE client sensor running on an ESP32 with ESPhome. In these cases, the device only sends real time datas like “ffa1fe72f6a2ff33fc5c5450a820ff4bfaac35fc0dfefaffffffff5710” (same “ffa1” header like above) . As I said before, the datas seem to be encrypted and I’m not able to understand the values.

The box saids it’s a 6-in-1 device as six values is measured :

pH
ORP
Temperature
TDS
EC
Cl (chlorine level)

But, I’m thinking Chlorine is a calculated value as they are 5 sensors tips on the floating device (for EC, the sensor tip has two separated parts). I don’t know if the device calculate the chlorine level by itself or it’s calculated in the app. Same thing for the Celcius/Fahrenheit temperature.

So, this is where I am. I plan to use an ESP32 with ESPhome BLE client to push the values in Home Assistant but I don’t know how to extract these values.

1 Like

I have just bought the Yieryi device too in addition to another integration I use based on BlueRiott devices. I honestly do not have time at the moment to dig on this, but I have seen a sort of combine effort in between ESPHome and Passive BLE Monitor which seems to include a parsing of the messages received from BLE devices. Again, not time to deep dive and learn how magic it is, but maybe an option to look to.

Out of curiosity what else did you use to sniff the traffic ? I have just bought an NRF52840 to work on the same direction as you and try to decode the messages to integrate it with HA. Next week will give it i try if i find sometime.

Hi,
I’ve just bought the same product and interested in HA integration.

I also try to decode the BLE protocol used, actualy without success.

I found that the “Hold” function lock the BLE caracteristics making them available (withiout changes) for another reader. this could help to make matchin beetween BLE raw values and app values.

I set “Hold” through the android app (or via the buton), save record in the app. connect to the BLE-YC01 device with nRF Connect android app, Read the 0xFF01 service / 0xFF02 Characteristic

I’ll try to have many records with only 1 value change and find data location into the raw bytes.

maybe my experience helps.

BLE raw frames seems to be common on differents Yieryi devices. https://community.home-assistant.io/t/blue-connect-pool-measurements/118901/170
the app still the same with all the connected device they made.

1 Like

Hi Jimenbar, I use the NRF52840 USB dongle with the associated WireShark profile as seen on this site : Overview | BLE Sniffer with nRF52840 | Adafruit Learning System on my Windows PC. Nothing else required

1 Like

I tried to decompress the android app to understand the exchange but, seriously, I’m totally lost in the code…

i also tried to reverse engineer the android app, but it was created with flutter, so the actual app code is inside the libapp.so (resources/libs/…/libapp.so)

This seems to be very hard to decompile though :frowning:

Hello, I am not from HA, but OpenHAB and I have a lot of Esp32 devices using EspHome to integrate with OH using MQTT. I found this topic searching for the BLE-YC01 device, that I just have bought some days ago (I still no have it physically), but very interested to integrate with OH.

Well, the case is that I ask directly to some of the brands of this device, and send me some information, that I would share with all of you, as could be better to develop between more than one!. I think that the best could be integrate with EspHome or C++ in Esp32 devices, and send the data to MQTT.

I don’t know how to share the information, as can’t attach the excel with the information. Probably better to send to interested people? The protocol doesn’t seems very complicated, but I am newbie with BT communications.

Ruben
PD: I post it in the next posts, is not so much, but probably enough

Bluetooth communication protocol												
												
Basic Information												
1	Bluetooth name:	BEL-9100	i.e. product name(BLE-9100,BLE-Ph01,BLE-9909,…)									
2	Service UUID	FF01										
3	Feature UUID	FF02	read and write									
												
												
read data												
1	After the Bluetooth connection is successful, subscribe to the characteristic UUID FF02,											
2	Read the characteristic UUID FF02 to return the data											
3	The data format is as follows: (The data is a byte array, a total of 24) (requires decryption with deCode(), please refer to the encrpt data form)											
												
	serial number	data	significance									
	0	1	data									
		2	Calibration data									
	1	fixed at 2										
	2	0	BLE-None,	Product name code								
		12	BLE-9100,									
		BLE-9100										
	3	DO(mg/L)Value high 8 bits										
	4	DO(mg/L)lower 8 bits of value										
	5	DO(%)Value high 8 bits										
	6	DO(%)lower 8 bits of value										
	7											
	8											
	9											
	10											
	11											
	12											
	13	8-bit high temperature value										
	14	Temperature value lower 8 bits										
	15	Battery voltage value high 8 bits										
	16	Battery voltage value lower 8 bits										
	17	flag bit		0b 0000 0000	7	6	5	4	3	2	1	0
								hold reading	Backlight status			
	18											
	19											
	20											
	21											
	22											
	23	Check Digit	Please refer to the CheckSum form									
												
												
data input												
	Write the characteristic UUID FF02 in the format (write does not require encryption)											
	byte array	Control the backlight										
	0	0x01										
	1	0x00										
	2	check code										
	3											
	4											
	5											
	6											
	7											
	8											
	9											
	10											
	11											
	12											
	13											
	14											
	15											
	16											
	17											
	18											

2 Likes
"void enCode(uint8 *pValue,uint8 len){
	uint8 tmp=0;
	uint8 hibit=0;
	uint8 lobit=0;
	uint8 hibit1=0;
	uint8 lobit1=0;
	//uint8 bit=0;
	//uint8 ecode[8]={1,3,4,5,2,6,3,4};
	for(int i=0;i<len-1;i++)
	{
		tmp=~pValue[i];
		hibit=(tmp&0xAA)>>1;
		lobit=(tmp&0x55)<<1;
		tmp=~pValue[i+1];
		hibit1=(tmp&0xAA)>>1;
		lobit1=(tmp&0x55)<<1;

		pValue[i]=hibit|lobit1;
		pValue[i+1]=hibit1|lobit;		
	
	}
}"

"void deCode(uint8 *pValue,uint8 len){
	uint8 tmp=0;
	uint8 hibit=0;
	uint8 lobit=0;
	uint8 hibit1=0;
	uint8 lobit1=0;
	//uint8 bit=0;
	//uint8 ecode[8]={1,3,4,5,2,6,3,4};
	for(int i=len-1;i>0;i--)
	{
		tmp=pValue[i];
		hibit1=(tmp&0x55)<<1;
		lobit1=(tmp&0xAA)>>1;
		tmp=pValue[i-1];	
		hibit=(tmp&0x55)<<1;
		lobit=(tmp&0xAA)>>1;

		pValue[i]=~(hibit1|lobit);
		pValue[i-1]=~(hibit|lobit1);		
	
	}
}"

uint8 checksum(const uint8* data, uint8 len)		
{		
	uint8 i = 0;	
	uint8 chksum = 0;	
	for(i = 0; i < len; i ++)	
		chksum = chksum^data[i];
	return chksum;	
}		

2 Likes

Hi @RubenKona ,

Suggestion. In order to share the excel you could probalby add an hyperlink in here to your public are of any public cloud you use where you could upload excel and from there any interested folk could download it.

I honestly do not have time (unfortunately) to dig on this and i am not a BLE expert although i am very interested, but i believe that with the above info plus ESPHome BLE functionality plus the lambda funtionality it could be done.

Is not necessary finally to post the Excel, as the above posts with code has all the same information that has the Excel.

I am also not expert in BLE, and with EspHOME lambda, and also don’t have yet the device, but I try to do it when my unit arrive. In any case, maybe some of the above, could dig a little more and find useful this information :wink:

Ruben

1 Like

Hi @RubenKona
Was you able to do some tests ?
I have a similar sensor, the BLE-C600 with more inputs (EC / TDS / SALT / PH / ORP and temp).
I did convert your c++ code to python one and for now i’m only able to find the ORP value on the data received.

Anas

Hi @anasm2010 , can you share your python code? Maybe I can test it with my two floating devices.

Thanks

Here is a script example that can decode the full packet.
For now i’m locking for a solution to disable the auto-turn off function … Any idea?

import asyncio

from bleak import BleakClient

import time

from decodeInt import Messure

address = "C0:00:00:00:8A:D1"

# MODEL_NBR_UUID = "00002a24-0000-1000-8000-00805f9b34fb"

MODEL_NBR_UUID = "0000180D-0000-1000-8000-00805F9B34FB"

Battery_Service = "00002a19-0000-1000-8000-00805f9b34fb"

Vendor_specific = "0000ff10-0000-1000-8000-00805f9b34fb"

"""

00001800-0000-1000-8000-00805f9b34fb (Handle: 1): Generic Access Profile

00001801-0000-1000-8000-00805f9b34fb (Handle: 8): Generic Attribute Profile

0000180a-0000-1000-8000-00805f9b34fb (Handle: 12): Device Information

0000180f-0000-1000-8000-00805f9b34fb (Handle: 31): Battery Service

0000ff01-0000-1000-8000-00805f9b34fb (Handle: 36): Vendor specific

"""

class Messure(object):

    def __init__(self, frame : str) -> None:

        self.frame = frame

       

        self.packet = self.decode(frame)

       

        self.data = self.packet[0]

        self.constant = self.packet[1]

        self.product_name_code = self.packet[2]

       

        self.hold_reading = self.packet[17] >> 4

        self.backlight_status = (self.packet[17] & 0x0F) >> 3

        self.battery = self.decode_position(15)

       

        self.ec = self.decode_position(5)

        self.tds = self.decode_position(7)

        self.salt_tds = self.decode_position(9)

        self.salt_sg = self.decode_position(11) ##TO DO

        self.ph = self.decode_position(3)/100

        self.orp = self.decode_position(20)

        self.temperature = self.decode_position(13)/10

       

    def decode(self, byte_frame : bytes ):

        frame_array = [int(x) for x in byte_frame]

        size = len(frame_array)

        for i in range(size-1, 0 , -1):

            tmp=frame_array[i]

            hibit1=(tmp&0x55)<<1

            lobit1=(tmp&0xAA)>>1

            tmp=frame_array[i-1]

            hibit=(tmp&0x55)<<1

            lobit=(tmp&0xAA)>>1

            frame_array[i]=0xff -(hibit1|lobit)

            frame_array[i-1]= 0xff -(hibit|lobit1)

       

        return frame_array

   

    def reverse_bytes(self, bytes : list):

        return (bytes[0] << 8) + bytes[1]

       

    def decode_position(self,idx):

        return self.reverse_bytes(self.packet[idx:idx+2])

   

    def show_values(self):

        return_string = f"h=[{self.hold_reading}/bl={self.backlight_status}/B={self.battery}] EC={self.ec:4}  TDS={self.tds:4}  SALT(TDS)={self.salt_tds:4}  SALT(S.G.)={self.salt_sg:4}  pH={self.ph:4}  ORP(mV)={self.orp:4}  Temperature(C)={self.temperature:4} "

        return return_string

   

async def main(address):

    async with BleakClient(address) as client:

        print(f"Connected: {client.is_connected}")

       

        paired = await client.pair(protection_level=2)

        print(f"Paired: {paired}")

       

        old_time = time.time()

        print(time.time() - old_time)

        while True:

            model_number = await client.read_gatt_char(Vendor_specific)    

            print(Messure(model_number).show_values())

            time.sleep(1)

asyncio.run(main(address))
2 Likes

Try to check the saved data buttom and assign an interval, it may keep it alive.

Yes thank you, it seems to work on the app. However, once I switch to the python script it goes off again after 5min. I assume the app is sending a request that force it to stay on … I need to find what.

2 Likes

Hi,

Here is a functional code for an ESP32 with Esphome associated with Home Assistant. Every 30 minutes, the BLE device is polled for new datas.

time:
  - platform: homeassistant
    id: homeassistant_time
    on_time:
      # Turn on BLE client every 30 minutes for 2 minutes
      - seconds: 0
        minutes: /30
        then:
          - switch.turn_on: ble_switch
          - delay: 2min
          - switch.turn_off: ble_switch


esp32_ble_tracker: #### Stop the active scan ####
  scan_parameters: 
    active: false

######################################################
##                                                  ##
##   To initiate to connection with the BLE device  ##
##                                                  ##
######################################################

ble_client: 
  - mac_address: YO:UR:_A:DD:RE:SS #Use the MAC address of your BLE device
    id: ble_yc01
    on_connect: #### Actions to perform when connecting to the BLE device ####
      then:
      #### Wait until characteristic is discovered ####
        - wait_until:   
            lambda: |-
              esphome::ble_client::BLEClient* client = id(ble_yc01);
            
              auto service_uuid = 0xFF01;
              auto char_uuid = 0xFF02;
              
              //#### When waiting for connection, we extract the available characteristics ####
              esphome::ble_client::BLECharacteristic* chr = client->get_characteristic(service_uuid, char_uuid);
              
              return chr != nullptr;
              
        #### Official connection to the BLE device with the desired characteristic ####
        - lambda: |- 
            ESP_LOGD("ble_client_lambda", "Connected to BLE-YC01");
            
            esphome::ble_client::BLEClient* client = id(ble_yc01);
            
            auto service_uuid = 0xFF01; 
            auto char_uuid = 0xFF02;
            
            esphome::ble_client::BLECharacteristic* chr = client->get_characteristic(service_uuid, char_uuid);
            
            if (chr == nullptr) {
                ESP_LOGW("ble_client", "[0xFF01] Characteristic not found.  State update can not be written.");
            }
        
    on_disconnect:
      then:
        - lambda: |-
            ESP_LOGD("ble_client", "Disconnected from BLE-YC01");
            
######################################################
##                                                  ##
##     Sensors associated with the BLE device       ##
##                                                  ##
######################################################
            
sensor: #### Template sensor as their values are publish from a lambda or the BLE client ####

  - platform: template
    name: "BLE-YC01 EC"
    id: ble_yc01_ec_sensor
    unit_of_measurement: "µS/cm"
    accuracy_decimals: 0
    state_class: measurement
    icon: mdi:water-opacity
    
  - platform: template
    name: "BLE-YC01 TDS"
    id: ble_yc01_tds_sensor
    unit_of_measurement: "ppm"
    accuracy_decimals: 0
    state_class: measurement
    icon: mdi:water-opacity
    
  - platform: template
    name: "BLE-YC01 Temperature"
    id: ble_yc01_temperature_sensor
    unit_of_measurement: "F"
    accuracy_decimals: 1
    state_class: measurement
    device_class: temperature
    
  - platform: template
    name: "BLE-YC01 ORP"
    id: ble_yc01_orp_sensor
    unit_of_measurement: "mV"
    accuracy_decimals: 0
    state_class: measurement
    device_class: voltage
    
  - platform: template
    name: "BLE-YC01 pH"
    id: ble_yc01_ph_sensor
    unit_of_measurement: "pH"
    accuracy_decimals: 2
    state_class: measurement
    icon: mdi:ph

  - platform: template
    name: "BLE-YC01 batterie"
    id: ble_yc01_battery
    unit_of_measurement: "%"
    accuracy_decimals: 0
    state_class: measurement
    device_class: battery
    icon: mdi:battery
    
  - platform: ble_client ####  Sensor required to manage values coming from the BLE device ####
    ble_client_id: ble_yc01
    id: ble_yc01_sensor
    internal: true
    service_uuid: FF01
    characteristic_uuid: FF02
    notify: true
    #### Lambda to decode values and push to the associated sensors ####
    lambda: |-
    
      if (x.size() == 0) return NAN;
      
      //ESP_LOGD("ble_client.receive", "value received with %d bytes: [%.*s]", x.size(), x.size(), &x[0]); // ####  Useful for debugging ####
 
 
      // ### DECODING ###
      uint8_t tmp = 0;
      uint8_t hibit = 0;
      uint8_t lobit = 0;
      uint8_t hibit1 = 0;
      uint8_t lobit1 = 0;
      auto message = x;
      
      for (int i = x.size() -1 ; i > 0; i--) {
        tmp=message[i];
        hibit1=(tmp&0x55)<<1;
        lobit1=(tmp&0xAA)>>1;
        tmp=message[i-1];	
        hibit=(tmp&0x55)<<1;
        lobit=(tmp&0xAA)>>1;
        
        message[i]=~(hibit1|lobit);
        message[i-1]=~(hibit|lobit1);

      }
      
      //ESP_LOGD("ble_client.receive", "value received with %d bytes: [%.*s]", message.size(), message.size(), &message[0]); // #### For debug ####


      // #### Extraction of individual values ####
      auto temp = ((message[13]<<8) + message[14]);
      auto ph = ((message[3]<<8) + message[4]);
      auto orp = ((message[20]<<8) + message[21]);
      auto battery = ((message[15]<<8) + message[16]);
      auto ec = ((message[5]<<8) + message[6]);
      auto tds = ((message[7]<<8) + message[8]);
      
      // #### Sensors updated with new values
      id(ble_yc01_temperature_sensor).publish_state(((temp/10.0) * (9.0/5.0)) + 32.0);
      id(ble_yc01_ph_sensor).publish_state(ph/100.0);
      id(ble_yc01_orp_sensor).publish_state(orp);
      id(ble_yc01_battery).publish_state(battery/45);
      id(ble_yc01_ec_sensor).publish_state(ec);
      id(ble_yc01_tds_sensor).publish_state(tds);
       
      return 0.0; // this sensor isn't actually used other than to hook into raw value and publish to template sensors


switch:  #### To switch on and off the communication with the BLE device ####
  - platform: ble_client
    id: ble_switch
    ble_client_id: ble_yc01
    name: "Enable BLE-YC01"

Here is a way to calculate free chlorine in HA. For now, I can’t figure out how to integrate it directly in Esphome.

The formula comes from raspipool github: https://github.com/segalion/raspipool

sensors:
  - platform: template
    sensors:
      chlore_libre_spa:
        value_template: "{{ ( 0.23 * (1 + 0 / 100 ) * ( 14 - states('sensor.ble_yc01_ph')|float(0.0)) ** ( 1 / (400 - states('sensor.ble_yc01_orp')|float(0.0)) ) * ( states('sensor.ble_yc01_ph')|float(0.0) -4.1) ** ( ( states('sensor.ble_yc01_orp')|float(0.0) - 516)/145) + 10.0 ** ( (states('sensor.ble_yc01_orp')|float(0.0) + states('sensor.ble_yc01_ph')|float(0.0) * 70 -1282) / 40 ) ) |round(1) }}"
        unit_of_measurement: ppm
        icon_template: mdi:react
        friendly_name: "Chlore libre estimé spa"
5 Likes