Inspired by a Hackaday post I recently bought a cheap Bluetooth thermometer/hygrometer to use with Home Assistant. The idea was to flash it to output temperature and humidity data over BLE (Bluetooth Low Energy) in the BTHome format. Unfortunately I bought an incompatible model… This post records what I learnt through trying to get it working via BLE notifications, in the hope someone else might find my experience helpful.
PCB Model: YZ6045B v4
Bluetooth name: LT_5806
App name: Qaqa
- ESPHome can use one of two frameworks for programming the ESP32 platform: Arduino or ESP-IDF. Arduino is the default, but for the BLE stack to work, you need to use ESP-IDF. Switching between the two can’t be done properly using an OTA update, which is worth knowing in advance if a wired firmware update is going to involve soldering or dismantling something. And yes, my target device was already embedded in a wall before I read this!
esp32:
board: esp32dev
framework:
type: esp-idf
- The documentation warns you that the BLE stack takes a lot of RAM, so not to add too many components into your design. What it doesn’t mention is that BLE also takes a lot of power, which may be a problem if you are also using WiFi. I suffered from boot-loops and brownout detector log messages before I realised what the problem was. In particular, I couldn’t use an AZDelivery ESP32 D1 Mini dev board for prototyping, as its onboard voltage regulator couldn’t supply enough power. The Sonoff MiniR4 I am using as my target device didn’t work either when powered via a USB-TTL adaptor, but was fine when using its mains power supply.
- The starting point for getting a BLE connection is to use the
esp32_ble_tracker
component. This listens in to BLE devices that periodically advertise their presence. Some devices don’t advertise until you actively probe them, but the YZ6045B can be scanned passively.
esp32_ble_tracker:
scan_parameters:
active: False
continuous: True
- Some devices advertise their sensor readings, so listening in passively is all that is needed (this is how BTHome works). Unfortunately, the YZ6045B only advertises its device name, e.g.
LT_1234
, so you need to open an active connection to read the temperature and humidity values. Theble_client
component opens a connection to a specific device. You can find the MAC address to connect to using theble_tracker
logs, but it may be easier to use a phone app; I used BLE Scanner.
ble_client:
- id: lt_temp
mac_address: F6:2F:E3:91:58:06
auto_connect: True
- Once the connection is open, you need to read sensor data from a particular BLE service and ‘characteristic’. For some characteristics, you can just read their current value. For others, you have to subscribe for updates, and the device notifies once a value is available - this is the case for the YZ6045B. A BLE scanner app can reveal what services and characteristics are available, and whether you have to use a ‘read’ or ‘notify’ to get the data.
- Luckily for me, Rémi Peyronnet had already done the hard work of reverse-engineering the app for this device, and had identified which characteristic to use, and the data format. While Rémi decoded the data using a Python script for Domoticz, I implemented the decoding as an ESPHome sensor:
sensor:
- platform: ble_client
type: characteristic
ble_client_id: lt_temp
id: lt_temp_raw
service_uuid: '0000FFE5-0000-1000-8000-00805F9B34FB'
characteristic_uuid: '0000FFE8-0000-1000-8000-00805F9B34FB'
notify: True
internal: True
lambda: |-
float lt_temperature = ((x[5] << 8) + x[6]) / 10.0f;
float lt_humidity = ((x[7] << 8) + x[8]) / 10.0f;
id(temp_out).publish_state(lt_temperature);
id(humidity_out).publish_state(lt_humidity);
return {};
- platform: template
id: temp_out
name: LT Temp Temperature
device_class: temperature
state_class: measurement
unit_of_measurement: '°C'
- platform: template
id: humidity_out
name: LT Temp Humidity
device_class: humidity
state_class: measurement
unit_of_measurement: '%'
- The documentation implies that you can use an
on_notify
automation to process incoming data from the BLE client sensor. In practice, just usinglambda
works fine whether you are polling or using notifications, andon_notify
doesn’t get access to the raw BLE data. It is passed a singlefloat
as input, while the raw data is astd::vector<uint8_t>
. - As a single BLE reading contains several actual measurements (temperature and humidity), the lamba above doesn’t return a value, but publishes the measurements to separate template sensors.
- While the examples above are enough to get the sensor working, I extended them to do a bit more. The lambda in the version below does some integrity checks on the received data and also calculates dew point from the temperature and relative humidity.
- The YZ6045B data format includes a Celsius/Fahrenheit indicator. I was going to add some code to convert the reading to Celsius when the sensor was reading in Fahrenheit, but it proved unnecessary: the data field shows you which unit is displayed on the device screen, but on my device at least, the transmitted data is always in Celsius.
- Various people have suggested that maintaining an open BLE connection will drain the CR2032 battery in the YZ6045B very quickly. To avoid this, I used an
interval
component with able_client
switch to close the connection once a reading is received, and then open it again 5 minutes later. I also added a sensor for the BLE battery level service. To get this working properly, I had to add some flags to let me check that I had received both sets of data (environmental and battery) before closing the connection, as you don’t know which order they will arrive in or how long you will have to wait for the next notification. I also needed to buffer the battery data through another template sensor, so that it wouldn’t show as unavailable once I closed the BLE connection.
Here’s the full version of my code:
esp32_ble_tracker:
scan_parameters:
active: False
continuous: True
ble_client:
- id: lt_temp
mac_address: F6:2F:E3:91:58:06
auto_connect: True
switch:
- platform: ble_client
ble_client_id: lt_temp
id: ble_enable
globals:
- id: check_received
type: int
restore_value: False
initial_value: '0'
# Temperature notification format:
# (From https://www.lprp.fr/2022/07/capteur-bluetooth-le-temperature-dans-domoticz-par-reverse-engineering-et-mqtt-auto-discovery-domoticz-et-home-assistant/)
# Octets Contenu
# 0,1 Header (0xAA0xAA)
# 2 Type de données 162 (0xA2) : hygrométrie (température + humidité)
# 3,4 Taille des données : 6 (Big Endian)
# 5,6 Température en Big Indian, à diviser par 10
# 7,8 Humidité en Big Indian, à diviser par 10
# 9 Indicateur de pile
# 10 Unité ; 0 pour Celcius
# 11 Checksum des données (somme de tous les octets de 0 à 10, modulo 256)
# 12 Footer (0x55)
sensor:
- platform: ble_client
type: characteristic
ble_client_id: lt_temp
id: lt_temp_raw
service_uuid: '0000FFE5-0000-1000-8000-00805F9B34FB'
characteristic_uuid: '0000FFE8-0000-1000-8000-00805F9B34FB'
notify: True
internal: True
lambda: |-
// Check notification is expected format
if (x[0] == 0xAA && x[1] == 0xAA && x[2] == 0xA2 && x[3] == 0x0 && x[4] == 0x6 && x[12] == 0x55) {
// Check checksum is OK
if ((x[0] + x[1] + x[2] + x[3] + x[4] + x[5] + x[6] + x[7] + x[8] + x[9] + x[10]) % 256 == x[11]) {
float lt_temperature = ((x[5] << 8) + x[6]) / 10.0f;
float lt_humidity = ((x[7] << 8) + x[8]) / 10.0f;
float lt_dewpoint = (243.5f * (log(lt_humidity / 100.0f) + ((17.67f * lt_temperature ) /
(243.5f + lt_temperature))) / (17.67f - log(lt_humidity / 100.0f) -
((17.67f * lt_temperature) / (243.5f + lt_temperature))));
ESP_LOGD("lt_temp", "Notification received: Temperature: %2.1f, humidity: %2.1f, dewpoint: %2.1f, units: %d", lt_temperature, lt_humidity, lt_dewpoint, x[10]);
id(temp_out).publish_state(lt_temperature);
id(humidity_out).publish_state(lt_humidity);
id(dewpoint_out).publish_state(lt_dewpoint);
// Disconnect once we have a good reading for battery and environment
id(check_received) |= 1;
if (id(check_received) == 3) {
id(check_received) = 0;
id(ble_enable).turn_off();
}
return {};
} else {
ESP_LOGD("lt_temp", "Notification received with bad checksum");
return {};
}
} else {
ESP_LOGD("lt_temp", "Bad notification format");
return {};
}
- platform: template
id: temp_out
name: LT Temp Temperature
device_class: temperature
state_class: measurement
unit_of_measurement: '°C'
- platform: template
id: humidity_out
name: LT Temp Humidity
device_class: humidity
state_class: measurement
unit_of_measurement: '%'
- platform: template
id: dewpoint_out
name: "LT Temp Dew Point"
accuracy_decimals: 1
device_class: temperature
state_class: measurement
unit_of_measurement: '°C'
icon: 'mdi:thermometer-water'
- platform: template
name: "LT Temp battery level"
id: battery_out
device_class: battery
state_class: measurement
unit_of_measurement: '%'
- platform: ble_client
id: lt_temp_battery
internal: True
type: characteristic
ble_client_id: lt_temp
service_uuid: '180f'
characteristic_uuid: '2a19'
update_interval: 25s
lambda: |-
// Disconnect once we have a good reading for battery and environment
id(battery_out).publish_state((float) x[0]);
id(check_received) |= 2;
if (id(check_received) == 3) {
id(check_received) = 0;
id(ble_enable).turn_off();
}
return {};
- platform: ble_rssi
mac_address: F6:2F:E3:91:58:06
name: "LT Temp RSSI"
filters:
- throttle_average: 60s
interval:
# Every 5 minutes, try to connect to BLE temperature sensor for a max of 30s.
- interval: 5min
startup_delay: 30s
then:
- switch.turn_on: ble_enable
- delay: 30s
- switch.turn_off: ble_enable
I’ve now had the above code running for a month, and the YZ6045B hasn’t yet dropped from its starting point of 90% battery.
I hope someone else finds this interesting or helpful. Please feel free to add any suggestions for further improvements.