ESPHome 2026.1.4
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 <array>
7#include <span>
8
9#ifdef USE_ESP32
10
11namespace esphome {
12namespace bthome_mithermometer {
13
14static const char *const TAG = "bthome_mithermometer";
15
16static const char *format_mac_address(std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buffer, uint64_t address) {
17 std::array<uint8_t, MAC_ADDRESS_SIZE> mac{};
18 for (size_t i = 0; i < MAC_ADDRESS_SIZE; i++) {
19 mac[i] = (address >> ((MAC_ADDRESS_SIZE - 1 - i) * 8)) & 0xFF;
20 }
21
22 format_mac_addr_upper(mac.data(), buffer.data());
23 return buffer.data();
24}
25
26static bool get_bthome_value_length(uint8_t obj_type, size_t &value_length) {
27 switch (obj_type) {
28 case 0x00: // packet id
29 case 0x01: // battery
30 case 0x09: // count (uint8)
31 case 0x0F: // generic boolean
32 case 0x10: // power (bool)
33 case 0x11: // opening
34 case 0x15: // battery low
35 case 0x16: // battery charging
36 case 0x17: // carbon monoxide
37 case 0x18: // cold
38 case 0x19: // connectivity
39 case 0x1A: // door
40 case 0x1B: // garage door
41 case 0x1C: // gas
42 case 0x1D: // heat
43 case 0x1E: // light
44 case 0x1F: // lock
45 case 0x20: // moisture
46 case 0x21: // motion
47 case 0x22: // moving
48 case 0x23: // occupancy
49 case 0x24: // plug
50 case 0x25: // presence
51 case 0x26: // problem
52 case 0x27: // running
53 case 0x28: // safety
54 case 0x29: // smoke
55 case 0x2A: // sound
56 case 0x2B: // tamper
57 case 0x2C: // vibration
58 case 0x2D: // water leak
59 case 0x2E: // humidity (uint8)
60 case 0x2F: // moisture (uint8)
61 case 0x46: // UV index
62 case 0x57: // temperature (sint8)
63 case 0x58: // temperature (0.35C step)
64 case 0x59: // count (sint8)
65 case 0x60: // channel
66 value_length = 1;
67 return true;
68 case 0x02: // temperature (0.01C)
69 case 0x03: // humidity
70 case 0x06: // mass (kg)
71 case 0x07: // mass (lb)
72 case 0x08: // dewpoint
73 case 0x0C: // voltage (mV)
74 case 0x0D: // pm2.5
75 case 0x0E: // pm10
76 case 0x12: // CO2
77 case 0x13: // TVOC
78 case 0x14: // moisture
79 case 0x3D: // count (uint16)
80 case 0x3F: // rotation
81 case 0x40: // distance (mm)
82 case 0x41: // distance (m)
83 case 0x43: // current (A)
84 case 0x44: // speed
85 case 0x45: // temperature (0.1C)
86 case 0x47: // volume (L)
87 case 0x48: // volume (mL)
88 case 0x49: // volume flow rate
89 case 0x4A: // voltage (0.1V)
90 case 0x51: // acceleration
91 case 0x52: // gyroscope
92 case 0x56: // conductivity
93 case 0x5A: // count (sint16)
94 case 0x5D: // current (sint16)
95 case 0x5E: // direction
96 case 0x5F: // precipitation
97 case 0x61: // rotational speed
98 case 0xF0: // button event
99 value_length = 2;
100 return true;
101 case 0x04: // pressure
102 case 0x05: // illuminance
103 case 0x0A: // energy
104 case 0x0B: // power
105 case 0x42: // duration
106 case 0x4B: // gas (uint24)
107 case 0xF2: // firmware version (uint24)
108 value_length = 3;
109 return true;
110 case 0x3E: // count (uint32)
111 case 0x4C: // gas (uint32)
112 case 0x4D: // energy (uint32)
113 case 0x4E: // volume (uint32)
114 case 0x4F: // water (uint32)
115 case 0x50: // timestamp
116 case 0x55: // volume storage
117 case 0x5B: // count (sint32)
118 case 0x5C: // power (sint32)
119 case 0x62: // speed (sint32)
120 case 0x63: // acceleration (sint32)
121 case 0xF1: // firmware version (uint32)
122 value_length = 4;
123 return true;
124 default:
125 return false;
126 }
127}
128
130 char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
131 ESP_LOGCONFIG(TAG, "BTHome MiThermometer");
132 ESP_LOGCONFIG(TAG, " MAC Address: %s", format_mac_address(addr_buf, this->address_));
133 LOG_SENSOR(" ", "Temperature", this->temperature_);
134 LOG_SENSOR(" ", "Humidity", this->humidity_);
135 LOG_SENSOR(" ", "Battery Level", this->battery_level_);
136 LOG_SENSOR(" ", "Battery Voltage", this->battery_voltage_);
137 LOG_SENSOR(" ", "Signal Strength", this->signal_strength_);
138}
139
141 bool matched = false;
142 for (auto &service_data : device.get_service_datas()) {
143 if (this->handle_service_data_(service_data, device)) {
144 matched = true;
145 }
146 }
147 if (matched && this->signal_strength_ != nullptr) {
148 this->signal_strength_->publish_state(device.get_rssi());
149 }
150 return matched;
151}
152
154 const esp32_ble_tracker::ESPBTDevice &device) {
155 if (!service_data.uuid.contains(0xD2, 0xFC)) {
156 return false;
157 }
158
159 const auto &data = service_data.data;
160 if (data.size() < 2) {
161 ESP_LOGVV(TAG, "BTHome data too short: %zu", data.size());
162 return false;
163 }
164
165 const uint8_t adv_info = data[0];
166 const bool is_encrypted = adv_info & 0x01;
167 const bool mac_included = adv_info & 0x02;
168 const bool is_trigger_based = adv_info & 0x04;
169 const uint8_t version = (adv_info >> 5) & 0x07;
170
171 if (version != 0x02) {
172 ESP_LOGVV(TAG, "Unsupported BTHome version %u", version);
173 return false;
174 }
175
176 char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
177 if (is_encrypted) {
178 ESP_LOGV(TAG, "Ignoring encrypted BTHome frame from %s", device.address_str_to(addr_buf));
179 return false;
180 }
181
182 size_t payload_index = 1;
183 uint64_t source_address = device.address_uint64();
184
185 if (mac_included) {
186 if (data.size() < 7) {
187 ESP_LOGVV(TAG, "BTHome payload missing MAC address");
188 return false;
189 }
190 source_address = 0;
191 for (int i = 5; i >= 0; i--) {
192 source_address = (source_address << 8) | data[1 + i];
193 }
194 payload_index = 7;
195 }
196
197 if (source_address != this->address_) {
198 ESP_LOGVV(TAG, "BTHome frame from unexpected device %s", format_mac_address(addr_buf, source_address));
199 return false;
200 }
201
202 if (payload_index >= data.size()) {
203 ESP_LOGVV(TAG, "BTHome payload empty after header");
204 return false;
205 }
206
207 bool reported = false;
208 size_t offset = payload_index;
209 uint8_t last_type = 0;
210
211 while (offset < data.size()) {
212 const uint8_t obj_type = data[offset++];
213 size_t value_length = 0;
214 bool has_length_byte = obj_type == 0x53; // text objects include explicit length
215
216 if (has_length_byte) {
217 if (offset >= data.size()) {
218 break;
219 }
220 value_length = data[offset++];
221 } else {
222 if (!get_bthome_value_length(obj_type, value_length)) {
223 ESP_LOGVV(TAG, "Unknown BTHome object 0x%02X", obj_type);
224 break;
225 }
226 }
227
228 if (value_length == 0) {
229 break;
230 }
231
232 if (offset + value_length > data.size()) {
233 ESP_LOGVV(TAG, "BTHome object length exceeds payload");
234 break;
235 }
236
237 const uint8_t *value = &data[offset];
238 offset += value_length;
239
240 if (obj_type < last_type) {
241 ESP_LOGVV(TAG, "BTHome objects not in ascending order");
242 }
243 last_type = obj_type;
244
245 switch (obj_type) {
246 case 0x00: { // packet id
247 const uint8_t packet_id = value[0];
248 if (this->last_packet_id_.has_value() && *this->last_packet_id_ == packet_id) {
249 return reported;
250 }
251 this->last_packet_id_ = packet_id;
252 break;
253 }
254 case 0x01: { // battery percentage
255 if (this->battery_level_ != nullptr) {
256 this->battery_level_->publish_state(value[0]);
257 reported = true;
258 }
259 break;
260 }
261 case 0x0C: { // battery voltage (mV)
262 if (this->battery_voltage_ != nullptr) {
263 const uint16_t raw = encode_uint16(value[1], value[0]);
264 this->battery_voltage_->publish_state(raw * 0.001f);
265 reported = true;
266 }
267 break;
268 }
269 case 0x02: { // temperature
270 if (this->temperature_ != nullptr) {
271 const int16_t raw = encode_uint16(value[1], value[0]);
272 this->temperature_->publish_state(raw * 0.01f);
273 reported = true;
274 }
275 break;
276 }
277 case 0x03: { // humidity
278 if (this->humidity_ != nullptr) {
279 const uint16_t raw = encode_uint16(value[1], value[0]);
280 this->humidity_->publish_state(raw * 0.01f);
281 reported = true;
282 }
283 break;
284 }
285 default:
286 break;
287 }
288 }
289
290 if (reported) {
291 ESP_LOGD(TAG, "BTHome data%sfrom %s", is_trigger_based ? " (triggered) " : " ", device.address_str_to(addr_buf));
292 }
293
294 return reported;
295}
296
297} // namespace bthome_mithermometer
298} // namespace esphome
299
300#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)
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:76
Providing packet encoding functions for exchanging data with a remote host.
Definition a01nyub.cpp:7
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:463
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:897