Read Govee Temperature Humidity BLE Advertisements with esp32 and esphome!

Hey Folks!

Inspired by custom component https://github.com/Home-Is-Where-You-Hang-Your-Hack/sensor.goveetemp_bt_hci I wanted to use these awesome sensors throughout my house but I have sections of the house I knew the BLE advertisements would not be able to reach my PC that is running home assistant so I wanted to see if it would be possible to use ESP32 bluetooth capability to read these advertisements. There’s already multiple ESPHome components that do something similar but I am a n00b at C++ and it was quite daunting trying to start writing my own component for esphome. Luckily, we dont have to do that as ESPHome has a built in feature/callback that can parse such messages with a lambda function.

Relevant ESP Home config:

esp32_ble_tracker:
  on_ble_manufacturer_data_advertise:
    - mac_address: A4:C1:38:11:22:33 #update to your govee device mac address
      manufacturer_id: '0001'
      then:
        - lambda: |-
            int basenum = (int16_t(x[2]) << 16) + (int16_t(x[3]) << 8) + int16_t(x[4]);
            ESP_LOGD("ble_adv", "goveesensor basenum: (%d)", basenum);
            
            bool is_negative = false;
            if (basenum & 0x800000) {
                is_negative = true;
                basenum = basenum ^ 0x800000;
            }
            float temperature = basenum / 10000.0f;
            float humidity = (basenum % 1000) / 10.0f;
            float battery_level = uint16_t(x[5]) / 1.0f;
            if (is_negative) {
                temperature = -temperature;
            }
            
            ESP_LOGD("ble_adv", "  Temperature: %.2f°C", temperature);
            ESP_LOGD("ble_adv", "  Humidity: %.2f", humidity);
            ESP_LOGD("ble_adv", "  Battery Level: %.0f percent", battery_level);
            id(govee1_temp).publish_state(temperature);
            id(govee1_humidity).publish_state(humidity);
            id(govee1_battery).publish_state(battery_level);

  #use this section to determine manufacturer id(mine was '0001'), can be commented out after initial discovery
  on_ble_advertise:
    - mac_address: A4:C1:38:11:22:33 #update to your govee device mac address
      then:
        - lambda: |-
            ESP_LOGD("ble_adv", "New BLE device");
            ESP_LOGD("ble_adv", "  address: %s", x.address_str().c_str());
            ESP_LOGD("ble_adv", "  name: %s", x.get_name().c_str());
            ESP_LOGD("ble_adv", "  Advertised service UUIDs:");
            for (auto uuid : x.get_service_uuids()) {
                ESP_LOGD("ble_adv", "    service uuid %s", uuid.to_string().c_str());
            }
            ESP_LOGD("ble_adv", "  Advertised service data:");
            for (auto data : x.get_service_datas()) {
                ESP_LOGD("ble_adv", "    service data uuid - %s: (length %i)", data.uuid.to_string().c_str(), data.data.size());
            }
            ESP_LOGD("ble_adv", "  Advertised manufacturer data:");
            for (auto data : x.get_manufacturer_datas()) {
                ESP_LOGD("ble_adv", "    mfg data uuid %s: (length %i)", data.uuid.to_string().c_str(), data.data.size());
            }

sensor:
  - platform: template
    name: "Master Bath Temperature"
    id: govee1_temp
    unit_of_measurement: "°C"
    accuracy_decimals: 2
  - platform: template
    name: "Master Bath Humidity"
    id: govee1_humidity
    unit_of_measurement: '%'
    accuracy_decimals: 2
  - platform: template
    name: "Master Bath Sensor Battery Level"
    id: govee1_battery
    unit_of_measurement: '%'
    accuracy_decimals: 0

Note(s):
I’ve confirmed this works with Govee H5101 model, which based on the lovely work by @Thrilleratplay should also work with a slight change in message format for Govee models 5102 and 5072 as well other models should be fairly straightforward to adapt, assuming they use similar structure.
Be sure to comment with your code if you find a new model working with a similar method!

5 Likes

I was able to do the same for the H5074. Same concept, different lambda due to the differences in the H5074 data encoding. This has been working for several weeks without issue. I use the humidity sensors to turn on the exhaust fans to run automatically to keep the humidity down when the shower is used. Working well so far.

