ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
ecocomfort2_hub.cpp
Go to the documentation of this file.
2#ifdef USE_ESP32
3#include "ecocomfort2_hub.h"
4#include "esphome/core/log.h"
5
6#include <cstdio>
7
8namespace esphome::ecocomfort2 {
9
10ESPHOME_LOG_TAG(TAG, "ecocomfort2");
11
13#ifdef USE_BINARY_SENSOR
14 if (this->connected_binary_sensor_ != nullptr)
16#endif
17#ifdef USE_TIME
18 // The clock is written after each time-source sync
19 if (this->time_ != nullptr)
20 this->time_->add_on_time_sync_callback([this]() { this->sync_clock_(); });
21#endif
22}
23
25 if (!this->is_ready()) {
26 ESP_LOGD(TAG, "[%s] Not ready, skipping update", this->parent_->address_str());
27 return;
28 }
29 // The GATT client runs the requests one after the other
30 this->read_char_(this->char_handle_state_);
31 this->read_char_(this->char_handle_oper_);
32}
33
35 auto status = esp_ble_gattc_read_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), handle,
36 ESP_GATT_AUTH_REQ_NONE);
37 if (status != ESP_GATT_OK) {
38 ESP_LOGW(TAG, "[%s] Read request for handle 0x%x failed, status=%d", this->parent_->address_str(), handle, status);
39 }
40}
41
43 ESP_LOGCONFIG(TAG,
44 "Ecocomfort2 Hub:\n"
45 " Address: %s",
46 this->parent_->address_str());
47 LOG_UPDATE_INTERVAL(this);
48#ifdef USE_TIME
49 ESP_LOGCONFIG(TAG, " Clock sync: %s", YESNO(this->time_ != nullptr));
50#endif
51#ifdef USE_SENSOR
52 LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
53 LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
54 LOG_SENSOR(" ", "VOC", this->voc_sensor_);
55 LOG_SENSOR(" ", "Direction", this->direction_sensor_);
56#endif
57#ifdef USE_BINARY_SENSOR
58 LOG_BINARY_SENSOR(" ", "Connected", this->connected_binary_sensor_);
59 LOG_BINARY_SENSOR(" ", "Boost", this->boost_binary_sensor_);
60#endif
61#ifdef USE_TEXT_SENSOR
62 LOG_TEXT_SENSOR(" ", "Firmware", this->firmware_text_sensor_);
63#endif
64}
65
66void Ecocomfort2Hub::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
67 esp_ble_gattc_cb_param_t *param) {
68 switch (event) {
69 case ESP_GATTC_DISCONNECT_EVT: {
70 ESP_LOGD(TAG, "[%s] Disconnected: reason=%d", this->parent_->address_str(), param->disconnect.reason);
73 this->status_set_warning();
74 this->char_handle_oper_ = 0;
75 this->char_handle_state_ = 0;
76 this->char_handle_info_ = 0;
77 this->char_handle_clock_ = 0;
78 this->set_connected_(false);
79 this->publish_unknown_();
80 break;
81 }
82 case ESP_GATTC_SEARCH_CMPL_EVT: {
83 const auto service_uuid = espbt::ESPBTUUID::from_raw("f4b827c3-e660-4bc8-bdf6-3c8e9b845e0d");
84 const auto oper_uuid = espbt::ESPBTUUID::from_raw("b9d6f678-bc0d-4a73-90c8-60b0f07301f1");
85 const auto state_uuid = espbt::ESPBTUUID::from_raw("438d3433-7e5a-459a-a8e4-66343fad2bb0");
86 const auto info_uuid = espbt::ESPBTUUID::from_raw("f5f56229-dd4f-480f-a829-9189269d8b37");
87 const auto clock_uuid = espbt::ESPBTUUID::from_raw("82788997-49e4-4533-b949-7ed433678044");
88 // Keep the handles, not the characteristics: ble_client frees its services once all nodes are established.
89 auto *oper = this->parent_->get_characteristic(service_uuid, oper_uuid);
90 auto *state = this->parent_->get_characteristic(service_uuid, state_uuid);
91 if (oper == nullptr || state == nullptr) {
92 ESP_LOGW(TAG, "[%s] C_OPER or C_STATE characteristic not found, disconnecting", this->parent_->address_str());
93 this->parent_->disconnect();
94 break;
95 }
96 this->char_handle_oper_ = oper->handle;
97 this->char_handle_state_ = state->handle;
98 // The version and clock characteristics only feed optional features, so a unit without them still works
99 auto *info = this->parent_->get_characteristic(service_uuid, info_uuid);
100 this->char_handle_info_ = info != nullptr ? info->handle : 0;
101 auto *clock = this->parent_->get_characteristic(service_uuid, clock_uuid);
102 this->char_handle_clock_ = clock != nullptr ? clock->handle : 0;
103 this->node_state = espbt::ClientState::ESTABLISHED;
104 this->status_set_warning();
105
106 // A bonded device may already have re-encrypted the link before discovery finished.
107 if (this->parent_->is_paired()) {
108 this->on_paired_();
109 break;
110 }
111 ESP_LOGI(TAG, "[%s] Requesting BLE encryption", this->parent_->address_str());
112 if (this->parent_->pair() != ESP_OK) {
113 ESP_LOGW(TAG, "[%s] BLE pair request failed, disconnecting", this->parent_->address_str());
114 this->parent_->disconnect();
115 break;
116 }
118 ESP_LOGW(TAG, "[%s] BLE authentication timed out, disconnecting", this->parent_->address_str());
119 this->parent_->disconnect();
120 });
121 break;
122 }
123 case ESP_GATTC_READ_CHAR_EVT: {
124 if (param->read.conn_id != this->parent_->get_conn_id()) {
125 break;
126 }
127 const uint16_t handle = param->read.handle;
128 if (handle == this->char_handle_oper_) {
129 if (this->read_ok_(param))
130 this->on_oper_(param->read.value, param->read.value_len);
131 } else if (handle == this->char_handle_state_) {
132 if (this->read_ok_(param))
133 this->on_state_(param->read.value, param->read.value_len);
134 } else if (handle == this->char_handle_info_) {
135 if (this->read_ok_(param))
136 this->on_info_(param->read.value, param->read.value_len);
137 } else {
138 break;
139 }
140 break;
141 }
142 default:
143 break;
144 }
145}
146
147bool Ecocomfort2Hub::read_ok_(const esp_ble_gattc_cb_param_t *param) {
148 if (param->read.status == ESP_GATT_INSUF_ENCRYPTION || param->read.status == ESP_GATT_INSUF_AUTHENTICATION) {
149 // ble_client reconnects and pairs again
150 ESP_LOGW(TAG, "[%s] Encryption lost, disconnecting", this->parent_->address_str());
151 this->parent_->disconnect();
152 return false;
153 }
154 if (param->read.status != ESP_GATT_OK) {
155 ESP_LOGW(TAG, "[%s] Read of handle 0x%x failed, status=%d", this->parent_->address_str(), param->read.handle,
156 param->read.status);
157 return false;
158 }
159 return true;
160}
161
162void Ecocomfort2Hub::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
163 if (event != ESP_GAP_BLE_AUTH_CMPL_EVT || !this->parent_->check_addr(param->ble_security.auth_cmpl.bd_addr) ||
164 this->node_state != espbt::ClientState::ESTABLISHED) {
165 return;
166 }
167 if (!param->ble_security.auth_cmpl.success) {
169 ESP_LOGW(TAG, "[%s] BLE authentication failed, reason=%d, disconnecting", this->parent_->address_str(),
170 param->ble_security.auth_cmpl.fail_reason);
171 this->parent_->disconnect();
172 return;
173 }
174 this->on_paired_();
175}
176
179 this->status_clear_warning();
180 ESP_LOGI(TAG, "[%s] Encryption confirmed, device ready", this->parent_->address_str());
181 this->set_connected_(true);
182#ifdef USE_TIME
183 if (this->time_ != nullptr)
184 this->sync_clock_();
185#endif
186#ifdef USE_TEXT_SENSOR
187 if (this->firmware_text_sensor_ != nullptr && this->char_handle_info_ != 0)
188 this->read_char_(this->char_handle_info_);
189#endif
190 this->update();
191}
192
193void Ecocomfort2Hub::set_connected_(bool connected) {
194#ifdef USE_BINARY_SENSOR
195 if (this->connected_binary_sensor_ != nullptr)
196 this->connected_binary_sensor_->publish_state(connected);
197#endif
198}
199
200// Readings from a unit that is out of reach would otherwise look current for hours
202#ifdef USE_SENSOR
203 for (auto *sensor : {this->temperature_sensor_, this->humidity_sensor_, this->voc_sensor_, this->direction_sensor_}) {
204 if (sensor != nullptr)
205 sensor->publish_state(NAN);
206 }
207#endif
208#ifdef USE_BINARY_SENSOR
209 if (this->boost_binary_sensor_ != nullptr)
211#endif
212}
213
214// C_OPER: mode byte, speed/flags byte
215void Ecocomfort2Hub::on_oper_(const uint8_t *data, uint16_t len) {
216 if (len < 2) {
217 ESP_LOGW(TAG, "[%s] C_OPER too short: %u bytes", this->parent_->address_str(), len);
218 return;
219 }
220 this->mode_ = data[0];
221 this->speed_ = data[1];
222 ESP_LOGD(TAG, "[%s] C_OPER: mode=0x%02x, speed=0x%02x", this->parent_->address_str(), this->mode_, this->speed_);
223#ifdef ECOCOMFORT2_CHILD_COUNT
224 for (auto *child : this->children_) {
225 child->on_status();
226 }
227#endif
228}
229
230// C_STATE: bytes 0-1 the mode and speed the unit runs; byte 1 also has the boost flag (0x40) and the advanced
231// control step (0x20). Byte 2 the air flow direction, bytes 3-4 temperature and 5-6 humidity in hundredths,
232// 7-8 VOC in ppb
233void Ecocomfort2Hub::on_state_(const uint8_t *data, uint16_t len) {
234 if (len < 9) {
235 ESP_LOGW(TAG, "[%s] C_STATE too short: %u bytes", this->parent_->address_str(), len);
236 return;
237 }
238 const float temperature = static_cast<int16_t>(encode_uint16(data[3], data[4])) / 100.0f;
239 const float humidity = encode_uint16(data[5], data[6]) / 100.0f;
240 const uint16_t voc = encode_uint16(data[7], data[8]);
241 ESP_LOGD(TAG, "[%s] C_STATE: mode=0x%02x, speed=0x%02x, %.2f °C, %.2f %%, %u ppb, direction=%u",
242 this->parent_->address_str(), data[0], data[1], temperature, humidity, voc, data[2]);
243#ifdef USE_BINARY_SENSOR
244 if (this->boost_binary_sensor_ != nullptr)
245 this->boost_binary_sensor_->publish_state((data[1] & FLAG_BOOST) != 0);
246#endif
247#ifdef USE_SENSOR
248 if (this->temperature_sensor_ != nullptr)
249 this->temperature_sensor_->publish_state(temperature);
250 if (this->humidity_sensor_ != nullptr)
251 this->humidity_sensor_->publish_state(humidity);
252 if (this->voc_sensor_ != nullptr)
253 this->voc_sensor_->publish_state(voc);
254 if (this->direction_sensor_ != nullptr)
255 this->direction_sensor_->publish_state(data[2]);
256#endif
257}
258
259// C_INFO: firmware version packed as 4-bit major, 6-bit minor and 6-bit patch
260void Ecocomfort2Hub::on_info_(const uint8_t *data, uint16_t len) {
261 if (len < 2) {
262 ESP_LOGW(TAG, "[%s] C_INFO too short: %u bytes", this->parent_->address_str(), len);
263 return;
264 }
265 const uint16_t field = encode_uint16(data[0] & 0x0F, data[1]);
266 char version[12];
267 snprintf(version, sizeof(version), "%u.%u.%u", data[0] >> 4, field >> 6, field & 0x3F);
268 ESP_LOGD(TAG, "[%s] Firmware %s", this->parent_->address_str(), version);
269#ifdef USE_TEXT_SENSOR
270 if (this->firmware_text_sensor_ != nullptr)
271 this->firmware_text_sensor_->publish_state(version);
272#endif
273}
274
275#ifdef USE_TIME
276// C_CLOCK: two-digit year, month, day, weekday with Monday as 0, hour, minute, second, automatic DST flag
278 if (!this->is_ready() || this->char_handle_clock_ == 0)
279 return;
280 const ESPTime now = this->time_->now();
281 if (!now.is_valid())
282 return;
283 // ESPTime counts Sunday=1 .. Saturday=7, the unit Monday=0 .. Sunday=6
284 const uint8_t weekday = (now.day_of_week + 5) % 7;
285 // The time source already applies DST and every sync rewrites the clock, so the unit must not shift it again
286 uint8_t data[8]{
287 static_cast<uint8_t>(now.year % 100), now.month, now.day_of_month, weekday, now.hour, now.minute, now.second, 0};
288 ESP_LOGD(TAG, "[%s] Syncing clock to %04u-%02u-%02u %02u:%02u:%02u", this->parent_->address_str(), now.year,
289 now.month, now.day_of_month, now.hour, now.minute, now.second);
290 auto status =
291 esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_clock_,
292 sizeof(data), data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
293 if (status != ESP_GATT_OK) {
294 ESP_LOGW(TAG, "[%s] C_CLOCK write failed, status=%d", this->parent_->address_str(), status);
295 }
296}
297#endif
298
299void Ecocomfort2Hub::write_oper(uint8_t mode, uint8_t speed) {
300 if (!this->is_ready()) {
301 ESP_LOGW(TAG, "[%s] Not ready, cannot write C_OPER", this->parent_->address_str());
302 return;
303 }
304 uint8_t data[2]{mode, speed};
305 ESP_LOGD(TAG, "[%s] Writing C_OPER: mode=0x%02x, speed=0x%02x", this->parent_->address_str(), mode, speed);
306 auto status =
307 esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_oper_,
308 sizeof(data), data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
309 if (status != ESP_GATT_OK) {
310 ESP_LOGW(TAG, "[%s] C_OPER write failed, status=%d", this->parent_->address_str(), status);
311 return;
312 }
314}
315
316#ifdef ECOCOMFORT2_CHILD_COUNT
318 this->children_.push_back(obj);
319 obj->set_parent(this);
320}
321#endif
322
323} // namespace esphome::ecocomfort2
324
325#endif // USE_ESP32
BedjetMode mode
BedJet operating mode.
uint8_t status
Definition bl0942.h:8
bool cancel_timeout(const char *name)
Cancel a timeout function.
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 status_clear_warning()
Definition component.h:291
void set_parent(T *parent)
Set the parent of this object.
Definition helpers.h:2005
void invalidate_state()
Clear the state — sets has_state() to false and fires callbacks with nullopt.
void publish_state(bool new_state)
Publish a new state to the front-end.
void publish_initial_state(bool new_state)
Publish the initial state, this will not make the callback manager send callbacks and is meant only f...
binary_sensor::BinarySensor * boost_binary_sensor_
static constexpr uint32_t READBACK_DELAY_MS
static constexpr uint32_t AUTH_TIMEOUT_ID
bool is_ready() const
True once the link is established and paired.
void on_state_(const uint8_t *data, uint16_t len)
text_sensor::TextSensor * firmware_text_sensor_
void on_info_(const uint8_t *data, uint16_t len)
StaticVector< Ecocomfort2Client *, ECOCOMFORT2_CHILD_COUNT > children_
binary_sensor::BinarySensor * connected_binary_sensor_
static constexpr uint32_t READBACK_TIMEOUT_ID
void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override
void register_child(Ecocomfort2Client *obj)
bool read_ok_(const esp_ble_gattc_cb_param_t *param)
void write_oper(uint8_t mode, uint8_t speed)
Write C_OPER (mode byte, speed/flags byte) and read it back shortly after.
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override
void on_oper_(const uint8_t *data, uint16_t len)
static constexpr uint32_t AUTH_TIMEOUT_MS
BLECharacteristic * get_characteristic(espbt::ESPBTUUID service, espbt::ESPBTUUID chr)
void publish_state(float state)
Publish a new state to the front-end.
Definition sensor.cpp:65
void publish_state(const std::string &state)
Definition text_sensor.h:41
ESPTime now()
Get the time in the currently defined timezone.
void add_on_time_sync_callback(F &&callback)
int speed
Definition fan.h:3
bool state
Definition fan.h:2
ESPHOME_LOG_TAG(TAG, "ecocomfort2.fan")
const void size_t len
Definition hal.h:68
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
A more user-friendly version of struct tm from time.h.
Definition time.h:23
uint8_t minute
minutes after the hour [0-59]
Definition time.h:32
uint8_t second
seconds after the minute [0-60]
Definition time.h:30
uint8_t hour
hours since midnight [0-23]
Definition time.h:34
bool is_valid(bool check_day_of_week=true, bool check_day_of_year=true) const
Check if this ESPTime is valid (year >= 2019 and the requested fields are in range).
Definition time.h:82
uint8_t day_of_month
day of the month [1-31]
Definition time.h:38
uint16_t year
year
Definition time.h:44
uint8_t month
month; january=1 [1-12]
Definition time.h:42
uint8_t day_of_week
day of the week; sunday=1 [1-7]
Definition time.h:36
uint16_t temperature
Definition sun_gtil2.cpp:12
spi_device_handle_t handle