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