sensor:
#  - platform: wifi_signal
#    name: "WiFi Signal Strength"
#    update_interval: 60s
  
  - platform: template
    name: "Guest Bathroom Humidity"
    id: guest_bathroom_humidity
    unit_of_measurement: '%'
    icon: "mdi:water-percent"
  - platform: template
    name: "Master Bathroom Humidity"
    id: master_bathroom_humidity
    unit_of_measurement: '%'
    icon: "mdi:water-percent"
  - platform: template
    name: "Master Bathroom Temperature"
    id: master_bathroom_temperature
    unit_of_measurement: 'F'
    icon: "mdi:thermometer"
  - platform: template
    name: "Master Bathroom RSSI"
    id: master_bathroom_rssi
    unit_of_measurement: 'dB'
    icon: "mdi:wifi"
  - platform: template
    name: "Master Bathroom Battery"
    id: master_bathroom_battery
    unit_of_measurement: '%'
    icon: "mdi:battery"

esp32_ble_tracker:
  on_ble_advertise:
    - mac_address: XX:XX:XX:XX:XX:XX
      then:
        - lambda: |-
            for (auto data : x.get_manufacturer_datas()) {
                if(data.data.size()==7) {
                  uint16_t hum_lsb = data.data[3] + (data.data[4] << 8);
                  float humidity= float(hum_lsb)/100.0;
                  int16_t temp_lsb = data.data[1] + (data.data[2] << 8);
                  float temperature = float(temp_lsb)/100*9.0/5.0 + 32.0;
                  int16_t battery=data.data[5];
                  int16_t rssi=x.get_rssi();
                  id(master_bathroom_humidity).publish_state(humidity);
                  id(master_bathroom_temperature).publish_state(temperature);
                  id(master_bathroom_rssi).publish_state(rssi);
                  id(master_bathroom_battery).publish_state(battery);

                }
            }
4 Likes

I’m happy to see this working – I have a 5075 and I’m hoping to adjust what you all have done to make it work for it. Of course, I need to do some research to figure out what is happening here. I’d love to move this from the component and to the esp32 to be able to centralize the sensor and have more consistent readings.

Bought several H5051 sensors recently. The only update I had to make to @John.McDowell script was checking the data size was ==9 instead of ==7. Really appreciate the work put in to get this going!

esp32_ble_tracker:
  on_ble_advertise:
    - mac_address: XX:XX:XX:XX:XX:XX
      then:
        - lambda: |-
            for (auto data : x.get_manufacturer_datas()) {
              if(data.data.size()==9) {
                uint16_t hum_lsb = data.data[3] + (data.data[4] << 8);
                float humidity= float(hum_lsb)/100.0;
                int16_t temp_lsb = data.data[1] + (data.data[2] << 8);
                float temperature = float(temp_lsb)/100*9.0/5.0 + 32.0;
                int16_t battery=data.data[5];
                int16_t rssi=x.get_rssi();
                id(govee_h5051_1_humidity).publish_state(humidity);
                id(govee_h5051_1_temperature).publish_state(temperature);
                id(govee_h5051_1_rssi).publish_state(rssi);
                id(govee_h5051_1_battery).publish_state(battery);
              }
            }

This is great. Thanks all for the work you put in.

Does this work for negative temperature numbers? I have the H5075, and when the temperature goes below 0C, it get super weird numbers using the encoding described in https://github.com/Thrilleratplay/GoveeWatcher

@mmohoney cc

I do not understand how to receive the manufacturer_id. I have a 5072, and am able to see it get discovered:

[16:53:35][D][ble_adv:947]: New BLE device
[16:53:35][D][ble_adv:948]:   address: *redacted*
[16:53:35][D][ble_adv:949]:   name: GVH5072_3528
[16:53:35][D][ble_adv:950]:   Advertised service UUIDs:
[16:53:35][D][ble_adv:952]:     - EC:88
[16:53:35][D][ble_adv:954]:   Advertised service data:
[16:53:35][D][ble_adv:958]:   Advertised manufacturer data:
[16:53:35][D][ble_adv:960]:     - EC:88: (length 6)
[16:53:35][D][ble_adv:960]:     - 00:4C: (length 23)

Can anyone help me interpret the manufacturer data? Am I supposed to convert those values or is something else wrong?

It is stored in two’s complement from @Thrilleratplay script. Two’s Complement if you are interested. I’ll see if I can update the esp script this week if I have time.

1 Like

@bline79

