Integrating a YZ6045B bluetooth temperature/humidity sensor

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. The ble_client component opens a connection to a specific device. You can find the MAC address to connect to using the ble_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 using lambda works fine whether you are polling or using notifications, and on_notify doesn’t get access to the raw BLE data. It is passed a single float as input, while the raw data is a std::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 a ble_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.

1 Like