ESPHome 2026.3.0-dev
Loading...
Searching...
No Matches
bthome_ble.cpp
Go to the documentation of this file.
1#include "bthome_ble.h"
2
4#include "esphome/core/log.h"
5
6#include <algorithm>
7#include <array>
8#include <cstring>
9#include <span>
10
11#ifdef USE_ESP32
12
13#include "mbedtls/ccm.h"
14
15namespace esphome {
16namespace bthome_mithermometer {
17
18static const char *const TAG = "bthome_mithermometer";
19static constexpr size_t BTHOME_BINDKEY_SIZE = 16;
20static constexpr size_t BTHOME_NONCE_SIZE = 13;
21static constexpr size_t BTHOME_MIC_SIZE = 4;
22static constexpr size_t BTHOME_COUNTER_SIZE = 4;
23
24static const char *format_mac_address(std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buffer, uint64_t address) {
25 std::array<uint8_t, MAC_ADDRESS_SIZE> mac{};
26 for (size_t i = 0; i < MAC_ADDRESS_SIZE; i++) {
27 mac[i] = (address >> ((MAC_ADDRESS_SIZE - 1 - i) * 8)) & 0xFF;
28 }
29
30 format_mac_addr_upper(mac.data(), buffer.data());
31 return buffer.data();
32}
33
34static bool get_bthome_value_length(uint8_t obj_type, size_t &value_length) {
35 switch (obj_type) {
36 case 0x00: // packet id
37 case 0x01: // battery
38 case 0x09: // count (uint8)
39 case 0x0F: // generic boolean
40 case 0x10: // power (bool)
41 case 0x11: // opening
42 case 0x15: // battery low
43 case 0x16: // battery charging
44 case 0x17: // carbon monoxide
45 case 0x18: // cold
46 case 0x19: // connectivity
47 case 0x1A: // door
48 case 0x1B: // garage door
49 case 0x1C: // gas
50 case 0x1D: // heat
51 case 0x1E: // light
52 case 0x1F: // lock
53 case 0x20: // moisture
54 case 0x21: // motion
55 case 0x22: // moving
56 case 0x23: // occupancy
57 case 0x24: // plug
58 case 0x25: // presence
59 case 0x26: // problem
60 case 0x27: // running
61 case 0x28: // safety
62 case 0x29: // smoke
63 case 0x2A: // sound
64 case 0x2B: // tamper
65 case 0x2C: // vibration
66 case 0x2D: // water leak
67 case 0x2E: // humidity (uint8)
68 case 0x2F: // moisture (uint8)
69 case 0x46: // UV index
70 case 0x57: // temperature (sint8)
71 case 0x58: // temperature (0.35C step)
72 case 0x59: // count (sint8)
73 case 0x60: // channel
74 value_length = 1;
75 return true;
76 case 0x02: // temperature (0.01C)
77 case 0x03: // humidity
78 case 0x06: // mass (kg)
79 case 0x07: // mass (lb)
80 case 0x08: // dewpoint
81 case 0x0C: // voltage (mV)
82 case 0x0D: // pm2.5
83 case 0x0E: // pm10
84 case 0x12: // CO2
85 case 0x13: // TVOC
86 case 0x14: // moisture
87 case 0x3D: // count (uint16)
88 case 0x3F: // rotation
89 case 0x40: // distance (mm)
90 case 0x41: // distance (m)
91 case 0x43: // current (A)
92 case 0x44: // speed
93 case 0x45: // temperature (0.1C)
94 case 0x47: // volume (L)
95 case 0x48: // volume (mL)
96 case 0x49: // volume flow rate
97 case 0x4A: // voltage (0.1V)
98 case 0x51: // acceleration
99 case 0x52: // gyroscope
100 case 0x56: // conductivity
101 case 0x5A: // count (sint16)
102 case 0x5D: // current (sint16)
103 case 0x5E: // direction
104 case 0x5F: // precipitation
105 case 0x61: // rotational speed
106 case 0xF0: // button event
107 value_length = 2;
108 return true;
109 case 0x04: // pressure
110 case 0x05: // illuminance
111 case 0x0A: // energy
112 case 0x0B: // power
113 case 0x42: // duration
114 case 0x4B: // gas (uint24)
115 case 0xF2: // firmware version (uint24)
116 value_length = 3;
117 return true;
118 case 0x3E: // count (uint32)
119 case 0x4C: // gas (uint32)
120 case 0x4D: // energy (uint32)
121 case 0x4E: // volume (uint32)
122 case 0x4F: // water (uint32)
123 case 0x50: // timestamp
124 case 0x55: // volume storage
125 case 0x5B: // count (sint32)
126 case 0x5C: // power (sint32)
127 case 0x62: // speed (sint32)
128 case 0x63: // acceleration (sint32)
129 case 0xF1: // firmware version (uint32)
130 value_length = 4;
131 return true;
132 default:
133 return false;
134 }
135}
136
138 char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
139 ESP_LOGCONFIG(TAG, "BTHome MiThermometer");
140 ESP_LOGCONFIG(TAG, " MAC Address: %s", format_mac_address(addr_buf, this->address_));
141 if (this->has_bindkey_) {
142 char bindkey_hex[format_hex_pretty_size(BTHOME_BINDKEY_SIZE)];
143 ESP_LOGCONFIG(TAG, " Bindkey: %s", format_hex_pretty_to(bindkey_hex, this->bindkey_, BTHOME_BINDKEY_SIZE, '.'));
144 }
145 LOG_SENSOR(" ", "Temperature", this->temperature_);
146 LOG_SENSOR(" ", "Humidity", this->humidity_);
147 LOG_SENSOR(" ", "Battery Level", this->battery_level_);
148 LOG_SENSOR(" ", "Battery Voltage", this->battery_voltage_);
149 LOG_SENSOR(" ", "Signal Strength", this->signal_strength_);
150}
151
153 bool matched = false;
154 for (auto &service_data : device.get_service_datas()) {
155 if (this->handle_service_data_(service_data, device)) {
156 matched = true;
157 }
158 }
159 if (matched && this->signal_strength_ != nullptr) {
160 this->signal_strength_->publish_state(device.get_rssi());
161 }
162 return matched;
163}
164
165void BTHomeMiThermometer::set_bindkey(std::initializer_list<uint8_t> bindkey) {
166 if (bindkey.size() != sizeof(this->bindkey_)) {
167 ESP_LOGW(TAG, "BTHome bindkey size mismatch: %zu", bindkey.size());
168 return;
169 }
170 std::copy(bindkey.begin(), bindkey.end(), this->bindkey_);
171 this->has_bindkey_ = true;
172}
173
174bool BTHomeMiThermometer::decrypt_bthome_payload_(const std::vector<uint8_t> &data, uint64_t source_address,
175 std::vector<uint8_t> &payload) const {
176 if (data.size() <= 1 + BTHOME_COUNTER_SIZE + BTHOME_MIC_SIZE) {
177 ESP_LOGVV(TAG, "Encrypted BTHome payload too short: %zu", data.size());
178 return false;
179 }
180
181 const size_t ciphertext_size = data.size() - 1 - BTHOME_COUNTER_SIZE - BTHOME_MIC_SIZE;
182 payload.resize(ciphertext_size);
183
184 std::array<uint8_t, MAC_ADDRESS_SIZE> mac{};
185 for (size_t i = 0; i < MAC_ADDRESS_SIZE; i++) {
186 mac[i] = (source_address >> ((MAC_ADDRESS_SIZE - 1 - i) * 8)) & 0xFF;
187 }
188
189 std::array<uint8_t, BTHOME_NONCE_SIZE> nonce{};
190 memcpy(nonce.data(), mac.data(), mac.size());
191 nonce[6] = 0xD2;
192 nonce[7] = 0xFC;
193 nonce[8] = data[0];
194 memcpy(nonce.data() + 9, &data[data.size() - BTHOME_COUNTER_SIZE - BTHOME_MIC_SIZE], BTHOME_COUNTER_SIZE);
195
196 const uint8_t *ciphertext = data.data() + 1;
197 const uint8_t *mic = data.data() + data.size() - BTHOME_MIC_SIZE;
198
199 mbedtls_ccm_context ctx;
200 mbedtls_ccm_init(&ctx);
201
202 int ret = mbedtls_ccm_setkey(&ctx, MBEDTLS_CIPHER_ID_AES, this->bindkey_, BTHOME_BINDKEY_SIZE * 8);
203 if (ret) {
204 ESP_LOGVV(TAG, "mbedtls_ccm_setkey() failed.");
205 mbedtls_ccm_free(&ctx);
206 return false;
207 }
208
209 ret = mbedtls_ccm_auth_decrypt(&ctx, ciphertext_size, nonce.data(), nonce.size(), nullptr, 0, ciphertext,
210 payload.data(), mic, BTHOME_MIC_SIZE);
211 mbedtls_ccm_free(&ctx);
212 if (ret) {
213 ESP_LOGVV(TAG, "BTHome decryption failed (ret=%d).", ret);
214 return false;
215 }
216 return true;
217}
218
220 const esp32_ble_tracker::ESPBTDevice &device) {
221 if (!service_data.uuid.contains(0xD2, 0xFC)) {
222 return false;
223 }
224
225 const auto &data = service_data.data;
226 if (data.size() < 2) {
227 ESP_LOGVV(TAG, "BTHome data too short: %zu", data.size());
228 return false;
229 }
230
231 const uint8_t adv_info = data[0];
232 const bool is_encrypted = adv_info & 0x01;
233 const bool mac_included = adv_info & 0x02;
234 const bool is_trigger_based = adv_info & 0x04;
235 const uint8_t version = (adv_info >> 5) & 0x07;
236
237 if (version != 0x02) {
238 ESP_LOGVV(TAG, "Unsupported BTHome version %u", version);
239 return false;
240 }
241
242 uint64_t source_address = device.address_uint64();
243 bool address_matches = source_address == this->address_;
244 if (!is_encrypted && mac_included && data.size() >= 7) {
245 uint64_t advertised_address = 0;
246 for (int i = 5; i >= 0; i--) {
247 advertised_address = (advertised_address << 8) | data[1 + i];
248 }
249 address_matches = address_matches || advertised_address == this->address_;
250 }
251
252 if (is_encrypted && !this->has_bindkey_) {
253 if (address_matches) {
254 char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
255 ESP_LOGE(TAG, "Encrypted BTHome frame received but no bindkey configured for %s",
256 device.address_str_to(addr_buf));
257 }
258 return false;
259 }
260
261 if (!is_encrypted && this->has_bindkey_) {
262 if (address_matches) {
263 char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
264 ESP_LOGE(TAG, "Unencrypted BTHome frame received with bindkey configured for %s",
265 device.address_str_to(addr_buf));
266 }
267 return false;
268 }
269 std::vector<uint8_t> decrypted_payload;
270 const uint8_t *payload = nullptr;
271 size_t payload_size = 0;
272
273 if (is_encrypted) {
274 if (!this->decrypt_bthome_payload_(data, source_address, decrypted_payload)) {
275 char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
276 ESP_LOGVV(TAG, "Failed to decrypt BTHome frame from %s", device.address_str_to(addr_buf));
277 return false;
278 }
279 payload = decrypted_payload.data();
280 payload_size = decrypted_payload.size();
281 } else {
282 payload = data.data() + 1;
283 payload_size = data.size() - 1;
284 }
285
286 if (mac_included) {
287 if (payload_size < 6) {
288 ESP_LOGVV(TAG, "BTHome payload missing MAC address");
289 return false;
290 }
291 source_address = 0;
292 for (int i = 5; i >= 0; i--) {
293 source_address = (source_address << 8) | payload[i];
294 }
295 payload += 6;
296 payload_size -= 6;
297 }
298
299 char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
300 if (source_address != this->address_) {
301 ESP_LOGVV(TAG, "BTHome frame from unexpected device %s", format_mac_address(addr_buf, source_address));
302 return false;
303 }
304
305 if (payload_size == 0) {
306 ESP_LOGVV(TAG, "BTHome payload empty after header");
307 return false;
308 }
309
310 bool reported = false;
311 size_t offset = 0;
312 uint8_t last_type = 0;
313
314 while (offset < payload_size) {
315 const uint8_t obj_type = payload[offset++];
316 size_t value_length = 0;
317 bool has_length_byte = obj_type == 0x53; // text objects include explicit length
318
319 if (has_length_byte) {
320 if (offset >= payload_size) {
321 break;
322 }
323 value_length = payload[offset++];
324 } else {
325 if (!get_bthome_value_length(obj_type, value_length)) {
326 ESP_LOGVV(TAG, "Unknown BTHome object 0x%02X", obj_type);
327 break;
328 }
329 }
330
331 if (value_length == 0) {
332 break;
333 }
334
335 if (offset + value_length > payload_size) {
336 ESP_LOGVV(TAG, "BTHome object length exceeds payload");
337 break;
338 }
339
340 const uint8_t *value = &payload[offset];
341 offset += value_length;
342
343 if (obj_type < last_type) {
344 ESP_LOGVV(TAG, "BTHome objects not in ascending order");
345 }
346 last_type = obj_type;
347
348 switch (obj_type) {
349 case 0x00: { // packet id
350 const uint8_t packet_id = value[0];
351 if (this->last_packet_id_.has_value() && *this->last_packet_id_ == packet_id) {
352 return reported;
353 }
354 this->last_packet_id_ = packet_id;
355 break;
356 }
357 case 0x01: { // battery percentage
358 if (this->battery_level_ != nullptr) {
359 this->battery_level_->publish_state(value[0]);
360 reported = true;
361 }
362 break;
363 }
364 case 0x0C: { // battery voltage (mV)
365 if (this->battery_voltage_ != nullptr) {
366 const uint16_t raw = encode_uint16(value[1], value[0]);
367 this->battery_voltage_->publish_state(raw * 0.001f);
368 reported = true;
369 }
370 break;
371 }
372 case 0x02: { // temperature
373 if (this->temperature_ != nullptr) {
374 const int16_t raw = encode_uint16(value[1], value[0]);
375 this->temperature_->publish_state(raw * 0.01f);
376 reported = true;
377 }
378 break;
379 }
380 case 0x03: { // humidity
381 if (this->humidity_ != nullptr) {
382 const uint16_t raw = encode_uint16(value[1], value[0]);
383 this->humidity_->publish_state(raw * 0.01f);
384 reported = true;
385 }
386 break;
387 }
388 default:
389 break;
390 }
391 }
392
393 if (reported) {
394 ESP_LOGD(TAG, "BTHome data%sfrom %s", is_trigger_based ? " (triggered) " : " ", device.address_str_to(addr_buf));
395 }
396
397 return reported;
398}
399
400} // namespace bthome_mithermometer
401} // namespace esphome
402
403#endif
uint8_t address
Definition bl0906.h:4
uint8_t raw[35]
Definition bl0939.h:0
bool handle_service_data_(const esp32_ble_tracker::ServiceData &service_data, const esp32_ble_tracker::ESPBTDevice &device)
void set_bindkey(std::initializer_list< uint8_t > bindkey)
bool decrypt_bthome_payload_(const std::vector< uint8_t > &data, uint64_t source_address, std::vector< uint8_t > &payload) const
bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override
bool contains(uint8_t data1, uint8_t data2) const
Definition ble_uuid.cpp:112
const char * address_str_to(std::span< char, MAC_ADDRESS_PRETTY_BUFFER_SIZE > buf) const
Format MAC address into provided buffer, returns pointer to buffer for convenience.
const std::vector< ServiceData > & get_service_datas() const
bool has_value() const
Definition optional.h:92
void publish_state(float state)
Publish a new state to the front-end.
Definition sensor.cpp:65
Providing packet encoding functions for exchanging data with a remote host.
Definition a01nyub.cpp:7
char * format_hex_pretty_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length, char separator)
Format byte array as uppercase hex to buffer (base implementation).
Definition helpers.cpp:353
constexpr size_t format_hex_pretty_size(size_t byte_count)
Calculate buffer size needed for format_hex_pretty_to with separator: "XX:XX:...:XX\0".
Definition helpers.h:1103
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:653
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:1170
uint32_t payload_size()