From 4cae63bbaa8deed355e7d262c58a6e105ca865c2 Mon Sep 17 00:00:00 2001 From: gledian Date: Tue, 13 Oct 2026 02:27:16 +0200 Subject: [PATCH] [ecocomfort2] Add the read-only sensors and clock sync (Part 2) (#15447) Co-authored-by: gledian Co-authored-by: Claude Opus 4.6 Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston --- esphome/components/ecocomfort2/__init__.py | 7 +- .../components/ecocomfort2/binary_sensor.py | 33 +++ .../ecocomfort2/ecocomfort2_hub.cpp | 233 +++++++++++++++--- .../components/ecocomfort2/ecocomfort2_hub.h | 60 +++++ esphome/components/ecocomfort2/sensor.py | 62 +++++ esphome/components/ecocomfort2/text_sensor.py | 24 ++ tests/components/ecocomfort2/common.yaml | 35 +++ 7 files changed, 420 insertions(+), 34 deletions(-) create mode 100644 esphome/components/ecocomfort2/binary_sensor.py create mode 100644 esphome/components/ecocomfort2/sensor.py create mode 100644 esphome/components/ecocomfort2/text_sensor.py diff --git a/esphome/components/ecocomfort2/__init__.py b/esphome/components/ecocomfort2/__init__.py index 472ac8f7cf8..8a0e1654188 100644 --- a/esphome/components/ecocomfort2/__init__.py +++ b/esphome/components/ecocomfort2/__init__.py @@ -1,12 +1,12 @@ import esphome.codegen as cg -from esphome.components import ble_client, esp32_ble +from esphome.components import ble_client, esp32_ble, time from esphome.components.esp32_ble import ( CONF_AUTH_REQ_MODE, CONF_IO_CAPABILITY, CONF_MAX_KEY_SIZE, ) import esphome.config_validation as cv -from esphome.const import CONF_ID +from esphome.const import CONF_ID, CONF_TIME_ID from esphome.cpp_generator import MockObj import esphome.final_validate as fv from esphome.types import ConfigType @@ -27,6 +27,7 @@ CONFIG_SCHEMA = ( cv.Schema( { cv.GenerateID(): cv.declare_id(Ecocomfort2Hub), + cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock), } ) .extend(ble_client.BLE_CLIENT_SCHEMA) @@ -52,6 +53,8 @@ async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await ble_client.register_ble_node(var, config) + if (time_id := config.get(CONF_TIME_ID)) is not None: + cg.add(var.set_time(await cg.get_variable(time_id))) def _final_validate(config: ConfigType) -> ConfigType: diff --git a/esphome/components/ecocomfort2/binary_sensor.py b/esphome/components/ecocomfort2/binary_sensor.py new file mode 100644 index 00000000000..15e5b69d2d7 --- /dev/null +++ b/esphome/components/ecocomfort2/binary_sensor.py @@ -0,0 +1,33 @@ +import esphome.codegen as cg +from esphome.components import binary_sensor +from esphome.components.const import CONF_CONNECTED +import esphome.config_validation as cv +from esphome.const import DEVICE_CLASS_CONNECTIVITY, ENTITY_CATEGORY_DIAGNOSTIC +from esphome.types import ConfigType + +from . import CONF_ECOCOMFORT2_ID, ECOCOMFORT2_CLIENT_SCHEMA + +CODEOWNERS = ["@gledian"] +DEPENDENCIES = ["ecocomfort2"] + +CONF_BOOST = "boost" + +CONFIG_SCHEMA = cv.All( + ECOCOMFORT2_CLIENT_SCHEMA.extend( + { + cv.Optional(CONF_CONNECTED): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_CONNECTIVITY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_BOOST): binary_sensor.binary_sensor_schema(), + } + ), + cv.has_at_least_one_key(CONF_CONNECTED, CONF_BOOST), +) + + +async def to_code(config: ConfigType) -> None: + hub = await cg.get_variable(config[CONF_ECOCOMFORT2_ID]) + sensors = binary_sensor.sub_binary_sensors(config) + await sensors(CONF_CONNECTED, hub.set_connected_binary_sensor) + await sensors(CONF_BOOST, hub.set_boost_binary_sensor) diff --git a/esphome/components/ecocomfort2/ecocomfort2_hub.cpp b/esphome/components/ecocomfort2/ecocomfort2_hub.cpp index a20fdd9112c..00ce0f20804 100644 --- a/esphome/components/ecocomfort2/ecocomfort2_hub.cpp +++ b/esphome/components/ecocomfort2/ecocomfort2_hub.cpp @@ -3,22 +3,39 @@ #include "ecocomfort2_hub.h" #include "esphome/core/log.h" +#include + namespace esphome::ecocomfort2 { ESPHOME_LOG_TAG(TAG, "ecocomfort2"); -static const espbt::ESPBTUUID SERVICE_UUID = espbt::ESPBTUUID::from_raw("f4b827c3-e660-4bc8-bdf6-3c8e9b845e0d"); -static const espbt::ESPBTUUID C_OPER_UUID = espbt::ESPBTUUID::from_raw("b9d6f678-bc0d-4a73-90c8-60b0f07301f1"); +void Ecocomfort2Hub::setup() { +#ifdef USE_BINARY_SENSOR + if (this->connected_binary_sensor_ != nullptr) + this->connected_binary_sensor_->publish_initial_state(false); +#endif +#ifdef USE_TIME + // The clock is written after each time-source sync + if (this->time_ != nullptr) + this->time_->add_on_time_sync_callback([this]() { this->sync_clock_(); }); +#endif +} void Ecocomfort2Hub::update() { if (!this->is_ready()) { ESP_LOGD(TAG, "[%s] Not ready, skipping update", this->parent_->address_str()); return; } - auto status = esp_ble_gattc_read_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), - this->char_handle_oper_, ESP_GATT_AUTH_REQ_NONE); + // The GATT client runs the requests one after the other + this->read_char_(this->char_handle_state_); + this->read_char_(this->char_handle_oper_); +} + +void Ecocomfort2Hub::read_char_(uint16_t handle) { + auto status = esp_ble_gattc_read_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), handle, + ESP_GATT_AUTH_REQ_NONE); if (status != ESP_GATT_OK) { - ESP_LOGW(TAG, "[%s] C_OPER read request failed, status=%d", this->parent_->address_str(), status); + ESP_LOGW(TAG, "[%s] Read request for handle 0x%x failed, status=%d", this->parent_->address_str(), handle, status); } } @@ -28,6 +45,22 @@ void Ecocomfort2Hub::dump_config() { " Address: %s", this->parent_->address_str()); LOG_UPDATE_INTERVAL(this); +#ifdef USE_TIME + ESP_LOGCONFIG(TAG, " Clock sync: %s", YESNO(this->time_ != nullptr)); +#endif +#ifdef USE_SENSOR + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); + LOG_SENSOR(" ", "VOC", this->voc_sensor_); + LOG_SENSOR(" ", "Direction", this->direction_sensor_); +#endif +#ifdef USE_BINARY_SENSOR + LOG_BINARY_SENSOR(" ", "Connected", this->connected_binary_sensor_); + LOG_BINARY_SENSOR(" ", "Boost", this->boost_binary_sensor_); +#endif +#ifdef USE_TEXT_SENSOR + LOG_TEXT_SENSOR(" ", "Firmware", this->firmware_text_sensor_); +#endif } void Ecocomfort2Hub::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, @@ -39,17 +72,34 @@ void Ecocomfort2Hub::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if this->cancel_timeout(READBACK_TIMEOUT_ID); this->status_set_warning(); this->char_handle_oper_ = 0; + this->char_handle_state_ = 0; + this->char_handle_info_ = 0; + this->char_handle_clock_ = 0; + this->set_connected_(false); + this->publish_unknown_(); break; } case ESP_GATTC_SEARCH_CMPL_EVT: { - // Keep the handle, not the characteristic: ble_client frees its services once all nodes are established. - auto *chr = this->parent_->get_characteristic(SERVICE_UUID, C_OPER_UUID); - if (chr == nullptr) { - ESP_LOGW(TAG, "[%s] No C_OPER characteristic found, disconnecting", this->parent_->address_str()); + const auto service_uuid = espbt::ESPBTUUID::from_raw("f4b827c3-e660-4bc8-bdf6-3c8e9b845e0d"); + const auto oper_uuid = espbt::ESPBTUUID::from_raw("b9d6f678-bc0d-4a73-90c8-60b0f07301f1"); + const auto state_uuid = espbt::ESPBTUUID::from_raw("438d3433-7e5a-459a-a8e4-66343fad2bb0"); + const auto info_uuid = espbt::ESPBTUUID::from_raw("f5f56229-dd4f-480f-a829-9189269d8b37"); + const auto clock_uuid = espbt::ESPBTUUID::from_raw("82788997-49e4-4533-b949-7ed433678044"); + // Keep the handles, not the characteristics: ble_client frees its services once all nodes are established. + auto *oper = this->parent_->get_characteristic(service_uuid, oper_uuid); + auto *state = this->parent_->get_characteristic(service_uuid, state_uuid); + if (oper == nullptr || state == nullptr) { + ESP_LOGW(TAG, "[%s] C_OPER or C_STATE characteristic not found, disconnecting", this->parent_->address_str()); this->parent_->disconnect(); break; } - this->char_handle_oper_ = chr->handle; + this->char_handle_oper_ = oper->handle; + this->char_handle_state_ = state->handle; + // The version and clock characteristics only feed optional features, so a unit without them still works + auto *info = this->parent_->get_characteristic(service_uuid, info_uuid); + this->char_handle_info_ = info != nullptr ? info->handle : 0; + auto *clock = this->parent_->get_characteristic(service_uuid, clock_uuid); + this->char_handle_clock_ = clock != nullptr ? clock->handle : 0; this->node_state = espbt::ClientState::ESTABLISHED; this->status_set_warning(); @@ -71,31 +121,22 @@ void Ecocomfort2Hub::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if break; } case ESP_GATTC_READ_CHAR_EVT: { - if (param->read.conn_id != this->parent_->get_conn_id() || param->read.handle != this->char_handle_oper_) { + if (param->read.conn_id != this->parent_->get_conn_id()) { break; } - if (param->read.status == ESP_GATT_INSUF_ENCRYPTION || param->read.status == ESP_GATT_INSUF_AUTHENTICATION) { - // ble_client reconnects and pairs again - ESP_LOGW(TAG, "[%s] Encryption lost, disconnecting", this->parent_->address_str()); - this->parent_->disconnect(); + const uint16_t handle = param->read.handle; + if (handle == this->char_handle_oper_) { + if (this->read_ok_(param)) + this->on_oper_(param->read.value, param->read.value_len); + } else if (handle == this->char_handle_state_) { + if (this->read_ok_(param)) + this->on_state_(param->read.value, param->read.value_len); + } else if (handle == this->char_handle_info_) { + if (this->read_ok_(param)) + this->on_info_(param->read.value, param->read.value_len); + } else { break; } - if (param->read.status != ESP_GATT_OK) { - ESP_LOGW(TAG, "[%s] C_OPER read failed, status=%d", this->parent_->address_str(), param->read.status); - break; - } - if (param->read.value_len < 2) { - ESP_LOGW(TAG, "[%s] C_OPER too short: %u bytes", this->parent_->address_str(), param->read.value_len); - break; - } - this->mode_ = param->read.value[0]; - this->speed_ = param->read.value[1]; - ESP_LOGD(TAG, "[%s] C_OPER: mode=0x%02x, speed=0x%02x", this->parent_->address_str(), this->mode_, this->speed_); -#ifdef ECOCOMFORT2_CHILD_COUNT - for (auto *child : this->children_) { - child->on_status(); - } -#endif break; } default: @@ -103,6 +144,21 @@ void Ecocomfort2Hub::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if } } +bool Ecocomfort2Hub::read_ok_(const esp_ble_gattc_cb_param_t *param) { + if (param->read.status == ESP_GATT_INSUF_ENCRYPTION || param->read.status == ESP_GATT_INSUF_AUTHENTICATION) { + // ble_client reconnects and pairs again + ESP_LOGW(TAG, "[%s] Encryption lost, disconnecting", this->parent_->address_str()); + this->parent_->disconnect(); + return false; + } + if (param->read.status != ESP_GATT_OK) { + ESP_LOGW(TAG, "[%s] Read of handle 0x%x failed, status=%d", this->parent_->address_str(), param->read.handle, + param->read.status); + return false; + } + return true; +} + void Ecocomfort2Hub::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { if (event != ESP_GAP_BLE_AUTH_CMPL_EVT || !this->parent_->check_addr(param->ble_security.auth_cmpl.bd_addr) || this->node_state != espbt::ClientState::ESTABLISHED) { @@ -122,9 +178,122 @@ void Ecocomfort2Hub::on_paired_() { this->cancel_timeout(AUTH_TIMEOUT_ID); this->status_clear_warning(); ESP_LOGI(TAG, "[%s] Encryption confirmed, device ready", this->parent_->address_str()); + this->set_connected_(true); +#ifdef USE_TIME + if (this->time_ != nullptr) + this->sync_clock_(); +#endif +#ifdef USE_TEXT_SENSOR + if (this->firmware_text_sensor_ != nullptr && this->char_handle_info_ != 0) + this->read_char_(this->char_handle_info_); +#endif this->update(); } +void Ecocomfort2Hub::set_connected_(bool connected) { +#ifdef USE_BINARY_SENSOR + if (this->connected_binary_sensor_ != nullptr) + this->connected_binary_sensor_->publish_state(connected); +#endif +} + +// Readings from a unit that is out of reach would otherwise look current for hours +void Ecocomfort2Hub::publish_unknown_() { +#ifdef USE_SENSOR + for (auto *sensor : {this->temperature_sensor_, this->humidity_sensor_, this->voc_sensor_, this->direction_sensor_}) { + if (sensor != nullptr) + sensor->publish_state(NAN); + } +#endif +} + +// C_OPER: mode byte, speed/flags byte +void Ecocomfort2Hub::on_oper_(const uint8_t *data, uint16_t len) { + if (len < 2) { + ESP_LOGW(TAG, "[%s] C_OPER too short: %u bytes", this->parent_->address_str(), len); + return; + } + this->mode_ = data[0]; + this->speed_ = data[1]; + ESP_LOGD(TAG, "[%s] C_OPER: mode=0x%02x, speed=0x%02x", this->parent_->address_str(), this->mode_, this->speed_); +#ifdef USE_BINARY_SENSOR + if (this->boost_binary_sensor_ != nullptr) + this->boost_binary_sensor_->publish_state((this->speed_ & FLAG_BOOST) != 0); +#endif +#ifdef ECOCOMFORT2_CHILD_COUNT + for (auto *child : this->children_) { + child->on_status(); + } +#endif +} + +// C_STATE: bytes 3-4 temperature and 5-6 humidity in hundredths, 7-8 VOC in ppb, byte 2 the air flow direction +void Ecocomfort2Hub::on_state_(const uint8_t *data, uint16_t len) { + if (len < 9) { + ESP_LOGW(TAG, "[%s] C_STATE too short: %u bytes", this->parent_->address_str(), len); + return; + } + const float temperature = static_cast(encode_uint16(data[3], data[4])) / 100.0f; + const float humidity = encode_uint16(data[5], data[6]) / 100.0f; + const uint16_t voc = encode_uint16(data[7], data[8]); + ESP_LOGD(TAG, "[%s] C_STATE: %.2f °C, %.2f %%, %u ppb, direction=%u", this->parent_->address_str(), temperature, + humidity, voc, data[2]); +#ifdef USE_SENSOR + if (this->temperature_sensor_ != nullptr) + this->temperature_sensor_->publish_state(temperature); + if (this->humidity_sensor_ != nullptr) + this->humidity_sensor_->publish_state(humidity); + if (this->voc_sensor_ != nullptr) + this->voc_sensor_->publish_state(voc); + if (this->direction_sensor_ != nullptr) + this->direction_sensor_->publish_state(data[2]); +#endif +} + +// C_INFO: firmware version packed as 4-bit major, 6-bit minor and 6-bit patch +void Ecocomfort2Hub::on_info_(const uint8_t *data, uint16_t len) { + if (len < 2) { + ESP_LOGW(TAG, "[%s] C_INFO too short: %u bytes", this->parent_->address_str(), len); + return; + } + const uint16_t field = encode_uint16(data[0] & 0x0F, data[1]); + char version[12]; + snprintf(version, sizeof(version), "%u.%u.%u", data[0] >> 4, field >> 6, field & 0x3F); + ESP_LOGD(TAG, "[%s] Firmware %s", this->parent_->address_str(), version); +#ifdef USE_TEXT_SENSOR + if (this->firmware_text_sensor_ != nullptr) + this->firmware_text_sensor_->publish_state(version); +#endif +} + +#ifdef USE_TIME +// C_CLOCK: hour, minute, second, weekday with Monday as 0, day, month, year as two bytes +void Ecocomfort2Hub::sync_clock_() { + if (!this->is_ready() || this->char_handle_clock_ == 0) + return; + const ESPTime now = this->time_->now(); + if (!now.is_valid()) + return; + // ESPTime counts Sunday=1 .. Saturday=7, the unit Monday=0 .. Sunday=6 + const uint8_t weekday = (now.day_of_week + 5) % 7; + uint8_t data[8]{now.hour, + now.minute, + now.second, + weekday, + now.day_of_month, + now.month, + static_cast(now.year >> 8), + static_cast(now.year)}; + ESP_LOGD(TAG, "[%s] Syncing clock to %02u:%02u:%02u", this->parent_->address_str(), now.hour, now.minute, now.second); + auto status = + esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_clock_, + sizeof(data), data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); + if (status != ESP_GATT_OK) { + ESP_LOGW(TAG, "[%s] C_CLOCK write failed, status=%d", this->parent_->address_str(), status); + } +} +#endif + void Ecocomfort2Hub::write_oper(uint8_t mode, uint8_t speed) { if (!this->is_ready()) { ESP_LOGW(TAG, "[%s] Not ready, cannot write C_OPER", this->parent_->address_str()); @@ -139,7 +308,7 @@ void Ecocomfort2Hub::write_oper(uint8_t mode, uint8_t speed) { ESP_LOGW(TAG, "[%s] C_OPER write failed, status=%d", this->parent_->address_str(), status); return; } - this->set_timeout(READBACK_TIMEOUT_ID, READBACK_DELAY_MS, [this]() { this->update(); }); + this->set_timeout(READBACK_TIMEOUT_ID, READBACK_DELAY_MS, [this]() { this->read_char_(this->char_handle_oper_); }); } #ifdef ECOCOMFORT2_CHILD_COUNT diff --git a/esphome/components/ecocomfort2/ecocomfort2_hub.h b/esphome/components/ecocomfort2/ecocomfort2_hub.h index 5a609d9283e..ed046fe0495 100644 --- a/esphome/components/ecocomfort2/ecocomfort2_hub.h +++ b/esphome/components/ecocomfort2/ecocomfort2_hub.h @@ -8,6 +8,19 @@ #include "esphome/core/helpers.h" #include "ecocomfort2_child.h" +#ifdef USE_SENSOR +#include "esphome/components/sensor/sensor.h" +#endif +#ifdef USE_BINARY_SENSOR +#include "esphome/components/binary_sensor/binary_sensor.h" +#endif +#ifdef USE_TEXT_SENSOR +#include "esphome/components/text_sensor/text_sensor.h" +#endif +#ifdef USE_TIME +#include "esphome/components/time/real_time_clock.h" +#endif + namespace esphome::ecocomfort2 { namespace espbt = esphome::esp32_ble_tracker; @@ -33,6 +46,7 @@ static constexpr uint8_t FLAG_NIGHT = 0x80; /** Hub component for Ecocomfort 2.0 VMC via BLE. */ class Ecocomfort2Hub : public esphome::ble_client::BLEClientNode, public PollingComponent { public: + void setup() override; void update() override; void dump_config() override; float get_setup_priority() const override { return setup_priority::BLUETOOTH; } @@ -44,6 +58,23 @@ class Ecocomfort2Hub : public esphome::ble_client::BLEClientNode, public Polling #ifdef ECOCOMFORT2_CHILD_COUNT void register_child(Ecocomfort2Client *obj); #endif +#ifdef USE_SENSOR + void set_temperature_sensor(sensor::Sensor *s) { this->temperature_sensor_ = s; } + void set_humidity_sensor(sensor::Sensor *s) { this->humidity_sensor_ = s; } + void set_voc_sensor(sensor::Sensor *s) { this->voc_sensor_ = s; } + void set_direction_sensor(sensor::Sensor *s) { this->direction_sensor_ = s; } +#endif +#ifdef USE_BINARY_SENSOR + void set_connected_binary_sensor(binary_sensor::BinarySensor *s) { this->connected_binary_sensor_ = s; } + void set_boost_binary_sensor(binary_sensor::BinarySensor *s) { this->boost_binary_sensor_ = s; } +#endif +#ifdef USE_TEXT_SENSOR + void set_firmware_text_sensor(text_sensor::TextSensor *s) { this->firmware_text_sensor_ = s; } +#endif +#ifdef USE_TIME + /// Keeps the unit's clock in step with this time source while connected. + void set_time(time::RealTimeClock *time) { this->time_ = time; } +#endif /// True once the link is established and paired. bool is_ready() const { return this->node_state == espbt::ClientState::ESTABLISHED && this->parent_->is_paired(); } @@ -62,11 +93,40 @@ class Ecocomfort2Hub : public esphome::ble_client::BLEClientNode, public Polling static constexpr uint32_t READBACK_DELAY_MS = 300; void on_paired_(); + void set_connected_(bool connected); + void publish_unknown_(); + void read_char_(uint16_t handle); + bool read_ok_(const esp_ble_gattc_cb_param_t *param); + void on_oper_(const uint8_t *data, uint16_t len); + void on_state_(const uint8_t *data, uint16_t len); + void on_info_(const uint8_t *data, uint16_t len); +#ifdef USE_TIME + void sync_clock_(); +#endif #ifdef ECOCOMFORT2_CHILD_COUNT StaticVector children_; +#endif +#ifdef USE_SENSOR + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *humidity_sensor_{nullptr}; + sensor::Sensor *voc_sensor_{nullptr}; + sensor::Sensor *direction_sensor_{nullptr}; +#endif +#ifdef USE_BINARY_SENSOR + binary_sensor::BinarySensor *connected_binary_sensor_{nullptr}; + binary_sensor::BinarySensor *boost_binary_sensor_{nullptr}; +#endif +#ifdef USE_TEXT_SENSOR + text_sensor::TextSensor *firmware_text_sensor_{nullptr}; +#endif +#ifdef USE_TIME + time::RealTimeClock *time_{nullptr}; #endif uint16_t char_handle_oper_{0}; + uint16_t char_handle_state_{0}; + uint16_t char_handle_info_{0}; + uint16_t char_handle_clock_{0}; uint8_t mode_{OPER_OFF}; uint8_t speed_{SPEED_OFF}; }; diff --git a/esphome/components/ecocomfort2/sensor.py b/esphome/components/ecocomfort2/sensor.py new file mode 100644 index 00000000000..878cc05f874 --- /dev/null +++ b/esphome/components/ecocomfort2/sensor.py @@ -0,0 +1,62 @@ +import esphome.codegen as cg +from esphome.components import sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_DIRECTION, + CONF_HUMIDITY, + CONF_TEMPERATURE, + CONF_VOC, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS, + ENTITY_CATEGORY_DIAGNOSTIC, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_PARTS_PER_BILLION, + UNIT_PERCENT, +) +from esphome.types import ConfigType + +from . import CONF_ECOCOMFORT2_ID, ECOCOMFORT2_CLIENT_SCHEMA + +CODEOWNERS = ["@gledian"] +DEPENDENCIES = ["ecocomfort2"] + +CONFIG_SCHEMA = cv.All( + ECOCOMFORT2_CLIENT_SCHEMA.extend( + { + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_VOC): sensor.sensor_schema( + unit_of_measurement=UNIT_PARTS_PER_BILLION, + accuracy_decimals=0, + device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_DIRECTION): sensor.sensor_schema( + accuracy_decimals=0, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } + ), + cv.has_at_least_one_key(CONF_TEMPERATURE, CONF_HUMIDITY, CONF_VOC, CONF_DIRECTION), +) + + +async def to_code(config: ConfigType) -> None: + hub = await cg.get_variable(config[CONF_ECOCOMFORT2_ID]) + sensors = sensor.sub_sensors(config) + await sensors(CONF_TEMPERATURE, hub.set_temperature_sensor) + await sensors(CONF_HUMIDITY, hub.set_humidity_sensor) + await sensors(CONF_VOC, hub.set_voc_sensor) + await sensors(CONF_DIRECTION, hub.set_direction_sensor) diff --git a/esphome/components/ecocomfort2/text_sensor.py b/esphome/components/ecocomfort2/text_sensor.py new file mode 100644 index 00000000000..4f2ac365399 --- /dev/null +++ b/esphome/components/ecocomfort2/text_sensor.py @@ -0,0 +1,24 @@ +import esphome.codegen as cg +from esphome.components import text_sensor +import esphome.config_validation as cv +from esphome.const import CONF_VERSION, ENTITY_CATEGORY_DIAGNOSTIC +from esphome.types import ConfigType + +from . import CONF_ECOCOMFORT2_ID, ECOCOMFORT2_CLIENT_SCHEMA + +CODEOWNERS = ["@gledian"] +DEPENDENCIES = ["ecocomfort2"] + +CONFIG_SCHEMA = ECOCOMFORT2_CLIENT_SCHEMA.extend( + { + cv.Required(CONF_VERSION): text_sensor.text_sensor_schema( + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } +) + + +async def to_code(config: ConfigType) -> None: + hub = await cg.get_variable(config[CONF_ECOCOMFORT2_ID]) + sensors = text_sensor.sub_text_sensors(config) + await sensors(CONF_VERSION, hub.set_firmware_text_sensor) diff --git a/tests/components/ecocomfort2/common.yaml b/tests/components/ecocomfort2/common.yaml index b4951e804f8..84fd33a983b 100644 --- a/tests/components/ecocomfort2/common.yaml +++ b/tests/components/ecocomfort2/common.yaml @@ -3,6 +3,14 @@ esp32_ble: auth_req_mode: sc_bond max_key_size: 16 +wifi: + ssid: MySSID + password: password1 + +time: + - platform: sntp + id: sntp_time + ble_client: - mac_address: 01:02:03:04:05:06 id: ecocomfort2_ble_1 @@ -12,6 +20,7 @@ ble_client: ecocomfort2: - id: ecocomfort2_hub_1 ble_client_id: ecocomfort2_ble_1 + time_id: sntp_time - id: ecocomfort2_hub_2 ble_client_id: ecocomfort2_ble_2 @@ -22,3 +31,29 @@ fan: - platform: ecocomfort2 name: Ecocomfort2 Fan 2 ecocomfort2_id: ecocomfort2_hub_2 + +sensor: + - platform: ecocomfort2 + ecocomfort2_id: ecocomfort2_hub_1 + temperature: + name: Ecocomfort2 Temperature + humidity: + name: Ecocomfort2 Humidity + voc: + name: Ecocomfort2 VOC + direction: + name: Ecocomfort2 Direction + +binary_sensor: + - platform: ecocomfort2 + ecocomfort2_id: ecocomfort2_hub_1 + connected: + name: Ecocomfort2 Connected + boost: + name: Ecocomfort2 Boost + +text_sensor: + - platform: ecocomfort2 + ecocomfort2_id: ecocomfort2_hub_2 + version: + name: Ecocomfort2 Firmware