I have H5072s (as well as H5051s). Just got it working right now. Should note is late here and I did about 20 mins of verification so far. The manufacture data you care about is EC88.

     - mac_address: XX:XX:XX:XX:XX:XX
       manufacturer_id: 'EC88'

However you can use the built in function to give you back manufacture data and parse by length if you want to go that route as well. Either way will work, and using the UUID is probably more correct. Here is a working snippet you can paste in below

    - mac_address: XX:XX:XX:XX:XX:XX
      then:
        - lambda: |-
            for (auto data : x.get_manufacturer_datas()) {
              if(data.data.size()==6) {
                const int basenum = (int16_t(data.data[1]) << 16) + (int16_t(data.data[2]) << 8) + int16_t(data.data[3]);
                const float temperature = (basenum / 10000.0f)*9.0/5.0 + 32.0;
                const float humidity = (basenum % 1000) / 10.0f;
                const float battery_level = uint16_t(data.data[4]) / 1.0f;
                int16_t rssi=x.get_rssi();
                id(govee_h5072_7_humidity).publish_state(humidity);
                id(govee_h5072_7_temperature).publish_state(temperature);
                id(govee_h5072_7_rssi).publish_state(rssi);
                id(govee_h5072_7_battery).publish_state(battery_level);
              } 
            }
3 Likes

@WhichWayWazzit - wanted to ping you with the same question - does this method for the H5101 work for negative numbers temperatures?

Any chance you can post the full piece of .yaml code for this? I am getting a compile error for the x.get_rssi() call.

Thanks for the work!! This worked well for me and Govee H5101 sensors.

Anyone have any ideas about how to do something similar with a remote Raspberry Pi 4b’s bluetooth?

For anyone using a Govee 5075 sensor, the following script will get you up and running:

- platform: template
    name: "Kitchen Humidity"
    id: kitchen_humidity
    unit_of_measurement: '%'
    icon: "mdi:water-percent"
  - platform: template
    name: "Kitchen Temperature"
    id: kitchen_temperature
    unit_of_measurement: '°C'
    icon: "mdi:thermometer"
  - platform: template
    name: "Kitchen Battery"
    id: kitchen_battery
    unit_of_measurement: '%'
    icon: "mdi:battery"
- mac_address: A4:C1:38:AA:BB:CC #kitchen
      manufacturer_id: EC88
      then:
        - lambda: |-
            const int basenum = (int16_t(x[1]) << 16) + (int16_t(x[2]) << 8) + int16_t(x[3]);
            ESP_LOGD("ble_adv", "goveesensor basenum: (%d)", basenum);
            const float temperature = basenum / 10000.0f;
            const float humidity = (basenum % 1000) / 10.0f;
            const float battery_level = uint16_t(x[4]) / 1.0f;
            ESP_LOGD("ble_adv", "  Temperature: %.2f°C", temperature);
            ESP_LOGD("ble_adv", "  Humidity: %.2f", humidity);
            ESP_LOGD("ble_adv", "  Battery Level: %.0f percent", battery_level);
            id(kitchen_temperature).publish_state(temperature);
            id(kitchen_humidity).publish_state(humidity);
            id(kitchen_battery).publish_state(battery_level);

Complete noob questions, but does anyone know of a way to abstract this to a script that can take parameters? I have 4 of these sensors around the house and while the code isn’t that long, I’d like to have one function that can take the sensor data and a name and handle the decoding / publishing instead of having multiple copies…

4 Likes

Sorry for the lack of updates here. I just figured out how to do negative temps on these as well after a bit of trial/error.

esp32_ble_tracker:
  on_ble_manufacturer_data_advertise:
    - mac_address: A4:C1:38:11:22:33
      manufacturer_id: '0001'
      then:
        - lambda: |-
            int basenum = (int16_t(x[2]) << 16) + (int16_t(x[3]) << 8) + int16_t(x[4]);
            ESP_LOGD("ble_adv", "goveesensor basenum: (%d)", basenum);
            
            bool is_negative = false;
            if (basenum & 0x800000) {
                is_negative = true;
                basenum = basenum ^ 0x800000;
            }
            float temperature = basenum / 10000.0f;
            float humidity = (basenum % 1000) / 10.0f;
            float battery_level = uint16_t(x[5]) / 1.0f;
            if (is_negative) {
                temperature = -temperature;
            }
            
            ESP_LOGD("ble_adv", "  Temperature: %.2f°C", temperature);
            ESP_LOGD("ble_adv", "  Humidity: %.2f", humidity);
            ESP_LOGD("ble_adv", "  Battery Level: %.0f percent", battery_level);
            id(govee1_temp).publish_state(temperature);
            id(govee1_humidity).publish_state(humidity);
            id(govee1_battery).publish_state(battery_level);

