ESPHome 2026.9.0-dev
Loading...
Searching...
No Matches
thermopro_ble.cpp
Go to the documentation of this file.
1#include "thermopro_ble.h"
2#include <cmath>
3#include "esphome/core/log.h"
4
6
7// this size must be large enough to hold the largest data frame
8// of all supported devices
9static constexpr std::size_t MAX_DATA_SIZE = 24;
10
11struct DeviceParserMapping {
12 const char *prefix;
13 DeviceParser parser;
14};
15
16static float tp96_battery(uint16_t voltage);
17
18static optional<ParseResult> parse_tp972(const uint8_t *data, std::size_t data_size);
19static optional<ParseResult> parse_tp96(const uint8_t *data, std::size_t data_size);
20static optional<ParseResult> parse_tp3(const uint8_t *data, std::size_t data_size);
21
22static const char *const TAG = "thermopro_ble";
23
24static const struct DeviceParserMapping DEVICE_PARSER_MAP[] = {
25 {"TP972", parse_tp972}, {"TP970", parse_tp96}, {"TP96", parse_tp96}, {"TP3", parse_tp3}};
26
28 ESP_LOGCONFIG(TAG, "ThermoPro BLE");
29 LOG_SENSOR(" ", "Temperature", this->temperature_);
30 LOG_SENSOR(" ", "External temperature", this->external_temperature_);
31 LOG_SENSOR(" ", "Humidity", this->humidity_);
32 LOG_SENSOR(" ", "Battery Level", this->battery_level_);
33}
34
36 // check for matching mac address
37 if (device.address_uint64() != this->address_) {
38 ESP_LOGVV(TAG, "parse_device(): unknown MAC address.");
39 return false;
40 }
41
42 // check for valid device type
44 if (this->device_parser_ == nullptr) {
45 ESP_LOGVV(TAG, "parse_device(): invalid device type.");
46 return false;
47 }
48
49 char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
50 ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str_to(addr_buf));
51
52 // publish signal strength
53 float signal_strength = float(device.get_rssi());
54 if (this->signal_strength_ != nullptr)
55 this->signal_strength_->publish_state(signal_strength);
56
57 bool success = false;
58 for (auto &service_data : device.get_manufacturer_datas()) {
59 // check maximum data size
60 std::size_t data_size = service_data.data.size() + 2;
61 if (data_size > MAX_DATA_SIZE) {
62 ESP_LOGVV(TAG, "parse_device(): maximum data size exceeded!");
63 continue;
64 }
65
66 // reconstruct whole record from 2 byte uuid and data
67 uint16_t svc_uuid16 = service_data.uuid.uuid16();
68 uint8_t data[MAX_DATA_SIZE] = {static_cast<uint8_t>(svc_uuid16), static_cast<uint8_t>(svc_uuid16 >> 8)};
69 std::copy(service_data.data.begin(), service_data.data.end(), std::begin(data) + 2);
70
71 // dispatch data to parser
72 optional<ParseResult> result = this->device_parser_(data, data_size);
73 if (!result.has_value()) {
74 continue;
75 }
76
77 // publish sensor values
78 if (result->temperature.has_value() && this->temperature_ != nullptr)
79 this->temperature_->publish_state(*result->temperature);
80 if (result->external_temperature.has_value() && this->external_temperature_ != nullptr)
81 this->external_temperature_->publish_state(*result->external_temperature);
82 if (result->humidity.has_value() && this->humidity_ != nullptr)
83 this->humidity_->publish_state(*result->humidity);
84 if (result->battery_level.has_value() && this->battery_level_ != nullptr)
85 this->battery_level_->publish_state(*result->battery_level);
86
87 success = true;
88 }
89
90 return success;
91}
92
94 // check for changed device name (should only happen on initial call)
95 if (this->device_name_ == device_name) {
96 return;
97 }
98
99 // remember device name
100 this->device_name_ = device_name;
101
102 // try to find device parser
103 for (const auto &mapping : DEVICE_PARSER_MAP) {
104 if (device_name.starts_with(mapping.prefix)) {
105 this->device_parser_ = mapping.parser;
106 return;
107 }
108 }
109
110 // device type unknown
111 this->device_parser_ = nullptr;
112 ESP_LOGVV(TAG, "update_device_type_(): unknown device type %s.", device_name.c_str());
113}
114
115static inline uint16_t read_uint16(const uint8_t *data, std::size_t offset) {
116 return static_cast<uint16_t>(data[offset + 0]) | (static_cast<uint16_t>(data[offset + 1]) << 8);
117}
118
119static inline int16_t read_int16(const uint8_t *data, std::size_t offset) {
120 return static_cast<int16_t>(read_uint16(data, offset));
121}
122
123static inline uint32_t read_uint32(const uint8_t *data, std::size_t offset) {
124 return static_cast<uint32_t>(data[offset + 0]) | (static_cast<uint32_t>(data[offset + 1]) << 8) |
125 (static_cast<uint32_t>(data[offset + 2]) << 16) | (static_cast<uint32_t>(data[offset + 3]) << 24);
126}
127
128// Battery calculation used with permission from:
129// https://github.com/Bluetooth-Devices/thermopro-ble/blob/main/src/thermopro_ble/parser.py
130//
131// TP96x battery values appear to be a voltage reading, probably in millivolts.
132// This means that calculating battery life from it is a non-linear function.
133// Examining the curve, it looked fairly close to a curve from the tanh function.
134// So, I created a script to use Tensorflow to optimize an equation in the format
135// A*tanh(B*x+C)+D
136// Where A,B,C,D are the variables to optimize for. This yielded the below function
137static float tp96_battery(uint16_t voltage) {
138 float level = 52.317286f * std::tanh(static_cast<float>(voltage) / 273.624277936f - 8.76485439394f) + 51.06925f;
139 return std::max(0.0f, std::min(level, 100.0f));
140}
141
142static optional<ParseResult> parse_tp972(const uint8_t *data, std::size_t data_size) {
143 if (data_size != 23) {
144 ESP_LOGVV(TAG, "parse_tp972(): payload has wrong size of %d (!= 23)!", data_size);
145 return {};
146 }
147
148 ParseResult result;
149
150 // ambient temperature, 2 bytes, 16-bit unsigned integer, -54 °C offset
151 result.external_temperature = static_cast<float>(read_uint16(data, 1)) - 54.0f;
152
153 // battery level, 2 bytes, 16-bit unsigned integer, voltage (convert to percentage)
154 result.battery_level = tp96_battery(read_uint16(data, 3));
155
156 // internal temperature, 4 bytes, float, -54 °C offset
157 result.temperature = static_cast<float>(read_uint32(data, 9)) - 54.0f;
158
159 return result;
160}
161
162static optional<ParseResult> parse_tp96(const uint8_t *data, std::size_t data_size) {
163 if (data_size != 7) {
164 ESP_LOGVV(TAG, "parse_tp96(): payload has wrong size of %d (!= 7)!", data_size);
165 return {};
166 }
167
168 ParseResult result;
169
170 // internal temperature, 2 bytes, 16-bit unsigned integer, -30 °C offset
171 result.temperature = static_cast<float>(read_uint16(data, 1)) - 30.0f;
172
173 // battery level, 2 bytes, 16-bit unsigned integer, voltage (convert to percentage)
174 result.battery_level = tp96_battery(read_uint16(data, 3));
175
176 // ambient temperature, 2 bytes, 16-bit unsigned integer, -30 °C offset
177 result.external_temperature = static_cast<float>(read_uint16(data, 5)) - 30.0f;
178
179 return result;
180}
181
182static optional<ParseResult> parse_tp3(const uint8_t *data, std::size_t data_size) {
183 if (data_size < 6) {
184 ESP_LOGVV(TAG, "parse_tp3(): payload has wrong size of %d (< 6)!", data_size);
185 return {};
186 }
187
188 ParseResult result;
189
190 // temperature, 2 bytes, 16-bit signed integer, 0.1 °C
191 result.temperature = static_cast<float>(read_int16(data, 1)) * 0.1f;
192
193 // humidity, 1 byte, 8-bit unsigned integer, 1.0 %
194 result.humidity = static_cast<float>(data[3]);
195
196 // battery level, 2 bits (0-2)
197 result.battery_level = static_cast<float>(data[4] & 0x3) * 50.0f;
198
199 return result;
200}
201
202} // namespace esphome::thermopro_ble
StringRef is a reference to a string owned by something else.
Definition string_ref.h:26
constexpr const char * c_str() const
Definition string_ref.h:73
bool starts_with(const StringRef &prefix) const
True if the view begins with the given prefix (std::string::starts_with-like)
Definition string_ref.h:80
ESPDEPRECATED("Use address_str_to() instead. Removed in 2027.2.0.", "2026.8.0") std const char * address_str_to(char *buf) const
Return MAC as "XX:XX:XX:XX:XX:XX" string.
uint64_t address_uint64() const
Return MAC as packed uint64 (byte 0 in LSB — matches esp32's address_uint64).
const std::vector< ServiceData > & get_manufacturer_datas() const
Definition ble_device.h:224
StringRef get_name() const
Advertised name as a view into the fixed buffer (always NUL-terminated, so c_str() is safe); converts...
Definition ble_device.h:221
void publish_state(float state)
Publish a new state to the front-end.
Definition sensor.cpp:68
bool parse_device(const ble_device_base::ESPBTDevice &device) override
void update_device_type_(StringRef device_name)
std::span< const uint8_t > data
optional< ParseResult >(*)(const uint8_t *data, std::size_t data_size) DeviceParser
static void uint32_t