Soehnle Scale Integration in Home Assistant via Bluetooth proxy – A reverse engineering approach by using logging

Hello community,

inspired by the reddit article I had decided to try to integrate my Soehnle Shape Sense Connect 200 scale as well to have the data in my dashboard. Searching for an existing integration was not successful and therefore I had decided to do a reverse engineering as well. But my approach to get it done was a little bit different then ForsakenSyllabub8193 and therefore I want to describe the procedure and result here.

Prerequisites

Development has been done on HA 2026.7 with ESPHome Device Builder 2026.7 as well. Furthermore you need the Bluetooth integration in HA. I’m using a housed ESP32 which is named at Amazon " Waveshare ESP32-GEEK Development Board" and this is directly plugged into an old smartphone charger and this is plugged in my sleeping room were the scale is located.

Some coding parts below are the result of prompting with MS CoPilot.

Preparation:

As the Soehnle MyScale app communicates via Bluetooth with the scale, the way to go is via an ESP32 as Bluetooth proxy. How to make the ESP32 a Bluetooth proxy is described here. Add the proxy then to the Bluetooth integration by clicking just on the “Add entry” button. Then open the ESPHome Device Builder app and click the “Create device” button. That creates a new YAML file in the directory root/homeassistant/esphome. Then click on the “Edit” button of your new created device and then on “Install”. This will the run the compilation of a lot of C++ source files and finally create from them the firmware of the Bluetooth proxy. The firmware will be flashed to the ESP32 and after flashing ESPHome Builder starts logging.

The work:

To instantiate a new BLE client as base for HA sensors the MAC address of the Bluetooth device is necessary. The MAC address can be fetched from the Bluetooth integration in HA by just clicking on the three dots of one of the Bluetooth proxies and then selecting “Download diagnostics”. Then searching for keywords like scale, Soehnle or Shape will show the required data in the downloaded JSON file. The line with address is the MAC address. In this case also the service UUIDs are provided

          "name": "Shape200",
          "address": "XX:XX:XX:XX:XX:XX",
          "rssi": -80,
          "manufacturer_data": {},
          "service_data": {
            "0000181d-0000-1000-8000-00805f9b34fb": {
              "__type": "<class 'bytes'>",
              "repr": "b''"
            }
          },
          "service_uuids": [
            "0000181d-0000-1000-8000-00805f9b34fb",
            "0000181c-0000-1000-8000-00805f9b34fb",
            "0000181b-0000-1000-8000-00805f9b34fb"
          ],

If the service UUID(s) are not provided it is possible to determine them by using the ESP32_BLE_TRACKER component in ESPHome: Just add the following coding to your existing YAML (enter your MAC address for scale_mac):

substitutions:
  scale_mac: "XX:XX:XX:XX:XX:XX"

packages:
  esphome.bluetooth-proxy: github://esphome/bluetooth-proxies/esp32-generic/esp32-generic-s3.yaml@main

esp32:
  variant: ESP32S3

logger:
  level: DEBUG

esp32_ble:
  max_connections: 4

ble_client:
  - mac_address: ${scale_mac}
    id: soehnle_scale
    on_connect:
        then:
          - lambda: |-
              ESP_LOGD("Scale", "Connected.");
    on_disconnect:
        then:
          - lambda: |-
              ESP_LOGD("Scale", "Connection closed.");

# BLE Tracker Configuration
esp32_ble_tracker:
  scan_parameters:
     interval: 2000ms
     window: 2000ms
     active: true
  on_ble_advertise:
    - mac_address: ${scale_mac}
      then:
        - lambda: |-
            ESP_LOGD("Scale", "  name: %s", x.get_name().c_str());
            ESP_LOGD("Scale", "  Advertised service UUIDs:");
            char buf[esphome::esp32_ble::UUID_STR_LEN];
            for (auto uuid : x.get_service_uuids()) {
              ESP_LOGD("Scale", "    - %s", uuid.to_str(buf));
            }
            ESP_LOGD("Scale", "  Advertised service data:");
            for (auto service_data : x.get_service_datas()) {
              ESP_LOGD("Scale", "  Service UUID - %s: (length %i)", service_data.uuid.to_str(buf), service_data.data.size());
            }