relevant section above, I’ll update my first post as well. Essentially you have 6 hex chars to store data in, eg the largest number is 0xFFFFFF of which the value is encoded at the middle of min and max, or 0x800000. If you notice the value is greater than 0x800000, then the number has ‘rolled’ over to negative territory and you need to subtract of 0x800000 from the basenum and take the temp as a negtive value.

1 Like

I have gotten both an H5075 and an H5101 working, each on its’ own ESP32. I would like to get multiple h5101s working on a single ESP32. I have tried a number of things but don’t seem to be able to accomplish this. Has anyone done this??

I have 9 sensors on one dedicated govee esp. I’m out now but maybe later I can post the code. It’s extremely basic and just copy and paste for each sensor.

Thank you for quick reply. I look forward to seeing the code.

Sorry, took a bit more time than I thought.

Anyways looking at it, it is as simple as just duplicating your -mac_address: xx:xx:xx:xx:xx block inside the on_ble_adverstise block. Looking at his makes the developer in me sad as it is just copy and paste, but it has been rock solid for 2.5 months. Don’t think I will touch it tbh.

Just duplicate your code with a new mac_address for the specified ids.

 on_ble_advertise:
    - mac_address: 1x:xx:xx:xx:xx
      then:
        - lambda: |-
            for (auto data : x.get_manufacturer_datas()) {
              if(data.data.size()==9) {
                uint16_t hum_lsb = data.data[3] + (data.data[4] << 8);
                float humidity= float(hum_lsb)/100.0;
                int16_t temp_lsb = data.data[1] + (data.data[2] << 8);
                float temperature = float(temp_lsb)/100*9.0/5.0 + 32.0;
                int16_t battery=data.data[5];
                int16_t rssi=x.get_rssi();
                id(govee_h5051_1_humidity).publish_state(humidity);
                id(govee_h5051_1_temperature).publish_state(temperature);
                id(govee_h5051_1_rssi).publish_state(rssi);
                id(govee_h5051_1_battery).publish_state(battery);
              }
            }
            
    - mac_address: 2x:xx:xx:xx:xx
      then:
        - lambda: |-
            for (auto data : x.get_manufacturer_datas()) {
              if(data.data.size()==9) {
                uint16_t hum_lsb = data.data[3] + (data.data[4] << 8);
                float humidity= float(hum_lsb)/100.0;
                int16_t temp_lsb = data.data[1] + (data.data[2] << 8);
                float temperature = float(temp_lsb)/100*9.0/5.0 + 32.0;
                int16_t battery=data.data[5];
                int16_t rssi=x.get_rssi();
                id(govee_h5051_2_humidity).publish_state(humidity);
                id(govee_h5051_2_temperature).publish_state(temperature);
                id(govee_h5051_2_rssi).publish_state(rssi);
                id(govee_h5051_2_battery).publish_state(battery);
              } 
            }

Well, i finally got an m5 stack and got this working with a 5074! Amazing. When i tried to include another mac address for a model 5057 using this code it failed.

I’ve tried a couple versions of code, including removing the manufacturer Id, trying to use the if statement etc. My current version which provides the error is:

