ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
xiaomi_body_scale.cpp
Go to the documentation of this file.
1#include "xiaomi_body_scale.h"
4#include "esphome/core/log.h"
5
6#include <cstring>
7
9
10ESPHOME_LOG_TAG(TAG, "xiaomi_body_scale");
11
12// Encrypted MiBeacon frame without a MAC: frame control, device id, frame count, cipher, counter, tag
13static constexpr size_t DEVICE_ID_POS = 2;
14static constexpr size_t FRAME_COUNT_POS = 4;
15static constexpr size_t CIPHER_POS = 5;
16static constexpr size_t CIPHER_SIZE = 12;
17static constexpr size_t COUNTER_POS = CIPHER_POS + CIPHER_SIZE;
18static constexpr size_t TAG_POS = COUNTER_POS + 3;
19static constexpr size_t TAG_SIZE = 4;
20static constexpr size_t FRAME_SIZE = TAG_POS + TAG_SIZE;
21// Frame control: has data (0x40) and encrypted (0x08)
22static constexpr uint8_t FRAME_ENCRYPTED_DATA = 0x48;
23static constexpr uint8_t OBJECT_SIZE = 9;
24static constexpr uint16_t OBJECT_S200_MEASUREMENT = 0x4E16;
25static constexpr uint16_t OBJECT_S400_MEASUREMENT = 0x6E16;
26static constexpr uint32_t STABILIZED_RESET_ID = 0;
27static constexpr uint32_t STABILIZED_RESET_MS = 1000;
28
29XiaomiBodyScale::XiaomiBodyScale(uint64_t address, const char *bindkey) : address_(address) {
30 parse_hex(bindkey, this->bindkey_, sizeof(this->bindkey_));
31}
32
34 uint8_t mac[MAC_ADDRESS_SIZE];
36 char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
37 ESP_LOGCONFIG(TAG,
38 "Xiaomi Body Scale\n"
39 " MAC Address: %s",
40 format_mac_addr_upper(mac, mac_buf));
41 LOG_SENSOR(" ", "Weight", this->weight_);
42 LOG_SENSOR(" ", "Impedance Low (50 kHz)", this->impedance_low_);
43 LOG_SENSOR(" ", "Impedance High (250 kHz)", this->impedance_high_);
44 LOG_SENSOR(" ", "Heart Rate", this->heart_rate_);
45 LOG_SENSOR(" ", "Profile ID", this->profile_id_);
46 LOG_BINARY_SENSOR(" ", "Stabilized", this->stabilized_);
47}
48
49bool XiaomiBodyScale::decrypt_(const uint8_t *frame, uint8_t *plaintext) const {
50 uint8_t nonce[MAC_ADDRESS_SIZE + 6];
51 for (size_t i = 0; i < MAC_ADDRESS_SIZE; i++)
52 nonce[i] = static_cast<uint8_t>(this->address_ >> (i * 8)); // MAC, reversed
53 memcpy(nonce + MAC_ADDRESS_SIZE, frame + DEVICE_ID_POS, 3); // device id + frame count
54 memcpy(nonce + MAC_ADDRESS_SIZE + 3, frame + COUNTER_POS, 3);
55 static constexpr uint8_t AUTH_DATA[1] = {0x11};
56 return ble_device_base::aes_ccm_auth_decrypt(this->bindkey_, nonce, sizeof(nonce), AUTH_DATA, sizeof(AUTH_DATA),
57 frame + CIPHER_POS, CIPHER_SIZE, plaintext, frame + TAG_POS, TAG_SIZE);
58}
59
61 if (this->stabilized_ == nullptr)
62 return;
63 this->stabilized_->publish_state(stabilized);
64 // Clear it again so the next measurement is seen as a new one
65 if (stabilized)
66 this->set_timeout(STABILIZED_RESET_ID, STABILIZED_RESET_MS, [this]() { this->stabilized_->publish_state(false); });
67}
68
70 // Weight x10 (11 bits), heart rate - 50 (7 bits), impedance x10 (14 bits). A measurement sends weight, heart
71 // rate and the 50 kHz impedance (the larger value), then a packet with only the 250 kHz impedance.
72 const uint16_t weight = packed & 0x7FF;
73 const uint8_t heart_rate = (packed >> 11) & 0x7F;
74 const uint16_t impedance = packed >> 18;
75 ESP_LOGD(TAG, "weight=%u heart_rate=%u impedance=%u", weight, heart_rate, impedance);
76
77 if (weight != 0 && this->weight_ != nullptr)
78 this->weight_->publish_state(weight / 10.0f);
79 if (heart_rate > 0 && heart_rate < 127 && this->heart_rate_ != nullptr)
80 this->heart_rate_->publish_state(heart_rate + 50.0f);
81
82 if (weight == 0 && heart_rate == 0) {
83 // Impedance only: the final 250 kHz packet (bare feet); all zero: stepped off
84 if (impedance != 0 && this->impedance_high_ != nullptr)
85 this->impedance_high_->publish_state(impedance / 10.0f);
86 this->publish_stabilized_(impedance != 0);
87 } else if (impedance != 0) {
88 if (this->impedance_low_ != nullptr)
89 this->impedance_low_->publish_state(impedance / 10.0f);
90 this->publish_stabilized_(false);
91 } else {
92 // Weight without impedance: measurement complete (with socks)
93 this->publish_stabilized_(true);
94 }
95}
96
98 if (device.address_uint64() != this->address_)
99 return false;
100
101 for (const auto &service_data : device.get_service_datas()) {
102 if (service_data.data.size() != FRAME_SIZE || !service_data.uuid.contains(0x95, 0xFE))
103 continue;
104 const uint8_t *frame = service_data.data.data();
105 // The bindkey is required, so plaintext frames are never trusted
106 if ((frame[0] & FRAME_ENCRYPTED_DATA) != FRAME_ENCRYPTED_DATA)
107 continue;
108
109 if (frame[FRAME_COUNT_POS] == this->last_frame_count_)
110 continue;
111 uint8_t object[CIPHER_SIZE];
112 if (!this->decrypt_(frame, object)) {
113 ESP_LOGW(TAG, "Decryption failed, wrong bindkey?");
114 continue;
115 }
116 // Only an authenticated frame may advance the duplicate filter
117 this->last_frame_count_ = frame[FRAME_COUNT_POS];
118
119 // Both objects hold the profile ID, a packed uint32 (LE) and a timestamp (not published)
120 const uint16_t value_type = encode_uint16(object[1], object[0]);
121 const uint8_t *data = object + 3;
122 const uint32_t packed = encode_uint32(data[4], data[3], data[2], data[1]);
123 const bool sized = object[2] == OBJECT_SIZE;
124 if (sized && value_type == OBJECT_S400_MEASUREMENT) {
125 this->publish_s400_(packed);
126 } else if (sized && value_type == OBJECT_S200_MEASUREMENT) {
127 // Weight x100 only
128 if (packed != 0 && this->weight_ != nullptr)
129 this->weight_->publish_state(packed / 100.0f);
130 } else {
131 ESP_LOGVV(TAG, "Unknown object 0x%04X, length %u", value_type, object[2]);
132 continue;
133 }
134 if (this->profile_id_ != nullptr)
135 this->profile_id_->publish_state(data[0]);
136 return true;
137 }
138 return false;
139}
140
141} // namespace esphome::xiaomi_body_scale
uint8_t address
Definition bl0906.h:4
void set_timeout(const char *name, uint32_t timeout, std::function< void()> &&f)
Set a timeout function with a const char* name.
Definition component.cpp:96
void publish_state(bool new_state)
Publish a new state to the front-end.
uint64_t address_uint64() const
Return MAC as packed uint64 (byte 0 in LSB — matches esp32's address_uint64).
const std::vector< ServiceData > & get_service_datas() const
Definition ble_device.h:225
void publish_state(float state)
Publish a new state to the front-end.
Definition sensor.cpp:65
XiaomiBodyScale(uint64_t address, const char *bindkey)
binary_sensor::BinarySensor * stabilized_
bool parse_device(const ble_device_base::ESPBTDevice &device) override
bool decrypt_(const uint8_t *frame, uint8_t *plaintext) const
void uint64_to_mac_msb_first(uint64_t address, uint8_t out[6])
Unpack a uint64 BLE address into printable (MSB-first) byte order — the order bd_addr_t / esp_bd_addr...
Definition ble_device.h:169
bool aes_ccm_auth_decrypt(const uint8_t key[16], const uint8_t *nonce, size_t nonce_len, const uint8_t *aad, size_t aad_len, const uint8_t *ciphertext, size_t ct_len, uint8_t *plaintext, const uint8_t *tag, size_t tag_len)
ESPHOME_LOG_TAG(TAG, "xiaomi_body_scale")
size_t parse_hex(const char *str, size_t length, uint8_t *data, size_t count)
Parse bytes from a hex-encoded string into a byte array.
Definition helpers.cpp:370
constexpr uint32_t encode_uint32(uint8_t byte1, uint8_t byte2, uint8_t byte3, uint8_t byte4)
Encode a 32-bit value given four bytes in most to least significant byte order.
Definition helpers.h:988
constexpr uint16_t encode_uint16(uint8_t msb, uint8_t lsb)
Encode a 16-bit value given the most and least significant byte.
Definition helpers.h:980
char * format_mac_addr_upper(const uint8_t *mac, char *output)
Format MAC address as XX:XX:XX:XX:XX:XX (uppercase, colon separators)
Definition helpers.h:1600
static void uint32_t