Reverse Engineered the Hatch Baby Grow BLE device

Hello! First time posting, but, since I spent a great deal of time trying to find a solution myself, I figured I’d make a post here for anyone else.

The Problem

I got a Hatch Baby Grow baby changing scale. This scale uses Bluetooth Low Energy (BLE) and connects to the Hatch Baby app on your phone.

Personally, I hated the app. Its clunky, didn’t integrate with my baby tracking system, and the UI to just use the scale is somewhat buried. And worst yet, it natively does not work with Home Assistant.

The Solution

After lots of trial and error, the simplest solution to get the Hatch Baby Grow scale to work with Home Assistant was to use an ESP32 BLE client, and parse the raw BLE service characteristics. Some trial and error was required to discover which service ID and characteristic was needed to connect to, and how to parse the raw data. (I highly recommend using the nRF Connect app when doing this investigation yourselves!). For anyone curious, the weight is not in the BLE advertisements, but instead in a service that requires a connection for. It displays the weight in 100ths of a gram, so, it is fairly jittery. There also seems to be a semi-linear error to the value that required correcting.

I also discovered my current solution would drain the battery since the current SDK has the manual connect and disconnect commands broken (see github issue here). So, I lazily opted to use a Battery Eliminator kit for the Hatch baby Grow so I wouldn’t need to worry about battery life, and kept the auto connect logic on. This means the scale is always reporting the weight measurements, which does impact the battery life a lot. I hope to fix this once that issue gets resolved, so you can simply connect to the device, read the weight, then disconnect to save on battery,

And then lastly I added several helper template sensors to calibrate the raw weight values, as the raw values are both not zeroed, but also have a semi-linear error. (More trial and error was also needed to find the correct error correction equation. I assume the original Hatch Baby app has their own way to correct this, so, I had to make a best guess suing my own measurements. Based on the values I recorded, a power regression seemed to be the best fit fix.)

Parts list

  • Any ESP32 with a BLE adapter will work. I picked an ESP32-S3 up off of amazon for ~$15, but I’m sure there are better pricing out there elsewhere.
  • 3 AA battery eliminator kit (4.5V). I also picked one up off of Amazon for ~$15. I’m also sure there’s better pricing elsewhere too.

ESP32 config

This is the ESPHome config required. Make sure when setting up your project you pick the correct board for whatever device you ended up getting as well.

I also left some commented code in there for some battery optimizations I wanted to have, but because of a current bug, they won’t work, hence the need for the Battery Eliminator adapter.

substitutions:
  name: hatch-grow-connector
  friendly_name: Hatch Grow Connector

esp32:
  variant: ESP32S3

ota:
  - platform: esphome
    encryption:

esphome:
  name: ${name}
  name_add_mac_suffix: false
  friendly_name: ${friendly_name}

api:
  encryption:
    key: <your key>

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

# uncomment after https://github.com/esphome/issues/issues/6443 is resolved
# esp32_ble_tracker:

ble_client:
  - name: Hatch Baby Grow
    id: ble_client_hatch_baby_grow
    mac_address: <find MAC from nRF Connect App>
    # uncomment after https://github.com/esphome/issues/issues/6443 is resolved
    # auto_connect: false
    