esp32_ble_tracker:
  on_ble_advertise:
    #Govee 5074 -- humidor
    - mac_address: E3:60:58:E1:C0:34
      then:
        - lambda: |-
            for (auto data : x.get_manufacturer_datas()) {
                if(data.data.size()==7) {
                  uint16_t hum_lsb = data.data[3] + (data.data[4] << 8);
                  float humidity= float(hum_lsb)/100.0;
                  int16_t temp_lsb = data.data[1] + (data.data[2] << 8);
                  float temperature = float(temp_lsb)/100*9.0/5.0 + 32.0;
                  int16_t battery=data.data[5];
                  int16_t rssi=x.get_rssi();
                  id(humidor_humidity).publish_state(humidity);
                  id(humidor_temperature).publish_state(temperature);
                  id(humidor_rssi).publish_state(rssi);
                  id(humidor_battery).publish_state(battery);

                }
            }
    #Govee 5075 - office Screen
    - mac_address: A4:C1:38:37:6F:15
      then:
        - lambda: |-
            const int basenum = (int16_t(x[1]) << 16) + (int16_t(x[2]) << 8) + int16_t(x[3]);
            ESP_LOGD("ble_adv", "goveesensor basenum: (%d)", basenum);
            const float temperature = basenum / 10000.0f;
            const float humidity = (basenum % 1000) / 10.0f;
            const float battery_level = uint16_t(x[4]) / 1.0f;
            ESP_LOGD("ble_adv", "  Temperature: %.2f°C", temperature);
            ESP_LOGD("ble_adv", "  Humidity: %.2f", humidity);
            ESP_LOGD("ble_adv", "  Battery Level: %.0f percent", battery_level);
            id(office_temperature).publish_state(temperature);
            id(office_humidity).publish_state(humidity);
            id(office_battery).publish_state(battery_level);

The error i get is:

src/main.cpp:340:37: error: no match for 'operator[]' (operand types are 'const esphome::esp32_ble_tracker::ESPBTDevice' and 'int')
       const int basenum = (int16_t(x[1]) << 16) + (int16_t(x[2]) << 8) + int16_t(x[3]);
                                     ^
src/main.cpp:340:61: error: no match for 'operator[]' (operand types are 'const esphome::esp32_ble_tracker::ESPBTDevice' and 'int')
       const int basenum = (int16_t(x[1]) << 16) + (int16_t(x[2]) << 8) + int16_t(x[3]);
                                                             ^
src/main.cpp:340:83: error: no match for 'operator[]' (operand types are 'const esphome::esp32_ble_tracker::ESPBTDevice' and 'int')
       const int basenum = (int16_t(x[1]) << 16) + (int16_t(x[2]) << 8) + int16_t(x[3]);
                                                                                   ^
src/main.cpp:344:45: error: no match for 'operator[]' (operand types are 'const esphome::esp32_ble_tracker::ESPBTDevice' and 'int')
       const float battery_level = uint16_t(x[4]) / 1.0f;
                                             ^
*** [.pioenvs/govee/src/main.cpp.o] Error 1

Appreciate any help.

How do you know how to get this info?

OK – this gets me a little closer: I was looking at the python code for the govee advertisments on the original HACS plugin. It looks like 5072 and 5075 have the same config. So i used this config.> it added entities but seemed to go into a loop where the upload never finished.

esp32_ble_tracker:
  on_ble_advertise:
    #Govee 5074 -- humidor
    - mac_address: E3:60:58:E1:C0:34
      then:
        - lambda: |-
            for (auto data : x.get_manufacturer_datas()) {
                if(data.data.size()==7) {
                  uint16_t hum_lsb = data.data[3] + (data.data[4] << 8);
                  float humidity= float(hum_lsb)/100.0;
                  int16_t temp_lsb = data.data[1] + (data.data[2] << 8);
                  float temperature = float(temp_lsb)/100*9.0/5.0 + 32.0;
                  int16_t battery=data.data[5];
                  int16_t rssi=x.get_rssi();
                  id(humidor_humidity).publish_state(humidity);
                  id(humidor_temperature).publish_state(temperature);
                  id(humidor_rssi).publish_state(rssi);
                  id(humidor_battery).publish_state(battery);

                }
            }
    #Govee 5075 - office Screen
    - mac_address: A4:C1:38:37:6F:15
      then:
        - lambda: |-
            for (auto data : x.get_manufacturer_datas()) {
              if(data.data.size()==6) {
                const int basenum = (int16_t(data.data[1]) << 16) + (int16_t(data.data[2]) << 8) + int16_t(data.data[3]);
                const float temperature = (basenum / 10000.0f)*9.0/5.0 + 32.0;
                const float humidity = (basenum % 1000) / 10.0f;
                const float battery_level = uint16_t(data.data[4]) / 1.0f;
                int16_t rssi=x.get_rssi();
                id(office_humidity).publish_state(humidity);
                id(office_temperature).publish_state(temperature);
                id(office_rssi).publish_state(rssi);
                id(office_battery).publish_state(battery_level);
              } 
            }
1 Like

OK-- maybe esp32_ble_tracker constantly loops… because when i terminate the compile after it finds the sensor it seems to update. Maybe working as designed?