Save the YAML and click on the Install button. After flashing the firmware to the ESP and while logging runs do a body composition measurement with your scale and then the result in the log will look like (and which confirms the UUIDs from the JSON file):

[20:52:27.770][D][Scale:053]: name: Shape200
[20:52:27.771][D][Scale:054]: Advertised service UUIDs:
[20:52:27.771][D][Scale:057]: - 0x181D
[20:52:27.771][D][Scale:057]: - 0x181C
[20:52:27.771][D][Scale:057]: - 0x181B
[20:52:27.771][D][Scale:059]: Advertised service data:
[20:52:27.771][D][Scale:061]: Service UUID - 0x181D: (length 0)

Now for the service UUIDs the characteristic UUIDs are required. In case of standardized service UUIDs you may ask an AI like MS CoPilot or Anthropics Claude for these UUIDs. If they have no information a further tool like nRF Connect must be used on your mobile device on which also the scale app is running. With the characteristics UUIDs log sensors can be created in the YAML. For this replace parts of the YAML coding by

ble_client:
  - mac_address: ${scale_mac}
    id: soehnle_scale

sensor:
  - platform: ble_client
    type: characteristic
    ble_client_id: soehnle_scale
    id: weight_measurement
    name: "Weight Measurement"
    service_uuid: "0000181d-0000-1000-8000-00805f9b34fb"
    characteristic_uuid: "00002a9d-0000-1000-8000-00805f9b34fb"
    notify: true
    update_interval: never
    lambda: |-
      // read raw data
       auto weight_data = x;
       ESP_LOGD("Scale", "Body Composition Raw (%d bytes):", weight_data.size());
       for (int i = 0; i < weight_data.size(); i++) {
         ESP_LOGD("Scale", "%02X ", weight_data[i]);
       }
       uint16_t return_something = weight_data[1];
       // Return for ESPHome-Sensor
       return return_something;

  - platform: ble_client
    type: characteristic
    ble_client_id: soehnle_scale
    id: body_composition_measurement
    name: "Body Composition Measurement"
    service_uuid: "0000181b-0000-1000-8000-00805f9b34fb"
    characteristic_uuid: "00002a9c-0000-1000-8000-00805f9b34fb"
    notify: true
    update_interval: never
    lambda: |-
      // get raw data
      auto comp_data = x;
      ESP_LOGD("Scale", "Body Composition Raw (%d bytes):", comp_data.size());
      for (int i = 0; i < comp_data.size(); i++) {
        ESP_LOGD("Scale", "%02X ", comp_data[i]);
      }
       uint16_t return_something2 = comp_data[1];
       // Return for ESPHome-Sensor
       return return_something2;

In the coding only 2 sensors are used. In general that needs to be done for each valid combination of service and characteristic UUID. But for the Soehnle scale this is sufficient.

Save the YAML file, flash the changes to the ESP. While the logger runs do a scale measurement and the log will look like:

[20:52:40.602][D][ble_sensor:109]: [Weight Measurement] ESP_GATTC_NOTIFY_EVT: handle=0x32, value=0x1e
[20:52:40.606][D][ble_sensor:109]: [Body Composition Measurement] ESP_GATTC_NOTIFY_EVT: handle=0x32, value=0x1e
[20:52:40.606][D][Scale:097]: Body Composition Raw (20 bytes):
[20:52:40.611][D][Scale:099]: 1E 
[20:52:40.611][D][Scale:099]: 05 
[20:52:40.611][D][Scale:099]: F7 
[20:52:40.611][D][Scale:099]: 00 
[20:52:40.615][D][Scale:099]: EA 
[20:52:40.616][D][Scale:099]: 07 
[20:52:40.619][D][Scale:099]: 07 
[20:52:40.619][D][Scale:099]: 15 
[20:52:40.673][D][Scale:099]: 12 
[20:52:40.673][D][Scale:099]: 18 
[20:52:40.673][D][Scale:099]: 1B 
[20:52:40.673][D][Scale:099]: 01 
[20:52:40.673][D][Scale:099]: AA 
[20:52:40.673][D][Scale:099]: 0D 
[20:52:40.673][D][Scale:099]: 89 
[20:52:40.673][D][Scale:099]: 01 
[20:52:40.673][D][Scale:099]: 2C 
[20:52:40.673][D][Scale:099]: 02 
[20:52:40.674][D][Scale:099]: EB 
[20:52:40.674][D][Scale:099]: 03

As can be seen, the first sensor does not return data but the second one. While logging is running perform a scale measurement for one or two different person with different composition, note down the scale data and for each case the byte stream from above. Convert the byte sequence in a byte string like 1E:05:F7:00:EA:07:07:15:12:18:1B:01:AA:0D:89:01:2C:02:EB:03
and prompt an AI with your composition data and the corresponding byte string for an analysis of the string and ask which byte composition will return the weight and the muscle, water and fat portion. Sure, it is possible to create the byte string output also by the machine but that requires an additional C++ function which has to be provided in a separate .h file in the ESPHome directory and including the file in the YAML. For those who would like to know that as well, see at the end “Convenience stuff: byte string”.

The result
For the Soehnle Shape Sense Connect 200 scale the sensor part from the YAML coding above turns into (CoPilot has not only identified the data composition but I also asked for the small piece of C++ coding in the lambda)

sensor:
# Measurement sensor
  - platform: ble_client
    type: characteristic
    ble_client_id: soehnle_scale
    id: body_composition_measurement
    name: "Body Composition Measurement"
    service_uuid: "0000181b-0000-1000-8000-00805f9b34fb"
    characteristic_uuid: "00002a9c-0000-1000-8000-00805f9b34fb"
    notify: true
    update_interval: never
    lambda: |-
      // get raw data
      auto comp_data = x;
      // calculate data
      uint16_t weight_raw = (comp_data[19] << 8) | comp_data[18];
      float weight = weight_raw * 0.1f; 
      id(scale_weight).publish_state(weight);
      uint16_t fat_raw = (comp_data[3] << 8) | comp_data[2];
      float fat = fat_raw * 0.1f; 
      id(scale_fat).publish_state(fat);
      uint16_t muscle_raw = (comp_data[15] << 8) | comp_data[14];
      float muscle = muscle_raw * 0.1f; 
      id(scale_muscle).publish_state(muscle);
      uint16_t water_raw = (comp_data[17] << 8) | comp_data[16];
      float water = water_raw * 0.1f; 
      id(scale_water).publish_state(water);
      return weight;

  # Sensors to be used in HA
  - platform: template
    name: "Scale Weight"
    id: scale_weight
    unit_of_measurement: "kg"
    accuracy_decimals: 2
  - platform: template
    name: "Scale Fat Portion"
    id: scale_fat
    unit_of_measurement: "%"
    accuracy_decimals: 1
  - platform: template
    name: "Scale Muscle Portion"
    id: scale_muscle
    unit_of_measurement: "%"
    accuracy_decimals: 1
  - platform: template
    name: "Scale Water Portion"
    id: scale_water
    unit_of_measurement: "%"
    accuracy_decimals: 1

The Soehnle scale transmits the data in little endian format and therefore the bit shift operation (<<) of the high byte and the logical or (|) with the low byte creates a word type which is then converted to a float type by multiplication with 0.1 (HA needs float for the sensors and the scale transmits the data without decimal places). The calculated data are then pushed by the measurement sensor to the HA sensors by the function id(<HA_sensor>).publish_state(<<Calc_part>);
The HA sensor data are written into InfluxDB and I use Grafana to visualize them in my main dashboard. For sure the data can be used for automation like “IF scale_weight > 100 lock fridge” :smiley:

Convenience stuff: byte string
Use the File Editor app in HA to create a file scale_utils.h in the directory root/homeassistant/esphome. Copy the following coding in this file and save:

#pragma once
#include <string>
#include <vector>

inline std::string to_hex_colon(const std::vector<uint8_t>& data) {
    std::string out;
    out.reserve(data.size() * 3);
    const char* lut = "0123456789ABCDEF";
    for (size_t i = 0; i < data.size(); i++) {
        uint8_t b = data[i];
        out.push_back(lut[b >> 4]);
        out.push_back(lut[b & 0x0F]);
        if (i + 1 < data.size()) {
            out.push_back(':');
        }
    }
    return out;
}

In the esphome section in your YAML file add the lines

  includes:
    - scale_utils.h

This includes the file during compilation and hence also scale_utils.h gets compiled.
Finally replace the YAML coding lines

       for (int i = 0; i < weight_data.size(); i++) {
         ESP_LOGD("Scale", "%02X ", weight_data[i]);
       }

by

       auto weight_hex = to_hex_colon(weight_data);
       ESP_LOGD("Scale", "Body Composition Raw: %s", weight_hex.c_str());

respectively

      for (int i = 0; i < comp_data.size(); i++) {
        ESP_LOGD("Scale", "%02X ", comp_data[i]);
      }

by

       auto comp_hex = to_hex_colon(comp_data);
       ESP_LOGD("Scale", "Body Composition Raw: %s", comp_hex.c_str());

The log returns then

[17:18:20.324][D][Scale:073]: Body Composition Raw (20 bytes):
[17:18:20.328][D][Scale:075]: Body Composition Raw: 1E:05:FA:00:EA:07:07:17:0E:32:01:01:7E:0D:88:01:22:02:DE:03

You may wonder why the byte string looks different then above? Not really, because measured on different days and time :smiley:

Bravo!

Just some ideas:
The Android app Openscale supports your scale and has been around for a while - I used it for many years. You may not have stumbled across it - your AI and LLM co-authors may benefit from being made aware of it for your updated version coding sessions and releases.

You may wish to cross-check the code on GitHub to see if you could benefit from their reverse engineering algorithm endeavours. In contrast to your ESPHome approach, it has MQTT capabilities that can inject the readings into HomeAssistant and supports a very wide, and ever increasing range of devices, most user submitted using their detailed guidelines.

Have a look at the mathematical formulas they use to derive related information such as body fat, water, and BMI that you might wish to add functionality to your app as derived sensors, or pre-calculate them on the ESPHome device where you already have the raw data - taking careful note of floats, double integers and the suchlike variable types. Be aware most of the authors of these formulas are Asian and their traditionally smaller body size can skew the figures, and may need adjustment for European and other nationalities to reflect reality. Read the medical background papers and understand how they arrived at the algorithm.

You may be able to broaden your app to support other scales too, based on the UUID listings, check-summing and data unpacking formats, as well as investigate other existing HomeAssistant HACS apps that also use BLE to read and decode data from other scales.

Look at functionality that offers tare/differential readings useful for weighing babies and pets, where the combined weight has the individual weight subtracted to arrive at the final result - a two step process, as well as supports different individuals, recognising them by their existing recorded weight. Best prompt if two are similar and let the human choose.

Be aware that a lot of BLE devices only (by design) support ONE session at a time. This may cause unexplained disconnect and error messages if you are not aware of this, and attempt to use your phone app and ESP32 simultaneously. Ask me how I know… :wink:

Stand on the shoulders of giants.

Hope I have given you some ideas to work with. Looking forward to your expanded app…