sensor:
  - platform: ble_client
    name: Raw Weight
    id: sensor_ble_client_raw_weight
    ble_client_id: ble_client_hatch_baby_grow
    characteristic_uuid: 02230001-5efd-47eb-9c1a-de53f7a2b232 # discovered using nRF connect App
    type: characteristic
    service_uuid: 02230000-5efd-47eb-9c1a-de53f7a2b232 # discovered using nRF connect App
    icon: "mdi:scale"
    device_class: weight
    notify: true
    state_class: measurement
    unit_of_measurement: "g"
    lambda: |-
      // 1. Safety check: Ensure we received at least 4 bytes to read the first 3 safely
      if (x.size() < 4) {
        ESP_LOGW("ble_lambda", "Received incomplete data payload");
        return {}; 
      }

      // 2. Map the 3 data bytes according to little-endian order
      uint32_t byte_1 = x[0]; // Least Significant Byte (LSB)
      uint32_t byte_2 = x[1]; // Middle Byte
      uint32_t byte_3 = x[2]; // Most Significant Byte (MSB)
      // byte 4 seems to be a counter, counting up with each data read and restarting. So I ignored it. 

      // 3. Combine the 3 bytes into a single 32-bit unsigned integer using bitwise shifts
      uint32_t raw_weight = (byte_3 << 16) | (byte_2 << 8) | byte_1;

      // 4. Log the result to the ESPHome terminal for easy troubleshooting
      // ESP_LOGD("ble_lambda", "Parsed 24-bit Raw Weight: %u", raw_weight);

      // 5. Send the final value to Home Assistant, divided by 100 to convert from hundredths of a gram to grams
      return (float)raw_weight/100.0;
    accuracy_decimals: 2
  - platform: ble_client
    type: characteristic
    id: sensor_ble_client_battery
    ble_client_id: ble_client_hatch_baby_grow
    name: "Battery Level"
    service_uuid: '180f'
    characteristic_uuid: '2a19'
    icon: 'mdi:battery'
    unit_of_measurement: '%'
    accuracy_decimals: 0
    notify: true
    entity_category: diagnostic
    state_class: measurement
    device_class: "battery"

# uncomment after https://github.com/esphome/issues/issues/6443 is resolved
# button:
#   - platform: template
#     name: "Connect to Hatch Baby Grow"
#     on_press:
#       then:
#         - esp32_ble_tracker.stop_scan:
#         - delay:
#             seconds: '1'
#         - ble_client.connect: ble_client_hatch_baby_grow

#     icon: "mdi:bluetooth-audio"
#   - platform: template
#     name: "Disconnect from Hatch Baby Grow"
#     on_press:
#       - ble_client.disconnect: ble_client_hatch_baby_grow
#     icon: "mdi:bluetooth-off"

Template Sensors

I created these sensors using the GUI (so no YAML, sorry), and they help with both displaying the weight, calibrating the weight, and zeroing the scale.

  • A de-bounced raw weight, using a statistics helper using mean, and a max age of 3 seconds. This helps filter noise from the jittery sensor.
  • An input number that will store the zeroed value (essentially, the raw weight when nothing is on the scale).
    • min of 0 and max of 1000 seems to work as the raw value hovers around ~550.
  • A template sensor that shows the calibrated weight (raw weight, minus the zeroed weight, and then fed through a power regression to error correct)
    • These values were determined using calibrated weighs using the original app, and then the raw reads from from the esp32. I found a power regression fit better than linear regression.
      {% set zeroed_val = ((states('sensor.family_room_hatch_grow_connector_raw_weight_debounced') | float(default=0.0)) - (states('input_number.hatch_baby_grow_zeroed_value') | float(default=0.0))) %}
      {{ 0.840326 * (zeroed_val ** 1.00995) }}
      
  • A template sensor that converts the calibrated weight into pounds.
  • A template sensor that displayed the weight in pounds as a readable pounds and ounces.
  • A template button that on press records the de-bounced weight to the input number helper above.

What this has allowed me to do

Once I got all the above created, I was able to create some great automations with the scale! For example:

  • We have a lamp nearby that will now auto turn on and off whenever the scale is in use.
  • I can use Assist to record the weight to our baby tracking app hands free.
  • I created a dashboard that replaces the Hatch Baby app, making manual use of the scale much simpler and faster.

edit: fixed ESP config to remove unused bluetooth proxy package in favor of leaner ota/platform config

Hi,
I like to learn from real world examples, so thanks for sharing!
I see that the bluetooth-proxy was included, so was curious why (not needed correct)?

I was originally trying to make it as a bluetooth proxy, so you’re right! it is old!

However, it does include some built in config I was using, like the default esp32 platform configs, and the OTA configs. So, you could remove that and replace it with:

esp32:
  variant: ESP32S3

ota:
  - platform: esphome
    encryption:

I’ll update the config above since it is cleaner and a slightly smaller bundle. Good catch!

edit:

Also, if you want logging, you’ll also need the

logger:
  level: DEBUG

configuration too.