From 0982ab58ac1ff8c027bae542908e204b5d053728 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Tue, 29 Oct 2024 19:53:36 +0100 Subject: [PATCH 01/46] fix build error (#7694) Co-authored-by: Tomasz Duda --- esphome/core/helpers.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 8f94f624f1..dae60a4e1d 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -691,9 +691,11 @@ void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parame void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter) wifi_get_macaddr(STATION_IF, mac); } -#elif defined(USE_RP2040) && defined(USE_WIFI) +#elif defined(USE_RP2040) void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter) +#ifdef USE_WIFI WiFi.macAddress(mac); +#endif } #elif defined(USE_LIBRETINY) void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter) From bac6880a1e9df93a38cb6a7ba467c28a78209c2f Mon Sep 17 00:00:00 2001 From: Ilia Sotnikov Date: Wed, 30 Oct 2024 02:32:55 +0300 Subject: [PATCH 02/46] fix: [climate] Allow substitutions in `visual.temperature_step.{target_temperature,current_temperature}` (#7679) --- esphome/components/climate/__init__.py | 29 +++++++++++++------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py index b302e2ab4e..ec68940726 100644 --- a/esphome/components/climate/__init__.py +++ b/esphome/components/climate/__init__.py @@ -119,10 +119,21 @@ visual_temperature = cv.float_with_unit( ) -def single_visual_temperature(value): - if isinstance(value, dict): - return value +VISUAL_TEMPERATURE_STEP_SCHEMA = cv.Schema( + { + cv.Required(CONF_TARGET_TEMPERATURE): visual_temperature, + cv.Required(CONF_CURRENT_TEMPERATURE): visual_temperature, + } +) + +def visual_temperature_step(value): + + # Allow defining target/current temperature steps separately + if isinstance(value, dict): + return VISUAL_TEMPERATURE_STEP_SCHEMA(value) + + # Otherwise, use the single value for both properties value = visual_temperature(value) return VISUAL_TEMPERATURE_STEP_SCHEMA( { @@ -141,16 +152,6 @@ ControlTrigger = climate_ns.class_( "ControlTrigger", automation.Trigger.template(ClimateCall.operator("ref")) ) -VISUAL_TEMPERATURE_STEP_SCHEMA = cv.Any( - single_visual_temperature, - cv.Schema( - { - cv.Required(CONF_TARGET_TEMPERATURE): visual_temperature, - cv.Required(CONF_CURRENT_TEMPERATURE): visual_temperature, - } - ), -) - CLIMATE_SCHEMA = ( cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA) .extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA) @@ -162,7 +163,7 @@ CLIMATE_SCHEMA = ( { cv.Optional(CONF_MIN_TEMPERATURE): cv.temperature, cv.Optional(CONF_MAX_TEMPERATURE): cv.temperature, - cv.Optional(CONF_TEMPERATURE_STEP): VISUAL_TEMPERATURE_STEP_SCHEMA, + cv.Optional(CONF_TEMPERATURE_STEP): visual_temperature_step, cv.Optional(CONF_MIN_HUMIDITY): cv.percentage_int, cv.Optional(CONF_MAX_HUMIDITY): cv.percentage_int, } From aae2ee2ecb34222ad61b52064f10006e86fc0fa3 Mon Sep 17 00:00:00 2001 From: Jordan Zucker Date: Tue, 29 Oct 2024 18:03:10 -0700 Subject: [PATCH 03/46] Add in area and device to the prometheus labels (#7692) --- .../prometheus/prometheus_handler.cpp | 151 ++++++++++++++++-- .../prometheus/prometheus_handler.h | 27 +++- tests/components/prometheus/common.yaml | 54 +++++++ 3 files changed, 208 insertions(+), 24 deletions(-) diff --git a/esphome/components/prometheus/prometheus_handler.cpp b/esphome/components/prometheus/prometheus_handler.cpp index 63b3fdf53f..5d1861202a 100644 --- a/esphome/components/prometheus/prometheus_handler.cpp +++ b/esphome/components/prometheus/prometheus_handler.cpp @@ -7,53 +7,56 @@ namespace prometheus { void PrometheusHandler::handleRequest(AsyncWebServerRequest *req) { AsyncResponseStream *stream = req->beginResponseStream("text/plain; version=0.0.4; charset=utf-8"); + std::string area = App.get_area(); + std::string node = App.get_name(); + std::string friendly_name = App.get_friendly_name(); #ifdef USE_SENSOR this->sensor_type_(stream); for (auto *obj : App.get_sensors()) - this->sensor_row_(stream, obj); + this->sensor_row_(stream, obj, area, node, friendly_name); #endif #ifdef USE_BINARY_SENSOR this->binary_sensor_type_(stream); for (auto *obj : App.get_binary_sensors()) - this->binary_sensor_row_(stream, obj); + this->binary_sensor_row_(stream, obj, area, node, friendly_name); #endif #ifdef USE_FAN this->fan_type_(stream); for (auto *obj : App.get_fans()) - this->fan_row_(stream, obj); + this->fan_row_(stream, obj, area, node, friendly_name); #endif #ifdef USE_LIGHT this->light_type_(stream); for (auto *obj : App.get_lights()) - this->light_row_(stream, obj); + this->light_row_(stream, obj, area, node, friendly_name); #endif #ifdef USE_COVER this->cover_type_(stream); for (auto *obj : App.get_covers()) - this->cover_row_(stream, obj); + this->cover_row_(stream, obj, area, node, friendly_name); #endif #ifdef USE_SWITCH this->switch_type_(stream); for (auto *obj : App.get_switches()) - this->switch_row_(stream, obj); + this->switch_row_(stream, obj, area, node, friendly_name); #endif #ifdef USE_LOCK this->lock_type_(stream); for (auto *obj : App.get_locks()) - this->lock_row_(stream, obj); + this->lock_row_(stream, obj, area, node, friendly_name); #endif #ifdef USE_TEXT_SENSOR this->text_sensor_type_(stream); for (auto *obj : App.get_text_sensors()) - this->text_sensor_row_(stream, obj); + this->text_sensor_row_(stream, obj, area, node, friendly_name); #endif req->send(stream); @@ -69,25 +72,53 @@ std::string PrometheusHandler::relabel_name_(EntityBase *obj) { return item == relabel_map_name_.end() ? obj->get_name() : item->second; } +void PrometheusHandler::add_area_label_(AsyncResponseStream *stream, std::string &area) { + if (!area.empty()) { + stream->print(F("\",area=\"")); + stream->print(area.c_str()); + } +} + +void PrometheusHandler::add_node_label_(AsyncResponseStream *stream, std::string &node) { + if (!node.empty()) { + stream->print(F("\",node=\"")); + stream->print(node.c_str()); + } +} + +void PrometheusHandler::add_friendly_name_label_(AsyncResponseStream *stream, std::string &friendly_name) { + if (!friendly_name.empty()) { + stream->print(F("\",friendly_name=\"")); + stream->print(friendly_name.c_str()); + } +} + // Type-specific implementation #ifdef USE_SENSOR void PrometheusHandler::sensor_type_(AsyncResponseStream *stream) { stream->print(F("#TYPE esphome_sensor_value gauge\n")); stream->print(F("#TYPE esphome_sensor_failed gauge\n")); } -void PrometheusHandler::sensor_row_(AsyncResponseStream *stream, sensor::Sensor *obj) { +void PrometheusHandler::sensor_row_(AsyncResponseStream *stream, sensor::Sensor *obj, std::string &area, + std::string &node, std::string &friendly_name) { if (obj->is_internal() && !this->include_internal_) return; if (!std::isnan(obj->state)) { // We have a valid value, output this value stream->print(F("esphome_sensor_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); stream->print(F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); stream->print(F("\"} 0\n")); // Data itself stream->print(F("esphome_sensor_value{id=\"")); stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); stream->print(F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); stream->print(F("\",unit=\"")); @@ -99,6 +130,9 @@ void PrometheusHandler::sensor_row_(AsyncResponseStream *stream, sensor::Sensor // Invalid state stream->print(F("esphome_sensor_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); stream->print(F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); stream->print(F("\"} 1\n")); @@ -112,19 +146,26 @@ void PrometheusHandler::binary_sensor_type_(AsyncResponseStream *stream) { stream->print(F("#TYPE esphome_binary_sensor_value gauge\n")); stream->print(F("#TYPE esphome_binary_sensor_failed gauge\n")); } -void PrometheusHandler::binary_sensor_row_(AsyncResponseStream *stream, binary_sensor::BinarySensor *obj) { +void PrometheusHandler::binary_sensor_row_(AsyncResponseStream *stream, binary_sensor::BinarySensor *obj, + std::string &area, std::string &node, std::string &friendly_name) { if (obj->is_internal() && !this->include_internal_) return; if (obj->has_state()) { // We have a valid value, output this value stream->print(F("esphome_binary_sensor_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); stream->print(F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); stream->print(F("\"} 0\n")); // Data itself stream->print(F("esphome_binary_sensor_value{id=\"")); stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); stream->print(F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); stream->print(F("\"} ")); @@ -134,6 +175,9 @@ void PrometheusHandler::binary_sensor_row_(AsyncResponseStream *stream, binary_s // Invalid state stream->print(F("esphome_binary_sensor_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); stream->print(F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); stream->print(F("\"} 1\n")); @@ -148,17 +192,24 @@ void PrometheusHandler::fan_type_(AsyncResponseStream *stream) { stream->print(F("#TYPE esphome_fan_speed gauge\n")); stream->print(F("#TYPE esphome_fan_oscillation gauge\n")); } -void PrometheusHandler::fan_row_(AsyncResponseStream *stream, fan::Fan *obj) { +void PrometheusHandler::fan_row_(AsyncResponseStream *stream, fan::Fan *obj, std::string &area, std::string &node, + std::string &friendly_name) { if (obj->is_internal() && !this->include_internal_) return; stream->print(F("esphome_fan_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); stream->print(F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); stream->print(F("\"} 0\n")); // Data itself stream->print(F("esphome_fan_value{id=\"")); stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); stream->print(F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); stream->print(F("\"} ")); @@ -168,6 +219,9 @@ void PrometheusHandler::fan_row_(AsyncResponseStream *stream, fan::Fan *obj) { if (obj->get_traits().supports_speed()) { stream->print(F("esphome_fan_speed{id=\"")); stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); stream->print(F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); stream->print(F("\"} ")); @@ -178,6 +232,9 @@ void PrometheusHandler::fan_row_(AsyncResponseStream *stream, fan::Fan *obj) { if (obj->get_traits().supports_oscillation()) { stream->print(F("esphome_fan_oscillation{id=\"")); stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); stream->print(F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); stream->print(F("\"} ")); @@ -193,12 +250,16 @@ void PrometheusHandler::light_type_(AsyncResponseStream *stream) { stream->print(F("#TYPE esphome_light_color gauge\n")); stream->print(F("#TYPE esphome_light_effect_active gauge\n")); } -void PrometheusHandler::light_row_(AsyncResponseStream *stream, light::LightState *obj) { +void PrometheusHandler::light_row_(AsyncResponseStream *stream, light::LightState *obj, std::string &area, + std::string &node, std::string &friendly_name) { if (obj->is_internal() && !this->include_internal_) return; // State stream->print(F("esphome_light_state{id=\"")); stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); stream->print(F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); stream->print(F("\"} ")); @@ -211,6 +272,9 @@ void PrometheusHandler::light_row_(AsyncResponseStream *stream, light::LightStat color.as_rgbw(&r, &g, &b, &w); stream->print(F("esphome_light_color{id=\"")); stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); stream->print(F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); stream->print(F("\",channel=\"brightness\"} ")); @@ -218,6 +282,9 @@ void PrometheusHandler::light_row_(AsyncResponseStream *stream, light::LightStat stream->print(F("\n")); stream->print(F("esphome_light_color{id=\"")); stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); stream->print(F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); stream->print(F("\",channel=\"r\"} ")); @@ -225,6 +292,9 @@ void PrometheusHandler::light_row_(AsyncResponseStream *stream, light::LightStat stream->print(F("\n")); stream->print(F("esphome_light_color{id=\"")); stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); stream->print(F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); stream->print(F("\",channel=\"g\"} ")); @@ -232,6 +302,9 @@ void PrometheusHandler::light_row_(AsyncResponseStream *stream, light::LightStat stream->print(F("\n")); stream->print(F("esphome_light_color{id=\"")); stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); stream->print(F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); stream->print(F("\",channel=\"b\"} ")); @@ -239,6 +312,9 @@ void PrometheusHandler::light_row_(AsyncResponseStream *stream, light::LightStat stream->print(F("\n")); stream->print(F("esphome_light_color{id=\"")); stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); stream->print(F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); stream->print(F("\",channel=\"w\"} ")); @@ -249,12 +325,18 @@ void PrometheusHandler::light_row_(AsyncResponseStream *stream, light::LightStat if (effect == "None") { stream->print(F("esphome_light_effect_active{id=\"")); stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); stream->print(F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); stream->print(F("\",effect=\"None\"} 0\n")); } else { stream->print(F("esphome_light_effect_active{id=\"")); stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); stream->print(F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); stream->print(F("\",effect=\"")); @@ -269,19 +351,26 @@ void PrometheusHandler::cover_type_(AsyncResponseStream *stream) { stream->print(F("#TYPE esphome_cover_value gauge\n")); stream->print(F("#TYPE esphome_cover_failed gauge\n")); } -void PrometheusHandler::cover_row_(AsyncResponseStream *stream, cover::Cover *obj) { +void PrometheusHandler::cover_row_(AsyncResponseStream *stream, cover::Cover *obj, std::string &area, std::string &node, + std::string &friendly_name) { if (obj->is_internal() && !this->include_internal_) return; if (!std::isnan(obj->position)) { // We have a valid value, output this value stream->print(F("esphome_cover_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); stream->print(F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); stream->print(F("\"} 0\n")); // Data itself stream->print(F("esphome_cover_value{id=\"")); stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); stream->print(F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); stream->print(F("\"} ")); @@ -290,6 +379,9 @@ void PrometheusHandler::cover_row_(AsyncResponseStream *stream, cover::Cover *ob if (obj->get_traits().get_supports_tilt()) { stream->print(F("esphome_cover_tilt{id=\"")); stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); stream->print(F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); stream->print(F("\"} ")); @@ -300,6 +392,9 @@ void PrometheusHandler::cover_row_(AsyncResponseStream *stream, cover::Cover *ob // Invalid state stream->print(F("esphome_cover_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); stream->print(F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); stream->print(F("\"} 1\n")); @@ -312,17 +407,24 @@ void PrometheusHandler::switch_type_(AsyncResponseStream *stream) { stream->print(F("#TYPE esphome_switch_value gauge\n")); stream->print(F("#TYPE esphome_switch_failed gauge\n")); } -void PrometheusHandler::switch_row_(AsyncResponseStream *stream, switch_::Switch *obj) { +void PrometheusHandler::switch_row_(AsyncResponseStream *stream, switch_::Switch *obj, std::string &area, + std::string &node, std::string &friendly_name) { if (obj->is_internal() && !this->include_internal_) return; stream->print(F("esphome_switch_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); stream->print(F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); stream->print(F("\"} 0\n")); // Data itself stream->print(F("esphome_switch_value{id=\"")); stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); stream->print(F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); stream->print(F("\"} ")); @@ -336,17 +438,24 @@ void PrometheusHandler::lock_type_(AsyncResponseStream *stream) { stream->print(F("#TYPE esphome_lock_value gauge\n")); stream->print(F("#TYPE esphome_lock_failed gauge\n")); } -void PrometheusHandler::lock_row_(AsyncResponseStream *stream, lock::Lock *obj) { +void PrometheusHandler::lock_row_(AsyncResponseStream *stream, lock::Lock *obj, std::string &area, std::string &node, + std::string &friendly_name) { if (obj->is_internal() && !this->include_internal_) return; stream->print(F("esphome_lock_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); stream->print(F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); stream->print(F("\"} 0\n")); // Data itself stream->print(F("esphome_lock_value{id=\"")); stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); stream->print(F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); stream->print(F("\"} ")); @@ -361,19 +470,26 @@ void PrometheusHandler::text_sensor_type_(AsyncResponseStream *stream) { stream->print(F("#TYPE esphome_text_sensor_value gauge\n")); stream->print(F("#TYPE esphome_text_sensor_failed gauge\n")); } -void PrometheusHandler::text_sensor_row_(AsyncResponseStream *stream, text_sensor::TextSensor *obj) { +void PrometheusHandler::text_sensor_row_(AsyncResponseStream *stream, text_sensor::TextSensor *obj, std::string &area, + std::string &node, std::string &friendly_name) { if (obj->is_internal() && !this->include_internal_) return; if (obj->has_state()) { // We have a valid value, output this value stream->print(F("esphome_text_sensor_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); stream->print(F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); stream->print(F("\"} 0\n")); // Data itself stream->print(F("esphome_text_sensor_value{id=\"")); stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); stream->print(F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); stream->print(F("\",value=\"")); @@ -385,6 +501,9 @@ void PrometheusHandler::text_sensor_row_(AsyncResponseStream *stream, text_senso // Invalid state stream->print(F("esphome_text_sensor_failed{id=\"")); stream->print(relabel_id_(obj).c_str()); + add_area_label_(stream, area); + add_node_label_(stream, node); + add_friendly_name_label_(stream, friendly_name); stream->print(F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); stream->print(F("\"} 1\n")); diff --git a/esphome/components/prometheus/prometheus_handler.h b/esphome/components/prometheus/prometheus_handler.h index 512e1bee4f..5d08aca63a 100644 --- a/esphome/components/prometheus/prometheus_handler.h +++ b/esphome/components/prometheus/prometheus_handler.h @@ -60,61 +60,72 @@ class PrometheusHandler : public AsyncWebHandler, public Component { protected: std::string relabel_id_(EntityBase *obj); std::string relabel_name_(EntityBase *obj); + void add_area_label_(AsyncResponseStream *stream, std::string &area); + void add_node_label_(AsyncResponseStream *stream, std::string &node); + void add_friendly_name_label_(AsyncResponseStream *stream, std::string &friendly_name); #ifdef USE_SENSOR /// Return the type for prometheus void sensor_type_(AsyncResponseStream *stream); /// Return the sensor state as prometheus data point - void sensor_row_(AsyncResponseStream *stream, sensor::Sensor *obj); + void sensor_row_(AsyncResponseStream *stream, sensor::Sensor *obj, std::string &area, std::string &node, + std::string &friendly_name); #endif #ifdef USE_BINARY_SENSOR /// Return the type for prometheus void binary_sensor_type_(AsyncResponseStream *stream); /// Return the sensor state as prometheus data point - void binary_sensor_row_(AsyncResponseStream *stream, binary_sensor::BinarySensor *obj); + void binary_sensor_row_(AsyncResponseStream *stream, binary_sensor::BinarySensor *obj, std::string &area, + std::string &node, std::string &friendly_name); #endif #ifdef USE_FAN /// Return the type for prometheus void fan_type_(AsyncResponseStream *stream); /// Return the sensor state as prometheus data point - void fan_row_(AsyncResponseStream *stream, fan::Fan *obj); + void fan_row_(AsyncResponseStream *stream, fan::Fan *obj, std::string &area, std::string &node, + std::string &friendly_name); #endif #ifdef USE_LIGHT /// Return the type for prometheus void light_type_(AsyncResponseStream *stream); /// Return the Light Values state as prometheus data point - void light_row_(AsyncResponseStream *stream, light::LightState *obj); + void light_row_(AsyncResponseStream *stream, light::LightState *obj, std::string &area, std::string &node, + std::string &friendly_name); #endif #ifdef USE_COVER /// Return the type for prometheus void cover_type_(AsyncResponseStream *stream); /// Return the switch Values state as prometheus data point - void cover_row_(AsyncResponseStream *stream, cover::Cover *obj); + void cover_row_(AsyncResponseStream *stream, cover::Cover *obj, std::string &area, std::string &node, + std::string &friendly_name); #endif #ifdef USE_SWITCH /// Return the type for prometheus void switch_type_(AsyncResponseStream *stream); /// Return the switch Values state as prometheus data point - void switch_row_(AsyncResponseStream *stream, switch_::Switch *obj); + void switch_row_(AsyncResponseStream *stream, switch_::Switch *obj, std::string &area, std::string &node, + std::string &friendly_name); #endif #ifdef USE_LOCK /// Return the type for prometheus void lock_type_(AsyncResponseStream *stream); /// Return the lock Values state as prometheus data point - void lock_row_(AsyncResponseStream *stream, lock::Lock *obj); + void lock_row_(AsyncResponseStream *stream, lock::Lock *obj, std::string &area, std::string &node, + std::string &friendly_name); #endif #ifdef USE_TEXT_SENSOR /// Return the type for prometheus void text_sensor_type_(AsyncResponseStream *stream); /// Return the lock Values state as prometheus data point - void text_sensor_row_(AsyncResponseStream *stream, text_sensor::TextSensor *obj); + void text_sensor_row_(AsyncResponseStream *stream, text_sensor::TextSensor *obj, std::string &area, std::string &node, + std::string &friendly_name); #endif web_server_base::WebServerBase *base_; diff --git a/tests/components/prometheus/common.yaml b/tests/components/prometheus/common.yaml index 7aa509f8b2..68ef2a2f58 100644 --- a/tests/components/prometheus/common.yaml +++ b/tests/components/prometheus/common.yaml @@ -1,3 +1,8 @@ +esphome: + name: livingroomdevice + friendly_name: Living Room Device + area: Living Room + wifi: ssid: MySSID password: password1 @@ -14,6 +19,9 @@ sensor: update_interval: 60s text_sensor: + - platform: version + name: "ESPHome Version" + hide_timestamp: true - platform: template id: template_text_sensor1 lambda: |- @@ -24,6 +32,52 @@ text_sensor: } update_interval: 60s +binary_sensor: + - platform: template + id: template_binary_sensor1 + lambda: |- + if (millis() > 10000) { + return true; + } else { + return false; + } + +switch: + - platform: template + id: template_switch1 + lambda: |- + if (millis() > 10000) { + return true; + } else { + return false; + } + optimistic: true + +fan: + - platform: template + id: template_fan1 + +cover: + - platform: template + id: template_cover1 + lambda: |- + if (millis() > 10000) { + return COVER_OPEN; + } else { + return COVER_CLOSED; + } + +lock: + - platform: template + id: template_lock1 + lambda: |- + if (millis() > 10000) { + return LOCK_STATE_LOCKED; + } else { + return LOCK_STATE_UNLOCKED; + } + optimistic: true + prometheus: include_internal: true relabel: From ee3ee3a63b784767ea907f2f52935db34f0e1267 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 30 Oct 2024 12:10:58 +1100 Subject: [PATCH 04/46] [http_request] Implement `on_error` trigger for requests (#7696) --- esphome/components/http_request/__init__.py | 12 ++++++++++++ esphome/components/http_request/http_request.h | 11 ++++++++--- tests/components/http_request/common.yaml | 2 ++ 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/esphome/components/http_request/__init__.py b/esphome/components/http_request/__init__.py index 0407bbd326..78064fb4b4 100644 --- a/esphome/components/http_request/__init__.py +++ b/esphome/components/http_request/__init__.py @@ -6,6 +6,7 @@ from esphome.const import ( CONF_ESP8266_DISABLE_SSL_SUPPORT, CONF_ID, CONF_METHOD, + CONF_ON_ERROR, CONF_TIMEOUT, CONF_TRIGGER_ID, CONF_URL, @@ -185,6 +186,13 @@ HTTP_REQUEST_ACTION_SCHEMA = cv.Schema( cv.Optional(CONF_ON_RESPONSE): automation.validate_automation( {cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(HttpRequestResponseTrigger)} ), + cv.Optional(CONF_ON_ERROR): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + automation.Trigger.template() + ) + } + ), cv.Optional(CONF_MAX_RESPONSE_BUFFER_SIZE, default="1kB"): cv.validate_bytes, } ) @@ -272,5 +280,9 @@ async def http_request_action_to_code(config, action_id, template_arg, args): ], conf, ) + for conf in config.get(CONF_ON_ERROR, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) + cg.add(var.register_error_trigger(trigger)) + await automation.build_automation(trigger, [], conf) return var diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h index d87d9b8a45..4ed2c834f8 100644 --- a/esphome/components/http_request/http_request.h +++ b/esphome/components/http_request/http_request.h @@ -135,8 +135,8 @@ class HttpRequestComponent : public Component { protected: const char *useragent_{nullptr}; - bool follow_redirects_; - uint16_t redirect_limit_; + bool follow_redirects_{}; + uint16_t redirect_limit_{}; uint16_t timeout_{4500}; uint32_t watchdog_timeout_{0}; }; @@ -157,6 +157,8 @@ template class HttpRequestSendAction : public Action { void register_response_trigger(HttpRequestResponseTrigger *trigger) { this->response_triggers_.push_back(trigger); } + void register_error_trigger(Trigger<> *trigger) { this->error_triggers_.push_back(trigger); } + void set_max_response_buffer_size(size_t max_response_buffer_size) { this->max_response_buffer_size_ = max_response_buffer_size; } @@ -186,6 +188,8 @@ template class HttpRequestSendAction : public Action { auto container = this->parent_->start(this->url_.value(x...), this->method_.value(x...), body, headers); if (container == nullptr) { + for (auto *trigger : this->error_triggers_) + trigger->trigger(x...); return; } @@ -237,7 +241,8 @@ template class HttpRequestSendAction : public Action { std::map> headers_{}; std::map> json_{}; std::function json_func_{nullptr}; - std::vector response_triggers_; + std::vector response_triggers_{}; + std::vector *> error_triggers_{}; size_t max_response_buffer_size_{SIZE_MAX}; }; diff --git a/tests/components/http_request/common.yaml b/tests/components/http_request/common.yaml index 589b7fb4b4..593b85e435 100644 --- a/tests/components/http_request/common.yaml +++ b/tests/components/http_request/common.yaml @@ -12,6 +12,8 @@ esphome: url: https://esphome.io headers: Content-Type: application/json + on_error: + logger.log: "Request failed" on_response: then: - logger.log: From 6afd004ec54670ed27cd29202a7c290d258a8b64 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 31 Oct 2024 08:25:36 +1300 Subject: [PATCH 05/46] Bump pypa/gh-action-pypi-publish from 1.10.3 to 1.11.0 (#7700) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5072bec222..82d7ae5ee8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -65,7 +65,7 @@ jobs: pip3 install build python3 -m build - name: Publish - uses: pypa/gh-action-pypi-publish@v1.10.3 + uses: pypa/gh-action-pypi-publish@v1.11.0 deploy-docker: name: Build ESPHome ${{ matrix.platform }} From 765579dabbd607975901b34b350c4252f80ca3ab Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Wed, 30 Oct 2024 15:29:24 -0400 Subject: [PATCH 06/46] [es8311] Add es8311 dac component (#7693) --- CODEOWNERS | 1 + esphome/components/es8311/__init__.py | 0 esphome/components/es8311/audio_dac.py | 70 ++++++ esphome/components/es8311/es8311.cpp | 227 ++++++++++++++++++ esphome/components/es8311/es8311.h | 135 +++++++++++ esphome/components/es8311/es8311_const.h | 195 +++++++++++++++ esphome/components/i2s_audio/__init__.py | 4 +- esphome/const.py | 1 + tests/components/es8311/common.yaml | 15 ++ tests/components/es8311/test.esp32-ard.yaml | 5 + .../components/es8311/test.esp32-c3-ard.yaml | 5 + .../components/es8311/test.esp32-c3-idf.yaml | 5 + tests/components/es8311/test.esp32-idf.yaml | 5 + tests/components/es8311/test.esp8266-ard.yaml | 5 + 14 files changed, 670 insertions(+), 3 deletions(-) create mode 100644 esphome/components/es8311/__init__.py create mode 100644 esphome/components/es8311/audio_dac.py create mode 100644 esphome/components/es8311/es8311.cpp create mode 100644 esphome/components/es8311/es8311.h create mode 100644 esphome/components/es8311/es8311_const.h create mode 100644 tests/components/es8311/common.yaml create mode 100644 tests/components/es8311/test.esp32-ard.yaml create mode 100644 tests/components/es8311/test.esp32-c3-ard.yaml create mode 100644 tests/components/es8311/test.esp32-c3-idf.yaml create mode 100644 tests/components/es8311/test.esp32-idf.yaml create mode 100644 tests/components/es8311/test.esp8266-ard.yaml diff --git a/CODEOWNERS b/CODEOWNERS index 5eb1f863f2..8fbbacef59 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -131,6 +131,7 @@ esphome/components/ens160_base/* @latonita @vincentscode esphome/components/ens160_i2c/* @latonita esphome/components/ens160_spi/* @latonita esphome/components/ens210/* @itn3rd77 +esphome/components/es8311/* @kahrendt @kroimon esphome/components/esp32/* @esphome/core esphome/components/esp32_ble/* @Rapsssito @jesserockz esphome/components/esp32_ble_client/* @jesserockz diff --git a/esphome/components/es8311/__init__.py b/esphome/components/es8311/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/es8311/audio_dac.py b/esphome/components/es8311/audio_dac.py new file mode 100644 index 0000000000..1b450c3c11 --- /dev/null +++ b/esphome/components/es8311/audio_dac.py @@ -0,0 +1,70 @@ +import esphome.codegen as cg +from esphome.components import i2c +from esphome.components.audio_dac import AudioDac +import esphome.config_validation as cv +from esphome.const import CONF_BITS_PER_SAMPLE, CONF_ID, CONF_SAMPLE_RATE + +CODEOWNERS = ["@kroimon", "@kahrendt"] +DEPENDENCIES = ["i2c"] + +es8311_ns = cg.esphome_ns.namespace("es8311") +ES8311 = es8311_ns.class_("ES8311", AudioDac, cg.Component, i2c.I2CDevice) + +CONF_MIC_GAIN = "mic_gain" +CONF_USE_MCLK = "use_mclk" +CONF_USE_MICROPHONE = "use_microphone" + +es8311_resolution = es8311_ns.enum("ES8311Resolution") +ES8311_BITS_PER_SAMPLE_ENUM = { + 16: es8311_resolution.ES8311_RESOLUTION_16, + 24: es8311_resolution.ES8311_RESOLUTION_24, + 32: es8311_resolution.ES8311_RESOLUTION_32, +} + +es8311_mic_gain = es8311_ns.enum("ES8311MicGain") +ES8311_MIC_GAIN_ENUM = { + "MIN": es8311_mic_gain.ES8311_MIC_GAIN_MIN, + "0DB": es8311_mic_gain.ES8311_MIC_GAIN_0DB, + "6DB": es8311_mic_gain.ES8311_MIC_GAIN_6DB, + "12DB": es8311_mic_gain.ES8311_MIC_GAIN_12DB, + "18DB": es8311_mic_gain.ES8311_MIC_GAIN_18DB, + "24DB": es8311_mic_gain.ES8311_MIC_GAIN_24DB, + "30DB": es8311_mic_gain.ES8311_MIC_GAIN_30DB, + "36DB": es8311_mic_gain.ES8311_MIC_GAIN_36DB, + "42DB": es8311_mic_gain.ES8311_MIC_GAIN_42DB, + "MAX": es8311_mic_gain.ES8311_MIC_GAIN_MAX, +} + + +_validate_bits = cv.float_with_unit("bits", "bit") + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(ES8311), + cv.Optional(CONF_BITS_PER_SAMPLE, default="16bit"): cv.All( + _validate_bits, cv.enum(ES8311_BITS_PER_SAMPLE_ENUM) + ), + cv.Optional(CONF_MIC_GAIN, default="42DB"): cv.enum( + ES8311_MIC_GAIN_ENUM, upper=True + ), + cv.Optional(CONF_SAMPLE_RATE, default=16000): cv.int_range(min=1), + cv.Optional(CONF_USE_MCLK, default=True): cv.boolean, + cv.Optional(CONF_USE_MICROPHONE, default=False): cv.boolean, + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(0x18)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + + cg.add(var.set_bits_per_sample(config[CONF_BITS_PER_SAMPLE])) + cg.add(var.set_mic_gain(config[CONF_MIC_GAIN])) + cg.add(var.set_sample_frequency(config[CONF_SAMPLE_RATE])) + cg.add(var.set_use_mclk(config[CONF_USE_MCLK])) + cg.add(var.set_use_mic(config[CONF_USE_MICROPHONE])) diff --git a/esphome/components/es8311/es8311.cpp b/esphome/components/es8311/es8311.cpp new file mode 100644 index 0000000000..1cb1fbbe08 --- /dev/null +++ b/esphome/components/es8311/es8311.cpp @@ -0,0 +1,227 @@ +#include "es8311.h" +#include "es8311_const.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" +#include + +namespace esphome { +namespace es8311 { + +static const char *const TAG = "es8311"; + +// Mark the component as failed; use only in setup +#define ES8311_ERROR_FAILED(func) \ + if (!(func)) { \ + this->mark_failed(); \ + return; \ + } +// Return false; use outside of setup +#define ES8311_ERROR_CHECK(func) \ + if (!(func)) { \ + return false; \ + } + +void ES8311::setup() { + ESP_LOGCONFIG(TAG, "Setting up ES8311..."); + + // Reset + ES8311_ERROR_FAILED(this->write_byte(ES8311_REG00_RESET, 0x1F)); + ES8311_ERROR_FAILED(this->write_byte(ES8311_REG00_RESET, 0x00)); + + ES8311_ERROR_FAILED(this->configure_clock_()); + ES8311_ERROR_FAILED(this->configure_format_()); + ES8311_ERROR_FAILED(this->configure_mic_()); + + // Set initial volume + this->set_volume(0.75); // 0.75 = 0xBF = 0dB + + // Power up analog circuitry + ES8311_ERROR_FAILED(this->write_byte(ES8311_REG0D_SYSTEM, 0x01)); + // Enable analog PGA, enable ADC modulator + ES8311_ERROR_FAILED(this->write_byte(ES8311_REG0E_SYSTEM, 0x02)); + // Power up DAC + ES8311_ERROR_FAILED(this->write_byte(ES8311_REG12_SYSTEM, 0x00)); + // Enable output to HP drive + ES8311_ERROR_FAILED(this->write_byte(ES8311_REG13_SYSTEM, 0x10)); + // ADC Equalizer bypass, cancel DC offset in digital domain + ES8311_ERROR_FAILED(this->write_byte(ES8311_REG1C_ADC, 0x6A)); + // Bypass DAC equalizer + ES8311_ERROR_FAILED(this->write_byte(ES8311_REG37_DAC, 0x08)); + // Power On + ES8311_ERROR_FAILED(this->write_byte(ES8311_REG00_RESET, 0x80)); +} + +void ES8311::dump_config() { + ESP_LOGCONFIG(TAG, "ES8311 Audio Codec:"); + ESP_LOGCONFIG(TAG, " Use MCLK: %s", YESNO(this->use_mclk_)); + ESP_LOGCONFIG(TAG, " Use Microphone: %s", YESNO(this->use_mic_)); + ESP_LOGCONFIG(TAG, " DAC Bits per Sample: %" PRIu8, this->resolution_out_); + ESP_LOGCONFIG(TAG, " Sample Rate: %" PRIu32, this->sample_frequency_); + + if (this->is_failed()) { + ESP_LOGCONFIG(TAG, " Failed to initialize!"); + return; + } +} + +bool ES8311::set_volume(float volume) { + volume = clamp(volume, 0.0f, 1.0f); + uint8_t reg32 = remap(volume, 0.0f, 1.0f, 0, 255); + return this->write_byte(ES8311_REG32_DAC, reg32); +} + +float ES8311::volume() { + uint8_t reg32; + this->read_byte(ES8311_REG32_DAC, ®32); + return remap(reg32, 0, 255, 0.0f, 1.0f); +} + +uint8_t ES8311::calculate_resolution_value(ES8311Resolution resolution) { + switch (resolution) { + case ES8311_RESOLUTION_16: + return (3 << 2); + case ES8311_RESOLUTION_18: + return (2 << 2); + case ES8311_RESOLUTION_20: + return (1 << 2); + case ES8311_RESOLUTION_24: + return (0 << 2); + case ES8311_RESOLUTION_32: + return (4 << 2); + default: + return 0; + } +} + +const ES8311Coefficient *ES8311::get_coefficient(uint32_t mclk, uint32_t rate) { + for (const auto &coefficient : ES8311_COEFFICIENTS) { + if (coefficient.mclk == mclk && coefficient.rate == rate) + return &coefficient; + } + return nullptr; +} + +bool ES8311::configure_clock_() { + // Register 0x01: select clock source for internal MCLK and determine its frequency + uint8_t reg01 = 0x3F; // Enable all clocks + + uint32_t mclk_frequency = this->sample_frequency_ * this->mclk_multiple_; + if (!this->use_mclk_) { + reg01 |= BIT(7); // Use SCLK + mclk_frequency = this->sample_frequency_ * (int) this->resolution_out_ * 2; + } + if (this->mclk_inverted_) { + reg01 |= BIT(6); // Invert MCLK pin + } + ES8311_ERROR_CHECK(this->write_byte(ES8311_REG01_CLK_MANAGER, reg01)); + + // Get clock coefficients from coefficient table + auto *coefficient = get_coefficient(mclk_frequency, this->sample_frequency_); + if (coefficient == nullptr) { + ESP_LOGE(TAG, "Unable to configure sample rate %" PRIu32 "Hz with %" PRIu32 "Hz MCLK", this->sample_frequency_, + mclk_frequency); + return false; + } + + // Register 0x02 + uint8_t reg02; + ES8311_ERROR_CHECK(this->read_byte(ES8311_REG02_CLK_MANAGER, ®02)); + reg02 &= 0x07; + reg02 |= (coefficient->pre_div - 1) << 5; + reg02 |= coefficient->pre_mult << 3; + ES8311_ERROR_CHECK(this->write_byte(ES8311_REG02_CLK_MANAGER, reg02)); + + // Register 0x03 + const uint8_t reg03 = (coefficient->fs_mode << 6) | coefficient->adc_osr; + ES8311_ERROR_CHECK(this->write_byte(ES8311_REG03_CLK_MANAGER, reg03)); + + // Register 0x04 + ES8311_ERROR_CHECK(this->write_byte(ES8311_REG04_CLK_MANAGER, coefficient->dac_osr)); + + // Register 0x05 + const uint8_t reg05 = ((coefficient->adc_div - 1) << 4) | (coefficient->dac_div - 1); + ES8311_ERROR_CHECK(this->write_byte(ES8311_REG05_CLK_MANAGER, reg05)); + + // Register 0x06 + uint8_t reg06; + ES8311_ERROR_CHECK(this->read_byte(ES8311_REG06_CLK_MANAGER, ®06)); + if (this->sclk_inverted_) { + reg06 |= BIT(5); + } else { + reg06 &= ~BIT(5); + } + reg06 &= 0xE0; + if (coefficient->bclk_div < 19) { + reg06 |= (coefficient->bclk_div - 1) << 0; + } else { + reg06 |= (coefficient->bclk_div) << 0; + } + ES8311_ERROR_CHECK(this->write_byte(ES8311_REG06_CLK_MANAGER, reg06)); + + // Register 0x07 + uint8_t reg07; + ES8311_ERROR_CHECK(this->read_byte(ES8311_REG07_CLK_MANAGER, ®07)); + reg07 &= 0xC0; + reg07 |= coefficient->lrck_h << 0; + ES8311_ERROR_CHECK(this->write_byte(ES8311_REG07_CLK_MANAGER, reg07)); + + // Register 0x08 + ES8311_ERROR_CHECK(this->write_byte(ES8311_REG08_CLK_MANAGER, coefficient->lrck_l)); + + // Successfully configured the clock + return true; +} + +bool ES8311::configure_format_() { + // Configure I2S mode and format + uint8_t reg00; + ES8311_ERROR_CHECK(this->read_byte(ES8311_REG00_RESET, ®00)); + reg00 &= 0xBF; + ES8311_ERROR_CHECK(this->write_byte(ES8311_REG00_RESET, reg00)); + + // Configure SDP in resolution + uint8_t reg09 = calculate_resolution_value(this->resolution_in_); + ES8311_ERROR_CHECK(this->write_byte(ES8311_REG09_SDPIN, reg09)); + + // Configure SDP out resolution + uint8_t reg0a = calculate_resolution_value(this->resolution_out_); + ES8311_ERROR_CHECK(this->write_byte(ES8311_REG0A_SDPOUT, reg0a)); + + // Successfully configured the format + return true; +} + +bool ES8311::configure_mic_() { + uint8_t reg14 = 0x1A; // Enable analog MIC and max PGA gain + if (this->use_mic_) { + reg14 |= BIT(6); // Enable PDM digital microphone + } + ES8311_ERROR_CHECK(this->write_byte(ES8311_REG14_SYSTEM, reg14)); + + ES8311_ERROR_CHECK(this->write_byte(ES8311_REG16_ADC, this->mic_gain_)); // ADC gain scale up + ES8311_ERROR_CHECK(this->write_byte(ES8311_REG17_ADC, 0xC8)); // Set ADC gain + + // Successfully configured the microphones + return true; +} + +bool ES8311::set_mute_state_(bool mute_state) { + uint8_t reg31; + + this->is_muted_ = mute_state; + + if (!this->read_byte(ES8311_REG31_DAC, ®31)) { + return false; + } + + if (mute_state) { + reg31 |= BIT(6) | BIT(5); + } else { + reg31 &= ~(BIT(6) | BIT(5)); + } + + return this->write_byte(ES8311_REG31_DAC, reg31); +} + +} // namespace es8311 +} // namespace esphome diff --git a/esphome/components/es8311/es8311.h b/esphome/components/es8311/es8311.h new file mode 100644 index 0000000000..840a07204c --- /dev/null +++ b/esphome/components/es8311/es8311.h @@ -0,0 +1,135 @@ +#pragma once + +#include "esphome/components/audio_dac/audio_dac.h" +#include "esphome/components/i2c/i2c.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace es8311 { + +enum ES8311MicGain { + ES8311_MIC_GAIN_MIN = -1, + ES8311_MIC_GAIN_0DB, + ES8311_MIC_GAIN_6DB, + ES8311_MIC_GAIN_12DB, + ES8311_MIC_GAIN_18DB, + ES8311_MIC_GAIN_24DB, + ES8311_MIC_GAIN_30DB, + ES8311_MIC_GAIN_36DB, + ES8311_MIC_GAIN_42DB, + ES8311_MIC_GAIN_MAX +}; + +enum ES8311Resolution : uint8_t { + ES8311_RESOLUTION_16 = 16, + ES8311_RESOLUTION_18 = 18, + ES8311_RESOLUTION_20 = 20, + ES8311_RESOLUTION_24 = 24, + ES8311_RESOLUTION_32 = 32 +}; + +struct ES8311Coefficient { + uint32_t mclk; // mclk frequency + uint32_t rate; // sample rate + uint8_t pre_div; // the pre divider with range from 1 to 8 + uint8_t pre_mult; // the pre multiplier with x1, x2, x4 and x8 selection + uint8_t adc_div; // adcclk divider + uint8_t dac_div; // dacclk divider + uint8_t fs_mode; // single speed (0) or double speed (1) + uint8_t lrck_h; // adc lrck divider and dac lrck divider + uint8_t lrck_l; // + uint8_t bclk_div; // sclk divider + uint8_t adc_osr; // adc osr + uint8_t dac_osr; // dac osr +}; + +class ES8311 : public audio_dac::AudioDac, public Component, public i2c::I2CDevice { + public: + ///////////////////////// + // Component overrides // + ///////////////////////// + + void setup() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void dump_config() override; + + //////////////////////// + // AudioDac overrides // + //////////////////////// + + /// @brief Writes the volume out to the DAC + /// @param volume floating point between 0.0 and 1.0 + /// @return True if successful and false otherwise + bool set_volume(float volume) override; + + /// @brief Gets the current volume out from the DAC + /// @return floating point between 0.0 and 1.0 + float volume() override; + + /// @brief Disables mute for audio out + /// @return True if successful and false otherwise + bool set_mute_off() override { return this->set_mute_state_(false); } + + /// @brief Enables mute for audio out + /// @return True if successful and false otherwise + bool set_mute_on() override { return this->set_mute_state_(true); } + + bool is_muted() override { return this->is_muted_; } + + ////////////////////////////////// + // ES8311 configuration setters // + ////////////////////////////////// + + void set_use_mclk(bool use_mclk) { this->use_mclk_ = use_mclk; } + void set_bits_per_sample(ES8311Resolution resolution) { + this->resolution_in_ = resolution; + this->resolution_out_ = resolution; + } + void set_sample_frequency(uint32_t sample_frequency) { this->sample_frequency_ = sample_frequency; } + void set_use_mic(bool use_mic) { this->use_mic_ = use_mic; } + void set_mic_gain(ES8311MicGain mic_gain) { this->mic_gain_ = mic_gain; } + + protected: + /// @brief Computes the register value for the configured resolution (bits per sample) + /// @param resolution bits per sample enum for both audio in and audio out + /// @return register value + static uint8_t calculate_resolution_value(ES8311Resolution resolution); + + /// @brief Retrieves the appropriate registers values for the configured mclk and rate + /// @param mclk mlck frequency in Hz + /// @param rate sample rate frequency in Hz + /// @return ES8311Coeffecient containing appropriate register values to configure the ES8311 or nullptr if impossible + static const ES8311Coefficient *get_coefficient(uint32_t mclk, uint32_t rate); + + /// @brief Configures the ES8311 registers for the chosen sample rate + /// @return True if successful and false otherwise + bool configure_clock_(); + + /// @brief Configures the ES8311 registers for the chosen bits per sample + /// @return True if successful and false otherwise + bool configure_format_(); + + /// @brief Configures the ES8311 microphone registers + /// @return True if successful and false otherwise + bool configure_mic_(); + + /// @brief Mutes or unmute the DAC audio out + /// @param mute_state True to mute, false to unmute + /// @return + bool set_mute_state_(bool mute_state); + + bool use_mic_; + ES8311MicGain mic_gain_; + + bool use_mclk_; // true = use dedicated MCLK pin, false = use SCLK + bool sclk_inverted_{false}; // SCLK is inverted + bool mclk_inverted_{false}; // MCLK is inverted (ignored if use_mclk_ == false) + uint32_t mclk_multiple_{256}; // MCLK frequency is sample rate * mclk_multiple_ (ignored if use_mclk_ == false) + + uint32_t sample_frequency_; // in Hz + ES8311Resolution resolution_in_; + ES8311Resolution resolution_out_; +}; + +} // namespace es8311 +} // namespace esphome diff --git a/esphome/components/es8311/es8311_const.h b/esphome/components/es8311/es8311_const.h new file mode 100644 index 0000000000..7463a92ef1 --- /dev/null +++ b/esphome/components/es8311/es8311_const.h @@ -0,0 +1,195 @@ +#pragma once + +#include "es8311.h" + +namespace esphome { +namespace es8311 { + +// ES8311 register addresses +static const uint8_t ES8311_REG00_RESET = 0x00; // Reset +static const uint8_t ES8311_REG01_CLK_MANAGER = 0x01; // Clock Manager: select clk src for mclk, enable clock for codec +static const uint8_t ES8311_REG02_CLK_MANAGER = 0x02; // Clock Manager: clk divider and clk multiplier +static const uint8_t ES8311_REG03_CLK_MANAGER = 0x03; // Clock Manager: adc fsmode and osr +static const uint8_t ES8311_REG04_CLK_MANAGER = 0x04; // Clock Manager: dac osr +static const uint8_t ES8311_REG05_CLK_MANAGER = 0x05; // Clock Manager: clk divider for adc and dac +static const uint8_t ES8311_REG06_CLK_MANAGER = 0x06; // Clock Manager: bclk inverter BIT(5) and divider +static const uint8_t ES8311_REG07_CLK_MANAGER = 0x07; // Clock Manager: tri-state, lrck divider +static const uint8_t ES8311_REG08_CLK_MANAGER = 0x08; // Clock Manager: lrck divider +static const uint8_t ES8311_REG09_SDPIN = 0x09; // Serial Digital Port: DAC +static const uint8_t ES8311_REG0A_SDPOUT = 0x0A; // Serial Digital Port: ADC +static const uint8_t ES8311_REG0B_SYSTEM = 0x0B; // System +static const uint8_t ES8311_REG0C_SYSTEM = 0x0C; // System +static const uint8_t ES8311_REG0D_SYSTEM = 0x0D; // System: power up/down +static const uint8_t ES8311_REG0E_SYSTEM = 0x0E; // System: power up/down +static const uint8_t ES8311_REG0F_SYSTEM = 0x0F; // System: low power +static const uint8_t ES8311_REG10_SYSTEM = 0x10; // System +static const uint8_t ES8311_REG11_SYSTEM = 0x11; // System +static const uint8_t ES8311_REG12_SYSTEM = 0x12; // System: Enable DAC +static const uint8_t ES8311_REG13_SYSTEM = 0x13; // System +static const uint8_t ES8311_REG14_SYSTEM = 0x14; // System: select DMIC, select analog pga gain +static const uint8_t ES8311_REG15_ADC = 0x15; // ADC: adc ramp rate, dmic sense +static const uint8_t ES8311_REG16_ADC = 0x16; // ADC +static const uint8_t ES8311_REG17_ADC = 0x17; // ADC: volume +static const uint8_t ES8311_REG18_ADC = 0x18; // ADC: alc enable and winsize +static const uint8_t ES8311_REG19_ADC = 0x19; // ADC: alc maxlevel +static const uint8_t ES8311_REG1A_ADC = 0x1A; // ADC: alc automute +static const uint8_t ES8311_REG1B_ADC = 0x1B; // ADC: alc automute, adc hpf s1 +static const uint8_t ES8311_REG1C_ADC = 0x1C; // ADC: equalizer, hpf s2 +static const uint8_t ES8311_REG1D_ADCEQ = 0x1D; // ADCEQ: equalizer B0 +static const uint8_t ES8311_REG1E_ADCEQ = 0x1E; // ADCEQ: equalizer B0 +static const uint8_t ES8311_REG1F_ADCEQ = 0x1F; // ADCEQ: equalizer B0 +static const uint8_t ES8311_REG20_ADCEQ = 0x20; // ADCEQ: equalizer B0 +static const uint8_t ES8311_REG21_ADCEQ = 0x21; // ADCEQ: equalizer A1 +static const uint8_t ES8311_REG22_ADCEQ = 0x22; // ADCEQ: equalizer A1 +static const uint8_t ES8311_REG23_ADCEQ = 0x23; // ADCEQ: equalizer A1 +static const uint8_t ES8311_REG24_ADCEQ = 0x24; // ADCEQ: equalizer A1 +static const uint8_t ES8311_REG25_ADCEQ = 0x25; // ADCEQ: equalizer A2 +static const uint8_t ES8311_REG26_ADCEQ = 0x26; // ADCEQ: equalizer A2 +static const uint8_t ES8311_REG27_ADCEQ = 0x27; // ADCEQ: equalizer A2 +static const uint8_t ES8311_REG28_ADCEQ = 0x28; // ADCEQ: equalizer A2 +static const uint8_t ES8311_REG29_ADCEQ = 0x29; // ADCEQ: equalizer B1 +static const uint8_t ES8311_REG2A_ADCEQ = 0x2A; // ADCEQ: equalizer B1 +static const uint8_t ES8311_REG2B_ADCEQ = 0x2B; // ADCEQ: equalizer B1 +static const uint8_t ES8311_REG2C_ADCEQ = 0x2C; // ADCEQ: equalizer B1 +static const uint8_t ES8311_REG2D_ADCEQ = 0x2D; // ADCEQ: equalizer B2 +static const uint8_t ES8311_REG2E_ADCEQ = 0x2E; // ADCEQ: equalizer B2 +static const uint8_t ES8311_REG2F_ADCEQ = 0x2F; // ADCEQ: equalizer B2 +static const uint8_t ES8311_REG30_ADCEQ = 0x30; // ADCEQ: equalizer B2 +static const uint8_t ES8311_REG31_DAC = 0x31; // DAC: mute +static const uint8_t ES8311_REG32_DAC = 0x32; // DAC: volume +static const uint8_t ES8311_REG33_DAC = 0x33; // DAC: offset +static const uint8_t ES8311_REG34_DAC = 0x34; // DAC: drc enable, drc winsize +static const uint8_t ES8311_REG35_DAC = 0x35; // DAC: drc maxlevel, minilevel +static const uint8_t ES8311_REG36_DAC = 0x36; // DAC +static const uint8_t ES8311_REG37_DAC = 0x37; // DAC: ramprate +static const uint8_t ES8311_REG38_DACEQ = 0x38; // DACEQ: equalizer B0 +static const uint8_t ES8311_REG39_DACEQ = 0x39; // DACEQ: equalizer B0 +static const uint8_t ES8311_REG3A_DACEQ = 0x3A; // DACEQ: equalizer B0 +static const uint8_t ES8311_REG3B_DACEQ = 0x3B; // DACEQ: equalizer B0 +static const uint8_t ES8311_REG3C_DACEQ = 0x3C; // DACEQ: equalizer B1 +static const uint8_t ES8311_REG3D_DACEQ = 0x3D; // DACEQ: equalizer B1 +static const uint8_t ES8311_REG3E_DACEQ = 0x3E; // DACEQ: equalizer B1 +static const uint8_t ES8311_REG3F_DACEQ = 0x3F; // DACEQ: equalizer B1 +static const uint8_t ES8311_REG40_DACEQ = 0x40; // DACEQ: equalizer A1 +static const uint8_t ES8311_REG41_DACEQ = 0x41; // DACEQ: equalizer A1 +static const uint8_t ES8311_REG42_DACEQ = 0x42; // DACEQ: equalizer A1 +static const uint8_t ES8311_REG43_DACEQ = 0x43; // DACEQ: equalizer A1 +static const uint8_t ES8311_REG44_GPIO = 0x44; // GPIO: dac2adc for test +static const uint8_t ES8311_REG45_GP = 0x45; // GPIO: GP control +static const uint8_t ES8311_REGFA_I2C = 0xFA; // I2C: reset registers +static const uint8_t ES8311_REGFC_FLAG = 0xFC; // Flag +static const uint8_t ES8311_REGFD_CHD1 = 0xFD; // Chip: ID1 +static const uint8_t ES8311_REGFE_CHD2 = 0xFE; // Chip: ID2 +static const uint8_t ES8311_REGFF_CHVER = 0xFF; // Chip: Version + +// ES8311 clock divider coefficients +static const ES8311Coefficient ES8311_COEFFICIENTS[] = { + // clang-format off + + // mclk, rate, pre_ pre_ adc_ dac_ fs_ lrck lrck bclk_ adc_ dac_ + // div, mult, div, div, mode, _h, _l, div, osr, osr + + // 8k + {12288000, 8000, 0x06, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, + {18432000, 8000, 0x03, 0x02, 0x03, 0x03, 0x00, 0x05, 0xff, 0x18, 0x10, 0x20}, + {16384000, 8000, 0x08, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, + { 8192000, 8000, 0x04, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, + { 6144000, 8000, 0x03, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, + { 4096000, 8000, 0x02, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, + { 3072000, 8000, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, + { 2048000, 8000, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, + { 1536000, 8000, 0x03, 0x04, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, + { 1024000, 8000, 0x01, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, + + // 11.025k + {11289600, 11025, 0x04, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, + { 5644800, 11025, 0x02, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, + { 2822400, 11025, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, + { 1411200, 11025, 0x01, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, + + // 12k + {12288000, 12000, 0x04, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, + { 6144000, 12000, 0x02, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, + { 3072000, 12000, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, + { 1536000, 12000, 0x01, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, + + // 16k + {12288000, 16000, 0x03, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, + {18432000, 16000, 0x03, 0x02, 0x03, 0x03, 0x00, 0x02, 0xff, 0x0c, 0x10, 0x20}, + {16384000, 16000, 0x04, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, + { 8192000, 16000, 0x02, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, + { 6144000, 16000, 0x03, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, + { 4096000, 16000, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, + { 3072000, 16000, 0x03, 0x04, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, + { 2048000, 16000, 0x01, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, + { 1536000, 16000, 0x03, 0x08, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, + { 1024000, 16000, 0x01, 0x04, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x20}, + + // 22.05k + {11289600, 22050, 0x02, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + { 5644800, 22050, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + { 2822400, 22050, 0x01, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + { 1411200, 22050, 0x01, 0x04, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + + // 24k + {12288000, 24000, 0x02, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + {18432000, 24000, 0x03, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + { 6144000, 24000, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + { 3072000, 24000, 0x01, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + { 1536000, 24000, 0x01, 0x04, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + + // 32k + {12288000, 32000, 0x03, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + {18432000, 32000, 0x03, 0x04, 0x03, 0x03, 0x00, 0x02, 0xff, 0x0c, 0x10, 0x10}, + {16384000, 32000, 0x02, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + { 8192000, 32000, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + { 6144000, 32000, 0x03, 0x04, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + { 4096000, 32000, 0x01, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + { 3072000, 32000, 0x03, 0x08, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + { 2048000, 32000, 0x01, 0x04, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + { 1536000, 32000, 0x03, 0x08, 0x01, 0x01, 0x01, 0x00, 0x7f, 0x02, 0x10, 0x10}, + { 1024000, 32000, 0x01, 0x08, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + + // 44.1k + {11289600, 44100, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + { 5644800, 44100, 0x01, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + { 2822400, 44100, 0x01, 0x04, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + { 1411200, 44100, 0x01, 0x08, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + + // 48k + {12288000, 48000, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + {18432000, 48000, 0x03, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + { 6144000, 48000, 0x01, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + { 3072000, 48000, 0x01, 0x04, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + { 1536000, 48000, 0x01, 0x08, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + + // 64k + {12288000, 64000, 0x03, 0x04, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + {18432000, 64000, 0x03, 0x04, 0x03, 0x03, 0x01, 0x01, 0x7f, 0x06, 0x10, 0x10}, + {16384000, 64000, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + { 8192000, 64000, 0x01, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + { 6144000, 64000, 0x01, 0x04, 0x03, 0x03, 0x01, 0x01, 0x7f, 0x06, 0x10, 0x10}, + { 4096000, 64000, 0x01, 0x04, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + { 3072000, 64000, 0x01, 0x08, 0x03, 0x03, 0x01, 0x01, 0x7f, 0x06, 0x10, 0x10}, + { 2048000, 64000, 0x01, 0x08, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + { 1536000, 64000, 0x01, 0x08, 0x01, 0x01, 0x01, 0x00, 0xbf, 0x03, 0x18, 0x18}, + { 1024000, 64000, 0x01, 0x08, 0x01, 0x01, 0x01, 0x00, 0x7f, 0x02, 0x10, 0x10}, + + // 88.2k + {11289600, 88200, 0x01, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + { 5644800, 88200, 0x01, 0x04, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + { 2822400, 88200, 0x01, 0x08, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + { 1411200, 88200, 0x01, 0x08, 0x01, 0x01, 0x01, 0x00, 0x7f, 0x02, 0x10, 0x10}, + + // 96k + {12288000, 96000, 0x01, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + {18432000, 96000, 0x03, 0x04, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + { 6144000, 96000, 0x01, 0x04, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + { 3072000, 96000, 0x01, 0x08, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10}, + { 1536000, 96000, 0x01, 0x08, 0x01, 0x01, 0x01, 0x00, 0x7f, 0x02, 0x10, 0x10}, + + // clang-format on +}; + +} // namespace es8311 +} // namespace esphome diff --git a/esphome/components/i2s_audio/__init__.py b/esphome/components/i2s_audio/__init__.py index d376907925..fa515a585f 100644 --- a/esphome/components/i2s_audio/__init__.py +++ b/esphome/components/i2s_audio/__init__.py @@ -8,7 +8,7 @@ from esphome.components.esp32.const import ( VARIANT_ESP32S3, ) import esphome.config_validation as cv -from esphome.const import CONF_CHANNEL, CONF_ID, CONF_SAMPLE_RATE +from esphome.const import CONF_BITS_PER_SAMPLE, CONF_CHANNEL, CONF_ID, CONF_SAMPLE_RATE from esphome.cpp_generator import MockObjClass import esphome.final_validate as fv @@ -25,13 +25,11 @@ CONF_I2S_LRCLK_PIN = "i2s_lrclk_pin" CONF_I2S_AUDIO = "i2s_audio" CONF_I2S_AUDIO_ID = "i2s_audio_id" -CONF_BITS_PER_SAMPLE = "bits_per_sample" CONF_I2S_MODE = "i2s_mode" CONF_PRIMARY = "primary" CONF_SECONDARY = "secondary" CONF_USE_APLL = "use_apll" -CONF_BITS_PER_SAMPLE = "bits_per_sample" CONF_BITS_PER_CHANNEL = "bits_per_channel" CONF_MONO = "mono" CONF_LEFT = "left" diff --git a/esphome/const.py b/esphome/const.py index c39061631b..5645c9eaab 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -92,6 +92,7 @@ CONF_BINARY_SENSORS = "binary_sensors" CONF_BINDKEY = "bindkey" CONF_BIRTH_MESSAGE = "birth_message" CONF_BIT_DEPTH = "bit_depth" +CONF_BITS_PER_SAMPLE = "bits_per_sample" CONF_BLOCK = "block" CONF_BLUE = "blue" CONF_BOARD = "board" diff --git a/tests/components/es8311/common.yaml b/tests/components/es8311/common.yaml new file mode 100644 index 0000000000..d833d1c043 --- /dev/null +++ b/tests/components/es8311/common.yaml @@ -0,0 +1,15 @@ +esphome: + on_boot: + then: + - audio_dac.mute_off: + - audio_dac.mute_on: + - audio_dac.set_volume: + volume: 50% + +i2c: + - id: i2c_aic3204 + scl: ${scl_pin} + sda: ${sda_pin} + +audio_dac: + - platform: es8311 diff --git a/tests/components/es8311/test.esp32-ard.yaml b/tests/components/es8311/test.esp32-ard.yaml new file mode 100644 index 0000000000..63c3bd6afd --- /dev/null +++ b/tests/components/es8311/test.esp32-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO16 + sda_pin: GPIO17 + +<<: !include common.yaml diff --git a/tests/components/es8311/test.esp32-c3-ard.yaml b/tests/components/es8311/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/es8311/test.esp32-c3-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/es8311/test.esp32-c3-idf.yaml b/tests/components/es8311/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/es8311/test.esp32-c3-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/es8311/test.esp32-idf.yaml b/tests/components/es8311/test.esp32-idf.yaml new file mode 100644 index 0000000000..63c3bd6afd --- /dev/null +++ b/tests/components/es8311/test.esp32-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO16 + sda_pin: GPIO17 + +<<: !include common.yaml diff --git a/tests/components/es8311/test.esp8266-ard.yaml b/tests/components/es8311/test.esp8266-ard.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/es8311/test.esp8266-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml From d3563e4e9782299c7820ad4783a1813a361a9575 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Thu, 31 Oct 2024 06:30:46 +1100 Subject: [PATCH 07/46] [sdl] Allow window to be resized. (#7698) --- esphome/components/sdl/sdl_esphome.cpp | 27 +++++++++++++++++++++----- esphome/components/sdl/sdl_esphome.h | 1 + 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/esphome/components/sdl/sdl_esphome.cpp b/esphome/components/sdl/sdl_esphome.cpp index 5e17ca5650..8f0821a2fa 100644 --- a/esphome/components/sdl/sdl_esphome.cpp +++ b/esphome/components/sdl/sdl_esphome.cpp @@ -9,8 +9,9 @@ void Sdl::setup() { ESP_LOGD(TAG, "Starting setup"); SDL_Init(SDL_INIT_VIDEO); this->window_ = SDL_CreateWindow(App.get_name().c_str(), SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, - this->width_, this->height_, 0); + this->width_, this->height_, SDL_WINDOW_RESIZABLE); this->renderer_ = SDL_CreateRenderer(this->window_, -1, SDL_RENDERER_SOFTWARE); + SDL_RenderSetLogicalSize(this->renderer_, this->width_, this->height_); this->texture_ = SDL_CreateTexture(this->renderer_, SDL_PIXELFORMAT_RGB565, SDL_TEXTUREACCESS_STATIC, this->width_, this->height_); SDL_SetTextureBlendMode(this->texture_, SDL_BLENDMODE_BLEND); @@ -25,6 +26,10 @@ void Sdl::update() { this->y_low_ = this->height_; this->x_high_ = 0; this->y_high_ = 0; + this->redraw_(rect); +} + +void Sdl::redraw_(SDL_Rect &rect) { SDL_RenderCopy(this->renderer_, this->texture_, &rect, &rect); SDL_RenderPresent(this->renderer_); } @@ -33,15 +38,13 @@ void Sdl::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t * display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) { SDL_Rect rect{x_start, y_start, w, h}; if (this->rotation_ != display::DISPLAY_ROTATION_0_DEGREES || bitness != display::COLOR_BITNESS_565 || big_endian) { - display::Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset, - x_pad); + Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset, x_pad); } else { auto stride = x_offset + w + x_pad; auto data = ptr + (stride * y_offset + x_offset) * 2; SDL_UpdateTexture(this->texture_, &rect, data, stride * 2); } - SDL_RenderCopy(this->renderer_, this->texture_, &rect, &rect); - SDL_RenderPresent(this->renderer_); + this->redraw_(rect); } void Sdl::draw_pixel_at(int x, int y, Color color) { @@ -84,6 +87,20 @@ void Sdl::loop() { } break; + case SDL_WINDOWEVENT: + switch (e.window.event) { + case SDL_WINDOWEVENT_SIZE_CHANGED: + case SDL_WINDOWEVENT_EXPOSED: + case SDL_WINDOWEVENT_RESIZED: { + SDL_Rect rect{0, 0, this->width_, this->height_}; + this->redraw_(rect); + break; + } + default: + break; + } + break; + default: ESP_LOGV(TAG, "Event %d", e.type); break; diff --git a/esphome/components/sdl/sdl_esphome.h b/esphome/components/sdl/sdl_esphome.h index e4b2d9dd9f..4b0e59c9fe 100644 --- a/esphome/components/sdl/sdl_esphome.h +++ b/esphome/components/sdl/sdl_esphome.h @@ -38,6 +38,7 @@ class Sdl : public display::Display { protected: int get_width_internal() override { return this->width_; } int get_height_internal() override { return this->height_; } + void redraw_(SDL_Rect &rect); int width_{}; int height_{}; SDL_Renderer *renderer_{}; From e85157db4b246fd3c701c7b4195d4f52b735c554 Mon Sep 17 00:00:00 2001 From: Jason Nagin <33561705+JasonN3@users.noreply.github.com> Date: Wed, 30 Oct 2024 15:34:33 -0400 Subject: [PATCH 08/46] Add config for current temperature precision (#7699) --- esphome/components/mqtt/mqtt_climate.cpp | 6 ++++-- esphome/components/mqtt/mqtt_const.h | 4 ++++ tests/components/mqtt/common.yaml | 4 ++++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/esphome/components/mqtt/mqtt_climate.cpp b/esphome/components/mqtt/mqtt_climate.cpp index 49a8f06734..773d863835 100644 --- a/esphome/components/mqtt/mqtt_climate.cpp +++ b/esphome/components/mqtt/mqtt_climate.cpp @@ -71,8 +71,10 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo root[MQTT_MIN_TEMP] = traits.get_visual_min_temperature(); // max_temp root[MQTT_MAX_TEMP] = traits.get_visual_max_temperature(); - // temp_step - root["temp_step"] = traits.get_visual_target_temperature_step(); + // target_temp_step + root[MQTT_TARGET_TEMPERATURE_STEP] = traits.get_visual_target_temperature_step(); + // current_temp_step + root[MQTT_CURRENT_TEMPERATURE_STEP] = traits.get_visual_current_temperature_step(); // temperature units are always coerced to Celsius internally root[MQTT_TEMPERATURE_UNIT] = "C"; diff --git a/esphome/components/mqtt/mqtt_const.h b/esphome/components/mqtt/mqtt_const.h index c1c40c4b6d..445457a27f 100644 --- a/esphome/components/mqtt/mqtt_const.h +++ b/esphome/components/mqtt/mqtt_const.h @@ -51,6 +51,7 @@ constexpr const char *const MQTT_COMMAND_TOPIC = "cmd_t"; constexpr const char *const MQTT_CONFIGURATION_URL = "cu"; constexpr const char *const MQTT_CURRENT_HUMIDITY_TEMPLATE = "curr_hum_tpl"; constexpr const char *const MQTT_CURRENT_HUMIDITY_TOPIC = "curr_hum_t"; +constexpr const char *const MQTT_CURRENT_TEMPERATURE_STEP = "precision"; constexpr const char *const MQTT_CURRENT_TEMPERATURE_TEMPLATE = "curr_temp_tpl"; constexpr const char *const MQTT_CURRENT_TEMPERATURE_TOPIC = "curr_temp_t"; constexpr const char *const MQTT_DEVICE = "dev"; @@ -232,6 +233,7 @@ constexpr const char *const MQTT_TARGET_HUMIDITY_COMMAND_TEMPLATE = "hum_cmd_tpl constexpr const char *const MQTT_TARGET_HUMIDITY_COMMAND_TOPIC = "hum_cmd_t"; constexpr const char *const MQTT_TARGET_HUMIDITY_STATE_TEMPLATE = "hum_state_tpl"; constexpr const char *const MQTT_TARGET_HUMIDITY_STATE_TOPIC = "hum_stat_t"; +constexpr const char *const MQTT_TARGET_TEMPERATURE_STEP = "temp_step"; constexpr const char *const MQTT_TEMPERATURE_COMMAND_TEMPLATE = "temp_cmd_tpl"; constexpr const char *const MQTT_TEMPERATURE_COMMAND_TOPIC = "temp_cmd_t"; constexpr const char *const MQTT_TEMPERATURE_HIGH_COMMAND_TEMPLATE = "temp_hi_cmd_tpl"; @@ -313,6 +315,7 @@ constexpr const char *const MQTT_COMMAND_TOPIC = "command_topic"; constexpr const char *const MQTT_CONFIGURATION_URL = "configuration_url"; constexpr const char *const MQTT_CURRENT_HUMIDITY_TEMPLATE = "current_humidity_template"; constexpr const char *const MQTT_CURRENT_HUMIDITY_TOPIC = "current_humidity_topic"; +constexpr const char *const MQTT_CURRENT_TEMPERATURE_STEP = "precision"; constexpr const char *const MQTT_CURRENT_TEMPERATURE_TEMPLATE = "current_temperature_template"; constexpr const char *const MQTT_CURRENT_TEMPERATURE_TOPIC = "current_temperature_topic"; constexpr const char *const MQTT_DEVICE = "device"; @@ -494,6 +497,7 @@ constexpr const char *const MQTT_TARGET_HUMIDITY_COMMAND_TEMPLATE = "target_humi constexpr const char *const MQTT_TARGET_HUMIDITY_COMMAND_TOPIC = "target_humidity_command_topic"; constexpr const char *const MQTT_TARGET_HUMIDITY_STATE_TEMPLATE = "target_humidity_state_template"; constexpr const char *const MQTT_TARGET_HUMIDITY_STATE_TOPIC = "target_humidity_state_topic"; +constexpr const char *const MQTT_TARGET_TEMPERATURE_STEP = "temp_step"; constexpr const char *const MQTT_TEMPERATURE_COMMAND_TEMPLATE = "temperature_command_template"; constexpr const char *const MQTT_TEMPERATURE_COMMAND_TOPIC = "temperature_command_topic"; constexpr const char *const MQTT_TEMPERATURE_HIGH_COMMAND_TEMPLATE = "temperature_high_command_template"; diff --git a/tests/components/mqtt/common.yaml b/tests/components/mqtt/common.yaml index 5ed6335d65..e154be8b5c 100644 --- a/tests/components/mqtt/common.yaml +++ b/tests/components/mqtt/common.yaml @@ -200,6 +200,10 @@ climate: fan_only_cooling: true fan_with_cooling: true fan_with_heating: true + visual: + temperature_step: + target_temperature: 0.1 + current_temperature: 0.1 cover: - platform: template From 5a2fed35693f7191f81fe31236e69c2132c23d99 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Thu, 31 Oct 2024 09:28:18 +1100 Subject: [PATCH 09/46] [spi] Add mosi pin checks for displays (#7702) --- esphome/components/ili9xxx/display.py | 4 ++++ esphome/components/pcd8544/display.py | 12 +++++++---- esphome/components/ssd1306_spi/display.py | 8 +++++-- esphome/components/ssd1322_spi/display.py | 8 +++++-- esphome/components/ssd1325_spi/display.py | 8 +++++-- esphome/components/ssd1327_spi/display.py | 8 +++++-- esphome/components/ssd1331_spi/display.py | 8 +++++-- esphome/components/ssd1351_spi/display.py | 8 +++++-- esphome/components/st7567_spi/display.py | 8 +++++-- esphome/components/st7701s/display.py | 4 ++++ esphome/components/st7735/display.py | 17 +++++++++------ esphome/components/st7789v/display.py | 21 ++++++++++++------- .../components/waveshare_epaper/display.py | 8 +++++-- 13 files changed, 88 insertions(+), 34 deletions(-) diff --git a/esphome/components/ili9xxx/display.py b/esphome/components/ili9xxx/display.py index 68e3aa953d..739ad07843 100644 --- a/esphome/components/ili9xxx/display.py +++ b/esphome/components/ili9xxx/display.py @@ -196,6 +196,10 @@ CONFIG_SCHEMA = cv.All( _validate, ) +FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema( + "ili9xxx", require_miso=False, require_mosi=True +) + async def to_code(config): rhs = MODELS[config[CONF_MODEL]].new() diff --git a/esphome/components/pcd8544/display.py b/esphome/components/pcd8544/display.py index d7e72d1c81..2c24b133da 100644 --- a/esphome/components/pcd8544/display.py +++ b/esphome/components/pcd8544/display.py @@ -1,15 +1,15 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import pins +import esphome.codegen as cg from esphome.components import display, spi +import esphome.config_validation as cv from esphome.const import ( + CONF_CONTRAST, + CONF_CS_PIN, CONF_DC_PIN, CONF_ID, CONF_LAMBDA, CONF_PAGES, CONF_RESET_PIN, - CONF_CS_PIN, - CONF_CONTRAST, ) DEPENDENCIES = ["spi"] @@ -35,6 +35,10 @@ CONFIG_SCHEMA = cv.All( cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA), ) +FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema( + "pcd8544", require_miso=False, require_mosi=True +) + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) diff --git a/esphome/components/ssd1306_spi/display.py b/esphome/components/ssd1306_spi/display.py index 0af1168bde..4af41073d4 100644 --- a/esphome/components/ssd1306_spi/display.py +++ b/esphome/components/ssd1306_spi/display.py @@ -1,8 +1,8 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import pins +import esphome.codegen as cg from esphome.components import spi, ssd1306_base from esphome.components.ssd1306_base import _validate +import esphome.config_validation as cv from esphome.const import CONF_DC_PIN, CONF_ID, CONF_LAMBDA, CONF_PAGES AUTO_LOAD = ["ssd1306_base"] @@ -24,6 +24,10 @@ CONFIG_SCHEMA = cv.All( _validate, ) +FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema( + "ssd1306_spi", require_miso=False, require_mosi=True +) + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) diff --git a/esphome/components/ssd1322_spi/display.py b/esphome/components/ssd1322_spi/display.py index 88b3a53355..849e71abee 100644 --- a/esphome/components/ssd1322_spi/display.py +++ b/esphome/components/ssd1322_spi/display.py @@ -1,7 +1,7 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import pins +import esphome.codegen as cg from esphome.components import spi, ssd1322_base +import esphome.config_validation as cv from esphome.const import CONF_DC_PIN, CONF_ID, CONF_LAMBDA, CONF_PAGES CODEOWNERS = ["@kbx81"] @@ -24,6 +24,10 @@ CONFIG_SCHEMA = cv.All( cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA), ) +FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema( + "ssd1322_spi", require_miso=False, require_mosi=True +) + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) diff --git a/esphome/components/ssd1325_spi/display.py b/esphome/components/ssd1325_spi/display.py index a86dc751d5..e18db33c68 100644 --- a/esphome/components/ssd1325_spi/display.py +++ b/esphome/components/ssd1325_spi/display.py @@ -1,7 +1,7 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import pins +import esphome.codegen as cg from esphome.components import spi, ssd1325_base +import esphome.config_validation as cv from esphome.const import CONF_DC_PIN, CONF_ID, CONF_LAMBDA, CONF_PAGES CODEOWNERS = ["@kbx81"] @@ -24,6 +24,10 @@ CONFIG_SCHEMA = cv.All( cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA), ) +FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema( + "ssd1325_spi", require_miso=False, require_mosi=True +) + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) diff --git a/esphome/components/ssd1327_spi/display.py b/esphome/components/ssd1327_spi/display.py index 138e85eecd..b622c098ec 100644 --- a/esphome/components/ssd1327_spi/display.py +++ b/esphome/components/ssd1327_spi/display.py @@ -1,7 +1,7 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import pins +import esphome.codegen as cg from esphome.components import spi, ssd1327_base +import esphome.config_validation as cv from esphome.const import CONF_DC_PIN, CONF_ID, CONF_LAMBDA, CONF_PAGES CODEOWNERS = ["@kbx81"] @@ -24,6 +24,10 @@ CONFIG_SCHEMA = cv.All( cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA), ) +FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema( + "ssd1327_spi", require_miso=False, require_mosi=True +) + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) diff --git a/esphome/components/ssd1331_spi/display.py b/esphome/components/ssd1331_spi/display.py index c32ac60578..50895b3175 100644 --- a/esphome/components/ssd1331_spi/display.py +++ b/esphome/components/ssd1331_spi/display.py @@ -1,7 +1,7 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import pins +import esphome.codegen as cg from esphome.components import spi, ssd1331_base +import esphome.config_validation as cv from esphome.const import CONF_DC_PIN, CONF_ID, CONF_LAMBDA, CONF_PAGES CODEOWNERS = ["@kbx81"] @@ -24,6 +24,10 @@ CONFIG_SCHEMA = cv.All( cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA), ) +FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema( + "ssd1331_spi", require_miso=False, require_mosi=True +) + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) diff --git a/esphome/components/ssd1351_spi/display.py b/esphome/components/ssd1351_spi/display.py index 3f3409226c..bd7033c3d4 100644 --- a/esphome/components/ssd1351_spi/display.py +++ b/esphome/components/ssd1351_spi/display.py @@ -1,7 +1,7 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import pins +import esphome.codegen as cg from esphome.components import spi, ssd1351_base +import esphome.config_validation as cv from esphome.const import CONF_DC_PIN, CONF_ID, CONF_LAMBDA, CONF_PAGES CODEOWNERS = ["@kbx81"] @@ -24,6 +24,10 @@ CONFIG_SCHEMA = cv.All( cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA), ) +FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema( + "ssd1351_spi", require_miso=False, require_mosi=True +) + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) diff --git a/esphome/components/st7567_spi/display.py b/esphome/components/st7567_spi/display.py index aabe02a2d8..305aa35024 100644 --- a/esphome/components/st7567_spi/display.py +++ b/esphome/components/st7567_spi/display.py @@ -1,7 +1,7 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import pins +import esphome.codegen as cg from esphome.components import spi, st7567_base +import esphome.config_validation as cv from esphome.const import CONF_DC_PIN, CONF_ID, CONF_LAMBDA, CONF_PAGES CODEOWNERS = ["@latonita"] @@ -24,6 +24,10 @@ CONFIG_SCHEMA = cv.All( cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA), ) +FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema( + "st7567_spi", require_miso=False, require_mosi=True +) + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) diff --git a/esphome/components/st7701s/display.py b/esphome/components/st7701s/display.py index e73c2467da..c6ad43c14c 100644 --- a/esphome/components/st7701s/display.py +++ b/esphome/components/st7701s/display.py @@ -167,6 +167,10 @@ CONFIG_SCHEMA = cv.All( cv.only_with_esp_idf, ) +FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema( + "st7701s", require_miso=False, require_mosi=True +) + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) diff --git a/esphome/components/st7735/display.py b/esphome/components/st7735/display.py index d5bb2fa3d6..2761214315 100644 --- a/esphome/components/st7735/display.py +++ b/esphome/components/st7735/display.py @@ -1,17 +1,17 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import pins -from esphome.components import spi -from esphome.components import display +import esphome.codegen as cg +from esphome.components import display, spi +import esphome.config_validation as cv from esphome.const import ( CONF_DC_PIN, CONF_ID, + CONF_INVERT_COLORS, CONF_LAMBDA, CONF_MODEL, - CONF_RESET_PIN, CONF_PAGES, - CONF_INVERT_COLORS, + CONF_RESET_PIN, ) + from . import st7735_ns CODEOWNERS = ["@SenexCrenshaw"] @@ -68,6 +68,11 @@ CONFIG_SCHEMA = cv.All( ) +FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema( + "st7735", require_miso=False, require_mosi=True +) + + async def setup_st7735(var, config): await display.register_display(var, config) diff --git a/esphome/components/st7789v/display.py b/esphome/components/st7789v/display.py index 04dce2cf6c..8259eacf2d 100644 --- a/esphome/components/st7789v/display.py +++ b/esphome/components/st7789v/display.py @@ -1,22 +1,23 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import pins -from esphome.components import display, spi, power_supply +import esphome.codegen as cg +from esphome.components import display, power_supply, spi +import esphome.config_validation as cv from esphome.const import ( CONF_BACKLIGHT_PIN, + CONF_CS_PIN, CONF_DC_PIN, CONF_HEIGHT, CONF_ID, CONF_LAMBDA, CONF_MODEL, - CONF_RESET_PIN, - CONF_WIDTH, - CONF_POWER_SUPPLY, - CONF_ROTATION, - CONF_CS_PIN, CONF_OFFSET_HEIGHT, CONF_OFFSET_WIDTH, + CONF_POWER_SUPPLY, + CONF_RESET_PIN, + CONF_ROTATION, + CONF_WIDTH, ) + from . import st7789v_ns CONF_EIGHTBITCOLOR = "eightbitcolor" @@ -168,6 +169,10 @@ CONFIG_SCHEMA = cv.All( validate_st7789v, ) +FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema( + "st7789v", require_miso=False, require_mosi=True +) + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) diff --git a/esphome/components/waveshare_epaper/display.py b/esphome/components/waveshare_epaper/display.py index 4d3965449f..8287788de5 100644 --- a/esphome/components/waveshare_epaper/display.py +++ b/esphome/components/waveshare_epaper/display.py @@ -1,7 +1,7 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import core, pins +import esphome.codegen as cg from esphome.components import display, spi +import esphome.config_validation as cv from esphome.const import ( CONF_BUSY_PIN, CONF_DC_PIN, @@ -187,6 +187,10 @@ CONFIG_SCHEMA = cv.All( cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA), ) +FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema( + "waveshare_epaper", require_miso=False, require_mosi=True +) + async def to_code(config): model_type, model = MODELS[config[CONF_MODEL]] From 74ea1b60e35fa351b5a5fc380dfdc1879d22f043 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 31 Oct 2024 11:37:54 +1300 Subject: [PATCH 10/46] [CI] Fix webserver defines to be present based on platform, not just framework (#7703) --- esphome/core/defines.h | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/esphome/core/defines.h b/esphome/core/defines.h index b5511b57eb..3798ddba6a 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -86,8 +86,6 @@ #ifdef USE_ARDUINO #define USE_CAPTIVE_PORTAL #define USE_PROMETHEUS -#define USE_WEBSERVER -#define USE_WEBSERVER_PORT 80 // NOLINT #define USE_WIFI_WPA2_EAP #endif @@ -111,6 +109,8 @@ #define USE_SPEAKER #define USE_SPI #define USE_VOICE_ASSISTANT +#define USE_WEBSERVER +#define USE_WEBSERVER_PORT 80 // NOLINT #define USE_WIFI_11KV_SUPPORT #ifdef USE_ARDUINO @@ -147,6 +147,8 @@ #define USE_SHD_FIRMWARE_DATA \ {} +#define USE_WEBSERVER +#define USE_WEBSERVER_PORT 80 // NOLINT #endif #ifdef USE_RP2040 @@ -158,6 +160,8 @@ #ifdef USE_LIBRETINY #define USE_SOCKET_IMPL_LWIP_SOCKETS +#define USE_WEBSERVER +#define USE_WEBSERVER_PORT 80 // NOLINT #endif #ifdef USE_HOST From 8b7e061f3ac5427dcc931feffdd0251100945a57 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Thu, 31 Oct 2024 13:15:39 +1100 Subject: [PATCH 11/46] [touchscreen] Calibration fixes (#7704) --- esphome/components/touchscreen/__init__.py | 97 +++++++++---------- esphome/components/touchscreen/touchscreen.h | 12 +-- .../xpt2046/touchscreen/__init__.py | 39 ++------ tests/components/xpt2046/test.esp32-ard.yaml | 4 +- .../components/xpt2046/test.esp32-c3-ard.yaml | 2 +- .../components/xpt2046/test.esp32-c3-idf.yaml | 2 +- tests/components/xpt2046/test.esp32-idf.yaml | 2 +- .../components/xpt2046/test.esp8266-ard.yaml | 2 +- tests/components/xpt2046/test.rp2040-ard.yaml | 4 +- 9 files changed, 63 insertions(+), 101 deletions(-) diff --git a/esphome/components/touchscreen/__init__.py b/esphome/components/touchscreen/__init__.py index b2d3f60d2b..01a271a34e 100644 --- a/esphome/components/touchscreen/__init__.py +++ b/esphome/components/touchscreen/__init__.py @@ -1,21 +1,18 @@ -import esphome.config_validation as cv -import esphome.codegen as cg - -from esphome.components import display from esphome import automation - +import esphome.codegen as cg +from esphome.components import display +import esphome.config_validation as cv from esphome.const import ( + CONF_CALIBRATION, CONF_DISPLAY, - CONF_ON_TOUCH, - CONF_ON_RELEASE, - CONF_ON_UPDATE, - CONF_SWAP_XY, CONF_MIRROR_X, CONF_MIRROR_Y, + CONF_ON_RELEASE, + CONF_ON_TOUCH, + CONF_ON_UPDATE, + CONF_SWAP_XY, CONF_TRANSFORM, - CONF_CALIBRATION, ) - from esphome.core import coroutine_with_priority CODEOWNERS = ["@jesserockz", "@nielsnl68"] @@ -43,51 +40,45 @@ CONF_Y_MIN = "y_min" CONF_Y_MAX = "y_max" -def validate_calibration(config): - if CONF_CALIBRATION in config: - calibration_config = config[CONF_CALIBRATION] - if ( - cv.int_([CONF_X_MIN]) != 0 - and cv.int_(calibration_config[CONF_X_MAX]) != 0 - and abs( - cv.int_(calibration_config[CONF_X_MIN]) - - cv.int_(calibration_config[CONF_X_MAX]) - ) - < 10 - ): - raise cv.Invalid("Calibration X values difference must be more than 10") - - if ( - cv.int_(calibration_config[CONF_Y_MIN]) != 0 - and cv.int_(calibration_config[CONF_Y_MAX]) != 0 - and abs( - cv.int_(calibration_config[CONF_Y_MIN]) - - cv.int_(calibration_config[CONF_Y_MAX]) - ) - < 10 - ): - raise cv.Invalid("Calibration Y values difference must be more than 10") - - return config +def validate_calibration(calibration_config): + x_min = calibration_config[CONF_X_MIN] + x_max = calibration_config[CONF_X_MAX] + y_min = calibration_config[CONF_Y_MIN] + y_max = calibration_config[CONF_Y_MAX] + if x_max < x_min: + raise cv.Invalid( + "x_min must be smaller than x_max. To mirror the direction use the 'transform' options" + ) + if y_max < y_min: + raise cv.Invalid( + "y_min must be smaller than y_max. To mirror the direction use the 'transform' options" + ) + x_delta = x_max - x_min + y_delta = y_max - y_min + if x_delta < 10 or y_delta < 10: + raise cv.Invalid("Calibration value range must be greater than 10") + return calibration_config -def calibration_schema(default_max_values): - return cv.Schema( +CALIBRATION_SCHEMA = cv.All( + cv.Schema( { - cv.Optional(CONF_X_MIN, default=0): cv.int_range(min=0, max=4095), - cv.Optional(CONF_X_MAX, default=default_max_values): cv.int_range( - min=0, max=4095 - ), - cv.Optional(CONF_Y_MIN, default=0): cv.int_range(min=0, max=4095), - cv.Optional(CONF_Y_MAX, default=default_max_values): cv.int_range( - min=0, max=4095 - ), - }, - validate_calibration, + cv.Required(CONF_X_MIN): cv.int_range(min=0, max=4095), + cv.Required(CONF_X_MAX): cv.int_range(min=0, max=4095), + cv.Required(CONF_Y_MIN): cv.int_range(min=0, max=4095), + cv.Required(CONF_Y_MAX): cv.int_range(min=0, max=4095), + } + ), + validate_calibration, +) + + +def touchscreen_schema(default_touch_timeout=cv.UNDEFINED, calibration_required=False): + calibration = ( + cv.Required(CONF_CALIBRATION) + if calibration_required + else cv.Optional(CONF_CALIBRATION) ) - - -def touchscreen_schema(default_touch_timeout): return cv.Schema( { cv.GenerateID(CONF_DISPLAY): cv.use_id(display.Display), @@ -102,7 +93,7 @@ def touchscreen_schema(default_touch_timeout): cv.positive_time_period_milliseconds, cv.Range(max=cv.TimePeriod(milliseconds=65535)), ), - cv.Optional(CONF_CALIBRATION): calibration_schema(0), + calibration: CALIBRATION_SCHEMA, cv.Optional(CONF_ON_TOUCH): automation.validate_automation(single=True), cv.Optional(CONF_ON_UPDATE): automation.validate_automation(single=True), cv.Optional(CONF_ON_RELEASE): automation.validate_automation(single=True), diff --git a/esphome/components/touchscreen/touchscreen.h b/esphome/components/touchscreen/touchscreen.h index 21111f87b3..8016323d49 100644 --- a/esphome/components/touchscreen/touchscreen.h +++ b/esphome/components/touchscreen/touchscreen.h @@ -53,14 +53,10 @@ class Touchscreen : public PollingComponent { void set_swap_xy(bool swap) { this->swap_x_y_ = swap; } void set_calibration(int16_t x_min, int16_t x_max, int16_t y_min, int16_t y_max) { - this->x_raw_min_ = std::min(x_min, x_max); - this->x_raw_max_ = std::max(x_min, x_max); - this->y_raw_min_ = std::min(y_min, y_max); - this->y_raw_max_ = std::max(y_min, y_max); - if (x_min > x_max) - this->invert_x_ = true; - if (y_min > y_max) - this->invert_y_ = true; + this->x_raw_min_ = x_min; + this->x_raw_max_ = x_max; + this->y_raw_min_ = y_min; + this->y_raw_max_ = y_max; } Trigger *get_touch_trigger() { return &this->touch_trigger_; } diff --git a/esphome/components/xpt2046/touchscreen/__init__.py b/esphome/components/xpt2046/touchscreen/__init__.py index d45f309a3b..d91ae44789 100644 --- a/esphome/components/xpt2046/touchscreen/__init__.py +++ b/esphome/components/xpt2046/touchscreen/__init__.py @@ -1,9 +1,8 @@ -import esphome.codegen as cg -import esphome.config_validation as cv - from esphome import pins +import esphome.codegen as cg from esphome.components import spi, touchscreen -from esphome.const import CONF_ID, CONF_THRESHOLD, CONF_INTERRUPT_PIN +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_INTERRUPT_PIN, CONF_THRESHOLD CODEOWNERS = ["@numo68", "@nielsnl68"] DEPENDENCIES = ["spi"] @@ -15,13 +14,9 @@ XPT2046Component = XPT2046_ns.class_( spi.SPIDevice, ) -CONF_CALIBRATION_X_MIN = "calibration_x_min" -CONF_CALIBRATION_X_MAX = "calibration_x_max" -CONF_CALIBRATION_Y_MIN = "calibration_y_min" -CONF_CALIBRATION_Y_MAX = "calibration_y_max" - CONFIG_SCHEMA = cv.All( - touchscreen.TOUCHSCREEN_SCHEMA.extend( + touchscreen.touchscreen_schema(calibration_required=True) + .extend( cv.Schema( { cv.GenerateID(): cv.declare_id(XPT2046Component), @@ -29,30 +24,10 @@ CONFIG_SCHEMA = cv.All( pins.internal_gpio_input_pin_schema ), cv.Optional(CONF_THRESHOLD, default=400): cv.int_range(min=0, max=4095), - cv.Optional( - touchscreen.CONF_CALIBRATION - ): touchscreen.calibration_schema(4095), - cv.Optional(CONF_CALIBRATION_X_MIN): cv.invalid( - "Deprecated: use the new 'calibration' configuration variable" - ), - cv.Optional(CONF_CALIBRATION_X_MAX): cv.invalid( - "Deprecated: use the new 'calibration' configuration variable" - ), - cv.Optional(CONF_CALIBRATION_Y_MIN): cv.invalid( - "Deprecated: use the new 'calibration' configuration variable" - ), - cv.Optional(CONF_CALIBRATION_Y_MAX): cv.invalid( - "Deprecated: use the new 'calibration' configuration variable" - ), - cv.Optional(CONF_CALIBRATION_Y_MAX): cv.invalid( - "Deprecated: use the new 'calibration' configuration variable" - ), - cv.Optional("report_interval"): cv.invalid( - "Deprecated: use the 'update_interval' configuration variable" - ), }, ) - ).extend(spi.spi_device_schema()), + ) + .extend(spi.spi_device_schema()), ) diff --git a/tests/components/xpt2046/test.esp32-ard.yaml b/tests/components/xpt2046/test.esp32-ard.yaml index f15d1f9b41..9e305791e0 100644 --- a/tests/components/xpt2046/test.esp32-ard.yaml +++ b/tests/components/xpt2046/test.esp32-ard.yaml @@ -25,8 +25,8 @@ touchscreen: update_interval: 50ms threshold: 400 calibration: - x_min: 3860 - x_max: 280 + x_min: 280 + x_max: 3860 y_min: 340 y_max: 3860 on_touch: diff --git a/tests/components/xpt2046/test.esp32-c3-ard.yaml b/tests/components/xpt2046/test.esp32-c3-ard.yaml index ef4daa800d..c03fd6b345 100644 --- a/tests/components/xpt2046/test.esp32-c3-ard.yaml +++ b/tests/components/xpt2046/test.esp32-c3-ard.yaml @@ -25,7 +25,7 @@ touchscreen: update_interval: 50ms threshold: 400 calibration: - x_min: 3860 + x_min: 28 x_max: 280 y_min: 340 y_max: 3860 diff --git a/tests/components/xpt2046/test.esp32-c3-idf.yaml b/tests/components/xpt2046/test.esp32-c3-idf.yaml index ef4daa800d..787ca9b1ed 100644 --- a/tests/components/xpt2046/test.esp32-c3-idf.yaml +++ b/tests/components/xpt2046/test.esp32-c3-idf.yaml @@ -25,7 +25,7 @@ touchscreen: update_interval: 50ms threshold: 400 calibration: - x_min: 3860 + x_min: 50 x_max: 280 y_min: 340 y_max: 3860 diff --git a/tests/components/xpt2046/test.esp32-idf.yaml b/tests/components/xpt2046/test.esp32-idf.yaml index f15d1f9b41..e79997146b 100644 --- a/tests/components/xpt2046/test.esp32-idf.yaml +++ b/tests/components/xpt2046/test.esp32-idf.yaml @@ -25,7 +25,7 @@ touchscreen: update_interval: 50ms threshold: 400 calibration: - x_min: 3860 + x_min: 50 x_max: 280 y_min: 340 y_max: 3860 diff --git a/tests/components/xpt2046/test.esp8266-ard.yaml b/tests/components/xpt2046/test.esp8266-ard.yaml index 0daa25ad60..ab71f7b8bc 100644 --- a/tests/components/xpt2046/test.esp8266-ard.yaml +++ b/tests/components/xpt2046/test.esp8266-ard.yaml @@ -25,7 +25,7 @@ touchscreen: update_interval: 50ms threshold: 400 calibration: - x_min: 3860 + x_min: 50 x_max: 280 y_min: 340 y_max: 3860 diff --git a/tests/components/xpt2046/test.rp2040-ard.yaml b/tests/components/xpt2046/test.rp2040-ard.yaml index 8afc45d04d..622e69ac98 100644 --- a/tests/components/xpt2046/test.rp2040-ard.yaml +++ b/tests/components/xpt2046/test.rp2040-ard.yaml @@ -25,8 +25,8 @@ touchscreen: update_interval: 50ms threshold: 400 calibration: - x_min: 3860 - x_max: 280 + x_min: 280 + x_max: 3860 y_min: 340 y_max: 3860 on_touch: From a043022444609d11b2a8b040ae9515e54ce940f2 Mon Sep 17 00:00:00 2001 From: Faidon Liambotis Date: Thu, 31 Oct 2024 05:36:23 +0200 Subject: [PATCH 12/46] [font] Add support for "glyphsets" (#7429) Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com> --- docker/Dockerfile | 70 +- esphome/components/font/__init__.py | 303 +- requirements.txt | 3 + requirements_optional.txt | 1 - script/ci-custom.py | 2 +- tests/components/font/.gitattributes | 2 + tests/components/font/MatrixChunky8X.bdf | 7461 ++++++++++++++++++++++ tests/components/font/common.yaml | 24 + tests/components/font/test.host.yaml | 34 + tests/components/font/x11.pcf | Bin 0 -> 13368 bytes 10 files changed, 7771 insertions(+), 129 deletions(-) create mode 100644 tests/components/font/.gitattributes create mode 100644 tests/components/font/MatrixChunky8X.bdf create mode 100644 tests/components/font/x11.pcf diff --git a/docker/Dockerfile b/docker/Dockerfile index 52a4794f24..44ee879a12 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -40,25 +40,6 @@ RUN \ libcairo2=1.16.0-7 \ libmagic1=1:5.44-3 \ patch=2.7.6-7 \ - && ( \ - ( \ - [ "$TARGETARCH$TARGETVARIANT" = "armv7" ] && \ - apt-get install -y --no-install-recommends \ - build-essential=12.9 \ - python3-dev=3.11.2-1+b1 \ - zlib1g-dev=1:1.2.13.dfsg-1 \ - libjpeg-dev=1:2.1.5-2 \ - libfreetype-dev=2.12.1+dfsg-5+deb12u3 \ - libssl-dev=3.0.14-1~deb12u2 \ - libffi-dev=3.4.4-1 \ - libopenjp2-7=2.5.0-2 \ - libtiff6=4.5.0-6+deb12u1 \ - cargo=0.66.0+ds1-1 \ - pkg-config=1.8.1-1 \ - gcc-arm-linux-gnueabihf=4:12.2.0-3 \ - ) \ - || [ "$TARGETARCH$TARGETVARIANT" != "armv7" ] \ - ) \ && rm -rf \ /tmp/* \ /var/{cache,log}/* \ @@ -97,15 +78,48 @@ RUN \ # tmpfs is for https://github.com/rust-lang/cargo/issues/8719 COPY requirements.txt requirements_optional.txt / -RUN --mount=type=tmpfs,target=/root/.cargo if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \ - curl -L https://www.piwheels.org/cp311/cryptography-43.0.0-cp37-abi3-linux_armv7l.whl -o /tmp/cryptography-43.0.0-cp37-abi3-linux_armv7l.whl \ - && pip3 install --break-system-packages --no-cache-dir /tmp/cryptography-43.0.0-cp37-abi3-linux_armv7l.whl \ - && rm /tmp/cryptography-43.0.0-cp37-abi3-linux_armv7l.whl \ - && export PIP_EXTRA_INDEX_URL="https://www.piwheels.org/simple"; \ - fi; \ - CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse CARGO_HOME=/root/.cargo \ - pip3 install \ - --break-system-packages --no-cache-dir -r /requirements.txt -r /requirements_optional.txt +RUN --mount=type=tmpfs,target=/root/.cargo < len(y_): return 1 - raise cv.Invalid(f"Found duplicate glyph {x}") + return 0 -def validate_glyphs(value): - if isinstance(value, list): - value = cv.Schema([cv.string])(value) - value = cv.Schema([cv.string])(list(value)) +def flatten(lists) -> list: + """ + Given a list of lists, flatten it to a single list of all elements of all lists. + This wraps itertools.chain.from_iterable to make it more readable, and return a list + rather than a single use iterable. + """ + from itertools import chain - value.sort(key=functools.cmp_to_key(glyph_comparator)) - return value + return list(chain.from_iterable(lists)) -font_map = {} +def check_missing_glyphs(file, codepoints: Iterable, warning: bool = False): + """ + Check that the given font file actually contains the requested glyphs + :param file: A Truetype font file + :param codepoints: A list of codepoints to check + :param warning: If true, log a warning instead of raising an exception + """ - -def merge_glyphs(config): - glyphs = [] - glyphs.extend(config[CONF_GLYPHS]) - font_list = [(EFont(config[CONF_FILE], config[CONF_SIZE], config[CONF_GLYPHS]))] - if extras := config.get(CONF_EXTRAS): - extra_fonts = list( - map( - lambda x: EFont(x[CONF_FILE], config[CONF_SIZE], x[CONF_GLYPHS]), extras - ) + font = FONT_CACHE[file] + missing = [chr(x) for x in codepoints if font.get_char_index(x) == 0] + if missing: + # Only list up to 10 missing glyphs + missing.sort(key=functools.cmp_to_key(glyph_comparator)) + count = len(missing) + missing = missing[:10] + missing_str = "\n ".join( + f"{x} ({x.encode('unicode_escape')})" for x in missing ) - font_list.extend(extra_fonts) - for extra in extras: - glyphs.extend(extra[CONF_GLYPHS]) - validate_glyphs(glyphs) - font_map[config[CONF_ID]] = font_list + if count > 10: + missing_str += f"\n and {count - 10} more." + message = f"Font {Path(file).name} is missing {count} glyph{'s' if count != 1 else ''}:\n {missing_str}" + if warning: + _LOGGER.warning(message) + else: + raise cv.Invalid(message) + + +def validate_glyphs(config): + """ + Check for duplicate codepoints, then check that all requested codepoints actually + have glyphs defined in the appropriate font file. + """ + + # Collect all glyph codepoints and flatten to a list of chars + glyphspoints = flatten( + [x[CONF_GLYPHS] for x in config[CONF_EXTRAS]] + config[CONF_GLYPHS] + ) + # Convert a list of strings to a list of chars (one char strings) + glyphspoints = flatten([list(x) for x in glyphspoints]) + if len(set(glyphspoints)) != len(glyphspoints): + duplicates = {x for x in glyphspoints if glyphspoints.count(x) > 1} + dup_str = ", ".join(f"{x} ({x.encode('unicode_escape')})" for x in duplicates) + raise cv.Invalid( + f"Found duplicate glyph{'s' if len(duplicates) != 1 else ''}: {dup_str}" + ) + # convert to codepoints + glyphspoints = {ord(x) for x in glyphspoints} + fileconf = config[CONF_FILE] + setpoints = set( + flatten([glyphsets.unicodes_per_glyphset(x) for x in config[CONF_GLYPHSETS]]) + ) + # Make setpoints and glyphspoints disjoint + setpoints.difference_update(glyphspoints) + if fileconf[CONF_TYPE] == TYPE_LOCAL_BITMAP: + # Pillow only allows 256 glyphs per bitmap font. Not sure if that is a Pillow limitation + # or a file format limitation + if any(x >= 256 for x in setpoints.copy().union(glyphspoints)): + raise cv.Invalid("Codepoints in bitmap fonts must be in the range 0-255") + else: + # for TT fonts, check that glyphs are actually present + # Check extras against their own font, exclude from parent font codepoints + for extra in config[CONF_EXTRAS]: + points = {ord(x) for x in flatten(extra[CONF_GLYPHS])} + glyphspoints.difference_update(points) + setpoints.difference_update(points) + check_missing_glyphs(extra[CONF_FILE][CONF_PATH], points) + + # A named glyph that can't be provided is an error + check_missing_glyphs(fileconf[CONF_PATH], glyphspoints) + # A missing glyph from a set is a warning. + if not config[CONF_IGNORE_MISSING_GLYPHS]: + check_missing_glyphs(fileconf[CONF_PATH], setpoints, warning=True) + + # Populate the default after the above checks so that use of the default doesn't trigger errors + if not config[CONF_GLYPHS] and not config[CONF_GLYPHSETS]: + if fileconf[CONF_TYPE] == TYPE_LOCAL_BITMAP: + config[CONF_GLYPHS] = [DEFAULT_GLYPHS] + else: + # set a default glyphset, intersected with what the font actually offers + font = FONT_CACHE[fileconf[CONF_PATH]] + config[CONF_GLYPHS] = [ + chr(x) + for x in glyphsets.unicodes_per_glyphset(DEFAULT_GLYPHSET) + if font.get_char_index(x) != 0 + ] + return config @@ -120,7 +205,7 @@ def validate_truetype_file(value): ) if not any(map(value.lower().endswith, FONT_EXTENSIONS)): raise cv.Invalid(f"Only {FONT_EXTENSIONS} files are supported.") - return cv.file_(value) + return CORE.relative_config_path(cv.file_(value)) TYPE_LOCAL = "local" @@ -139,6 +224,10 @@ LOCAL_BITMAP_SCHEMA = cv.Schema( } ) +FULLPATH_SCHEMA = cv.maybe_simple_value( + {cv.Required(CONF_PATH): cv.string}, key=CONF_PATH +) + CONF_ITALIC = "italic" FONT_WEIGHTS = { "thin": 100, @@ -167,13 +256,13 @@ def _compute_local_font_path(value: dict) -> Path: return base_dir / key -def get_font_path(value, type) -> Path: - if type == TYPE_GFONTS: +def get_font_path(value, font_type) -> Path: + if font_type == TYPE_GFONTS: name = f"{value[CONF_FAMILY]}@{value[CONF_WEIGHT]}@{value[CONF_ITALIC]}@v1" return external_files.compute_local_file_dir(DOMAIN) / f"{name}.ttf" - if type == TYPE_WEB: + if font_type == TYPE_WEB: return _compute_local_font_path(value) / "font.ttf" - return None + assert False def download_gfont(value): @@ -203,7 +292,7 @@ def download_gfont(value): _LOGGER.debug("download_gfont: ttf_url=%s", ttf_url) external_files.download_content(ttf_url, path) - return value + return FULLPATH_SCHEMA(path) def download_web_font(value): @@ -212,7 +301,7 @@ def download_web_font(value): external_files.download_content(url, path) _LOGGER.debug("download_web_font: path=%s", path) - return value + return FULLPATH_SCHEMA(path) EXTERNAL_FONT_SCHEMA = cv.Schema( @@ -225,7 +314,6 @@ EXTERNAL_FONT_SCHEMA = cv.Schema( } ) - GFONTS_SCHEMA = cv.All( EXTERNAL_FONT_SCHEMA.extend( { @@ -259,10 +347,10 @@ def validate_file_shorthand(value): } if weight is not None: data[CONF_WEIGHT] = weight[1:] - return FILE_SCHEMA(data) + return font_file_schema(data) if value.startswith("http://") or value.startswith("https://"): - return FILE_SCHEMA( + return font_file_schema( { CONF_TYPE: TYPE_WEB, CONF_URL: value, @@ -270,14 +358,15 @@ def validate_file_shorthand(value): ) if value.endswith(".pcf") or value.endswith(".bdf"): - return FILE_SCHEMA( - { - CONF_TYPE: TYPE_LOCAL_BITMAP, - CONF_PATH: value, - } + value = convert_bitmap_to_pillow_font( + CORE.relative_config_path(cv.file_(value)) ) + return { + CONF_TYPE: TYPE_LOCAL_BITMAP, + CONF_PATH: value, + } - return FILE_SCHEMA( + return font_file_schema( { CONF_TYPE: TYPE_LOCAL, CONF_PATH: value, @@ -295,31 +384,35 @@ TYPED_FILE_SCHEMA = cv.typed_schema( ) -def _file_schema(value): +def font_file_schema(value): if isinstance(value, str): return validate_file_shorthand(value) return TYPED_FILE_SCHEMA(value) -FILE_SCHEMA = cv.All(_file_schema) +# Default if no glyphs or glyphsets are provided +DEFAULT_GLYPHSET = "GF_Latin_Kernel" +# default for bitmap fonts +DEFAULT_GLYPHS = ' !"%()+=,-.:/?0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz' -DEFAULT_GLYPHS = ( - ' !"%()+=,-.:/?0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz°' -) CONF_RAW_GLYPH_ID = "raw_glyph_id" FONT_SCHEMA = cv.Schema( { cv.Required(CONF_ID): cv.declare_id(Font), - cv.Required(CONF_FILE): FILE_SCHEMA, - cv.Optional(CONF_GLYPHS, default=DEFAULT_GLYPHS): validate_glyphs, + cv.Required(CONF_FILE): font_file_schema, + cv.Optional(CONF_GLYPHS, default=[]): cv.ensure_list(cv.string_strict), + cv.Optional(CONF_GLYPHSETS, default=[]): cv.ensure_list( + cv.one_of(*glyphsets.defined_glyphsets()) + ), + cv.Optional(CONF_IGNORE_MISSING_GLYPHS, default=False): cv.boolean, cv.Optional(CONF_SIZE, default=20): cv.int_range(min=1), cv.Optional(CONF_BPP, default=1): cv.one_of(1, 2, 4, 8), - cv.Optional(CONF_EXTRAS): cv.ensure_list( + cv.Optional(CONF_EXTRAS, default=[]): cv.ensure_list( cv.Schema( { - cv.Required(CONF_FILE): FILE_SCHEMA, - cv.Required(CONF_GLYPHS): validate_glyphs, + cv.Required(CONF_FILE): font_file_schema, + cv.Required(CONF_GLYPHS): cv.ensure_list(cv.string_strict), } ) ), @@ -328,7 +421,7 @@ FONT_SCHEMA = cv.Schema( }, ) -CONFIG_SCHEMA = cv.All(validate_pillow_installed, FONT_SCHEMA, merge_glyphs) +CONFIG_SCHEMA = cv.All(validate_pillow_installed, FONT_SCHEMA, validate_glyphs) # PIL doesn't provide a consistent interface for both TrueType and bitmap @@ -367,28 +460,20 @@ class BitmapFontWrapper: mask = self.getmask(glyph, mode="1") _, height = mask.size max_height = max(max_height, height) - return (max_height, 0) + return max_height, 0 class EFont: - def __init__(self, file, size, glyphs): - self.glyphs = glyphs + def __init__(self, file, size, codepoints): + self.codepoints = codepoints + path = file[CONF_PATH] + self.name = Path(path).name ftype = file[CONF_TYPE] if ftype == TYPE_LOCAL_BITMAP: - font = load_bitmap_font(CORE.relative_config_path(file[CONF_PATH])) - elif ftype == TYPE_LOCAL: - path = CORE.relative_config_path(file[CONF_PATH]) - font = load_ttf_font(path, size) - elif ftype in (TYPE_GFONTS, TYPE_WEB): - path = get_font_path(file, ftype) - font = load_ttf_font(path, size) + self.font = load_bitmap_font(path) else: - raise cv.Invalid(f"Could not load font: unknown type: {ftype}") - self.font = font - self.ascent, self.descent = font.getmetrics(glyphs) - - def has_glyph(self, glyph): - return glyph in self.glyphs + self.font = load_ttf_font(path, size) + self.ascent, self.descent = self.font.getmetrics(codepoints) def convert_bitmap_to_pillow_font(filepath): @@ -400,6 +485,7 @@ def convert_bitmap_to_pillow_font(filepath): copy_file_if_changed(filepath, local_bitmap_font_file) + local_pil_font_file = local_bitmap_font_file.with_suffix(".pil") with open(local_bitmap_font_file, "rb") as fp: try: try: @@ -409,28 +495,22 @@ def convert_bitmap_to_pillow_font(filepath): p = BdfFontFile.BdfFontFile(fp) # Convert to pillow-formatted fonts, which have a .pil and .pbm extension. - p.save(local_bitmap_font_file) + p.save(local_pil_font_file) except (SyntaxError, OSError) as err: raise core.EsphomeError( f"Failed to parse as bitmap font: '{filepath}': {err}" ) - local_pil_font_file = os.path.splitext(local_bitmap_font_file)[0] + ".pil" - return cv.file_(local_pil_font_file) + return str(local_pil_font_file) def load_bitmap_font(filepath): from PIL import ImageFont - # Convert bpf and pcf files to pillow fonts, first. - pil_font_path = convert_bitmap_to_pillow_font(filepath) - try: - font = ImageFont.load(str(pil_font_path)) + font = ImageFont.load(str(filepath)) except Exception as e: - raise core.EsphomeError( - f"Failed to load bitmap font file: {pil_font_path} : {e}" - ) + raise core.EsphomeError(f"Failed to load bitmap font file: {filepath}: {e}") return BitmapFontWrapper(font) @@ -441,7 +521,7 @@ def load_ttf_font(path, size): try: font = ImageFont.truetype(str(path), size) except Exception as e: - raise core.EsphomeError(f"Could not load truetype file {path}: {e}") + raise core.EsphomeError(f"Could not load TrueType file {path}: {e}") return TrueTypeFontWrapper(font) @@ -456,14 +536,35 @@ class GlyphInfo: async def to_code(config): - glyph_to_font_map = {} - font_list = font_map[config[CONF_ID]] - glyphs = [] - for font in font_list: - glyphs.extend(font.glyphs) - for glyph in font.glyphs: - glyph_to_font_map[glyph] = font - glyphs.sort(key=functools.cmp_to_key(glyph_comparator)) + """ + Collect all glyph codepoints, construct a map from a codepoint to a font file. + Codepoints are either explicit (glyphs key in top level or extras) or part of a glyphset. + Codepoints listed in extras use the extra font and override codepoints from glyphsets. + Achieve this by processing the base codepoints first, then the extras + """ + + # get the codepoints from glyphsets and flatten to a set of chrs. + point_set: set[str] = { + chr(x) + for x in flatten( + [glyphsets.unicodes_per_glyphset(x) for x in config[CONF_GLYPHSETS]] + ) + } + # get the codepoints from the glyphs key, flatten to a list of chrs and combine with the points from glyphsets + point_set.update(flatten(config[CONF_GLYPHS])) + size = config[CONF_SIZE] + # Create the codepoint to font file map + base_font = EFont(config[CONF_FILE], size, point_set) + point_font_map: dict[str, EFont] = {c: base_font for c in point_set} + # process extras, updating the map and extending the codepoint list + for extra in config[CONF_EXTRAS]: + extra_points = flatten(extra[CONF_GLYPHS]) + point_set.update(extra_points) + extra_font = EFont(extra[CONF_FILE], size, extra_points) + point_font_map.update({c: extra_font for c in extra_points}) + + codepoints = list(point_set) + codepoints.sort(key=functools.cmp_to_key(glyph_comparator)) glyph_args = {} data = [] bpp = config[CONF_BPP] @@ -473,10 +574,11 @@ async def to_code(config): else: mode = "L" scale = 256 // (1 << bpp) - for glyph in glyphs: - font = glyph_to_font_map[glyph].font - mask = font.getmask(glyph, mode=mode) - offset_x, offset_y = font.getoffset(glyph) + # create the data array for all glyphs + for codepoint in codepoints: + font = point_font_map[codepoint] + mask = font.font.getmask(codepoint, mode=mode) + offset_x, offset_y = font.font.getoffset(codepoint) width, height = mask.size glyph_data = [0] * ((height * width * bpp + 7) // 8) pos = 0 @@ -487,31 +589,34 @@ async def to_code(config): if pixel & (1 << (bpp - bit_num - 1)): glyph_data[pos // 8] |= 0x80 >> (pos % 8) pos += 1 - glyph_args[glyph] = GlyphInfo(len(data), offset_x, offset_y, width, height) + glyph_args[codepoint] = GlyphInfo(len(data), offset_x, offset_y, width, height) data += glyph_data rhs = [HexInt(x) for x in data] prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) + # Create the glyph table that points to data in the above array. glyph_initializer = [] - for glyph in glyphs: + for codepoint in codepoints: glyph_initializer.append( cg.StructInitializer( GlyphData, ( "a_char", - cg.RawExpression(f"(const uint8_t *){cpp_string_escape(glyph)}"), + cg.RawExpression( + f"(const uint8_t *){cpp_string_escape(codepoint)}" + ), ), ( "data", cg.RawExpression( - f"{str(prog_arr)} + {str(glyph_args[glyph].data_len)}" + f"{str(prog_arr)} + {str(glyph_args[codepoint].data_len)}" ), ), - ("offset_x", glyph_args[glyph].offset_x), - ("offset_y", glyph_args[glyph].offset_y), - ("width", glyph_args[glyph].width), - ("height", glyph_args[glyph].height), + ("offset_x", glyph_args[codepoint].offset_x), + ("offset_y", glyph_args[codepoint].offset_y), + ("width", glyph_args[codepoint].width), + ("height", glyph_args[codepoint].height), ) ) @@ -521,7 +626,7 @@ async def to_code(config): config[CONF_ID], glyphs, len(glyph_initializer), - font_list[0].ascent, - font_list[0].ascent + font_list[0].descent, + base_font.ascent, + base_font.ascent + base_font.descent, bpp, ) diff --git a/requirements.txt b/requirements.txt index 8cc26e4da0..e11e629743 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,6 +17,9 @@ aioesphomeapi==24.6.2 zeroconf==0.132.2 puremagic==1.27 ruamel.yaml==0.18.6 # dashboard_import +glyphsets==1.0.0 +pillow==10.4.0 +freetype-py==2.5.1 # esp-idf requires this, but doesn't bundle it by default # https://github.com/espressif/esp-idf/blob/220590d599e134d7a5e7f1e683cc4550349ffbf8/requirements.txt#L24 diff --git a/requirements_optional.txt b/requirements_optional.txt index 2d57c5fd96..7416753d55 100644 --- a/requirements_optional.txt +++ b/requirements_optional.txt @@ -1,2 +1 @@ -pillow==10.4.0 cairosvg==2.7.1 diff --git a/script/ci-custom.py b/script/ci-custom.py index 9a97d3e4a8..81e3da311a 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -58,7 +58,7 @@ file_types = ( ) cpp_include = ("*.h", "*.c", "*.cpp", "*.tcc") py_include = ("*.py",) -ignore_types = (".ico", ".png", ".woff", ".woff2", "", ".ttf", ".otf") +ignore_types = (".ico", ".png", ".woff", ".woff2", "", ".ttf", ".otf", ".pcf") LINT_FILE_CHECKS = [] LINT_CONTENT_CHECKS = [] diff --git a/tests/components/font/.gitattributes b/tests/components/font/.gitattributes new file mode 100644 index 0000000000..18d9a389e8 --- /dev/null +++ b/tests/components/font/.gitattributes @@ -0,0 +1,2 @@ +*.pcf -text + diff --git a/tests/components/font/MatrixChunky8X.bdf b/tests/components/font/MatrixChunky8X.bdf new file mode 100644 index 0000000000..89b3683180 --- /dev/null +++ b/tests/components/font/MatrixChunky8X.bdf @@ -0,0 +1,7461 @@ +STARTFONT 2.1 +FONT -Trip5-MatrixChunky8X-Medium-R-Normal--8-80-75-75-P-40-ISO10646-1 +SIZE 8 75 75 +FONTBOUNDINGBOX 8 8 -1 0 +COMMENT "Generated by fontforge, http://fontforge.sourceforge.net" +COMMENT "Trip5" +COMMENT "Conventional Chaos" +COMMENT "CC-BY" +STARTPROPERTIES 25 +FOUNDRY "Conventional Chaos" +FAMILY_NAME "MatrixChunky8X" +FONT_NAME "MatrixChunky8X" +FACE_NAME "MatrixChunky8X" +COPYRIGHT "https://github.com/trip5/Matrix-Fonts" +FONT_VERSION "001.000" +WEIGHT_NAME "Medium" +SLANT "R" +SETWIDTH_NAME "Normal" +ADD_STYLE_NAME "" +PIXEL_SIZE 8 +POINT_SIZE 80 +RESOLUTION_X 75 +RESOLUTION_Y 75 +SPACING "P" +AVERAGE_WIDTH 40 +CHARSET_REGISTRY "ISO10646" +CHARSET_ENCODING "1" +CHARSET_COLLECTIONS "ISO8859-2 ISO8859-9 ISO8859-4 ISO10646-1" +FONT_ASCENT 8 +FONT_DESCENT 0 +UNDERLINE_POSITION 0 +UNDERLINE_THICKNESS 1 +X_HEIGHT 6 +CAP_HEIGHT 8 +ENDPROPERTIES +CHARS 535 +STARTCHAR uni0000 +ENCODING 0 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 2 +BITMAP +A0 +00 +40 +00 +A0 +ENDCHAR +STARTCHAR space +ENCODING 32 +SWIDTH 250 0 +DWIDTH 2 0 +BBX 1 1 0 0 +BITMAP +00 +ENDCHAR +STARTCHAR exclam +ENCODING 33 +SWIDTH 250 0 +DWIDTH 2 0 +BBX 1 8 0 0 +BITMAP +80 +80 +80 +80 +80 +80 +00 +80 +ENDCHAR +STARTCHAR quotedbl +ENCODING 34 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 2 0 6 +BITMAP +A0 +A0 +ENDCHAR +STARTCHAR numbersign +ENCODING 35 +SWIDTH 750 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +50 +50 +F8 +50 +50 +F8 +50 +50 +ENDCHAR +STARTCHAR dollar +ENCODING 36 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +E0 +80 +E0 +20 +20 +E0 +40 +ENDCHAR +STARTCHAR percent +ENCODING 37 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +A0 +20 +40 +40 +80 +A0 +A0 +ENDCHAR +STARTCHAR ampersand +ENCODING 38 +SWIDTH 625 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +E0 +A0 +A0 +40 +B0 +A0 +B0 +D0 +ENDCHAR +STARTCHAR quotesingle +ENCODING 39 +SWIDTH 250 0 +DWIDTH 2 0 +BBX 1 3 0 5 +BITMAP +80 +80 +80 +ENDCHAR +STARTCHAR parenleft +ENCODING 40 +SWIDTH 1000 0 +DWIDTH 3 0 +BBX 2 8 0 0 +BITMAP +40 +40 +80 +80 +80 +80 +40 +40 +ENDCHAR +STARTCHAR parenright +ENCODING 41 +SWIDTH 1000 0 +DWIDTH 3 0 +BBX 2 8 0 0 +BITMAP +80 +80 +40 +40 +40 +40 +80 +80 +ENDCHAR +STARTCHAR asterisk +ENCODING 42 +SWIDTH 625 0 +DWIDTH 5 0 +BBX 4 4 0 4 +BITMAP +90 +60 +60 +90 +ENDCHAR +STARTCHAR plus +ENCODING 43 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 3 0 3 +BITMAP +40 +E0 +40 +ENDCHAR +STARTCHAR comma +ENCODING 44 +SWIDTH 1000 0 +DWIDTH 2 0 +BBX 1 2 0 0 +BITMAP +80 +80 +ENDCHAR +STARTCHAR hyphen +ENCODING 45 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 1 0 4 +BITMAP +E0 +ENDCHAR +STARTCHAR period +ENCODING 46 +SWIDTH 1000 0 +DWIDTH 2 0 +BBX 1 1 0 0 +BITMAP +80 +ENDCHAR +STARTCHAR slash +ENCODING 47 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +20 +20 +40 +40 +40 +80 +80 +80 +ENDCHAR +STARTCHAR zero +ENCODING 48 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +A0 +A0 +A0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR one +ENCODING 49 +SWIDTH 500 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +C0 +40 +40 +40 +40 +40 +E0 +ENDCHAR +STARTCHAR two +ENCODING 50 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +20 +20 +E0 +80 +80 +80 +E0 +ENDCHAR +STARTCHAR three +ENCODING 51 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +20 +20 +E0 +20 +20 +20 +E0 +ENDCHAR +STARTCHAR four +ENCODING 52 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +A0 +A0 +E0 +20 +20 +20 +20 +ENDCHAR +STARTCHAR five +ENCODING 53 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +80 +80 +E0 +20 +20 +20 +E0 +ENDCHAR +STARTCHAR six +ENCODING 54 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +80 +80 +E0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR seven +ENCODING 55 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +20 +20 +20 +20 +20 +20 +20 +ENDCHAR +STARTCHAR eight +ENCODING 56 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +A0 +A0 +E0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR nine +ENCODING 57 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +A0 +A0 +E0 +20 +20 +20 +E0 +ENDCHAR +STARTCHAR colon +ENCODING 58 +SWIDTH 1000 0 +DWIDTH 2 0 +BBX 1 3 0 3 +BITMAP +80 +00 +80 +ENDCHAR +STARTCHAR semicolon +ENCODING 59 +SWIDTH 1000 0 +DWIDTH 2 0 +BBX 1 4 0 2 +BITMAP +80 +00 +80 +80 +ENDCHAR +STARTCHAR less +ENCODING 60 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 2 +BITMAP +20 +40 +80 +40 +20 +ENDCHAR +STARTCHAR equal +ENCODING 61 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 3 0 3 +BITMAP +E0 +00 +E0 +ENDCHAR +STARTCHAR greater +ENCODING 62 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 2 +BITMAP +80 +40 +20 +40 +80 +ENDCHAR +STARTCHAR question +ENCODING 63 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +A0 +20 +60 +40 +40 +00 +40 +ENDCHAR +STARTCHAR at +ENCODING 64 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +F0 +90 +90 +B0 +B0 +80 +80 +F0 +ENDCHAR +STARTCHAR A +ENCODING 65 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +A0 +A0 +E0 +A0 +A0 +A0 +A0 +ENDCHAR +STARTCHAR B +ENCODING 66 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +A0 +A0 +C0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR C +ENCODING 67 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +A0 +80 +80 +80 +80 +A0 +E0 +ENDCHAR +STARTCHAR D +ENCODING 68 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +C0 +A0 +A0 +A0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR E +ENCODING 69 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +80 +80 +E0 +80 +80 +80 +E0 +ENDCHAR +STARTCHAR F +ENCODING 70 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +80 +80 +E0 +80 +80 +80 +80 +ENDCHAR +STARTCHAR G +ENCODING 71 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +E0 +80 +80 +B0 +90 +90 +90 +F0 +ENDCHAR +STARTCHAR H +ENCODING 72 +SWIDTH 625 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +90 +90 +90 +F0 +90 +90 +90 +90 +ENDCHAR +STARTCHAR I +ENCODING 73 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +40 +40 +40 +40 +40 +40 +E0 +ENDCHAR +STARTCHAR J +ENCODING 74 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +20 +20 +20 +20 +20 +A0 +A0 +E0 +ENDCHAR +STARTCHAR K +ENCODING 75 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +A0 +A0 +C0 +A0 +A0 +A0 +A0 +ENDCHAR +STARTCHAR L +ENCODING 76 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +80 +80 +80 +80 +80 +80 +80 +E0 +ENDCHAR +STARTCHAR M +ENCODING 77 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +88 +D8 +A8 +A8 +88 +88 +88 +88 +ENDCHAR +STARTCHAR N +ENCODING 78 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +90 +90 +D0 +B0 +90 +90 +90 +90 +ENDCHAR +STARTCHAR O +ENCODING 79 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +F0 +90 +90 +90 +90 +90 +90 +F0 +ENDCHAR +STARTCHAR P +ENCODING 80 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +A0 +A0 +E0 +80 +80 +80 +80 +ENDCHAR +STARTCHAR Q +ENCODING 81 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +F0 +90 +90 +90 +B0 +B0 +A0 +D0 +ENDCHAR +STARTCHAR R +ENCODING 82 +SWIDTH 500 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +A0 +A0 +C0 +A0 +A0 +A0 +A0 +ENDCHAR +STARTCHAR S +ENCODING 83 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +A0 +80 +E0 +20 +20 +A0 +E0 +ENDCHAR +STARTCHAR T +ENCODING 84 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +40 +40 +40 +40 +40 +40 +40 +ENDCHAR +STARTCHAR U +ENCODING 85 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +A0 +A0 +A0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR V +ENCODING 86 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +A0 +A0 +A0 +A0 +A0 +E0 +40 +ENDCHAR +STARTCHAR W +ENCODING 87 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +A8 +A8 +A8 +A8 +A8 +A8 +F8 +50 +ENDCHAR +STARTCHAR X +ENCODING 88 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +A0 +A0 +40 +A0 +A0 +A0 +A0 +ENDCHAR +STARTCHAR Y +ENCODING 89 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +A0 +A0 +E0 +40 +40 +40 +40 +ENDCHAR +STARTCHAR Z +ENCODING 90 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +20 +20 +40 +80 +80 +80 +E0 +ENDCHAR +STARTCHAR bracketleft +ENCODING 91 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +80 +80 +80 +80 +80 +80 +E0 +ENDCHAR +STARTCHAR backslash +ENCODING 92 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +80 +80 +40 +40 +40 +20 +20 +20 +ENDCHAR +STARTCHAR bracketright +ENCODING 93 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +20 +20 +20 +20 +20 +20 +E0 +ENDCHAR +STARTCHAR asciicircum +ENCODING 94 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 3 0 5 +BITMAP +40 +E0 +A0 +ENDCHAR +STARTCHAR underscore +ENCODING 95 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 1 0 0 +BITMAP +E0 +ENDCHAR +STARTCHAR grave +ENCODING 96 +SWIDTH 1000 0 +DWIDTH 3 0 +BBX 2 2 0 6 +BITMAP +80 +40 +ENDCHAR +STARTCHAR a +ENCODING 97 +SWIDTH 500 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +E0 +20 +E0 +A0 +E0 +ENDCHAR +STARTCHAR b +ENCODING 98 +SWIDTH 500 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +80 +80 +80 +E0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR c +ENCODING 99 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +E0 +80 +80 +80 +E0 +ENDCHAR +STARTCHAR d +ENCODING 100 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +20 +20 +20 +E0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR e +ENCODING 101 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +E0 +A0 +E0 +80 +E0 +ENDCHAR +STARTCHAR f +ENCODING 102 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +60 +40 +40 +E0 +40 +40 +40 +ENDCHAR +STARTCHAR g +ENCODING 103 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +E0 +A0 +E0 +20 +60 +ENDCHAR +STARTCHAR h +ENCODING 104 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +80 +80 +80 +E0 +A0 +A0 +A0 +A0 +ENDCHAR +STARTCHAR i +ENCODING 105 +SWIDTH 500 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +40 +00 +C0 +40 +40 +40 +E0 +ENDCHAR +STARTCHAR j +ENCODING 106 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +20 +00 +20 +20 +20 +A0 +E0 +ENDCHAR +STARTCHAR k +ENCODING 107 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +80 +80 +A0 +A0 +C0 +A0 +A0 +A0 +ENDCHAR +STARTCHAR l +ENCODING 108 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +C0 +40 +40 +40 +40 +40 +40 +E0 +ENDCHAR +STARTCHAR m +ENCODING 109 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 5 0 0 +BITMAP +F8 +A8 +A8 +A8 +A8 +ENDCHAR +STARTCHAR n +ENCODING 110 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +E0 +A0 +A0 +A0 +A0 +ENDCHAR +STARTCHAR o +ENCODING 111 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +E0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR p +ENCODING 112 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +E0 +A0 +E0 +80 +80 +ENDCHAR +STARTCHAR q +ENCODING 113 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +E0 +A0 +E0 +20 +20 +ENDCHAR +STARTCHAR r +ENCODING 114 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +E0 +80 +80 +80 +80 +ENDCHAR +STARTCHAR s +ENCODING 115 +SWIDTH 500 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +E0 +80 +E0 +20 +E0 +ENDCHAR +STARTCHAR t +ENCODING 116 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +40 +40 +E0 +40 +40 +40 +60 +ENDCHAR +STARTCHAR u +ENCODING 117 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +A0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR v +ENCODING 118 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +A0 +A0 +A0 +E0 +40 +ENDCHAR +STARTCHAR w +ENCODING 119 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 5 0 0 +BITMAP +A8 +A8 +A8 +F8 +50 +ENDCHAR +STARTCHAR x +ENCODING 120 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +A0 +A0 +40 +A0 +A0 +ENDCHAR +STARTCHAR y +ENCODING 121 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +A0 +A0 +E0 +20 +60 +ENDCHAR +STARTCHAR z +ENCODING 122 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +E0 +20 +40 +80 +E0 +ENDCHAR +STARTCHAR braceleft +ENCODING 123 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +60 +40 +40 +C0 +40 +40 +40 +60 +ENDCHAR +STARTCHAR bar +ENCODING 124 +SWIDTH 1000 0 +DWIDTH 2 0 +BBX 1 8 0 0 +BITMAP +80 +80 +80 +80 +80 +80 +80 +80 +ENDCHAR +STARTCHAR braceright +ENCODING 125 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +C0 +40 +40 +60 +40 +40 +40 +C0 +ENDCHAR +STARTCHAR asciitilde +ENCODING 126 +SWIDTH 500 0 +DWIDTH 4 0 +BBX 3 2 0 3 +BITMAP +60 +C0 +ENDCHAR +STARTCHAR exclamdown +ENCODING 161 +SWIDTH 250 0 +DWIDTH 2 0 +BBX 1 8 0 0 +BITMAP +80 +80 +00 +80 +80 +80 +80 +80 +ENDCHAR +STARTCHAR cent +ENCODING 162 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +40 +E0 +80 +80 +80 +E0 +40 +ENDCHAR +STARTCHAR sterling +ENCODING 163 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +70 +50 +40 +E0 +40 +40 +40 +F0 +ENDCHAR +STARTCHAR currency +ENCODING 164 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 6 0 1 +BITMAP +F0 +60 +90 +90 +60 +F0 +ENDCHAR +STARTCHAR yen +ENCODING 165 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +A0 +E0 +40 +E0 +40 +40 +40 +ENDCHAR +STARTCHAR brokenbar +ENCODING 166 +SWIDTH 1000 0 +DWIDTH 2 0 +BBX 1 8 0 0 +BITMAP +80 +80 +80 +00 +80 +80 +80 +80 +ENDCHAR +STARTCHAR section +ENCODING 167 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +60 +80 +E0 +A0 +A0 +E0 +20 +C0 +ENDCHAR +STARTCHAR dieresis +ENCODING 168 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +E0 +A0 +C0 +A0 +A0 +E0 +40 +ENDCHAR +STARTCHAR copyright +ENCODING 169 +SWIDTH 1000 0 +DWIDTH 7 0 +BBX 6 7 0 1 +BITMAP +78 +CC +B4 +A4 +B4 +CC +78 +ENDCHAR +STARTCHAR ordfeminine +ENCODING 170 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 3 +BITMAP +60 +A0 +E0 +00 +E0 +ENDCHAR +STARTCHAR guillemotleft +ENCODING 171 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 3 0 3 +BITMAP +50 +F0 +50 +ENDCHAR +STARTCHAR logicalnot +ENCODING 172 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +B8 +A8 +20 +20 +20 +28 +28 +38 +ENDCHAR +STARTCHAR uni00AD +ENCODING 173 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 1 0 4 +BITMAP +E0 +ENDCHAR +STARTCHAR registered +ENCODING 174 +SWIDTH 1000 0 +DWIDTH 7 0 +BBX 6 7 0 1 +BITMAP +78 +CC +B4 +A4 +A4 +CC +78 +ENDCHAR +STARTCHAR macron +ENCODING 175 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +B8 +A0 +20 +38 +20 +20 +20 +20 +ENDCHAR +STARTCHAR degree +ENCODING 176 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 3 0 5 +BITMAP +E0 +A0 +E0 +ENDCHAR +STARTCHAR plusminus +ENCODING 177 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 2 +BITMAP +40 +E0 +40 +00 +E0 +ENDCHAR +STARTCHAR uni00B2 +ENCODING 178 +SWIDTH 1000 0 +DWIDTH 3 0 +BBX 2 5 0 3 +BITMAP +C0 +40 +C0 +80 +C0 +ENDCHAR +STARTCHAR uni00B3 +ENCODING 179 +SWIDTH 1000 0 +DWIDTH 3 0 +BBX 2 5 0 3 +BITMAP +C0 +40 +C0 +40 +C0 +ENDCHAR +STARTCHAR acute +ENCODING 180 +SWIDTH 1000 0 +DWIDTH 3 0 +BBX 2 2 0 6 +BITMAP +40 +C0 +ENDCHAR +STARTCHAR mu +ENCODING 181 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 6 0 0 +BITMAP +A0 +A0 +A0 +A0 +E0 +80 +ENDCHAR +STARTCHAR paragraph +ENCODING 182 +SWIDTH 625 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +F0 +D0 +F0 +30 +30 +30 +30 +30 +ENDCHAR +STARTCHAR periodcentered +ENCODING 183 +SWIDTH 1000 0 +DWIDTH 2 0 +BBX 1 1 0 4 +BITMAP +80 +ENDCHAR +STARTCHAR cedilla +ENCODING 184 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +00 +00 +E0 +00 +00 +00 +E0 +ENDCHAR +STARTCHAR uni00B9 +ENCODING 185 +SWIDTH 1000 0 +DWIDTH 2 0 +BBX 1 5 0 3 +BITMAP +80 +80 +80 +80 +80 +ENDCHAR +STARTCHAR ordmasculine +ENCODING 186 +SWIDTH 500 0 +DWIDTH 4 0 +BBX 3 5 0 3 +BITMAP +E0 +A0 +E0 +00 +E0 +ENDCHAR +STARTCHAR guillemotright +ENCODING 187 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 3 0 3 +BITMAP +A0 +F0 +A0 +ENDCHAR +STARTCHAR onequarter +ENCODING 188 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +30 +60 +40 +F0 +40 +40 +60 +30 +ENDCHAR +STARTCHAR onehalf +ENCODING 189 +SWIDTH 1000 0 +DWIDTH 8 0 +BBX 7 8 0 0 +BITMAP +54 +54 +FE +54 +54 +54 +7C +28 +ENDCHAR +STARTCHAR threequarters +ENCODING 190 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +70 +50 +10 +F8 +40 +40 +50 +70 +ENDCHAR +STARTCHAR questiondown +ENCODING 191 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +00 +40 +40 +C0 +80 +A0 +E0 +ENDCHAR +STARTCHAR Agrave +ENCODING 192 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +80 +40 +00 +E0 +A0 +E0 +A0 +A0 +ENDCHAR +STARTCHAR Aacute +ENCODING 193 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +20 +40 +00 +E0 +A0 +E0 +A0 +A0 +ENDCHAR +STARTCHAR Acircumflex +ENCODING 194 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +A0 +00 +E0 +A0 +E0 +A0 +A0 +ENDCHAR +STARTCHAR Atilde +ENCODING 195 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +00 +E0 +A0 +E0 +A0 +A0 +A0 +ENDCHAR +STARTCHAR Adieresis +ENCODING 196 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +00 +E0 +A0 +E0 +A0 +A0 +A0 +ENDCHAR +STARTCHAR Aring +ENCODING 197 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +00 +E0 +A0 +E0 +A0 +A0 +A0 +ENDCHAR +STARTCHAR AE +ENCODING 198 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +F8 +A0 +A0 +F8 +A0 +A0 +A0 +B8 +ENDCHAR +STARTCHAR Ccedilla +ENCODING 199 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +80 +80 +80 +80 +80 +E0 +40 +ENDCHAR +STARTCHAR Egrave +ENCODING 200 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +80 +40 +00 +E0 +80 +E0 +80 +E0 +ENDCHAR +STARTCHAR Eacute +ENCODING 201 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +20 +40 +00 +E0 +80 +E0 +80 +E0 +ENDCHAR +STARTCHAR Ecircumflex +ENCODING 202 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +A0 +00 +E0 +80 +E0 +80 +E0 +ENDCHAR +STARTCHAR Edieresis +ENCODING 203 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +00 +E0 +80 +E0 +80 +80 +E0 +ENDCHAR +STARTCHAR Igrave +ENCODING 204 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +80 +40 +00 +E0 +40 +40 +40 +E0 +ENDCHAR +STARTCHAR Iacute +ENCODING 205 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +20 +40 +00 +E0 +40 +40 +40 +E0 +ENDCHAR +STARTCHAR Icircumflex +ENCODING 206 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +A0 +00 +E0 +40 +40 +40 +E0 +ENDCHAR +STARTCHAR Idieresis +ENCODING 207 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +00 +E0 +40 +40 +40 +40 +E0 +ENDCHAR +STARTCHAR Eth +ENCODING 208 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +60 +50 +50 +F0 +50 +50 +50 +60 +ENDCHAR +STARTCHAR Ntilde +ENCODING 209 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +60 +00 +90 +D0 +B0 +90 +90 +90 +ENDCHAR +STARTCHAR Ograve +ENCODING 210 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +20 +00 +E0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR Oacute +ENCODING 211 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +80 +00 +E0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR Ocircumflex +ENCODING 212 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +A0 +00 +E0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR Otilde +ENCODING 213 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +00 +E0 +A0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR Odieresis +ENCODING 214 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +00 +E0 +A0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR multiply +ENCODING 215 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 3 0 3 +BITMAP +A0 +40 +A0 +ENDCHAR +STARTCHAR Oslash +ENCODING 216 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +F0 +90 +90 +B0 +D0 +90 +90 +F0 +ENDCHAR +STARTCHAR Ugrave +ENCODING 217 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +80 +40 +00 +A0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR Uacute +ENCODING 218 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +20 +40 +00 +A0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR Ucircumflex +ENCODING 219 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +A0 +00 +A0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR Udieresis +ENCODING 220 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +00 +A0 +A0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR Yacute +ENCODING 221 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +20 +40 +00 +A0 +A0 +E0 +40 +40 +ENDCHAR +STARTCHAR Thorn +ENCODING 222 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +80 +80 +E0 +A0 +A0 +E0 +80 +80 +ENDCHAR +STARTCHAR germandbls +ENCODING 223 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +A0 +C0 +A0 +A0 +E0 +80 +80 +ENDCHAR +STARTCHAR agrave +ENCODING 224 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +20 +00 +E0 +20 +E0 +A0 +E0 +ENDCHAR +STARTCHAR aacute +ENCODING 225 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +80 +00 +E0 +20 +E0 +A0 +E0 +ENDCHAR +STARTCHAR acircumflex +ENCODING 226 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +A0 +00 +E0 +20 +E0 +A0 +E0 +ENDCHAR +STARTCHAR atilde +ENCODING 227 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +E0 +00 +E0 +20 +E0 +A0 +E0 +ENDCHAR +STARTCHAR adieresis +ENCODING 228 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +A0 +00 +E0 +20 +E0 +A0 +E0 +ENDCHAR +STARTCHAR aring +ENCODING 229 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +40 +00 +E0 +20 +E0 +A0 +E0 +ENDCHAR +STARTCHAR ae +ENCODING 230 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 5 0 0 +BITMAP +F8 +28 +F8 +A0 +F8 +ENDCHAR +STARTCHAR ccedilla +ENCODING 231 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +E0 +80 +80 +E0 +40 +ENDCHAR +STARTCHAR egrave +ENCODING 232 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +20 +00 +E0 +A0 +E0 +80 +E0 +ENDCHAR +STARTCHAR eacute +ENCODING 233 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +80 +00 +E0 +A0 +E0 +80 +E0 +ENDCHAR +STARTCHAR ecircumflex +ENCODING 234 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +A0 +00 +E0 +A0 +E0 +80 +E0 +ENDCHAR +STARTCHAR edieresis +ENCODING 235 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +A0 +00 +E0 +A0 +E0 +80 +E0 +ENDCHAR +STARTCHAR igrave +ENCODING 236 +SWIDTH 375 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +80 +40 +00 +C0 +40 +40 +40 +E0 +ENDCHAR +STARTCHAR iacute +ENCODING 237 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +20 +40 +00 +C0 +40 +40 +40 +E0 +ENDCHAR +STARTCHAR icircumflex +ENCODING 238 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +A0 +00 +C0 +40 +40 +40 +E0 +ENDCHAR +STARTCHAR idieresis +ENCODING 239 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +A0 +00 +C0 +40 +40 +40 +E0 +ENDCHAR +STARTCHAR eth +ENCODING 240 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +40 +A0 +20 +E0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR ntilde +ENCODING 241 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +E0 +00 +E0 +A0 +A0 +A0 +A0 +ENDCHAR +STARTCHAR ograve +ENCODING 242 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +80 +40 +00 +E0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR oacute +ENCODING 243 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +20 +40 +00 +E0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR ocircumflex +ENCODING 244 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +40 +A0 +00 +E0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR otilde +ENCODING 245 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 6 0 0 +BITMAP +E0 +00 +E0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR odieresis +ENCODING 246 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 6 0 0 +BITMAP +A0 +00 +E0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR divide +ENCODING 247 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 2 +BITMAP +40 +00 +E0 +00 +40 +ENDCHAR +STARTCHAR oslash +ENCODING 248 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 5 0 0 +BITMAP +F0 +B0 +D0 +90 +F0 +ENDCHAR +STARTCHAR ugrave +ENCODING 249 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +80 +40 +00 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR uacute +ENCODING 250 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +20 +40 +00 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR ucircumflex +ENCODING 251 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +40 +A0 +00 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR udieresis +ENCODING 252 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 6 0 0 +BITMAP +A0 +00 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR yacute +ENCODING 253 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +20 +40 +00 +A0 +A0 +E0 +20 +60 +ENDCHAR +STARTCHAR thorn +ENCODING 254 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +80 +80 +80 +E0 +A0 +A0 +E0 +80 +ENDCHAR +STARTCHAR ydieresis +ENCODING 255 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +00 +A0 +A0 +A0 +E0 +20 +60 +ENDCHAR +STARTCHAR Amacron +ENCODING 256 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +00 +E0 +A0 +E0 +A0 +A0 +A0 +ENDCHAR +STARTCHAR amacron +ENCODING 257 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +E0 +00 +E0 +20 +E0 +A0 +E0 +ENDCHAR +STARTCHAR Abreve +ENCODING 258 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +40 +00 +E0 +A0 +E0 +A0 +A0 +ENDCHAR +STARTCHAR abreve +ENCODING 259 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +40 +00 +E0 +20 +E0 +A0 +E0 +ENDCHAR +STARTCHAR Aogonek +ENCODING 260 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 4 8 0 0 +BITMAP +E0 +A0 +A0 +E0 +A0 +A0 +A0 +30 +ENDCHAR +STARTCHAR aogonek +ENCODING 261 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 4 6 0 0 +BITMAP +E0 +20 +E0 +A0 +E0 +30 +ENDCHAR +STARTCHAR Cacute +ENCODING 262 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +80 +00 +E0 +80 +80 +80 +E0 +ENDCHAR +STARTCHAR cacute +ENCODING 263 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +40 +80 +00 +E0 +80 +80 +E0 +ENDCHAR +STARTCHAR Ccircumflex +ENCODING 264 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +A0 +00 +E0 +80 +80 +80 +E0 +ENDCHAR +STARTCHAR ccircumflex +ENCODING 265 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +40 +A0 +00 +E0 +80 +80 +E0 +ENDCHAR +STARTCHAR Cdotaccent +ENCODING 266 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +00 +E0 +80 +80 +80 +80 +E0 +ENDCHAR +STARTCHAR cdotaccent +ENCODING 267 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +40 +00 +E0 +80 +80 +80 +E0 +ENDCHAR +STARTCHAR Ccaron +ENCODING 268 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +40 +00 +E0 +80 +80 +80 +E0 +ENDCHAR +STARTCHAR ccaron +ENCODING 269 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +A0 +40 +00 +E0 +80 +80 +E0 +ENDCHAR +STARTCHAR Dcaron +ENCODING 270 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +40 +00 +C0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR dcaron +ENCODING 271 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +30 +30 +20 +E0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR Dcroat +ENCODING 272 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +60 +50 +50 +F0 +50 +50 +50 +70 +ENDCHAR +STARTCHAR dcroat +ENCODING 273 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +20 +70 +20 +E0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR Emacron +ENCODING 274 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +00 +E0 +80 +E0 +80 +80 +E0 +ENDCHAR +STARTCHAR emacron +ENCODING 275 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +E0 +00 +E0 +A0 +E0 +80 +E0 +ENDCHAR +STARTCHAR Ebreve +ENCODING 276 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +40 +00 +E0 +80 +E0 +80 +E0 +ENDCHAR +STARTCHAR ebreve +ENCODING 277 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +40 +00 +E0 +A0 +E0 +80 +E0 +ENDCHAR +STARTCHAR Edotaccent +ENCODING 278 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +00 +E0 +80 +E0 +80 +80 +E0 +ENDCHAR +STARTCHAR edotaccent +ENCODING 279 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +40 +00 +E0 +A0 +E0 +80 +E0 +ENDCHAR +STARTCHAR Eogonek +ENCODING 280 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 4 8 0 0 +BITMAP +E0 +80 +80 +E0 +80 +80 +E0 +30 +ENDCHAR +STARTCHAR eogonek +ENCODING 281 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 6 0 0 +BITMAP +E0 +A0 +E0 +80 +E0 +40 +ENDCHAR +STARTCHAR Ecaron +ENCODING 282 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +40 +00 +E0 +80 +E0 +80 +E0 +ENDCHAR +STARTCHAR ecaron +ENCODING 283 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +40 +00 +E0 +A0 +E0 +80 +E0 +ENDCHAR +STARTCHAR Gcircumflex +ENCODING 284 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +40 +A0 +00 +E0 +80 +B0 +90 +F0 +ENDCHAR +STARTCHAR gcircumflex +ENCODING 285 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +A0 +00 +E0 +A0 +E0 +20 +60 +ENDCHAR +STARTCHAR Gbreve +ENCODING 286 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +A0 +40 +00 +E0 +80 +B0 +90 +F0 +ENDCHAR +STARTCHAR gbreve +ENCODING 287 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +40 +00 +E0 +A0 +E0 +20 +60 +ENDCHAR +STARTCHAR Gdotaccent +ENCODING 288 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +40 +00 +E0 +80 +80 +B0 +90 +F0 +ENDCHAR +STARTCHAR gdotaccent +ENCODING 289 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +40 +00 +E0 +A0 +E0 +20 +60 +ENDCHAR +STARTCHAR uni0122 +ENCODING 290 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +E0 +80 +80 +B0 +90 +90 +F0 +40 +ENDCHAR +STARTCHAR uni0123 +ENCODING 291 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +40 +00 +E0 +A0 +E0 +20 +60 +ENDCHAR +STARTCHAR Hcircumflex +ENCODING 292 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +A0 +00 +A0 +A0 +E0 +A0 +A0 +ENDCHAR +STARTCHAR hcircumflex +ENCODING 293 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +A0 +00 +80 +80 +E0 +A0 +A0 +ENDCHAR +STARTCHAR Hbar +ENCODING 294 +SWIDTH 750 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +50 +F8 +50 +70 +50 +50 +50 +50 +ENDCHAR +STARTCHAR hbar +ENCODING 295 +SWIDTH 625 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +40 +E0 +40 +40 +70 +50 +50 +50 +ENDCHAR +STARTCHAR Itilde +ENCODING 296 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +00 +E0 +40 +40 +40 +40 +E0 +ENDCHAR +STARTCHAR itilde +ENCODING 297 +SWIDTH 500 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +E0 +00 +C0 +40 +40 +40 +E0 +ENDCHAR +STARTCHAR Imacron +ENCODING 298 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +00 +E0 +40 +40 +40 +40 +E0 +ENDCHAR +STARTCHAR imacron +ENCODING 299 +SWIDTH 500 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +E0 +00 +C0 +40 +40 +40 +E0 +ENDCHAR +STARTCHAR Ibreve +ENCODING 300 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +40 +00 +E0 +40 +40 +40 +E0 +ENDCHAR +STARTCHAR ibreve +ENCODING 301 +SWIDTH 500 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +40 +00 +C0 +40 +40 +40 +E0 +ENDCHAR +STARTCHAR Iogonek +ENCODING 302 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +40 +40 +40 +40 +40 +E0 +40 +ENDCHAR +STARTCHAR iogonek +ENCODING 303 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +40 +00 +C0 +40 +40 +E0 +20 +ENDCHAR +STARTCHAR Idotaccent +ENCODING 304 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +00 +E0 +40 +40 +40 +40 +E0 +ENDCHAR +STARTCHAR dotlessi +ENCODING 305 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +C0 +40 +40 +40 +E0 +ENDCHAR +STARTCHAR IJ +ENCODING 306 +SWIDTH 875 0 +DWIDTH 7 0 +BBX 6 8 0 0 +BITMAP +E4 +44 +44 +44 +44 +54 +54 +FC +ENDCHAR +STARTCHAR ij +ENCODING 307 +SWIDTH 625 0 +DWIDTH 5 0 +BBX 5 7 -1 0 +BITMAP +48 +00 +C8 +48 +48 +68 +F8 +ENDCHAR +STARTCHAR Jcircumflex +ENCODING 308 +SWIDTH 625 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +20 +50 +00 +20 +20 +A0 +A0 +E0 +ENDCHAR +STARTCHAR jcircumflex +ENCODING 309 +SWIDTH 625 0 +DWIDTH 5 0 +BBX 4 7 0 0 +BITMAP +20 +50 +00 +20 +20 +A0 +E0 +ENDCHAR +STARTCHAR uni0136 +ENCODING 310 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +A0 +A0 +C0 +A0 +A0 +A0 +40 +ENDCHAR +STARTCHAR uni0137 +ENCODING 311 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +80 +80 +A0 +A0 +C0 +A0 +A0 +40 +ENDCHAR +STARTCHAR kgreenlandic +ENCODING 312 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 6 0 0 +BITMAP +A0 +A0 +C0 +A0 +A0 +A0 +ENDCHAR +STARTCHAR Lacute +ENCODING 313 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +80 +00 +80 +80 +80 +80 +E0 +ENDCHAR +STARTCHAR lacute +ENCODING 314 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +80 +00 +C0 +40 +40 +40 +E0 +ENDCHAR +STARTCHAR uni013B +ENCODING 315 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +80 +80 +80 +80 +80 +80 +E0 +40 +ENDCHAR +STARTCHAR uni013C +ENCODING 316 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +C0 +40 +40 +40 +40 +40 +E0 +40 +ENDCHAR +STARTCHAR Lcaron +ENCODING 317 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +A0 +80 +80 +80 +80 +80 +E0 +ENDCHAR +STARTCHAR lcaron +ENCODING 318 +SWIDTH 625 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +D0 +50 +40 +40 +40 +40 +40 +E0 +ENDCHAR +STARTCHAR Ldot +ENCODING 319 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +80 +80 +A0 +80 +80 +80 +80 +E0 +ENDCHAR +STARTCHAR ldot +ENCODING 320 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +C0 +40 +60 +40 +40 +40 +40 +E0 +ENDCHAR +STARTCHAR Lslash +ENCODING 321 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +40 +40 +60 +C0 +40 +40 +40 +70 +ENDCHAR +STARTCHAR lslash +ENCODING 322 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +C0 +40 +60 +C0 +40 +40 +40 +E0 +ENDCHAR +STARTCHAR Nacute +ENCODING 323 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +20 +40 +00 +90 +D0 +B0 +90 +90 +ENDCHAR +STARTCHAR nacute +ENCODING 324 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +20 +40 +00 +E0 +A0 +A0 +A0 +A0 +ENDCHAR +STARTCHAR uni0145 +ENCODING 325 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +90 +90 +D0 +B0 +90 +90 +90 +40 +ENDCHAR +STARTCHAR uni0146 +ENCODING 326 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +E0 +A0 +A0 +A0 +40 +ENDCHAR +STARTCHAR Ncaron +ENCODING 327 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +A0 +40 +00 +90 +D0 +B0 +90 +90 +ENDCHAR +STARTCHAR ncaron +ENCODING 328 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +40 +00 +E0 +A0 +A0 +A0 +A0 +ENDCHAR +STARTCHAR napostrophe +ENCODING 329 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +80 +80 +00 +70 +50 +50 +50 +50 +ENDCHAR +STARTCHAR Eng +ENCODING 330 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +F0 +90 +90 +90 +90 +90 +90 +20 +ENDCHAR +STARTCHAR eng +ENCODING 331 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 6 0 0 +BITMAP +E0 +A0 +A0 +A0 +A0 +20 +ENDCHAR +STARTCHAR Omacron +ENCODING 332 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +00 +E0 +A0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR omacron +ENCODING 333 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +E0 +00 +E0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR Obreve +ENCODING 334 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +40 +00 +E0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR obreve +ENCODING 335 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +A0 +40 +00 +E0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR Ohungarumlaut +ENCODING 336 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +00 +E0 +A0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR ohungarumlaut +ENCODING 337 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +A0 +00 +E0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR OE +ENCODING 338 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +F8 +A0 +A0 +B8 +A0 +A0 +A0 +F8 +ENDCHAR +STARTCHAR oe +ENCODING 339 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 5 0 0 +BITMAP +F8 +A8 +B8 +A0 +F8 +ENDCHAR +STARTCHAR Racute +ENCODING 340 +SWIDTH 500 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +20 +40 +00 +E0 +A0 +C0 +A0 +A0 +ENDCHAR +STARTCHAR racute +ENCODING 341 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +20 +40 +00 +E0 +80 +80 +80 +80 +ENDCHAR +STARTCHAR uni0156 +ENCODING 342 +SWIDTH 500 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +A0 +A0 +C0 +A0 +A0 +A0 +40 +ENDCHAR +STARTCHAR uni0157 +ENCODING 343 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 6 0 0 +BITMAP +E0 +80 +80 +80 +80 +40 +ENDCHAR +STARTCHAR Rcaron +ENCODING 344 +SWIDTH 500 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +40 +00 +E0 +A0 +C0 +A0 +A0 +ENDCHAR +STARTCHAR rcaron +ENCODING 345 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +40 +00 +E0 +80 +80 +80 +80 +ENDCHAR +STARTCHAR Sacute +ENCODING 346 +SWIDTH 500 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +80 +00 +E0 +80 +E0 +20 +E0 +ENDCHAR +STARTCHAR sacute +ENCODING 347 +SWIDTH 500 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +20 +40 +00 +E0 +80 +E0 +20 +E0 +ENDCHAR +STARTCHAR Scircumflex +ENCODING 348 +SWIDTH 500 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +A0 +00 +E0 +80 +E0 +20 +E0 +ENDCHAR +STARTCHAR scircumflex +ENCODING 349 +SWIDTH 500 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +A0 +00 +E0 +80 +E0 +20 +E0 +ENDCHAR +STARTCHAR Scedilla +ENCODING 350 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +80 +80 +E0 +20 +20 +E0 +40 +ENDCHAR +STARTCHAR scedilla +ENCODING 351 +SWIDTH 500 0 +DWIDTH 4 0 +BBX 3 6 0 0 +BITMAP +E0 +80 +E0 +20 +E0 +40 +ENDCHAR +STARTCHAR Scaron +ENCODING 352 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +40 +00 +E0 +80 +E0 +20 +E0 +ENDCHAR +STARTCHAR scaron +ENCODING 353 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +40 +00 +E0 +80 +E0 +20 +E0 +ENDCHAR +STARTCHAR uni0162 +ENCODING 354 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +40 +40 +40 +40 +40 +60 +20 +ENDCHAR +STARTCHAR uni0163 +ENCODING 355 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +40 +E0 +40 +40 +40 +60 +20 +ENDCHAR +STARTCHAR Tcaron +ENCODING 356 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +00 +E0 +40 +40 +40 +40 +40 +ENDCHAR +STARTCHAR tcaron +ENCODING 357 +SWIDTH 625 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +50 +50 +40 +E0 +40 +40 +40 +60 +ENDCHAR +STARTCHAR Tbar +ENCODING 358 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +40 +40 +E0 +40 +40 +40 +40 +ENDCHAR +STARTCHAR tbar +ENCODING 359 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +40 +E0 +40 +E0 +40 +40 +60 +ENDCHAR +STARTCHAR Utilde +ENCODING 360 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +00 +A0 +A0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR utilde +ENCODING 361 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +E0 +00 +A0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR Umacron +ENCODING 362 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +00 +A0 +A0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR umacron +ENCODING 363 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +E0 +00 +A0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR Ubreve +ENCODING 364 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +40 +00 +A0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR ubreve +ENCODING 365 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +A0 +40 +00 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR Uring +ENCODING 366 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +00 +A0 +A0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR uring +ENCODING 367 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +40 +00 +A0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR Uhungarumlaut +ENCODING 368 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +00 +A0 +A0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR uhungarumlaut +ENCODING 369 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +A0 +00 +A0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR Uogonek +ENCODING 370 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +A0 +A0 +A0 +A0 +A0 +E0 +20 +ENDCHAR +STARTCHAR uogonek +ENCODING 371 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +A0 +A0 +A0 +E0 +20 +ENDCHAR +STARTCHAR Wcircumflex +ENCODING 372 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +20 +50 +00 +A8 +A8 +A8 +F8 +50 +ENDCHAR +STARTCHAR wcircumflex +ENCODING 373 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 7 0 0 +BITMAP +20 +50 +00 +A8 +A8 +F8 +50 +ENDCHAR +STARTCHAR Ycircumflex +ENCODING 374 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +A0 +00 +A0 +A0 +E0 +40 +40 +ENDCHAR +STARTCHAR ycircumflex +ENCODING 375 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +A0 +00 +A0 +A0 +E0 +20 +60 +ENDCHAR +STARTCHAR Ydieresis +ENCODING 376 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +00 +A0 +A0 +E0 +40 +40 +40 +ENDCHAR +STARTCHAR Zacute +ENCODING 377 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +20 +40 +00 +E0 +20 +40 +80 +E0 +ENDCHAR +STARTCHAR zacute +ENCODING 378 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +20 +40 +00 +E0 +20 +40 +80 +E0 +ENDCHAR +STARTCHAR Zdotaccent +ENCODING 379 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +00 +E0 +20 +40 +80 +80 +E0 +ENDCHAR +STARTCHAR zdotaccent +ENCODING 380 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +40 +00 +E0 +20 +40 +80 +E0 +ENDCHAR +STARTCHAR Zcaron +ENCODING 381 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +40 +00 +E0 +20 +40 +80 +E0 +ENDCHAR +STARTCHAR zcaron +ENCODING 382 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +40 +00 +E0 +20 +40 +80 +E0 +ENDCHAR +STARTCHAR longs +ENCODING 383 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +80 +80 +80 +80 +80 +80 +80 +ENDCHAR +STARTCHAR Alphatonos +ENCODING 902 +SWIDTH 750 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +B8 +A8 +28 +38 +28 +28 +28 +28 +ENDCHAR +STARTCHAR Epsilontonos +ENCODING 904 +SWIDTH 750 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +B8 +A0 +20 +38 +20 +20 +20 +38 +ENDCHAR +STARTCHAR Etatonos +ENCODING 905 +SWIDTH 875 0 +DWIDTH 7 0 +BBX 6 8 0 0 +BITMAP +A4 +A4 +24 +3C +24 +24 +24 +24 +ENDCHAR +STARTCHAR Iotatonos +ENCODING 906 +SWIDTH 750 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +B8 +90 +10 +10 +10 +10 +10 +38 +ENDCHAR +STARTCHAR Omicrontonos +ENCODING 908 +SWIDTH 875 0 +DWIDTH 7 0 +BBX 6 8 0 0 +BITMAP +BC +A4 +24 +24 +24 +24 +24 +3C +ENDCHAR +STARTCHAR Upsilontonos +ENCODING 910 +SWIDTH 750 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +A8 +A8 +28 +38 +10 +10 +10 +10 +ENDCHAR +STARTCHAR Omegatonos +ENCODING 911 +SWIDTH 1000 0 +DWIDTH 8 0 +BBX 7 8 0 0 +BITMAP +BE +A2 +22 +22 +22 +36 +14 +36 +ENDCHAR +STARTCHAR Alpha +ENCODING 913 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +A0 +A0 +E0 +A0 +A0 +A0 +A0 +ENDCHAR +STARTCHAR Beta +ENCODING 914 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +A0 +A0 +C0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR Gamma +ENCODING 915 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +80 +80 +80 +80 +80 +80 +80 +ENDCHAR +STARTCHAR uni0394 +ENCODING 916 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +20 +20 +50 +50 +88 +88 +88 +F8 +ENDCHAR +STARTCHAR Epsilon +ENCODING 917 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +80 +80 +E0 +80 +80 +80 +E0 +ENDCHAR +STARTCHAR Zeta +ENCODING 918 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +20 +20 +40 +80 +80 +80 +E0 +ENDCHAR +STARTCHAR Eta +ENCODING 919 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +90 +90 +90 +F0 +90 +90 +90 +90 +ENDCHAR +STARTCHAR Theta +ENCODING 920 +SWIDTH 625 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +F0 +90 +90 +F0 +90 +90 +90 +F0 +ENDCHAR +STARTCHAR Iota +ENCODING 921 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +40 +40 +40 +40 +40 +40 +E0 +ENDCHAR +STARTCHAR Kappa +ENCODING 922 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +A0 +A0 +C0 +A0 +A0 +A0 +A0 +ENDCHAR +STARTCHAR Lambda +ENCODING 923 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +20 +20 +50 +50 +50 +88 +88 +88 +ENDCHAR +STARTCHAR Mu +ENCODING 924 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +88 +D8 +A8 +A8 +88 +88 +88 +88 +ENDCHAR +STARTCHAR Nu +ENCODING 925 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +90 +90 +D0 +B0 +90 +90 +90 +90 +ENDCHAR +STARTCHAR Xi +ENCODING 926 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +00 +00 +E0 +00 +00 +00 +E0 +ENDCHAR +STARTCHAR Omicron +ENCODING 927 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +F0 +90 +90 +90 +90 +90 +90 +F0 +ENDCHAR +STARTCHAR Pi +ENCODING 928 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +F0 +90 +90 +90 +90 +90 +90 +90 +ENDCHAR +STARTCHAR Rho +ENCODING 929 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +A0 +A0 +E0 +80 +80 +80 +80 +ENDCHAR +STARTCHAR Sigma +ENCODING 931 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +80 +40 +20 +40 +80 +80 +E0 +ENDCHAR +STARTCHAR Tau +ENCODING 932 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +40 +40 +40 +40 +40 +40 +40 +ENDCHAR +STARTCHAR Upsilon +ENCODING 933 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +A0 +A0 +E0 +40 +40 +40 +40 +ENDCHAR +STARTCHAR Phi +ENCODING 934 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +20 +F8 +A8 +A8 +A8 +A8 +F8 +20 +ENDCHAR +STARTCHAR Chi +ENCODING 935 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +A0 +A0 +40 +A0 +A0 +A0 +A0 +ENDCHAR +STARTCHAR Psi +ENCODING 936 +SWIDTH 750 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +A8 +A8 +A8 +F8 +20 +20 +20 +20 +ENDCHAR +STARTCHAR uni03A9 +ENCODING 937 +SWIDTH 750 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +F8 +88 +88 +88 +88 +D8 +50 +D8 +ENDCHAR +STARTCHAR Iotadieresis +ENCODING 938 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +00 +E0 +40 +40 +40 +40 +E0 +ENDCHAR +STARTCHAR Upsilondieresis +ENCODING 939 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +00 +A0 +A0 +E0 +40 +40 +40 +ENDCHAR +STARTCHAR alphatonos +ENCODING 940 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +20 +40 +00 +D0 +A0 +A0 +A0 +D0 +ENDCHAR +STARTCHAR epsilontonos +ENCODING 941 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +20 +40 +00 +E0 +80 +40 +80 +E0 +ENDCHAR +STARTCHAR etatonos +ENCODING 942 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +20 +40 +00 +E0 +A0 +A0 +A0 +20 +ENDCHAR +STARTCHAR iotatonos +ENCODING 943 +SWIDTH 500 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +20 +40 +00 +C0 +40 +40 +40 +60 +ENDCHAR +STARTCHAR alpha +ENCODING 945 +SWIDTH 625 0 +DWIDTH 5 0 +BBX 4 5 0 0 +BITMAP +D0 +A0 +A0 +A0 +D0 +ENDCHAR +STARTCHAR beta +ENCODING 946 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +60 +A0 +A0 +C0 +A0 +A0 +E0 +80 +ENDCHAR +STARTCHAR gamma +ENCODING 947 +SWIDTH 625 0 +DWIDTH 5 0 +BBX 4 5 0 0 +BITMAP +D0 +50 +70 +20 +20 +ENDCHAR +STARTCHAR delta +ENCODING 948 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +80 +40 +E0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR epsilon +ENCODING 949 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +E0 +80 +40 +80 +E0 +ENDCHAR +STARTCHAR zeta +ENCODING 950 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +20 +C0 +80 +80 +80 +E0 +20 +ENDCHAR +STARTCHAR eta +ENCODING 951 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +E0 +A0 +A0 +A0 +20 +ENDCHAR +STARTCHAR theta +ENCODING 952 +SWIDTH 500 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +A0 +A0 +E0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR iota +ENCODING 953 +SWIDTH 500 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +C0 +40 +40 +40 +60 +ENDCHAR +STARTCHAR kappa +ENCODING 954 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +A0 +A0 +C0 +A0 +A0 +ENDCHAR +STARTCHAR lambda +ENCODING 955 +SWIDTH 750 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +60 +20 +20 +50 +50 +88 +88 +88 +ENDCHAR +STARTCHAR uni03BC +ENCODING 956 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +A0 +A0 +A0 +E0 +80 +ENDCHAR +STARTCHAR nu +ENCODING 957 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +A0 +A0 +A0 +E0 +40 +ENDCHAR +STARTCHAR xi +ENCODING 958 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +80 +E0 +80 +60 +80 +80 +E0 +20 +ENDCHAR +STARTCHAR omicron +ENCODING 959 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +E0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR pi +ENCODING 960 +SWIDTH 750 0 +DWIDTH 6 0 +BBX 5 5 0 0 +BITMAP +F8 +50 +50 +50 +58 +ENDCHAR +STARTCHAR rho +ENCODING 961 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +E0 +A0 +A0 +E0 +80 +ENDCHAR +STARTCHAR sigma1 +ENCODING 962 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +E0 +80 +80 +E0 +20 +ENDCHAR +STARTCHAR sigma +ENCODING 963 +SWIDTH 625 0 +DWIDTH 5 0 +BBX 4 5 0 0 +BITMAP +F0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR tau +ENCODING 964 +SWIDTH 500 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +E0 +40 +40 +40 +60 +ENDCHAR +STARTCHAR upsilon +ENCODING 965 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +A0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR phi +ENCODING 966 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 5 0 0 +BITMAP +B8 +A8 +A8 +F8 +20 +ENDCHAR +STARTCHAR chi +ENCODING 967 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +A0 +A0 +40 +A0 +A0 +ENDCHAR +STARTCHAR psi +ENCODING 968 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 5 0 0 +BITMAP +A8 +A8 +A8 +F8 +20 +ENDCHAR +STARTCHAR omega +ENCODING 969 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 5 0 0 +BITMAP +88 +A8 +A8 +F8 +50 +ENDCHAR +STARTCHAR iotadieresis +ENCODING 970 +SWIDTH 500 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +A0 +00 +C0 +40 +40 +40 +60 +ENDCHAR +STARTCHAR upsilondieresis +ENCODING 971 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +A0 +00 +A0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR omicrontonos +ENCODING 972 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +20 +40 +00 +E0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR upsilontonos +ENCODING 973 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +20 +40 +00 +A0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR omegatonos +ENCODING 974 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +10 +20 +00 +88 +A8 +A8 +F8 +50 +ENDCHAR +STARTCHAR uni0401 +ENCODING 1025 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +00 +E0 +80 +E0 +80 +80 +E0 +ENDCHAR +STARTCHAR uni0404 +ENCODING 1028 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +70 +80 +80 +E0 +80 +80 +80 +70 +ENDCHAR +STARTCHAR uni0406 +ENCODING 1030 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +40 +40 +40 +40 +40 +40 +E0 +ENDCHAR +STARTCHAR uni0407 +ENCODING 1031 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +00 +E0 +40 +40 +40 +40 +E0 +ENDCHAR +STARTCHAR uni040E +ENCODING 1038 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +00 +A0 +A0 +E0 +20 +20 +60 +ENDCHAR +STARTCHAR uni0410 +ENCODING 1040 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +A0 +A0 +E0 +A0 +A0 +A0 +A0 +ENDCHAR +STARTCHAR uni0411 +ENCODING 1041 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +80 +80 +E0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR uni0412 +ENCODING 1042 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +A0 +A0 +C0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR uni0413 +ENCODING 1043 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +80 +80 +80 +80 +80 +80 +80 +ENDCHAR +STARTCHAR uni0414 +ENCODING 1044 +SWIDTH 750 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +30 +50 +50 +50 +50 +50 +F8 +88 +ENDCHAR +STARTCHAR uni0415 +ENCODING 1045 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +80 +80 +E0 +80 +80 +80 +E0 +ENDCHAR +STARTCHAR uni0416 +ENCODING 1046 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +A8 +A8 +A8 +F8 +A8 +A8 +A8 +A8 +ENDCHAR +STARTCHAR uni0417 +ENCODING 1047 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +A0 +20 +40 +20 +20 +A0 +E0 +ENDCHAR +STARTCHAR uni0418 +ENCODING 1048 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +88 +88 +98 +A8 +C8 +88 +88 +88 +ENDCHAR +STARTCHAR uni0419 +ENCODING 1049 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +A8 +88 +98 +A8 +C8 +88 +88 +88 +ENDCHAR +STARTCHAR uni041A +ENCODING 1050 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +A0 +A0 +C0 +A0 +A0 +A0 +A0 +ENDCHAR +STARTCHAR uni041B +ENCODING 1051 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +30 +50 +50 +50 +50 +50 +50 +D0 +ENDCHAR +STARTCHAR uni041C +ENCODING 1052 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +88 +D8 +A8 +A8 +88 +88 +88 +88 +ENDCHAR +STARTCHAR uni041D +ENCODING 1053 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +90 +90 +90 +F0 +90 +90 +90 +90 +ENDCHAR +STARTCHAR uni041E +ENCODING 1054 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +F0 +90 +90 +90 +90 +90 +90 +F0 +ENDCHAR +STARTCHAR uni041F +ENCODING 1055 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +F0 +90 +90 +90 +90 +90 +90 +90 +ENDCHAR +STARTCHAR uni0420 +ENCODING 1056 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +A0 +A0 +E0 +80 +80 +80 +80 +ENDCHAR +STARTCHAR uni0421 +ENCODING 1057 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +A0 +80 +80 +80 +80 +A0 +E0 +ENDCHAR +STARTCHAR uni0422 +ENCODING 1058 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +40 +40 +40 +40 +40 +40 +40 +ENDCHAR +STARTCHAR uni0423 +ENCODING 1059 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +90 +90 +90 +F0 +10 +10 +10 +70 +ENDCHAR +STARTCHAR uni0424 +ENCODING 1060 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +20 +F8 +A8 +A8 +A8 +A8 +F8 +20 +ENDCHAR +STARTCHAR uni0425 +ENCODING 1061 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +A0 +A0 +40 +A0 +A0 +A0 +A0 +ENDCHAR +STARTCHAR uni0426 +ENCODING 1062 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +A0 +A0 +A0 +A0 +A0 +A0 +A0 +F0 +ENDCHAR +STARTCHAR uni0427 +ENCODING 1063 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +A0 +A0 +E0 +20 +20 +20 +20 +ENDCHAR +STARTCHAR uni0428 +ENCODING 1064 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +A8 +A8 +A8 +A8 +A8 +A8 +A8 +F8 +ENDCHAR +STARTCHAR uni0429 +ENCODING 1065 +SWIDTH 1000 0 +DWIDTH 7 0 +BBX 6 8 0 0 +BITMAP +A8 +A8 +A8 +A8 +A8 +A8 +A8 +FC +ENDCHAR +STARTCHAR uni042A +ENCODING 1066 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +C0 +40 +40 +70 +50 +50 +50 +70 +ENDCHAR +STARTCHAR uni042B +ENCODING 1067 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +88 +88 +88 +E8 +A8 +A8 +A8 +E8 +ENDCHAR +STARTCHAR uni042C +ENCODING 1068 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +80 +80 +80 +E0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR uni042D +ENCODING 1069 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +E0 +10 +10 +70 +10 +10 +10 +E0 +ENDCHAR +STARTCHAR uni042E +ENCODING 1070 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +B8 +A8 +A8 +E8 +A8 +A8 +A8 +B8 +ENDCHAR +STARTCHAR uni042F +ENCODING 1071 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +A0 +A0 +60 +A0 +A0 +A0 +A0 +ENDCHAR +STARTCHAR uni0430 +ENCODING 1072 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +E0 +20 +E0 +A0 +E0 +ENDCHAR +STARTCHAR uni0431 +ENCODING 1073 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +60 +80 +E0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR uni0432 +ENCODING 1074 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +E0 +A0 +C0 +A0 +E0 +ENDCHAR +STARTCHAR uni0433 +ENCODING 1075 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +E0 +80 +80 +80 +80 +ENDCHAR +STARTCHAR uni0434 +ENCODING 1076 +SWIDTH 750 0 +DWIDTH 6 0 +BBX 5 5 0 0 +BITMAP +30 +50 +50 +F8 +88 +ENDCHAR +STARTCHAR uni0435 +ENCODING 1077 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +E0 +A0 +E0 +80 +E0 +ENDCHAR +STARTCHAR uni0436 +ENCODING 1078 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 5 0 0 +BITMAP +A8 +A8 +F8 +A8 +A8 +ENDCHAR +STARTCHAR uni0437 +ENCODING 1079 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +E0 +20 +40 +20 +E0 +ENDCHAR +STARTCHAR uni0438 +ENCODING 1080 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 5 0 0 +BITMAP +90 +B0 +D0 +90 +90 +ENDCHAR +STARTCHAR uni0439 +ENCODING 1081 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 7 0 0 +BITMAP +60 +00 +90 +B0 +D0 +90 +90 +ENDCHAR +STARTCHAR uni043A +ENCODING 1082 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +A0 +A0 +C0 +A0 +A0 +ENDCHAR +STARTCHAR uni043B +ENCODING 1083 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 5 0 0 +BITMAP +30 +50 +50 +50 +D0 +ENDCHAR +STARTCHAR uni043C +ENCODING 1084 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 5 0 0 +BITMAP +88 +D8 +A8 +88 +88 +ENDCHAR +STARTCHAR uni043D +ENCODING 1085 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +A0 +A0 +E0 +A0 +A0 +ENDCHAR +STARTCHAR uni043E +ENCODING 1086 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +E0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR uni043F +ENCODING 1087 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +E0 +A0 +A0 +A0 +A0 +ENDCHAR +STARTCHAR uni0440 +ENCODING 1088 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +E0 +A0 +E0 +80 +80 +ENDCHAR +STARTCHAR uni0441 +ENCODING 1089 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +E0 +80 +80 +80 +E0 +ENDCHAR +STARTCHAR uni0442 +ENCODING 1090 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +E0 +40 +40 +40 +40 +ENDCHAR +STARTCHAR uni0443 +ENCODING 1091 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +A0 +A0 +E0 +20 +60 +ENDCHAR +STARTCHAR uni0444 +ENCODING 1092 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +20 +20 +20 +F8 +A8 +A8 +F8 +20 +ENDCHAR +STARTCHAR uni0445 +ENCODING 1093 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +A0 +A0 +40 +A0 +A0 +ENDCHAR +STARTCHAR uni0446 +ENCODING 1094 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 5 0 0 +BITMAP +A0 +A0 +A0 +A0 +F0 +ENDCHAR +STARTCHAR uni0447 +ENCODING 1095 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +A0 +A0 +E0 +20 +20 +ENDCHAR +STARTCHAR uni0448 +ENCODING 1096 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 5 0 0 +BITMAP +A8 +A8 +A8 +A8 +F8 +ENDCHAR +STARTCHAR uni0449 +ENCODING 1097 +SWIDTH 1000 0 +DWIDTH 7 0 +BBX 6 5 0 0 +BITMAP +A8 +A8 +A8 +A8 +FC +ENDCHAR +STARTCHAR uni044A +ENCODING 1098 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 5 0 0 +BITMAP +C0 +40 +70 +50 +70 +ENDCHAR +STARTCHAR uni044B +ENCODING 1099 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 5 0 0 +BITMAP +88 +88 +E8 +A8 +E8 +ENDCHAR +STARTCHAR uni044C +ENCODING 1100 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +80 +80 +E0 +A0 +E0 +ENDCHAR +STARTCHAR uni044D +ENCODING 1101 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +C0 +20 +E0 +20 +C0 +ENDCHAR +STARTCHAR uni044E +ENCODING 1102 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 5 0 0 +BITMAP +B8 +A8 +E8 +A8 +B8 +ENDCHAR +STARTCHAR uni044F +ENCODING 1103 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +E0 +A0 +60 +A0 +A0 +ENDCHAR +STARTCHAR uni0451 +ENCODING 1105 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +A0 +00 +E0 +A0 +E0 +80 +E0 +ENDCHAR +STARTCHAR uni0454 +ENCODING 1108 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 5 0 0 +BITMAP +60 +80 +E0 +80 +60 +ENDCHAR +STARTCHAR uni0456 +ENCODING 1110 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +40 +00 +C0 +40 +40 +40 +E0 +ENDCHAR +STARTCHAR uni0457 +ENCODING 1111 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +A0 +00 +C0 +40 +40 +40 +E0 +ENDCHAR +STARTCHAR uni045E +ENCODING 1118 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +40 +00 +A0 +A0 +E0 +20 +60 +ENDCHAR +STARTCHAR uni0490 +ENCODING 1168 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +10 +E0 +80 +80 +80 +80 +80 +80 +ENDCHAR +STARTCHAR uni0491 +ENCODING 1169 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 6 0 0 +BITMAP +20 +C0 +80 +80 +80 +80 +ENDCHAR +STARTCHAR uni2002 +ENCODING 8194 +SWIDTH 375 0 +DWIDTH 3 0 +BBX 1 1 0 0 +BITMAP +00 +ENDCHAR +STARTCHAR uni2003 +ENCODING 8195 +SWIDTH 500 0 +DWIDTH 4 0 +BBX 1 1 0 0 +BITMAP +00 +ENDCHAR +STARTCHAR uni2009 +ENCODING 8201 +SWIDTH 1000 0 +DWIDTH 1 0 +BBX 1 1 6 0 +BITMAP +00 +ENDCHAR +STARTCHAR uni2010 +ENCODING 8208 +SWIDTH 1000 0 +DWIDTH 1 0 +BBX 1 1 0 4 +BITMAP +80 +ENDCHAR +STARTCHAR endash +ENCODING 8211 +SWIDTH 1000 0 +DWIDTH 3 0 +BBX 2 1 0 4 +BITMAP +C0 +ENDCHAR +STARTCHAR emdash +ENCODING 8212 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 1 0 4 +BITMAP +F0 +ENDCHAR +STARTCHAR uni2015 +ENCODING 8213 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 4 1 0 4 +BITMAP +F0 +ENDCHAR +STARTCHAR bullet +ENCODING 8226 +SWIDTH 1000 0 +DWIDTH 2 0 +BBX 1 1 0 4 +BITMAP +80 +ENDCHAR +STARTCHAR colonmonetary +ENCODING 8353 +SWIDTH 625 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +10 +F0 +A0 +A0 +A0 +A0 +F0 +40 +ENDCHAR +STARTCHAR uni20A2 +ENCODING 8354 +SWIDTH 625 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +F0 +80 +80 +B0 +A0 +A0 +A0 +F0 +ENDCHAR +STARTCHAR uni20A6 +ENCODING 8358 +SWIDTH 875 0 +DWIDTH 7 0 +BBX 6 8 0 0 +BITMAP +48 +48 +EC +58 +CC +48 +48 +48 +ENDCHAR +STARTCHAR uni20A9 +ENCODING 8361 +SWIDTH 1000 0 +DWIDTH 8 0 +BBX 7 8 0 0 +BITMAP +54 +54 +FE +54 +54 +54 +7C +28 +ENDCHAR +STARTCHAR uni20AA +ENCODING 8362 +SWIDTH 875 0 +DWIDTH 7 0 +BBX 6 6 0 0 +BITMAP +F4 +94 +B4 +B4 +A4 +BC +ENDCHAR +STARTCHAR dong +ENCODING 8363 +SWIDTH 625 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +20 +70 +20 +E0 +A0 +E0 +00 +E0 +ENDCHAR +STARTCHAR Euro +ENCODING 8364 +SWIDTH 625 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +30 +60 +40 +F0 +40 +40 +60 +30 +ENDCHAR +STARTCHAR uni20AD +ENCODING 8365 +SWIDTH 625 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +50 +50 +60 +F0 +60 +50 +50 +50 +ENDCHAR +STARTCHAR uni20AE +ENCODING 8366 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +40 +60 +40 +C0 +40 +40 +40 +ENDCHAR +STARTCHAR uni20B1 +ENCODING 8369 +SWIDTH 750 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +70 +D8 +50 +70 +40 +40 +40 +40 +ENDCHAR +STARTCHAR uni20B2 +ENCODING 8370 +SWIDTH 1000 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +40 +E0 +80 +B0 +90 +90 +F0 +40 +ENDCHAR +STARTCHAR uni20B4 +ENCODING 8372 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +70 +50 +10 +F8 +40 +50 +50 +70 +ENDCHAR +STARTCHAR uni20B5 +ENCODING 8373 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 7 0 0 +BITMAP +40 +E0 +80 +80 +80 +E0 +40 +ENDCHAR +STARTCHAR uni20B8 +ENCODING 8376 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +E0 +00 +E0 +40 +40 +40 +40 +40 +ENDCHAR +STARTCHAR uni20B9 +ENCODING 8377 +SWIDTH 625 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +F0 +20 +F0 +80 +40 +20 +10 +10 +ENDCHAR +STARTCHAR uni20BA +ENCODING 8378 +SWIDTH 625 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +E0 +40 +E0 +40 +40 +50 +50 +70 +ENDCHAR +STARTCHAR uni20BC +ENCODING 8380 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 7 0 0 +BITMAP +20 +20 +F8 +A8 +A8 +A8 +A8 +ENDCHAR +STARTCHAR uni20BD +ENCODING 8381 +SWIDTH 625 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +70 +50 +70 +40 +E0 +40 +40 +40 +ENDCHAR +STARTCHAR uni20BE +ENCODING 8382 +SWIDTH 750 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +20 +F8 +A8 +A8 +A0 +80 +40 +F8 +ENDCHAR +STARTCHAR uni20BF +ENCODING 8383 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +40 +E0 +A0 +C0 +A0 +A0 +E0 +40 +ENDCHAR +STARTCHAR uni20C0 +ENCODING 8384 +SWIDTH 500 0 +DWIDTH 4 0 +BBX 3 6 0 0 +BITMAP +E0 +80 +80 +E0 +00 +E0 +ENDCHAR +STARTCHAR uni2103 +ENCODING 8451 +SWIDTH 750 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +B8 +A8 +20 +20 +20 +20 +28 +38 +ENDCHAR +STARTCHAR uni2109 +ENCODING 8457 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +B8 +A0 +20 +38 +20 +20 +20 +20 +ENDCHAR +STARTCHAR uni2460 +ENCODING 9312 +SWIDTH 125 0 +DWIDTH 1 0 +BBX 1 1 4 0 +BITMAP +00 +ENDCHAR +STARTCHAR uni2461 +ENCODING 9313 +SWIDTH 250 0 +DWIDTH 2 0 +BBX 1 1 1 0 +BITMAP +00 +ENDCHAR +STARTCHAR uni2462 +ENCODING 9314 +SWIDTH 375 0 +DWIDTH 3 0 +BBX 1 1 3 0 +BITMAP +00 +ENDCHAR +STARTCHAR uni2463 +ENCODING 9315 +SWIDTH 500 0 +DWIDTH 4 0 +BBX 1 1 4 0 +BITMAP +00 +ENDCHAR +STARTCHAR uni2464 +ENCODING 9316 +SWIDTH 625 0 +DWIDTH 5 0 +BBX 1 1 4 0 +BITMAP +00 +ENDCHAR +STARTCHAR uni2465 +ENCODING 9317 +SWIDTH 750 0 +DWIDTH 6 0 +BBX 1 1 1 0 +BITMAP +00 +ENDCHAR +STARTCHAR uni2466 +ENCODING 9318 +SWIDTH 875 0 +DWIDTH 7 0 +BBX 1 1 3 0 +BITMAP +00 +ENDCHAR +STARTCHAR uni2467 +ENCODING 9319 +SWIDTH 1000 0 +DWIDTH 8 0 +BBX 1 1 4 0 +BITMAP +00 +ENDCHAR +STARTCHAR uni2468 +ENCODING 9320 +SWIDTH 1125 0 +DWIDTH 9 0 +BBX 1 1 4 0 +BITMAP +00 +ENDCHAR +STARTCHAR uni2469 +ENCODING 9321 +SWIDTH 1250 0 +DWIDTH 10 0 +BBX 1 1 4 0 +BITMAP +00 +ENDCHAR +STARTCHAR uni24EA +ENCODING 9450 +SWIDTH 0 0 +DWIDTH 0 0 +BBX 1 1 3 0 +BITMAP +00 +ENDCHAR +STARTCHAR H22073 +ENCODING 9633 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 6 0 1 +BITMAP +E0 +A0 +A0 +A0 +A0 +E0 +ENDCHAR +STARTCHAR uni4E00 +ENCODING 19968 +SWIDTH 1000 0 +DWIDTH 8 0 +BBX 7 2 0 3 +BITMAP +04 +FE +ENDCHAR +STARTCHAR uni4E03 +ENCODING 19971 +SWIDTH 1000 0 +DWIDTH 8 0 +BBX 7 8 0 0 +BITMAP +20 +20 +2E +F0 +20 +20 +22 +1E +ENDCHAR +STARTCHAR uni4E09 +ENCODING 19977 +SWIDTH 1000 0 +DWIDTH 8 0 +BBX 7 8 0 0 +BITMAP +7C +00 +00 +38 +00 +00 +04 +FE +ENDCHAR +STARTCHAR uni4E0A +ENCODING 19978 +SWIDTH 625 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +40 +40 +70 +40 +40 +40 +40 +F0 +ENDCHAR +STARTCHAR uni4E0B +ENCODING 19979 +SWIDTH 625 0 +DWIDTH 5 0 +BBX 4 8 0 0 +BITMAP +F0 +40 +60 +50 +40 +40 +40 +40 +ENDCHAR +STARTCHAR uni4E5D +ENCODING 20061 +SWIDTH 1000 0 +DWIDTH 8 0 +BBX 7 8 0 0 +BITMAP +20 +F8 +28 +28 +48 +48 +4A +8E +ENDCHAR +STARTCHAR uni4E8C +ENCODING 20108 +SWIDTH 1000 0 +DWIDTH 8 0 +BBX 7 6 0 1 +BITMAP +7C +00 +00 +00 +04 +FE +ENDCHAR +STARTCHAR uni4E94 +ENCODING 20116 +SWIDTH 1000 0 +DWIDTH 8 0 +BBX 7 8 0 0 +BITMAP +7C +10 +10 +7C +24 +24 +24 +FE +ENDCHAR +STARTCHAR uni516B +ENCODING 20843 +SWIDTH 1000 0 +DWIDTH 8 0 +BBX 7 8 0 0 +BITMAP +28 +28 +28 +28 +44 +44 +44 +82 +ENDCHAR +STARTCHAR uni516D +ENCODING 20845 +SWIDTH 1000 0 +DWIDTH 8 0 +BBX 7 8 0 0 +BITMAP +10 +FE +00 +28 +28 +44 +44 +82 +ENDCHAR +STARTCHAR uni5341 +ENCODING 21313 +SWIDTH 1000 0 +DWIDTH 8 0 +BBX 7 8 0 0 +BITMAP +10 +10 +FE +10 +10 +10 +10 +10 +ENDCHAR +STARTCHAR uni5348 +ENCODING 21320 +SWIDTH 625 0 +DWIDTH 5 0 +BBX 5 8 0 0 +BITMAP +40 +70 +A0 +20 +F8 +20 +20 +20 +ENDCHAR +STARTCHAR uni56DB +ENCODING 22235 +SWIDTH 1000 0 +DWIDTH 8 0 +BBX 7 8 0 0 +BITMAP +FE +AA +AA +AE +C2 +82 +FE +82 +ENDCHAR +STARTCHAR uni5929 +ENCODING 22825 +SWIDTH 1000 0 +DWIDTH 8 0 +BBX 7 8 0 0 +BITMAP +7C +10 +10 +FE +10 +10 +28 +C6 +ENDCHAR +STARTCHAR uni661F +ENCODING 26143 +SWIDTH 1000 0 +DWIDTH 8 0 +BBX 7 8 0 0 +BITMAP +FC +84 +FC +5E +90 +7C +10 +FE +ENDCHAR +STARTCHAR uni6708 +ENCODING 26376 +SWIDTH 1000 0 +DWIDTH 8 0 +BBX 7 8 0 0 +BITMAP +3E +22 +3E +22 +3E +22 +42 +86 +ENDCHAR +STARTCHAR uni671F +ENCODING 26399 +SWIDTH 1000 0 +DWIDTH 8 0 +BBX 7 8 0 0 +BITMAP +5E +FA +5E +7A +5E +EA +12 +A6 +ENDCHAR +STARTCHAR uniAE08 +ENCODING 44552 +SWIDTH 750 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +F8 +08 +00 +F8 +00 +F8 +88 +F8 +ENDCHAR +STARTCHAR uniBAA9 +ENCODING 47785 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +F8 +88 +F8 +20 +F8 +00 +F8 +08 +ENDCHAR +STARTCHAR uniC218 +ENCODING 49688 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +20 +20 +50 +88 +00 +F8 +20 +20 +ENDCHAR +STARTCHAR uniC624 +ENCODING 50724 +SWIDTH 750 0 +DWIDTH 6 0 +BBX 5 7 0 1 +BITMAP +70 +88 +88 +70 +20 +20 +F8 +ENDCHAR +STARTCHAR uniC694 +ENCODING 50836 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 7 0 1 +BITMAP +70 +88 +88 +70 +50 +50 +F8 +ENDCHAR +STARTCHAR uniC6D4 +ENCODING 50900 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +48 +A8 +48 +E8 +58 +38 +C0 +F8 +ENDCHAR +STARTCHAR uniC77C +ENCODING 51068 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +48 +A8 +48 +00 +F8 +38 +C0 +F8 +ENDCHAR +STARTCHAR uniC804 +ENCODING 51204 +SWIDTH 875 0 +DWIDTH 7 0 +BBX 6 8 0 0 +BITMAP +F4 +24 +4C +A4 +94 +44 +40 +7C +ENDCHAR +STARTCHAR uniD1A0 +ENCODING 53664 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 7 0 1 +BITMAP +F8 +80 +F0 +80 +F8 +20 +F8 +ENDCHAR +STARTCHAR uniD654 +ENCODING 54868 +SWIDTH 1000 0 +DWIDTH 8 0 +BBX 7 8 0 0 +BITMAP +24 +FC +54 +56 +24 +24 +FC +04 +ENDCHAR +STARTCHAR uniD6C4 +ENCODING 54980 +SWIDTH 1000 0 +DWIDTH 6 0 +BBX 5 8 0 0 +BITMAP +20 +F8 +50 +20 +00 +F8 +20 +20 +ENDCHAR +STARTCHAR uniFFE5 +ENCODING 65509 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 8 0 0 +BITMAP +A0 +A0 +E0 +40 +E0 +40 +40 +40 +ENDCHAR +STARTCHAR uniFFFD +ENCODING 65533 +SWIDTH 1000 0 +DWIDTH 4 0 +BBX 3 6 0 1 +BITMAP +A0 +A0 +40 +A0 +A0 +A0 +ENDCHAR +ENDFONT diff --git a/tests/components/font/common.yaml b/tests/components/font/common.yaml index a81457a05d..5be9faf5be 100644 --- a/tests/components/font/common.yaml +++ b/tests/components/font/common.yaml @@ -1,4 +1,12 @@ font: + - file: + type: gfonts + family: "Roboto" + weight: bold + italic: true + size: 32 + id: roboto32 + - file: "gfonts://Roboto" id: roboto size: 20 @@ -9,6 +17,10 @@ font: - file: "gfonts://Roboto" id: roboto_web size: 20 + - file: "gfonts://Roboto" + id: roboto_greek + size: 20 + glyphs: ["\u0300", "\u00C5", "\U000000C7"] - file: "https://github.com/IdreesInc/Monocraft/releases/download/v3.0/Monocraft.ttf" id: monocraft size: 20 @@ -20,6 +32,17 @@ font: - file: $component_dir/Monocraft.ttf id: monocraft3 size: 28 + - file: $component_dir/MatrixChunky8X.bdf + id: special_font + glyphs: + - '"' + - "'" + - '#$%&()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz°' + + - file: $component_dir/MatrixChunky8X.bdf + id: default_font + - file: $component_dir/x11.pcf + id: pcf_font i2c: scl: ${i2c_scl} @@ -36,3 +59,4 @@ display: it.print(0, 40, id(monocraft), "Hello, World!"); it.print(0, 60, id(monocraft2), "Hello, World!"); it.print(0, 80, id(monocraft3), "Hello, World!"); + it.print(0, 100, id(roboto_greek), "Hello κόσμε!"); diff --git a/tests/components/font/test.host.yaml b/tests/components/font/test.host.yaml index 017328ec83..c5399f2826 100644 --- a/tests/components/font/test.host.yaml +++ b/tests/components/font/test.host.yaml @@ -1,4 +1,12 @@ font: + - file: + type: gfonts + family: "Roboto" + weight: bold + italic: true + size: 32 + id: roboto32 + - file: "gfonts://Roboto" id: roboto size: 20 @@ -9,6 +17,10 @@ font: - file: "gfonts://Roboto" id: roboto_web size: 20 + - file: "gfonts://Roboto" + id: roboto_greek + size: 20 + glyphs: ["\u0300", "\u00C5", "\U000000C7"] - file: "https://github.com/IdreesInc/Monocraft/releases/download/v3.0/Monocraft.ttf" id: monocraft size: 20 @@ -20,4 +32,26 @@ font: - file: $component_dir/Monocraft.ttf id: monocraft3 size: 28 + - file: $component_dir/MatrixChunky8X.bdf + id: special_font + glyphs: + - '"' + - "'" + - '#$%&()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz°' + - file: $component_dir/MatrixChunky8X.bdf + id: default_font + +display: + - platform: sdl + id: sdl_display + dimensions: + width: 800 + height: 600 + lambda: |- + it.print(0, 0, id(roboto), "Hello, World!"); + it.print(0, 20, id(roboto_web), "Hello, World!"); + it.print(0, 40, id(roboto_greek), "Hello κόσμε!"); + it.print(0, 60, id(monocraft), "Hello, World!"); + it.print(0, 80, id(monocraft2), "Hello, World!"); + it.print(0, 100, id(monocraft3), "Hello, World!"); diff --git a/tests/components/font/x11.pcf b/tests/components/font/x11.pcf new file mode 100644 index 0000000000000000000000000000000000000000..19a38d4e3918b6bb4796fb23d34a24e2a8fa9656 GIT binary patch literal 13368 zcmbuG4Rl;bb;oD58-K)(tvF6BJBq!DgJZx-ZGj>gOHOPE=&g5G(&CkN zvnx4r;@D4XSznfeo2E1=X=^AYfu_{7lm?o%56E#$j|m3S9JfG^&PgbtC*=r|5>e8) zzyJGKFD8*)cqhL%cV_O~J9F>Md%N-`)S1Z5F~)>o0oY+q ze};<2NdNKW#;l_f_3M=koLps0%&*Tue`+;$gMM_U-%bTv8lQwY>pU5;8r-?K_BE+x zaIJ(IxJg1C+#+EMv`Y90bbxE|qY`AcKM;z6@G&X>Av~;>7I;)GPkOYa1;DO!Y1Z7SHfw8N=Aj4VW>vJ%}|su2H%hlSLk2V@;v-ELd86YN_Y!cQW!_r zErBIh?Ag@1y`^E>PE()E?n;?W^|v)O@9b!)zpc@1@5|=WadSsw)8;MhUekNCnWSlJ zu5W2KO{T4}{f?%F_AOplOE%vV&zSm#hK{!Ooz0D2tJ&JLqp`W8t?7e}W@~FxOS>m- zYiw(6-rnBS+S0MZlvQ?`wypIWn_4!Tt)~9=#%=YR8#`PdX5*InZ8+@M*0{N;t)2Nc zwYApOy>or3qp@XUYlB9vH5==W^;S<>8|d+=<38WSxfN3)$X88;1$ZS74hEV8x5bksMrzPGVsYulz)QsphAMho%R(b3Sj5pKtC zhg6jBHnX+u-M_BtTBvbXRa4em(^TVSyryRDx|+4MwI#OJ++0`VXQyUuS$53_w%0fK z=`5W;?L%Fc0jJ!A&%W;7&z_xDX%^CRncGA4|9k2AVVuEAGGbkLqp?sab$S> zp24xvaa*^+;*p8*!NJ4hV|H=q;P}18;X~ua!9(1Nqa)+@4vvnFFm&jk+J^2MJUCRe zZCPRLm_0bc%^sC>?&fyrRxC+g@6ZsYxD{Qa+zuUZrQ+x?_9hOG+xGaFe=ABFJWSi5 zD~&VV;^A?sN7OcQ2t97a!3ni#G4}mOxT#t!4U=@xGcf8Jz-Up9iW8DPrV)oF>E=2g z)bfkNOvqLYH1YlR$jAY=Z7+^SJa2=e!+|t1GGZSXJv4sM9vc}sSRBKY;%6Vg)IoP@ zZnb#i2qVVFxGAPGrCx3$GUeWe4-rYJ5PNPDhY6S@-45DvS=6G72T4FMzeD@o)GDwz zS3ca>kB*KWD2^)&QpTkcIy^q$E+22B`--EZ!#GkFlv*z?Bg10@Y=N?K0@AQi>p(9qES;?VHm5V1l<_Sg*An;@nv)xTkYiI0yRQFPtggd}cb!R^5P z6DX79Ay$4w%Xe=RLXa zALI6Z{Q5ca55!?VzvrsTg3>)SI5MKG=iSibe0Z7Grm?xvrj4m!``gx){9CzY!tU;a z(LvT5D36b!qAKb3W<}1CO3w<%KmXTLie|VIhTu4SpLZTbXfZTEAB@7Mfmd$mNq7og z;=NZ1d&{y#y51si?3+<4D2LbzG2k;Szz#5=G!q^D*Bd6dMV=k$N2FL*8 zF2Uv{FBvmyF>HV&Ou!cbU$dFV>^iW3xz5JV>@(h{ojI#1w}K6if!gsgXWE##5s1PT zz~)?R&P8V~^ShM3OV>aAG`B& zfZh2|!V5g27Xvs7d{400J;mYTZv93I+Z#2G@LQ!@;Oik-S7}NfbOC-U;%y?(YELnW3E^Y zJ75%!!=y2b7sD19fXCnrOc`@!6*Pkl=w6BLRp?%|0dnvVVD~E4u!Qy{v@hv~AwYi# zb}(V8m`7DR>;vXpbsDCPSsDfWF2&Z;hu|r|)-r4@s|Cg^W6UzfEMv?v#$1i9t1a^T zRpR{5$PXHG^XktX!wr`mb&Fps)sI0JsR#>VpeqKB&lyT_)JLC47x^5ZyxvEzXD08$ z0b-`LGzc5PM8H6uTq=<`3l-Jwi>R7HP8$iFMG1_9=H{vp!y~#w=+O} zIRxuc{Xal{gzU-JevsV=1o4U@TQCVRkevc3KAjM(L3T7}+16ZFf%?U*Ae-`Y5)>oL zCq}6&298e*#H@?4?x-<6$Bg+V<-fxBV#a*f_F}(+GHg6%Q;K@pO+o)d{$A4rLe*n zvmVyk9?F&GzFch(hZs0zd=0r1f<3U*_Uy@KtxsT2Ig`BuHtT>|xQY6$pwa^QNPzV8 z{HO|`t~ms{LH?#`)4b)w0tZ<G378!yI+gZQeC;wUzO z=Bpei&svM(rhTHe99TZMyr8`xFD%1758A7Gj>vXEU|Xdqfaa<(*TF83-+iF@5Boet zIRIyE?=LbBBUb^TD#f!FS!1W+myWk)ty_5zTC+IeBYQ79z8$bfnxWp0X9} z(OTMczRp50Hi{fzA;-?cp*{&VI1no%iX5O=vRv10l`pFvp#EqX7V;znV^$+)putBm ze;pjKKzcgo?}ARBQ}3EJ|TRtPItK}Tv`U4$ASJ$G9`8bq9_QVFzd-t@D@;(4jm<>zqb1~65)SNDd{qQ;{ z7K-Ok8LF%OZcx0|K^DTADA31!kq7=Gj`RMN*fxhY!Xaj?KC<7>#BpuALq@-YW$eOY659kAJ7PzyTy zw}MIwWH$jiPa#+cx-NE%)+xUitA91RLh;l4MRP5mlj^&CG^Z$Nzv)sbUqX82Sk%Q7 zC|}D#SNWJX8ka+78Wa;<7t*)TkqGk^E0L|t>xr8T>I#rF2`n|%)}&-2BO3G#fg`pcPv{9LTQ_JsDs z#p-iJ+~;Eu=PlILCclb7f-(i)@cAxn&%-8o4Kyx@+b=zU@C3C0mS*f#caaZM^fRvDKhU_TL4aK6*#|0Db~t&^tueOnMRerKe|v zu9m8*W1=T0P@^L5wo2QV4u1c5!%V&~O@ljhXTm?GgBG`i`kOLb7$mM(H zX6m~^dm|6-7~A{aD0{)ap2^;oX_w`JAT8q&2npc$GSK3z^ zt9iA+bMOKvW{Ot~a!?1-)wA|_NI(}n1JBvs_fS1A!_+UNr~U;Hobj1#2G2*-XY?(< zvZ?DXP;4&r-8I;+ip>r1b|`?>roE7aGvN5xl;ttMSU>)}&NAG+&i+;xTeyNSN6aN= z79T(7n7RB^Y#tBI%Lu?i4$0*_#I7)l&6OO8B^=771mbEo-!;5!S8&*_pd?|e=4~Lsc{CaZ>eu6J>q3%nBh`RaZ*+jy_J$K1=;>-+dx ze!$pf&=k!S@KQ@12{?r^ZpERE`e`Y>y{@i@VJZc^@pEaK|e_eZl;-`5W^^ z^S96-C&loTBgNw133s z720+nvu)kqFsS4x1!FE$y5krl8v&sc_~Q6i+yjrnG3q?hDte&_cpPYsp&|@XKMwDs z?P1Cv!Y|-Kzx*sE$Ec#4ejaxfdnh0BO4r^;v7xo_Xi@2|g*AnaBDVu}LnkTYl;4Lt zkuQaQZ@AzNE$e*!)UUDUN6))D6MQ9+DCKL*7aKcTr^DgQz2 z&ySsSD(cW4mQ9h*amk3`87FU4V-GjSvH(t z!^t(AS;Kkd&r9$rcoc}aISyZfC*iB`G<+M_-|nBnU!eRin1Y|dEAVrehW~-r;C0qc z5vl;~DfU;0*oN4nA?6-pIiX5e1dHJ+sDfos4a*@4tSxjs)WR*W85C3YaVQC$&<#2- z86fwe9OR)7_P_uT*U&!L4>k}7ozc)RjKVlf!2NI(9suGK`Zy4y5b+6p2EG8qCG<7; zCVU6J2QR>j@G~F=6})0AE(7x8p7Wb1*FyumAGX1rz}$8AE0|lw0U$RO_^UVu?8P&;5&lv$R_~bkg_Y}9;8CNH|Z%C z(!E`ol*z^OsoqSgv(J;^8Yff59ZOJw0*Ly*JmL>Xo&0Hfai(c%j?e zlgejJwl`(^?#`ON?tF?@ZMHveI@5bnrjQ;mh14F#r1ZmO)0;*yk znABbU@r>!pr}(jRp4p`eeQC7feZSF%h+X-3Vt1;~UDGZ^Z>oN4u)ah(ok-^s{XM3? zH%VCdv2}_W#F_VR^07hIQ@+Q%acgCs`qG&s@hHnzGJAKgakHXGXGn!f^ykS$Vz1dt z#CGMgyHmZpl;u>y4MsA}kGczKWyAmdxXI>|odnX2Ygd0dlS%bty=7;zUFk$T)0^!J z(su55;ie~^$P?XUihz@UGK>w?1E9~ zwLiV?c-h#v=Qdt45qHB7=ez}BAln+8@&>2M29r8}Yo$G%w5Q`A5I4^0l1_>+^m^{H zo_f|(&z70Y`U<5{$@HEyM*xGVZ(& J-u(Cf{|En5p|}75 literal 0 HcmV?d00001 From 749b9421329f0ca8453e7df0df0702a3d29e8276 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Thu, 31 Oct 2024 17:37:32 +1100 Subject: [PATCH 13/46] [lvlg] fix tests (#7708) --- tests/components/lvgl/common.yaml | 40 +------------------------------ 1 file changed, 1 insertion(+), 39 deletions(-) diff --git a/tests/components/lvgl/common.yaml b/tests/components/lvgl/common.yaml index cebc3caaa7..c7d635db1c 100644 --- a/tests/components/lvgl/common.yaml +++ b/tests/components/lvgl/common.yaml @@ -4,56 +4,18 @@ touchscreen: display: tft_display update_interval: 50ms threshold: 1 - calibration: - x_max: 240 - y_max: 320 font: - file: "$component_dir/roboto.ttf" id: roboto20 size: 20 - extras: - - file: '$component_dir/materialdesignicons-webfont.ttf' - glyphs: [ - "\U000F004B", - "\U0000f0ed", - "\U000F006E", - "\U000F012C", - "\U000F179B", - "\U000F0748", - "\U000F1A1B", - "\U000F02DC", - "\U000F0A02", - "\U000F035F", - "\U000F0156", - "\U000F0C5F", - "\U000f0084", - "\U000f0091", - ] + - file: "$component_dir/helvetica.ttf" id: helvetica20 - file: "$component_dir/roboto.ttf" id: roboto10 size: 10 bpp: 4 - extras: - - file: '$component_dir/materialdesignicons-webfont.ttf' - glyphs: [ - "\U000F004B", - "\U0000f0ed", - "\U000F006E", - "\U000F012C", - "\U000F179B", - "\U000F0748", - "\U000F1A1B", - "\U000F02DC", - "\U000F0A02", - "\U000F035F", - "\U000F0156", - "\U000F0C5F", - "\U000f0084", - "\U000f0091", - ] sensor: - platform: lvgl From cefbfb75bd4b5f8baaf5599c1c4221aab1919426 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 31 Oct 2024 23:46:35 +1300 Subject: [PATCH 14/46] [esp32_ble] Add disconnect as a virtual function to ``ESPBTClient`` (#7705) --- esphome/components/esp32_ble_client/ble_client_base.h | 2 +- esphome/components/esp32_ble_tracker/esp32_ble_tracker.h | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/esphome/components/esp32_ble_client/ble_client_base.h b/esphome/components/esp32_ble_client/ble_client_base.h index fd586e59d6..fca66c0b3c 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.h +++ b/esphome/components/esp32_ble_client/ble_client_base.h @@ -35,7 +35,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override; void connect() override; esp_err_t pair(); - void disconnect(); + void disconnect() override; void release_services(); bool connected() { return this->state_ == espbt::ClientState::ESTABLISHED; } diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index d2bb6a6e6d..2fc5da829d 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -11,9 +11,9 @@ #ifdef USE_ESP32 +#include #include #include -#include #include #include @@ -172,6 +172,7 @@ class ESPBTClient : public ESPBTDeviceListener { esp_ble_gattc_cb_param_t *param) = 0; virtual void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) = 0; virtual void connect() = 0; + virtual void disconnect() = 0; virtual void set_state(ClientState st) { this->state_ = st; } ClientState state() const { return state_; } int app_id; From 77bb46ff3bf17afb7bfd3fe963a86af014a17d4b Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Fri, 1 Nov 2024 02:54:34 -0700 Subject: [PATCH 15/46] handle bad pin schemas (#7711) Co-authored-by: Samuel Sieb --- esphome/components/esp32/gpio.py | 4 +++- esphome/components/esp8266/gpio.py | 12 +++++++----- esphome/components/host/gpio.py | 10 ++++++---- esphome/components/libretiny/gpio.py | 6 ++++-- esphome/components/rp2040/gpio.py | 8 +++++--- 5 files changed, 25 insertions(+), 15 deletions(-) diff --git a/esphome/components/esp32/gpio.py b/esphome/components/esp32/gpio.py index 558ff51af8..df01769a66 100644 --- a/esphome/components/esp32/gpio.py +++ b/esphome/components/esp32/gpio.py @@ -67,8 +67,10 @@ def _translate_pin(value): "This variable only supports pin numbers, not full pin schemas " "(with inverted and mode)." ) - if isinstance(value, int): + if isinstance(value, int) and not isinstance(value, bool): return value + if not isinstance(value, str): + raise cv.Invalid(f"Invalid pin number: {value}") try: return int(value) except ValueError: diff --git a/esphome/components/esp8266/gpio.py b/esphome/components/esp8266/gpio.py index c42bc9204f..53016d2130 100644 --- a/esphome/components/esp8266/gpio.py +++ b/esphome/components/esp8266/gpio.py @@ -1,6 +1,9 @@ -import logging from dataclasses import dataclass +import logging +from esphome import pins +import esphome.codegen as cg +import esphome.config_validation as cv from esphome.const import ( CONF_ANALOG, CONF_ID, @@ -14,10 +17,7 @@ from esphome.const import ( CONF_PULLUP, PLATFORM_ESP8266, ) -from esphome import pins from esphome.core import CORE, coroutine_with_priority -import esphome.config_validation as cv -import esphome.codegen as cg from . import boards from .const import KEY_BOARD, KEY_ESP8266, KEY_PIN_INITIAL_STATES, esp8266_ns @@ -48,8 +48,10 @@ def _translate_pin(value): "This variable only supports pin numbers, not full pin schemas " "(with inverted and mode)." ) - if isinstance(value, int): + if isinstance(value, int) and not isinstance(value, bool): return value + if not isinstance(value, str): + raise cv.Invalid(f"Invalid pin number: {value}") try: return int(value) except ValueError: diff --git a/esphome/components/host/gpio.py b/esphome/components/host/gpio.py index 180919de4f..0f22a790bd 100644 --- a/esphome/components/host/gpio.py +++ b/esphome/components/host/gpio.py @@ -1,5 +1,8 @@ import logging +from esphome import pins +import esphome.codegen as cg +import esphome.config_validation as cv from esphome.const import ( CONF_ID, CONF_INPUT, @@ -11,9 +14,6 @@ from esphome.const import ( CONF_PULLDOWN, CONF_PULLUP, ) -from esphome import pins -import esphome.config_validation as cv -import esphome.codegen as cg from .const import host_ns @@ -28,8 +28,10 @@ def _translate_pin(value): "This variable only supports pin numbers, not full pin schemas " "(with inverted and mode)." ) - if isinstance(value, int): + if isinstance(value, int) and not isinstance(value, bool): return value + if not isinstance(value, str): + raise cv.Invalid(f"Invalid pin number: {value}") try: return int(value) except ValueError: diff --git a/esphome/components/libretiny/gpio.py b/esphome/components/libretiny/gpio.py index 1d7b37cc9b..07eb0ce133 100644 --- a/esphome/components/libretiny/gpio.py +++ b/esphome/components/libretiny/gpio.py @@ -1,8 +1,8 @@ import logging +from esphome import pins import esphome.codegen as cg import esphome.config_validation as cv -from esphome import pins from esphome.const import ( CONF_ANALOG, CONF_ID, @@ -103,8 +103,10 @@ def _translate_pin(value): "This variable only supports pin numbers, not full pin schemas " "(with inverted and mode)." ) - if isinstance(value, int): + if isinstance(value, int) and not isinstance(value, bool): return value + if not isinstance(value, str): + raise cv.Invalid(f"Invalid pin number: {value}") try: return int(value) except ValueError: diff --git a/esphome/components/rp2040/gpio.py b/esphome/components/rp2040/gpio.py index 6ba0975a2c..58514f7db5 100644 --- a/esphome/components/rp2040/gpio.py +++ b/esphome/components/rp2040/gpio.py @@ -1,6 +1,8 @@ +from esphome import pins import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import ( + CONF_ANALOG, CONF_ID, CONF_INPUT, CONF_INVERTED, @@ -10,10 +12,8 @@ from esphome.const import ( CONF_OUTPUT, CONF_PULLDOWN, CONF_PULLUP, - CONF_ANALOG, ) from esphome.core import CORE -from esphome import pins from . import boards from .const import KEY_BOARD, KEY_RP2040, rp2040_ns @@ -41,8 +41,10 @@ def _translate_pin(value): "This variable only supports pin numbers, not full pin schemas " "(with inverted and mode)." ) - if isinstance(value, int): + if isinstance(value, int) and not isinstance(value, bool): return value + if not isinstance(value, str): + raise cv.Invalid(f"Invalid pin number: {value}") try: return int(value) except ValueError: From 01497c891d08c4a9a245eaadb982e302d43f0b58 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Sun, 3 Nov 2024 22:22:16 +0100 Subject: [PATCH 16/46] datetime fix build_language_schema (#7710) Co-authored-by: Tomasz Duda --- esphome/components/datetime/__init__.py | 6 +++--- tests/components/mqtt/common.yaml | 1 + tests/components/web_server/common_v3.yaml | 8 ++++++++ 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/esphome/components/datetime/__init__.py b/esphome/components/datetime/__init__.py index 7edf527e01..630bf6962c 100644 --- a/esphome/components/datetime/__init__.py +++ b/esphome/components/datetime/__init__.py @@ -70,8 +70,6 @@ def _validate_time_present(config): _DATETIME_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( - web_server.WEBSERVER_SORTING_SCHEMA, - cv.MQTT_COMMAND_COMPONENT_SCHEMA, cv.Schema( { cv.Optional(CONF_ON_VALUE): automation.validate_automation( @@ -81,7 +79,9 @@ _DATETIME_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( ), cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock), } - ), + ) + .extend(web_server.WEBSERVER_SORTING_SCHEMA) + .extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA) ).add_extra(_validate_time_present) diff --git a/tests/components/mqtt/common.yaml b/tests/components/mqtt/common.yaml index e154be8b5c..75c34bec56 100644 --- a/tests/components/mqtt/common.yaml +++ b/tests/components/mqtt/common.yaml @@ -230,6 +230,7 @@ datetime: id: test_date type: date state_topic: some/topic/date + command_topic: test_date/custom_command_topic qos: 2 subscribe_qos: 2 set_action: diff --git a/tests/components/web_server/common_v3.yaml b/tests/components/web_server/common_v3.yaml index 69f4b67f15..bdacaaddbe 100644 --- a/tests/components/web_server/common_v3.yaml +++ b/tests/components/web_server/common_v3.yaml @@ -35,3 +35,11 @@ switch: web_server: sorting_group_id: sorting_group_2 sorting_weight: -10 +datetime: + - platform: template + name: Pick a Date + type: datetime + optimistic: yes + web_server: + sorting_group_id: sorting_group_3 + sorting_weight: -5 From 2dca3d79e490abcf2a03e962cb7c88dc3bbfd83b Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 5 Nov 2024 11:32:18 +1100 Subject: [PATCH 17/46] [lvgl] Ensure images are configured before using them. (Bugfix) (#7721) --- esphome/components/lvgl/widgets/animimg.py | 7 ++++--- esphome/components/lvgl/widgets/img.py | 2 ++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/esphome/components/lvgl/widgets/animimg.py b/esphome/components/lvgl/widgets/animimg.py index 3b20008c3d..8adea72ad3 100644 --- a/esphome/components/lvgl/widgets/animimg.py +++ b/esphome/components/lvgl/widgets/animimg.py @@ -60,9 +60,10 @@ class AnimimgType(WidgetType): lvgl_components_required.add(CONF_IMAGE) lvgl_components_required.add(CONF_ANIMIMG) if CONF_SRC in config: - for x in config[CONF_SRC]: - await cg.get_variable(x) - srcs = [await lv_image.process(x) for x in config[CONF_SRC]] + srcs = [ + await lv_image.process(await cg.get_variable(x)) + for x in config[CONF_SRC] + ] src_id = cg.static_const_array(config[CONF_SRC_LIST_ID], srcs) count = len(config[CONF_SRC]) lv.animimg_set_src(w.obj, src_id, count) diff --git a/esphome/components/lvgl/widgets/img.py b/esphome/components/lvgl/widgets/img.py index 59b2c97c63..931d0c0b5b 100644 --- a/esphome/components/lvgl/widgets/img.py +++ b/esphome/components/lvgl/widgets/img.py @@ -1,3 +1,4 @@ +import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import CONF_ANGLE, CONF_MODE @@ -64,6 +65,7 @@ class ImgType(WidgetType): async def to_code(self, w: Widget, config): if src := config.get(CONF_SRC): + src = await cg.get_variable(src) lv.img_set_src(w.obj, await lv_image.process(src)) if (cf_angle := config.get(CONF_ANGLE)) is not None: pivot_x = config[CONF_PIVOT_X] From dcc537d0d43c2b8fdff7ecac86c8154c7ed78172 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 6 Nov 2024 10:45:40 +1300 Subject: [PATCH 18/46] [lvgl] Don't just throw key error if someone types a bad layout type (#7722) Co-authored-by: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> --- esphome/components/lvgl/schemas.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py index bb14c11ddd..516627708e 100644 --- a/esphome/components/lvgl/schemas.py +++ b/esphome/components/lvgl/schemas.py @@ -391,7 +391,9 @@ def container_validator(schema, widget_type: WidgetType): add_lv_use(ltype) if value == SCHEMA_EXTRACT: return result - result = result.extend(LAYOUT_SCHEMAS[ltype.lower()]) + result = result.extend( + LAYOUT_SCHEMAS.get(ltype.lower(), LAYOUT_SCHEMAS[df.TYPE_NONE]) + ) return result(value) return validator From 5bb4d042e48873812684fcad189c233c64698629 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 6 Nov 2024 11:54:47 +1100 Subject: [PATCH 19/46] [spi_device] rename mode to spi_mode (#7724) --- esphome/components/spi/__init__.py | 29 ++++++++++--------- esphome/components/spi_device/__init__.py | 19 +++--------- .../components/spi_device/test.esp32-ard.yaml | 2 +- .../spi_device/test.esp32-c3-ard.yaml | 2 +- .../spi_device/test.esp32-c3-idf.yaml | 2 +- .../components/spi_device/test.esp32-idf.yaml | 2 +- .../spi_device/test.esp8266-ard.yaml | 2 +- .../spi_device/test.rp2040-ard.yaml | 2 +- 8 files changed, 25 insertions(+), 35 deletions(-) diff --git a/esphome/components/spi/__init__.py b/esphome/components/spi/__init__.py index fdf19bb56e..52afbf365e 100644 --- a/esphome/components/spi/__init__.py +++ b/esphome/components/spi/__init__.py @@ -1,40 +1,37 @@ import re +from esphome import pins import esphome.codegen as cg -import esphome.config_validation as cv -import esphome.final_validate as fv from esphome.components.esp32.const import ( KEY_ESP32, - VARIANT_ESP32S2, - VARIANT_ESP32S3, VARIANT_ESP32C2, VARIANT_ESP32C3, VARIANT_ESP32C6, VARIANT_ESP32H2, + VARIANT_ESP32S2, + VARIANT_ESP32S3, ) -from esphome import pins +import esphome.config_validation as cv from esphome.const import ( CONF_CLK_PIN, + CONF_CS_PIN, + CONF_DATA_PINS, + CONF_DATA_RATE, CONF_ID, + CONF_INVERTED, CONF_MISO_PIN, CONF_MOSI_PIN, - CONF_SPI_ID, - CONF_CS_PIN, CONF_NUMBER, - CONF_INVERTED, + CONF_SPI_ID, KEY_CORE, KEY_TARGET_PLATFORM, KEY_VARIANT, - CONF_DATA_RATE, PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040, - CONF_DATA_PINS, -) -from esphome.core import ( - coroutine_with_priority, - CORE, ) +from esphome.core import CORE, coroutine_with_priority +import esphome.final_validate as fv CODEOWNERS = ["@esphome/core", "@clydebarrow"] spi_ns = cg.esphome_ns.namespace("spi") @@ -69,6 +66,10 @@ SPI_MODE_OPTIONS = { 1: SPIMode.MODE1, 2: SPIMode.MODE2, 3: SPIMode.MODE3, + "0": SPIMode.MODE0, + "1": SPIMode.MODE1, + "2": SPIMode.MODE2, + "3": SPIMode.MODE3, } CONF_SPI_MODE = "spi_mode" diff --git a/esphome/components/spi_device/__init__.py b/esphome/components/spi_device/__init__.py index 65e7ee6fc6..2f23d8a011 100644 --- a/esphome/components/spi_device/__init__.py +++ b/esphome/components/spi_device/__init__.py @@ -1,6 +1,6 @@ import esphome.codegen as cg -import esphome.config_validation as cv from esphome.components import spi +import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_MODE DEPENDENCIES = ["spi"] @@ -11,18 +11,6 @@ spi_device_ns = cg.esphome_ns.namespace("spi_device") spi_device = spi_device_ns.class_("SPIDeviceComponent", cg.Component, spi.SPIDevice) -Mode = spi.spi_ns.enum("SPIMode") -MODES = { - "0": Mode.MODE0, - "1": Mode.MODE1, - "2": Mode.MODE2, - "3": Mode.MODE3, - "MODE0": Mode.MODE0, - "MODE1": Mode.MODE1, - "MODE2": Mode.MODE2, - "MODE3": Mode.MODE3, -} - BitOrder = spi.spi_ns.enum("SPIBitOrder") ORDERS = { "msb_first": BitOrder.BIT_ORDER_MSB_FIRST, @@ -34,7 +22,9 @@ CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(CONF_ID): cv.declare_id(spi_device), cv.Optional(CONF_BIT_ORDER, default="msb_first"): cv.enum(ORDERS, lower=True), - cv.Optional(CONF_MODE, default="0"): cv.enum(MODES, upper=True), + cv.Optional(CONF_MODE): cv.invalid( + "The 'mode' option has been renamed to 'spi_mode'." + ), } ).extend(spi.spi_device_schema(False, "1MHz")) @@ -42,6 +32,5 @@ CONFIG_SCHEMA = cv.Schema( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) - cg.add(var.set_mode(config[CONF_MODE])) cg.add(var.set_bit_order(config[CONF_BIT_ORDER])) await spi.register_spi_device(var, config) diff --git a/tests/components/spi_device/test.esp32-ard.yaml b/tests/components/spi_device/test.esp32-ard.yaml index cad8ca49f8..b539cb3ec4 100644 --- a/tests/components/spi_device/test.esp32-ard.yaml +++ b/tests/components/spi_device/test.esp32-ard.yaml @@ -7,5 +7,5 @@ spi: spi_device: id: spi_device_test data_rate: 2MHz - mode: 3 + spi_mode: 3 bit_order: lsb_first diff --git a/tests/components/spi_device/test.esp32-c3-ard.yaml b/tests/components/spi_device/test.esp32-c3-ard.yaml index 49e2733676..99c0ac1ebb 100644 --- a/tests/components/spi_device/test.esp32-c3-ard.yaml +++ b/tests/components/spi_device/test.esp32-c3-ard.yaml @@ -7,5 +7,5 @@ spi: spi_device: id: spi_device_test data_rate: 2MHz - mode: 3 + spi_mode: 3 bit_order: lsb_first diff --git a/tests/components/spi_device/test.esp32-c3-idf.yaml b/tests/components/spi_device/test.esp32-c3-idf.yaml index 49e2733676..99c0ac1ebb 100644 --- a/tests/components/spi_device/test.esp32-c3-idf.yaml +++ b/tests/components/spi_device/test.esp32-c3-idf.yaml @@ -7,5 +7,5 @@ spi: spi_device: id: spi_device_test data_rate: 2MHz - mode: 3 + spi_mode: 3 bit_order: lsb_first diff --git a/tests/components/spi_device/test.esp32-idf.yaml b/tests/components/spi_device/test.esp32-idf.yaml index cad8ca49f8..b539cb3ec4 100644 --- a/tests/components/spi_device/test.esp32-idf.yaml +++ b/tests/components/spi_device/test.esp32-idf.yaml @@ -7,5 +7,5 @@ spi: spi_device: id: spi_device_test data_rate: 2MHz - mode: 3 + spi_mode: 3 bit_order: lsb_first diff --git a/tests/components/spi_device/test.esp8266-ard.yaml b/tests/components/spi_device/test.esp8266-ard.yaml index 1b191bdb6a..988825ce2d 100644 --- a/tests/components/spi_device/test.esp8266-ard.yaml +++ b/tests/components/spi_device/test.esp8266-ard.yaml @@ -7,5 +7,5 @@ spi: spi_device: id: spi_device_test data_rate: 2MHz - mode: 3 + spi_mode: 3 bit_order: lsb_first diff --git a/tests/components/spi_device/test.rp2040-ard.yaml b/tests/components/spi_device/test.rp2040-ard.yaml index c70493c70d..6020643f21 100644 --- a/tests/components/spi_device/test.rp2040-ard.yaml +++ b/tests/components/spi_device/test.rp2040-ard.yaml @@ -7,5 +7,5 @@ spi: spi_device: id: spi_device_test data_rate: 2MHz - mode: 3 + spi_mode: 3 bit_order: lsb_first From 80b4c264818df8ce79813d3f572e87b9c7d24bf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Mart=C3=ADn?= Date: Wed, 6 Nov 2024 01:56:48 +0100 Subject: [PATCH 20/46] feat(MQTT): Add `enable`, `disable` and `enable_on_boot` (#7716) --- esphome/components/mqtt/__init__.py | 33 +++++++++++++++++++++++++ esphome/components/mqtt/mqtt_client.cpp | 30 +++++++++++++++++++--- esphome/components/mqtt/mqtt_client.h | 29 ++++++++++++++++++++-- tests/components/mqtt/common.yaml | 3 +++ 4 files changed, 90 insertions(+), 5 deletions(-) diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index 8851581ea0..86d163e61d 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -22,6 +22,7 @@ from esphome.const import ( CONF_DISCOVERY_PREFIX, CONF_DISCOVERY_RETAIN, CONF_DISCOVERY_UNIQUE_ID_GENERATOR, + CONF_ENABLE_ON_BOOT, CONF_ID, CONF_KEEPALIVE, CONF_LEVEL, @@ -99,6 +100,8 @@ MQTTMessage = mqtt_ns.struct("MQTTMessage") MQTTClientComponent = mqtt_ns.class_("MQTTClientComponent", cg.Component) MQTTPublishAction = mqtt_ns.class_("MQTTPublishAction", automation.Action) MQTTPublishJsonAction = mqtt_ns.class_("MQTTPublishJsonAction", automation.Action) +MQTTEnableAction = mqtt_ns.class_("MQTTEnableAction", automation.Action) +MQTTDisableAction = mqtt_ns.class_("MQTTDisableAction", automation.Action) MQTTMessageTrigger = mqtt_ns.class_( "MQTTMessageTrigger", automation.Trigger.template(cg.std_string), cg.Component ) @@ -208,6 +211,7 @@ CONFIG_SCHEMA = cv.All( { cv.GenerateID(): cv.declare_id(MQTTClientComponent), cv.Required(CONF_BROKER): cv.string_strict, + cv.Optional(CONF_ENABLE_ON_BOOT, default=True): cv.boolean, cv.Optional(CONF_PORT, default=1883): cv.port, cv.Optional(CONF_USERNAME, default=""): cv.string, cv.Optional(CONF_PASSWORD, default=""): cv.string, @@ -325,6 +329,7 @@ async def to_code(config): cg.add_global(mqtt_ns.using) cg.add(var.set_broker_address(config[CONF_BROKER])) + cg.add(var.set_enable_on_boot(config[CONF_ENABLE_ON_BOOT])) cg.add(var.set_broker_port(config[CONF_PORT])) cg.add(var.set_username(config[CONF_USERNAME])) cg.add(var.set_password(config[CONF_PASSWORD])) @@ -555,3 +560,31 @@ async def register_mqtt_component(var, config): async def mqtt_connected_to_code(config, condition_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) return cg.new_Pvariable(condition_id, template_arg, paren) + + +@automation.register_action( + "mqtt.enable", + MQTTEnableAction, + cv.Schema( + { + cv.GenerateID(): cv.use_id(MQTTClientComponent), + } + ), +) +async def mqtt_enable_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) + + +@automation.register_action( + "mqtt.disable", + MQTTDisableAction, + cv.Schema( + { + cv.GenerateID(): cv.use_id(MQTTClientComponent), + } + ), +) +async def mqtt_disable_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index b5ac285026..106192c0e3 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -50,6 +50,8 @@ void MQTTClientComponent::setup() { } }); this->mqtt_backend_.set_on_disconnect([this](MQTTClientDisconnectReason reason) { + if (this->state_ == MQTT_CLIENT_DISABLED) + return; this->state_ = MQTT_CLIENT_DISCONNECTED; this->disconnect_reason_ = reason; }); @@ -77,8 +79,9 @@ void MQTTClientComponent::setup() { topic, [this](const std::string &topic, const std::string &payload) { this->send_device_info_(); }, 2); } - this->last_connected_ = millis(); - this->start_dnslookup_(); + if (this->enable_on_boot_) { + this->enable(); + } } void MQTTClientComponent::send_device_info_() { @@ -163,7 +166,9 @@ void MQTTClientComponent::dump_config() { ESP_LOGCONFIG(TAG, " Availability: '%s'", this->availability_.topic.c_str()); } } -bool MQTTClientComponent::can_proceed() { return network::is_disabled() || this->is_connected(); } +bool MQTTClientComponent::can_proceed() { + return network::is_disabled() || this->state_ == MQTT_CLIENT_DISABLED || this->is_connected(); +} void MQTTClientComponent::start_dnslookup_() { for (auto &subscription : this->subscriptions_) { @@ -339,6 +344,8 @@ void MQTTClientComponent::loop() { const uint32_t now = millis(); switch (this->state_) { + case MQTT_CLIENT_DISABLED: + return; // Return to avoid a reboot when disabled case MQTT_CLIENT_DISCONNECTED: if (now - this->connect_begin_ > 5000) { this->start_dnslookup_(); @@ -501,6 +508,23 @@ bool MQTTClientComponent::publish_json(const std::string &topic, const json::jso return this->publish(topic, message, qos, retain); } +void MQTTClientComponent::enable() { + if (this->state_ != MQTT_CLIENT_DISABLED) + return; + ESP_LOGD(TAG, "Enabling MQTT..."); + this->state_ = MQTT_CLIENT_DISCONNECTED; + this->last_connected_ = millis(); + this->start_dnslookup_(); +} + +void MQTTClientComponent::disable() { + if (this->state_ == MQTT_CLIENT_DISABLED) + return; + ESP_LOGD(TAG, "Disabling MQTT..."); + this->state_ = MQTT_CLIENT_DISABLED; + this->on_shutdown(); +} + /** Check if the message topic matches the given subscription topic * * INFO: MQTT spec mandates that topics must not be empty and must be valid NULL-terminated UTF-8 strings. diff --git a/esphome/components/mqtt/mqtt_client.h b/esphome/components/mqtt/mqtt_client.h index 887800f201..7ae3a6c5e8 100644 --- a/esphome/components/mqtt/mqtt_client.h +++ b/esphome/components/mqtt/mqtt_client.h @@ -87,7 +87,8 @@ struct MQTTDiscoveryInfo { }; enum MQTTClientState { - MQTT_CLIENT_DISCONNECTED = 0, + MQTT_CLIENT_DISABLED = 0, + MQTT_CLIENT_DISCONNECTED, MQTT_CLIENT_RESOLVING_ADDRESS, MQTT_CLIENT_CONNECTING, MQTT_CLIENT_CONNECTED, @@ -247,6 +248,9 @@ class MQTTClientComponent : public Component { void register_mqtt_component(MQTTComponent *component); bool is_connected(); + void set_enable_on_boot(bool enable_on_boot) { this->enable_on_boot_ = enable_on_boot; } + void enable(); + void disable(); void on_shutdown() override; @@ -314,10 +318,11 @@ class MQTTClientComponent : public Component { MQTTBackendLibreTiny mqtt_backend_; #endif - MQTTClientState state_{MQTT_CLIENT_DISCONNECTED}; + MQTTClientState state_{MQTT_CLIENT_DISABLED}; network::IPAddress ip_; bool dns_resolved_{false}; bool dns_resolve_error_{false}; + bool enable_on_boot_{true}; std::vector children_; uint32_t reboot_timeout_{300000}; uint32_t connect_begin_; @@ -414,6 +419,26 @@ template class MQTTConnectedCondition : public Condition MQTTClientComponent *parent_; }; +template class MQTTEnableAction : public Action { + public: + MQTTEnableAction(MQTTClientComponent *parent) : parent_(parent) {} + + void play(Ts... x) override { this->parent_->enable(); } + + protected: + MQTTClientComponent *parent_; +}; + +template class MQTTDisableAction : public Action { + public: + MQTTDisableAction(MQTTClientComponent *parent) : parent_(parent) {} + + void play(Ts... x) override { this->parent_->disable(); } + + protected: + MQTTClientComponent *parent_; +}; + } // namespace mqtt } // namespace esphome diff --git a/tests/components/mqtt/common.yaml b/tests/components/mqtt/common.yaml index 75c34bec56..d22fe9579f 100644 --- a/tests/components/mqtt/common.yaml +++ b/tests/components/mqtt/common.yaml @@ -10,6 +10,7 @@ mqtt: port: 1883 username: debug password: debug + enable_on_boot: false clean_session: True client_id: someclient use_abbreviations: false @@ -87,6 +88,8 @@ button: state_topic: some/topic/button qos: 2 on_press: + - mqtt.disable + - mqtt.enable - mqtt.publish: topic: some/topic/button payload: Hello From 248b0bc378b93b8be87899a65f47b4d7a95b866d Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Fri, 8 Nov 2024 07:05:23 +1100 Subject: [PATCH 21/46] [lvgl] Allow multiple LVGL instances (#7712) Co-authored-by: clydeps --- esphome/components/lvgl/__init__.py | 241 +++++++++++------- esphome/components/lvgl/automation.py | 14 +- .../components/lvgl/binary_sensor/__init__.py | 23 +- esphome/components/lvgl/light/__init__.py | 8 +- esphome/components/lvgl/lvcode.py | 4 +- esphome/components/lvgl/lvgl_esphome.cpp | 25 +- esphome/components/lvgl/lvgl_esphome.h | 14 +- esphome/components/lvgl/number/__init__.py | 25 +- esphome/components/lvgl/select/__init__.py | 21 +- esphome/components/lvgl/sensor/__init__.py | 22 +- esphome/components/lvgl/switch/__init__.py | 21 +- esphome/components/lvgl/text/__init__.py | 11 +- .../components/lvgl/text_sensor/__init__.py | 30 +-- esphome/components/lvgl/touchscreens.py | 5 +- esphome/components/lvgl/trigger.py | 12 +- esphome/components/lvgl/widgets/page.py | 5 +- tests/components/lvgl/test.host.yaml | 32 +++ 17 files changed, 287 insertions(+), 226 deletions(-) diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py index 4a1a26cc0b..7476c0a09c 100644 --- a/esphome/components/lvgl/__init__.py +++ b/esphome/components/lvgl/__init__.py @@ -27,7 +27,7 @@ from .encoders import ENCODERS_CONFIG, encoders_to_code, initial_focus_to_code from .gradient import GRADIENT_SCHEMA, gradients_to_code from .hello_world import get_hello_world from .lv_validation import lv_bool, lv_images_used -from .lvcode import LvContext, LvglComponent +from .lvcode import LvContext, LvglComponent, lvgl_static from .schemas import ( DISP_BG_SCHEMA, FLEX_OBJ_SCHEMA, @@ -152,41 +152,70 @@ def generate_lv_conf_h(): return LV_CONF_H_FORMAT.format("\n".join(definitions)) -def final_validation(config): - if pages := config.get(CONF_PAGES): - if all(p[df.CONF_SKIP] for p in pages): - raise cv.Invalid("At least one page must not be skipped") +def multi_conf_validate(configs: list[dict]): + displays = [config[df.CONF_DISPLAYS] for config in configs] + # flatten the display list + display_list = [disp for disps in displays for disp in disps] + if len(display_list) != len(set(display_list)): + raise cv.Invalid("A display ID may be used in only one LVGL instance") + base_config = configs[0] + for config in configs[1:]: + for item in ( + df.CONF_LOG_LEVEL, + df.CONF_COLOR_DEPTH, + df.CONF_BYTE_ORDER, + df.CONF_TRANSPARENCY_KEY, + ): + if base_config[item] != config[item]: + raise cv.Invalid( + f"Config item '{item}' must be the same for all LVGL instances" + ) + + +def final_validation(configs): + multi_conf_validate(configs) global_config = full_config.get() - for display_id in config[df.CONF_DISPLAYS]: - path = global_config.get_path_for_id(display_id)[:-1] - display = global_config.get_config_for_path(path) - if CONF_LAMBDA in display: - raise cv.Invalid("Using lambda: in display config not compatible with LVGL") - if display[CONF_AUTO_CLEAR_ENABLED]: - raise cv.Invalid( - "Using auto_clear_enabled: true in display config not compatible with LVGL" - ) - buffer_frac = config[CONF_BUFFER_SIZE] - if CORE.is_esp32 and buffer_frac > 0.5 and "psram" not in global_config: - LOGGER.warning("buffer_size: may need to be reduced without PSRAM") - for image_id in lv_images_used: - path = global_config.get_path_for_id(image_id)[:-1] - image_conf = global_config.get_config_for_path(path) - if image_conf[CONF_TYPE] in ("RGBA", "RGB24"): - raise cv.Invalid( - "Using RGBA or RGB24 in image config not compatible with LVGL", path - ) - for w in focused_widgets: - path = global_config.get_path_for_id(w) - widget_conf = global_config.get_config_for_path(path[:-1]) - if df.CONF_ADJUSTABLE in widget_conf and not widget_conf[df.CONF_ADJUSTABLE]: - raise cv.Invalid( - "A non adjustable arc may not be focused", - path, - ) + for config in configs: + if pages := config.get(CONF_PAGES): + if all(p[df.CONF_SKIP] for p in pages): + raise cv.Invalid("At least one page must not be skipped") + for display_id in config[df.CONF_DISPLAYS]: + path = global_config.get_path_for_id(display_id)[:-1] + display = global_config.get_config_for_path(path) + if CONF_LAMBDA in display: + raise cv.Invalid( + "Using lambda: in display config not compatible with LVGL" + ) + if display[CONF_AUTO_CLEAR_ENABLED]: + raise cv.Invalid( + "Using auto_clear_enabled: true in display config not compatible with LVGL" + ) + buffer_frac = config[CONF_BUFFER_SIZE] + if CORE.is_esp32 and buffer_frac > 0.5 and "psram" not in global_config: + LOGGER.warning("buffer_size: may need to be reduced without PSRAM") + for image_id in lv_images_used: + path = global_config.get_path_for_id(image_id)[:-1] + image_conf = global_config.get_config_for_path(path) + if image_conf[CONF_TYPE] in ("RGBA", "RGB24"): + raise cv.Invalid( + "Using RGBA or RGB24 in image config not compatible with LVGL", path + ) + for w in focused_widgets: + path = global_config.get_path_for_id(w) + widget_conf = global_config.get_config_for_path(path[:-1]) + if ( + df.CONF_ADJUSTABLE in widget_conf + and not widget_conf[df.CONF_ADJUSTABLE] + ): + raise cv.Invalid( + "A non adjustable arc may not be focused", + path, + ) -async def to_code(config): +async def to_code(configs): + config_0 = configs[0] + # Global configuration cg.add_library("lvgl/lvgl", "8.4.0") cg.add_define("USE_LVGL") # suppress default enabling of extra widgets @@ -203,53 +232,33 @@ async def to_code(config): add_define("LV_MEM_CUSTOM_INCLUDE", '"esphome/components/lvgl/lvgl_hal.h"') add_define( - "LV_LOG_LEVEL", f"LV_LOG_LEVEL_{df.LV_LOG_LEVELS[config[df.CONF_LOG_LEVEL]]}" + "LV_LOG_LEVEL", + f"LV_LOG_LEVEL_{df.LV_LOG_LEVELS[config_0[df.CONF_LOG_LEVEL]]}", ) cg.add_define( "LVGL_LOG_LEVEL", - cg.RawExpression(f"ESPHOME_LOG_LEVEL_{config[df.CONF_LOG_LEVEL]}"), + cg.RawExpression(f"ESPHOME_LOG_LEVEL_{config_0[df.CONF_LOG_LEVEL]}"), ) - add_define("LV_COLOR_DEPTH", config[df.CONF_COLOR_DEPTH]) + add_define("LV_COLOR_DEPTH", config_0[df.CONF_COLOR_DEPTH]) for font in helpers.lv_fonts_used: add_define(f"LV_FONT_{font.upper()}") - if config[df.CONF_COLOR_DEPTH] == 16: + if config_0[df.CONF_COLOR_DEPTH] == 16: add_define( "LV_COLOR_16_SWAP", - "1" if config[df.CONF_BYTE_ORDER] == "big_endian" else "0", + "1" if config_0[df.CONF_BYTE_ORDER] == "big_endian" else "0", ) add_define( "LV_COLOR_CHROMA_KEY", - await lvalid.lv_color.process(config[df.CONF_TRANSPARENCY_KEY]), + await lvalid.lv_color.process(config_0[df.CONF_TRANSPARENCY_KEY]), ) cg.add_build_flag("-Isrc") cg.add_global(lvgl_ns.using) - frac = config[CONF_BUFFER_SIZE] - if frac >= 0.75: - frac = 1 - elif frac >= 0.375: - frac = 2 - elif frac > 0.19: - frac = 4 - else: - frac = 8 - displays = [await cg.get_variable(display) for display in config[df.CONF_DISPLAYS]] - lv_component = cg.new_Pvariable( - config[CONF_ID], - displays, - frac, - config[df.CONF_FULL_REFRESH], - config[df.CONF_DRAW_ROUNDING], - config[df.CONF_RESUME_ON_INPUT], - ) - await cg.register_component(lv_component, config) - Widget.create(config[CONF_ID], lv_component, obj_spec, config) - for font in helpers.esphome_fonts_used: await cg.get_variable(font) cg.new_Pvariable(ID(f"{font}_engine", True, type=FontEngine), MockObj(font)) - default_font = config[df.CONF_DEFAULT_FONT] + default_font = config_0[df.CONF_DEFAULT_FONT] if not lvalid.is_lv_font(default_font): add_define( "LV_FONT_CUSTOM_DECLARE", f"LV_FONT_DECLARE(*{df.DEFAULT_ESPHOME_FONT})" @@ -265,39 +274,71 @@ async def to_code(config): add_define("LV_FONT_DEFAULT", df.DEFAULT_ESPHOME_FONT) else: add_define("LV_FONT_DEFAULT", await lvalid.lv_font.process(default_font)) + cg.add(lvgl_static.esphome_lvgl_init()) - lv_scr_act = get_scr_act(lv_component) - async with LvContext(lv_component): - await touchscreens_to_code(lv_component, config) - await encoders_to_code(lv_component, config) - await theme_to_code(config) - await styles_to_code(config) - await gradients_to_code(config) - await set_obj_properties(lv_scr_act, config) - await add_widgets(lv_scr_act, config) - await add_pages(lv_component, config) - await add_top_layer(lv_component, config) - await msgboxes_to_code(lv_component, config) - await disp_update(lv_component.get_disp(), config) + for config in configs: + frac = config[CONF_BUFFER_SIZE] + if frac >= 0.75: + frac = 1 + elif frac >= 0.375: + frac = 2 + elif frac > 0.19: + frac = 4 + else: + frac = 8 + displays = [ + await cg.get_variable(display) for display in config[df.CONF_DISPLAYS] + ] + lv_component = cg.new_Pvariable( + config[CONF_ID], + displays, + frac, + config[df.CONF_FULL_REFRESH], + config[df.CONF_DRAW_ROUNDING], + config[df.CONF_RESUME_ON_INPUT], + ) + await cg.register_component(lv_component, config) + Widget.create(config[CONF_ID], lv_component, obj_spec, config) + + lv_scr_act = get_scr_act(lv_component) + async with LvContext(): + await touchscreens_to_code(lv_component, config) + await encoders_to_code(lv_component, config) + await theme_to_code(config) + await styles_to_code(config) + await gradients_to_code(config) + await set_obj_properties(lv_scr_act, config) + await add_widgets(lv_scr_act, config) + await add_pages(lv_component, config) + await add_top_layer(lv_component, config) + await msgboxes_to_code(lv_component, config) + await disp_update(lv_component.get_disp(), config) # Set this directly since we are limited in how many methods can be added to the Widget class. Widget.widgets_completed = True - async with LvContext(lv_component): - await generate_triggers(lv_component) - await generate_page_triggers(lv_component, config) - await initial_focus_to_code(config) - for conf in config.get(CONF_ON_IDLE, ()): - templ = await cg.templatable(conf[CONF_TIMEOUT], [], cg.uint32) - idle_trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], lv_component, templ) - await build_automation(idle_trigger, [], conf) - for conf in config.get(df.CONF_ON_PAUSE, ()): - pause_trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], lv_component, True) - await build_automation(pause_trigger, [], conf) - for conf in config.get(df.CONF_ON_RESUME, ()): - resume_trigger = cg.new_Pvariable( - conf[CONF_TRIGGER_ID], lv_component, False - ) - await build_automation(resume_trigger, [], conf) + async with LvContext(): + await generate_triggers() + for config in configs: + lv_component = await cg.get_variable(config[CONF_ID]) + await generate_page_triggers(config) + await initial_focus_to_code(config) + for conf in config.get(CONF_ON_IDLE, ()): + templ = await cg.templatable(conf[CONF_TIMEOUT], [], cg.uint32) + idle_trigger = cg.new_Pvariable( + conf[CONF_TRIGGER_ID], lv_component, templ + ) + await build_automation(idle_trigger, [], conf) + for conf in config.get(df.CONF_ON_PAUSE, ()): + pause_trigger = cg.new_Pvariable( + conf[CONF_TRIGGER_ID], lv_component, True + ) + await build_automation(pause_trigger, [], conf) + for conf in config.get(df.CONF_ON_RESUME, ()): + resume_trigger = cg.new_Pvariable( + conf[CONF_TRIGGER_ID], lv_component, False + ) + await build_automation(resume_trigger, [], conf) + # This must be done after all widgets are created for comp in helpers.lvgl_components_required: cg.add_define(f"USE_LVGL_{comp.upper()}") if "transform_angle" in styles_used: @@ -312,7 +353,10 @@ async def to_code(config): def display_schema(config): value = cv.ensure_list(cv.use_id(Display))(config) - return value or [cv.use_id(Display)(config)] + value = value or [cv.use_id(Display)(config)] + if len(set(value)) != len(value): + raise cv.Invalid("Display IDs must be unique") + return value def add_hello_world(config): @@ -324,7 +368,7 @@ def add_hello_world(config): FINAL_VALIDATE_SCHEMA = final_validation -CONFIG_SCHEMA = ( +LVGL_SCHEMA = ( cv.polling_component_schema("1s") .extend(obj_schema(obj_spec)) .extend( @@ -393,3 +437,16 @@ CONFIG_SCHEMA = ( .extend(DISP_BG_SCHEMA) .add_extra(add_hello_world) ) + + +def lvgl_config_schema(config): + """ + Can't use cv.ensure_list here because it converts an empty config to an empty list, + rather than a default config. + """ + if not config or isinstance(config, dict): + return [LVGL_SCHEMA(config)] + return cv.Schema([LVGL_SCHEMA])(config) + + +CONFIG_SCHEMA = lvgl_config_schema diff --git a/esphome/components/lvgl/automation.py b/esphome/components/lvgl/automation.py index 48472354f8..58e3dd808b 100644 --- a/esphome/components/lvgl/automation.py +++ b/esphome/components/lvgl/automation.py @@ -137,20 +137,18 @@ async def disp_update(disp, config: dict): cv.maybe_simple_value( { cv.Required(CONF_ID): cv.use_id(lv_obj_t), - cv.GenerateID(CONF_LVGL_ID): cv.use_id(LvglComponent), }, key=CONF_ID, ), - cv.Schema( - { - cv.GenerateID(CONF_LVGL_ID): cv.use_id(LvglComponent), - } - ), + LVGL_SCHEMA, ), ) async def obj_invalidate_to_code(config, action_id, template_arg, args): - lv_comp = await cg.get_variable(config[CONF_LVGL_ID]) - widgets = await get_widgets(config) or [get_scr_act(lv_comp)] + if CONF_LVGL_ID in config: + lv_comp = await cg.get_variable(config[CONF_LVGL_ID]) + widgets = [get_scr_act(lv_comp)] + else: + widgets = await get_widgets(config) async def do_invalidate(widget: Widget): lv_obj.invalidate(widget.obj) diff --git a/esphome/components/lvgl/binary_sensor/__init__.py b/esphome/components/lvgl/binary_sensor/__init__.py index 56984405aa..ffbdc977b2 100644 --- a/esphome/components/lvgl/binary_sensor/__init__.py +++ b/esphome/components/lvgl/binary_sensor/__init__.py @@ -1,4 +1,3 @@ -import esphome.codegen as cg from esphome.components.binary_sensor import ( BinarySensor, binary_sensor_schema, @@ -6,36 +5,30 @@ from esphome.components.binary_sensor import ( ) import esphome.config_validation as cv -from ..defines import CONF_LVGL_ID, CONF_WIDGET -from ..lvcode import EVENT_ARG, LambdaContext, LvContext -from ..schemas import LVGL_SCHEMA +from ..defines import CONF_WIDGET +from ..lvcode import EVENT_ARG, LambdaContext, LvContext, lvgl_static from ..types import LV_EVENT, lv_pseudo_button_t from ..widgets import Widget, get_widgets, wait_for_widgets -CONFIG_SCHEMA = ( - binary_sensor_schema(BinarySensor) - .extend(LVGL_SCHEMA) - .extend( - { - cv.Required(CONF_WIDGET): cv.use_id(lv_pseudo_button_t), - } - ) +CONFIG_SCHEMA = binary_sensor_schema(BinarySensor).extend( + { + cv.Required(CONF_WIDGET): cv.use_id(lv_pseudo_button_t), + } ) async def to_code(config): sensor = await new_binary_sensor(config) - paren = await cg.get_variable(config[CONF_LVGL_ID]) widget = await get_widgets(config, CONF_WIDGET) widget = widget[0] assert isinstance(widget, Widget) await wait_for_widgets() async with LambdaContext(EVENT_ARG) as pressed_ctx: pressed_ctx.add(sensor.publish_state(widget.is_pressed())) - async with LvContext(paren) as ctx: + async with LvContext() as ctx: ctx.add(sensor.publish_initial_state(widget.is_pressed())) ctx.add( - paren.add_event_cb( + lvgl_static.add_event_cb( widget.obj, await pressed_ctx.get_lambda(), LV_EVENT.PRESSING, diff --git a/esphome/components/lvgl/light/__init__.py b/esphome/components/lvgl/light/__init__.py index 8031ae8221..dcdf67a520 100644 --- a/esphome/components/lvgl/light/__init__.py +++ b/esphome/components/lvgl/light/__init__.py @@ -4,9 +4,8 @@ from esphome.components.light import LightOutput import esphome.config_validation as cv from esphome.const import CONF_GAMMA_CORRECT, CONF_OUTPUT_ID -from ..defines import CONF_LVGL_ID, CONF_WIDGET +from ..defines import CONF_WIDGET from ..lvcode import LvContext -from ..schemas import LVGL_SCHEMA from ..types import LvType, lvgl_ns from ..widgets import get_widgets, wait_for_widgets @@ -18,16 +17,15 @@ CONFIG_SCHEMA = light.RGB_LIGHT_SCHEMA.extend( cv.Required(CONF_WIDGET): cv.use_id(lv_led_t), cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(LVLight), } -).extend(LVGL_SCHEMA) +) async def to_code(config): var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) await light.register_light(var, config) - paren = await cg.get_variable(config[CONF_LVGL_ID]) widget = await get_widgets(config, CONF_WIDGET) widget = widget[0] await wait_for_widgets() - async with LvContext(paren) as ctx: + async with LvContext() as ctx: ctx.add(var.set_obj(widget.obj)) diff --git a/esphome/components/lvgl/lvcode.py b/esphome/components/lvgl/lvcode.py index 37d6670b84..6b98cc4251 100644 --- a/esphome/components/lvgl/lvcode.py +++ b/esphome/components/lvgl/lvcode.py @@ -178,10 +178,9 @@ class LvContext(LambdaContext): added_lambda_count = 0 - def __init__(self, lv_component, args=None): + def __init__(self, args=None): self.args = args or LVGL_COMP_ARG super().__init__(parameters=self.args) - self.lv_component = lv_component async def __aexit__(self, exc_type, exc_val, exc_tb): await super().__aexit__(exc_type, exc_val, exc_tb) @@ -298,6 +297,7 @@ lv_expr = LvExpr("lv_") lv_obj = MockLv("lv_obj_") # Operations on the LVGL component lvgl_comp = MockObj(LVGL_COMP, "->") +lvgl_static = MockObj("LvglComponent", "::") # equivalent to cg.add() for the current code context diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index 70cfb859de..41346bc732 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -98,19 +98,24 @@ void LvglComponent::set_paused(bool paused, bool show_snow) { this->pause_callbacks_.call(paused); } +void LvglComponent::esphome_lvgl_init() { + lv_init(); + lv_update_event = static_cast(lv_event_register_id()); + lv_api_event = static_cast(lv_event_register_id()); +} void LvglComponent::add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event) { - lv_obj_add_event_cb(obj, callback, event, this); + lv_obj_add_event_cb(obj, callback, event, nullptr); } void LvglComponent::add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1, lv_event_code_t event2) { - this->add_event_cb(obj, callback, event1); - this->add_event_cb(obj, callback, event2); + add_event_cb(obj, callback, event1); + add_event_cb(obj, callback, event2); } void LvglComponent::add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1, lv_event_code_t event2, lv_event_code_t event3) { - this->add_event_cb(obj, callback, event1); - this->add_event_cb(obj, callback, event2); - this->add_event_cb(obj, callback, event3); + add_event_cb(obj, callback, event1); + add_event_cb(obj, callback, event2); + add_event_cb(obj, callback, event3); } void LvglComponent::add_page(LvPageType *page) { this->pages_.push_back(page); @@ -218,8 +223,10 @@ PauseTrigger::PauseTrigger(LvglComponent *parent, TemplatableValue paused) } #ifdef USE_LVGL_TOUCHSCREEN -LVTouchListener::LVTouchListener(uint16_t long_press_time, uint16_t long_press_repeat_time) { +LVTouchListener::LVTouchListener(uint16_t long_press_time, uint16_t long_press_repeat_time, LvglComponent *parent) { + this->set_parent(parent); lv_indev_drv_init(&this->drv_); + this->drv_.disp = parent->get_disp(); this->drv_.long_press_repeat_time = long_press_repeat_time; this->drv_.long_press_time = long_press_time; this->drv_.type = LV_INDEV_TYPE_POINTER; @@ -235,6 +242,7 @@ LVTouchListener::LVTouchListener(uint16_t long_press_time, uint16_t long_press_r } }; } + void LVTouchListener::update(const touchscreen::TouchPoints_t &tpoints) { this->touch_pressed_ = !this->parent_->is_paused() && !tpoints.empty(); if (this->touch_pressed_) @@ -405,9 +413,6 @@ LvglComponent::LvglComponent(std::vector displays, float buf buffer_frac_(buffer_frac), full_refresh_(full_refresh), resume_on_input_(resume_on_input) { - lv_init(); - lv_update_event = static_cast(lv_event_register_id()); - lv_api_event = static_cast(lv_event_register_id()); auto *display = this->displays_[0]; size_t buffer_pixels = display->get_width() * display->get_height() / this->buffer_frac_; auto buf_bytes = buffer_pixels * LV_COLOR_DEPTH / 8; diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index f357c4950c..dae07d5153 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -146,10 +146,14 @@ class LvglComponent : public PollingComponent { } } - void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event); - void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1, lv_event_code_t event2); - void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1, lv_event_code_t event2, - lv_event_code_t event3); + /** + * Initialize the LVGL library and register custom events. + */ + static void esphome_lvgl_init(); + static void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event); + static void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1, lv_event_code_t event2); + static void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1, lv_event_code_t event2, + lv_event_code_t event3); void add_page(LvPageType *page); void show_page(size_t index, lv_scr_load_anim_t anim, uint32_t time); void show_next_page(lv_scr_load_anim_t anim, uint32_t time); @@ -231,7 +235,7 @@ template class LvglCondition : public Condition, public P #ifdef USE_LVGL_TOUCHSCREEN class LVTouchListener : public touchscreen::TouchListener, public Parented { public: - LVTouchListener(uint16_t long_press_time, uint16_t long_press_repeat_time); + LVTouchListener(uint16_t long_press_time, uint16_t long_press_repeat_time, LvglComponent *parent); void update(const touchscreen::TouchPoints_t &tpoints) override; void release() override { touch_pressed_ = false; diff --git a/esphome/components/lvgl/number/__init__.py b/esphome/components/lvgl/number/__init__.py index 07f92635b5..b41a36bc0f 100644 --- a/esphome/components/lvgl/number/__init__.py +++ b/esphome/components/lvgl/number/__init__.py @@ -3,7 +3,7 @@ from esphome.components import number import esphome.config_validation as cv from esphome.cpp_generator import MockObj -from ..defines import CONF_ANIMATED, CONF_LVGL_ID, CONF_UPDATE_ON_RELEASE, CONF_WIDGET +from ..defines import CONF_ANIMATED, CONF_UPDATE_ON_RELEASE, CONF_WIDGET from ..lv_validation import animated from ..lvcode import ( API_EVENT, @@ -13,28 +13,23 @@ from ..lvcode import ( LvContext, lv, lv_add, + lvgl_static, ) -from ..schemas import LVGL_SCHEMA from ..types import LV_EVENT, LvNumber, lvgl_ns from ..widgets import get_widgets, wait_for_widgets LVGLNumber = lvgl_ns.class_("LVGLNumber", number.Number) -CONFIG_SCHEMA = ( - number.number_schema(LVGLNumber) - .extend(LVGL_SCHEMA) - .extend( - { - cv.Required(CONF_WIDGET): cv.use_id(LvNumber), - cv.Optional(CONF_ANIMATED, default=True): animated, - cv.Optional(CONF_UPDATE_ON_RELEASE, default=False): cv.boolean, - } - ) +CONFIG_SCHEMA = number.number_schema(LVGLNumber).extend( + { + cv.Required(CONF_WIDGET): cv.use_id(LvNumber), + cv.Optional(CONF_ANIMATED, default=True): animated, + cv.Optional(CONF_UPDATE_ON_RELEASE, default=False): cv.boolean, + } ) async def to_code(config): - paren = await cg.get_variable(config[CONF_LVGL_ID]) widget = await get_widgets(config, CONF_WIDGET) widget = widget[0] var = await number.new_number( @@ -58,10 +53,10 @@ async def to_code(config): if not config[CONF_UPDATE_ON_RELEASE] else LV_EVENT.RELEASED ) - async with LvContext(paren): + async with LvContext(): lv_add(var.set_control_lambda(await control.get_lambda())) lv_add( - paren.add_event_cb( + lvgl_static.add_event_cb( widget.obj, await event.get_lambda(), UPDATE_EVENT, event_code ) ) diff --git a/esphome/components/lvgl/select/__init__.py b/esphome/components/lvgl/select/__init__.py index 5e50b6b385..bd5ef8f237 100644 --- a/esphome/components/lvgl/select/__init__.py +++ b/esphome/components/lvgl/select/__init__.py @@ -1,25 +1,19 @@ -import esphome.codegen as cg from esphome.components import select import esphome.config_validation as cv from esphome.const import CONF_OPTIONS -from ..defines import CONF_ANIMATED, CONF_LVGL_ID, CONF_WIDGET, literal +from ..defines import CONF_ANIMATED, CONF_WIDGET, literal from ..lvcode import LvContext -from ..schemas import LVGL_SCHEMA from ..types import LvSelect, lvgl_ns from ..widgets import get_widgets, wait_for_widgets LVGLSelect = lvgl_ns.class_("LVGLSelect", select.Select) -CONFIG_SCHEMA = ( - select.select_schema(LVGLSelect) - .extend(LVGL_SCHEMA) - .extend( - { - cv.Required(CONF_WIDGET): cv.use_id(LvSelect), - cv.Optional(CONF_ANIMATED, default=False): cv.boolean, - } - ) +CONFIG_SCHEMA = select.select_schema(LVGLSelect).extend( + { + cv.Required(CONF_WIDGET): cv.use_id(LvSelect), + cv.Optional(CONF_ANIMATED, default=False): cv.boolean, + } ) @@ -28,9 +22,8 @@ async def to_code(config): widget = widget[0] options = widget.config.get(CONF_OPTIONS, []) selector = await select.new_select(config, options=options) - paren = await cg.get_variable(config[CONF_LVGL_ID]) await wait_for_widgets() - async with LvContext(paren) as ctx: + async with LvContext() as ctx: ctx.add( selector.set_widget( widget.var, diff --git a/esphome/components/lvgl/sensor/__init__.py b/esphome/components/lvgl/sensor/__init__.py index a2a2298c27..03b2638ed0 100644 --- a/esphome/components/lvgl/sensor/__init__.py +++ b/esphome/components/lvgl/sensor/__init__.py @@ -1,8 +1,7 @@ -import esphome.codegen as cg from esphome.components.sensor import Sensor, new_sensor, sensor_schema import esphome.config_validation as cv -from ..defines import CONF_LVGL_ID, CONF_WIDGET +from ..defines import CONF_WIDGET from ..lvcode import ( API_EVENT, EVENT_ARG, @@ -11,34 +10,29 @@ from ..lvcode import ( LambdaContext, LvContext, lv_add, + lvgl_static, ) -from ..schemas import LVGL_SCHEMA from ..types import LV_EVENT, LvNumber from ..widgets import Widget, get_widgets, wait_for_widgets -CONFIG_SCHEMA = ( - sensor_schema(Sensor) - .extend(LVGL_SCHEMA) - .extend( - { - cv.Required(CONF_WIDGET): cv.use_id(LvNumber), - } - ) +CONFIG_SCHEMA = sensor_schema(Sensor).extend( + { + cv.Required(CONF_WIDGET): cv.use_id(LvNumber), + } ) async def to_code(config): sensor = await new_sensor(config) - paren = await cg.get_variable(config[CONF_LVGL_ID]) widget = await get_widgets(config, CONF_WIDGET) widget = widget[0] assert isinstance(widget, Widget) await wait_for_widgets() async with LambdaContext(EVENT_ARG) as lamb: lv_add(sensor.publish_state(widget.get_value())) - async with LvContext(paren, LVGL_COMP_ARG): + async with LvContext(LVGL_COMP_ARG): lv_add( - paren.add_event_cb( + lvgl_static.add_event_cb( widget.obj, await lamb.get_lambda(), LV_EVENT.VALUE_CHANGED, diff --git a/esphome/components/lvgl/switch/__init__.py b/esphome/components/lvgl/switch/__init__.py index 8c090543f9..4e1e7f72e0 100644 --- a/esphome/components/lvgl/switch/__init__.py +++ b/esphome/components/lvgl/switch/__init__.py @@ -3,7 +3,7 @@ from esphome.components.switch import Switch, new_switch, switch_schema import esphome.config_validation as cv from esphome.cpp_generator import MockObj -from ..defines import CONF_LVGL_ID, CONF_WIDGET, literal +from ..defines import CONF_WIDGET, literal from ..lvcode import ( API_EVENT, EVENT_ARG, @@ -13,26 +13,21 @@ from ..lvcode import ( LvContext, lv, lv_add, + lvgl_static, ) -from ..schemas import LVGL_SCHEMA from ..types import LV_EVENT, LV_STATE, lv_pseudo_button_t, lvgl_ns from ..widgets import get_widgets, wait_for_widgets LVGLSwitch = lvgl_ns.class_("LVGLSwitch", Switch) -CONFIG_SCHEMA = ( - switch_schema(LVGLSwitch) - .extend(LVGL_SCHEMA) - .extend( - { - cv.Required(CONF_WIDGET): cv.use_id(lv_pseudo_button_t), - } - ) +CONFIG_SCHEMA = switch_schema(LVGLSwitch).extend( + { + cv.Required(CONF_WIDGET): cv.use_id(lv_pseudo_button_t), + } ) async def to_code(config): switch = await new_switch(config) - paren = await cg.get_variable(config[CONF_LVGL_ID]) widget = await get_widgets(config, CONF_WIDGET) widget = widget[0] await wait_for_widgets() @@ -45,10 +40,10 @@ async def to_code(config): widget.clear_state(LV_STATE.CHECKED) lv.event_send(widget.obj, API_EVENT, cg.nullptr) control.add(switch.publish_state(literal("v"))) - async with LvContext(paren) as ctx: + async with LvContext() as ctx: lv_add(switch.set_control_lambda(await control.get_lambda())) ctx.add( - paren.add_event_cb( + lvgl_static.add_event_cb( widget.obj, await checked_ctx.get_lambda(), LV_EVENT.VALUE_CHANGED, diff --git a/esphome/components/lvgl/text/__init__.py b/esphome/components/lvgl/text/__init__.py index a59e703591..89db139a6a 100644 --- a/esphome/components/lvgl/text/__init__.py +++ b/esphome/components/lvgl/text/__init__.py @@ -3,7 +3,7 @@ from esphome.components import text from esphome.components.text import new_text import esphome.config_validation as cv -from ..defines import CONF_LVGL_ID, CONF_WIDGET +from ..defines import CONF_WIDGET from ..lvcode import ( API_EVENT, EVENT_ARG, @@ -12,14 +12,14 @@ from ..lvcode import ( LvContext, lv, lv_add, + lvgl_static, ) -from ..schemas import LVGL_SCHEMA from ..types import LV_EVENT, LvText, lvgl_ns from ..widgets import get_widgets, wait_for_widgets LVGLText = lvgl_ns.class_("LVGLText", text.Text) -CONFIG_SCHEMA = text.TEXT_SCHEMA.extend(LVGL_SCHEMA).extend( +CONFIG_SCHEMA = text.TEXT_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(LVGLText), cv.Required(CONF_WIDGET): cv.use_id(LvText), @@ -29,7 +29,6 @@ CONFIG_SCHEMA = text.TEXT_SCHEMA.extend(LVGL_SCHEMA).extend( async def to_code(config): textvar = await new_text(config) - paren = await cg.get_variable(config[CONF_LVGL_ID]) widget = await get_widgets(config, CONF_WIDGET) widget = widget[0] await wait_for_widgets() @@ -39,10 +38,10 @@ async def to_code(config): control.add(textvar.publish_state(widget.get_value())) async with LambdaContext(EVENT_ARG) as lamb: lv_add(textvar.publish_state(widget.get_value())) - async with LvContext(paren): + async with LvContext(): lv_add(textvar.set_control_lambda(await control.get_lambda())) lv_add( - paren.add_event_cb( + lvgl_static.add_event_cb( widget.obj, await lamb.get_lambda(), LV_EVENT.VALUE_CHANGED, diff --git a/esphome/components/lvgl/text_sensor/__init__.py b/esphome/components/lvgl/text_sensor/__init__.py index ae39eec291..4728fd137a 100644 --- a/esphome/components/lvgl/text_sensor/__init__.py +++ b/esphome/components/lvgl/text_sensor/__init__.py @@ -1,4 +1,3 @@ -import esphome.codegen as cg from esphome.components.text_sensor import ( TextSensor, new_text_sensor, @@ -6,34 +5,35 @@ from esphome.components.text_sensor import ( ) import esphome.config_validation as cv -from ..defines import CONF_LVGL_ID, CONF_WIDGET -from ..lvcode import API_EVENT, EVENT_ARG, UPDATE_EVENT, LambdaContext, LvContext -from ..schemas import LVGL_SCHEMA +from ..defines import CONF_WIDGET +from ..lvcode import ( + API_EVENT, + EVENT_ARG, + UPDATE_EVENT, + LambdaContext, + LvContext, + lvgl_static, +) from ..types import LV_EVENT, LvText from ..widgets import get_widgets, wait_for_widgets -CONFIG_SCHEMA = ( - text_sensor_schema(TextSensor) - .extend(LVGL_SCHEMA) - .extend( - { - cv.Required(CONF_WIDGET): cv.use_id(LvText), - } - ) +CONFIG_SCHEMA = text_sensor_schema(TextSensor).extend( + { + cv.Required(CONF_WIDGET): cv.use_id(LvText), + } ) async def to_code(config): sensor = await new_text_sensor(config) - paren = await cg.get_variable(config[CONF_LVGL_ID]) widget = await get_widgets(config, CONF_WIDGET) widget = widget[0] await wait_for_widgets() async with LambdaContext(EVENT_ARG) as pressed_ctx: pressed_ctx.add(sensor.publish_state(widget.get_value())) - async with LvContext(paren) as ctx: + async with LvContext() as ctx: ctx.add( - paren.add_event_cb( + lvgl_static.add_event_cb( widget.obj, await pressed_ctx.get_lambda(), LV_EVENT.VALUE_CHANGED, diff --git a/esphome/components/lvgl/touchscreens.py b/esphome/components/lvgl/touchscreens.py index 4d430a428e..f2dd013f6d 100644 --- a/esphome/components/lvgl/touchscreens.py +++ b/esphome/components/lvgl/touchscreens.py @@ -33,13 +33,12 @@ def touchscreen_schema(config): return [TOUCHSCREENS_CONFIG(config)] -async def touchscreens_to_code(var, config): +async def touchscreens_to_code(lv_component, config): for tconf in config[CONF_TOUCHSCREENS]: lvgl_components_required.add(CONF_TOUCHSCREEN) touchscreen = await cg.get_variable(tconf[CONF_TOUCHSCREEN_ID]) lpt = tconf[CONF_LONG_PRESS_TIME].total_milliseconds lprt = tconf[CONF_LONG_PRESS_REPEAT_TIME].total_milliseconds - listener = cg.new_Pvariable(tconf[CONF_ID], lpt, lprt) - await cg.register_parented(listener, var) + listener = cg.new_Pvariable(tconf[CONF_ID], lpt, lprt, lv_component) lv.indev_drv_register(listener.get_drv()) cg.add(touchscreen.register_listener(listener)) diff --git a/esphome/components/lvgl/trigger.py b/esphome/components/lvgl/trigger.py index eb6e483203..fb856df04e 100644 --- a/esphome/components/lvgl/trigger.py +++ b/esphome/components/lvgl/trigger.py @@ -20,17 +20,16 @@ from .lvcode import ( lv, lv_add, lv_event_t_ptr, + lvgl_static, ) from .types import LV_EVENT from .widgets import widget_map -async def generate_triggers(lv_component): +async def generate_triggers(): """ Generate LVGL triggers for all defined widgets Must be done after all widgets completed - :param lv_component: The parent component - :return: """ for w in widget_map.values(): @@ -43,11 +42,10 @@ async def generate_triggers(lv_component): conf = conf[0] w.add_flag("LV_OBJ_FLAG_CLICKABLE") event = literal("LV_EVENT_" + LV_EVENT_MAP[event[3:].upper()]) - await add_trigger(conf, lv_component, w, event) + await add_trigger(conf, w, event) for conf in w.config.get(CONF_ON_VALUE, ()): await add_trigger( conf, - lv_component, w, LV_EVENT.VALUE_CHANGED, API_EVENT, @@ -63,7 +61,7 @@ async def generate_triggers(lv_component): lv.obj_align_to(w.obj, target, align, x, y) -async def add_trigger(conf, lv_component, w, *events): +async def add_trigger(conf, w, *events): tid = conf[CONF_TRIGGER_ID] trigger = cg.new_Pvariable(tid) args = w.get_args() + [(lv_event_t_ptr, "event")] @@ -72,4 +70,4 @@ async def add_trigger(conf, lv_component, w, *events): async with LambdaContext(EVENT_ARG, where=tid) as context: with LvConditional(w.is_selected()): lv_add(trigger.trigger(*value, literal("event"))) - lv_add(lv_component.add_event_cb(w.obj, await context.get_lambda(), *events)) + lv_add(lvgl_static.add_event_cb(w.obj, await context.get_lambda(), *events)) diff --git a/esphome/components/lvgl/widgets/page.py b/esphome/components/lvgl/widgets/page.py index 0e84ab6791..a754a9cb9a 100644 --- a/esphome/components/lvgl/widgets/page.py +++ b/esphome/components/lvgl/widgets/page.py @@ -20,6 +20,7 @@ from ..lvcode import ( add_line_marks, lv_add, lvgl_comp, + lvgl_static, ) from ..schemas import LVGL_SCHEMA from ..types import LvglAction, lv_page_t @@ -139,7 +140,7 @@ async def add_pages(lv_component, config): await add_widgets(page, pconf) -async def generate_page_triggers(lv_component, config): +async def generate_page_triggers(config): for pconf in config.get(CONF_PAGES, ()): page = (await get_widgets(pconf))[0] for ev in (CONF_ON_LOAD, CONF_ON_UNLOAD): @@ -149,7 +150,7 @@ async def generate_page_triggers(lv_component, config): async with LambdaContext(EVENT_ARG, where=id) as context: lv_add(trigger.trigger()) lv_add( - lv_component.add_event_cb( + lvgl_static.add_event_cb( page.obj, await context.get_lambda(), literal(f"LV_EVENT_SCREEN_{ev[3:].upper()}_START"), diff --git a/tests/components/lvgl/test.host.yaml b/tests/components/lvgl/test.host.yaml index 3a490bbe15..34918cb113 100644 --- a/tests/components/lvgl/test.host.yaml +++ b/tests/components/lvgl/test.host.yaml @@ -1,5 +1,12 @@ display: - platform: sdl + id: sdl0 + auto_clear_enabled: false + dimensions: + width: 480 + height: 320 + - platform: sdl + id: sdl1 auto_clear_enabled: false dimensions: width: 480 @@ -7,5 +14,30 @@ display: touchscreen: - platform: sdl + display: sdl0 + sdl_id: sdl0 lvgl: + - id: lvgl_0 + displays: sdl0 + - id: lvgl_1 + displays: sdl1 + on_idle: + timeout: 8s + then: + if: + condition: + lvgl.is_idle: + lvgl_id: lvgl_1 + timeout: 5s + then: + logger.log: Lvgl2 is idle + widgets: + - button: + align: center + widgets: + - label: + text: Click ME + on_click: + logger.log: Clicked + From c0658ffe2c31edcba1091dd05c922c36b3292c1c Mon Sep 17 00:00:00 2001 From: Ramil Valitov Date: Fri, 8 Nov 2024 01:10:58 +0300 Subject: [PATCH 22/46] [fix] deprecated legacy driver tsens (#7658) Co-authored-by: luar123 <49960470+luar123@users.noreply.github.com> --- .../internal_temperature.cpp | 51 ++++++++++++++++++- .../internal_temperature.h | 1 + 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/esphome/components/internal_temperature/internal_temperature.cpp b/esphome/components/internal_temperature/internal_temperature.cpp index 9ef5cbecd5..afa5583e59 100644 --- a/esphome/components/internal_temperature/internal_temperature.cpp +++ b/esphome/components/internal_temperature/internal_temperature.cpp @@ -8,8 +8,13 @@ extern "C" { uint8_t temprature_sens_read(); } #elif defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || \ - defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) + defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32H2) || \ + defined(USE_ESP32_VARIANT_ESP32C2) +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0) #include "driver/temp_sensor.h" +#else +#include "driver/temperature_sensor.h" +#endif // ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0) #endif // USE_ESP32_VARIANT #endif // USE_ESP32 #ifdef USE_RP2040 @@ -25,6 +30,13 @@ namespace esphome { namespace internal_temperature { static const char *const TAG = "internal_temperature"; +#ifdef USE_ESP32 +#if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)) && \ + (defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32S2) || \ + defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32C2)) +static temperature_sensor_handle_t tsensNew = NULL; +#endif // ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) && USE_ESP32_VARIANT +#endif // USE_ESP32 void InternalTemperatureSensor::update() { float temperature = NAN; @@ -36,7 +48,9 @@ void InternalTemperatureSensor::update() { temperature = (raw - 32) / 1.8f; success = (raw != 128); #elif defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || \ - defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) + defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32H2) || \ + defined(USE_ESP32_VARIANT_ESP32C2) +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0) temp_sensor_config_t tsens = TSENS_CONFIG_DEFAULT(); temp_sensor_set_config(tsens); temp_sensor_start(); @@ -47,6 +61,13 @@ void InternalTemperatureSensor::update() { esp_err_t result = temp_sensor_read_celsius(&temperature); temp_sensor_stop(); success = (result == ESP_OK); +#else + esp_err_t result = temperature_sensor_get_celsius(tsensNew, &temperature); + success = (result == ESP_OK); + if (!success) { + ESP_LOGE(TAG, "Failed to get temperature: %d", result); + } +#endif // ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0) #endif // USE_ESP32_VARIANT #endif // USE_ESP32 #ifdef USE_RP2040 @@ -75,6 +96,32 @@ void InternalTemperatureSensor::update() { } } +void InternalTemperatureSensor::setup() { +#ifdef USE_ESP32 +#if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)) && \ + (defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32S2) || \ + defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32C2)) + ESP_LOGCONFIG(TAG, "Setting up temperature sensor..."); + + temperature_sensor_config_t tsens_config = TEMPERATURE_SENSOR_CONFIG_DEFAULT(-10, 80); + + esp_err_t result = temperature_sensor_install(&tsens_config, &tsensNew); + if (result != ESP_OK) { + ESP_LOGE(TAG, "Failed to install temperature sensor: %d", result); + this->mark_failed(); + return; + } + + result = temperature_sensor_enable(tsensNew); + if (result != ESP_OK) { + ESP_LOGE(TAG, "Failed to enable temperature sensor: %d", result); + this->mark_failed(); + return; + } +#endif // ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) && USE_ESP32_VARIANT +#endif // USE_ESP32 +} + void InternalTemperatureSensor::dump_config() { LOG_SENSOR("", "Internal Temperature Sensor", this); } } // namespace internal_temperature diff --git a/esphome/components/internal_temperature/internal_temperature.h b/esphome/components/internal_temperature/internal_temperature.h index 0e46a69769..78e3bcef7d 100644 --- a/esphome/components/internal_temperature/internal_temperature.h +++ b/esphome/components/internal_temperature/internal_temperature.h @@ -8,6 +8,7 @@ namespace internal_temperature { class InternalTemperatureSensor : public sensor::Sensor, public PollingComponent { public: + void setup() override; void dump_config() override; void update() override; From d189cc1fbe406dc971bfcbb33e302bedd806cc05 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Fri, 8 Nov 2024 10:39:01 +1100 Subject: [PATCH 23/46] [lvgl] Fix id config for the lvgl component (Bugfix) (#7731) Co-authored-by: clydeps --- esphome/components/lvgl/automation.py | 52 +++++++++++++-------------- 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/esphome/components/lvgl/automation.py b/esphome/components/lvgl/automation.py index 58e3dd808b..c26ae54892 100644 --- a/esphome/components/lvgl/automation.py +++ b/esphome/components/lvgl/automation.py @@ -1,5 +1,4 @@ -from collections.abc import Awaitable -from typing import Callable +from typing import Any, Callable from esphome import automation import esphome.codegen as cg @@ -23,7 +22,6 @@ from .lvcode import ( UPDATE_EVENT, LambdaContext, LocalVariable, - LvglComponent, ReturnStatement, add_line_marks, lv, @@ -58,7 +56,7 @@ focused_widgets = set() async def action_to_code( widgets: list[Widget], - action: Callable[[Widget], Awaitable[None]], + action: Callable[[Widget], Any], action_id, template_arg, args, @@ -159,14 +157,12 @@ async def obj_invalidate_to_code(config, action_id, template_arg, args): @automation.register_action( "lvgl.update", LvglAction, - DISP_BG_SCHEMA.extend( - { - cv.GenerateID(): cv.use_id(LvglComponent), - } - ).add_extra(cv.has_at_least_one_key(CONF_DISP_BG_COLOR, CONF_DISP_BG_IMAGE)), + DISP_BG_SCHEMA.extend(LVGL_SCHEMA).add_extra( + cv.has_at_least_one_key(CONF_DISP_BG_COLOR, CONF_DISP_BG_IMAGE) + ), ) async def lvgl_update_to_code(config, action_id, template_arg, args): - widgets = await get_widgets(config) + widgets = await get_widgets(config, CONF_LVGL_ID) w = widgets[0] disp = literal(f"{w.obj}->get_disp()") async with LambdaContext(LVGL_COMP_ARG, where=action_id) as context: @@ -179,32 +175,33 @@ async def lvgl_update_to_code(config, action_id, template_arg, args): @automation.register_action( "lvgl.pause", LvglAction, - { - cv.GenerateID(): cv.use_id(LvglComponent), - cv.Optional(CONF_SHOW_SNOW, default=False): lv_bool, - }, + LVGL_SCHEMA.extend( + { + cv.Optional(CONF_SHOW_SNOW, default=False): lv_bool, + } + ), ) async def pause_action_to_code(config, action_id, template_arg, args): + lv_comp = await cg.get_variable(config[CONF_LVGL_ID]) async with LambdaContext(LVGL_COMP_ARG) as context: add_line_marks(where=action_id) lv_add(lvgl_comp.set_paused(True, config[CONF_SHOW_SNOW])) var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) - await cg.register_parented(var, config[CONF_ID]) + await cg.register_parented(var, lv_comp) return var @automation.register_action( "lvgl.resume", LvglAction, - { - cv.GenerateID(): cv.use_id(LvglComponent), - }, + LVGL_SCHEMA, ) async def resume_action_to_code(config, action_id, template_arg, args): + lv_comp = await cg.get_variable(config[CONF_LVGL_ID]) async with LambdaContext(LVGL_COMP_ARG, where=action_id) as context: lv_add(lvgl_comp.set_paused(False, False)) var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) - await cg.register_parented(var, config[CONF_ID]) + await cg.register_parented(var, lv_comp) return var @@ -263,14 +260,15 @@ def focused_id(value): ObjUpdateAction, cv.Any( cv.maybe_simple_value( - { - cv.Optional(CONF_GROUP): cv.use_id(lv_group_t), - cv.Required(CONF_ACTION): cv.one_of( - "MARK", "RESTORE", "NEXT", "PREVIOUS", upper=True - ), - cv.GenerateID(CONF_LVGL_ID): cv.use_id(LvglComponent), - cv.Optional(CONF_FREEZE, default=False): cv.boolean, - }, + LVGL_SCHEMA.extend( + { + cv.Optional(CONF_GROUP): cv.use_id(lv_group_t), + cv.Required(CONF_ACTION): cv.one_of( + "MARK", "RESTORE", "NEXT", "PREVIOUS", upper=True + ), + cv.Optional(CONF_FREEZE, default=False): cv.boolean, + } + ), key=CONF_ACTION, ), cv.maybe_simple_value( From 3f123d7542f850e7abe30d6196ec99356a9e343c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 Nov 2024 12:42:36 +1300 Subject: [PATCH 24/46] Bump pypa/gh-action-pypi-publish from 1.11.0 to 1.12.2 (#7730) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 82d7ae5ee8..096b00f0f1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -65,7 +65,7 @@ jobs: pip3 install build python3 -m build - name: Publish - uses: pypa/gh-action-pypi-publish@v1.11.0 + uses: pypa/gh-action-pypi-publish@v1.12.2 deploy-docker: name: Build ESPHome ${{ matrix.platform }} From 2f77d316905f688fef7d899dfedc004ba6a73a49 Mon Sep 17 00:00:00 2001 From: David Woodhouse Date: Fri, 8 Nov 2024 03:38:13 +0000 Subject: [PATCH 25/46] OTA: Fix IPv6 and multiple address support (#7414) --- esphome/__main__.py | 4 +-- esphome/espota2.py | 69 +++++++++++++++++++------------------- esphome/helpers.py | 82 ++++++++++++++++++++++++++++++++++++--------- esphome/mqtt.py | 11 ++++-- esphome/zeroconf.py | 8 ++--- 5 files changed, 117 insertions(+), 57 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index cf2741dbdb..85ab3cc00c 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -38,7 +38,7 @@ from esphome.const import ( SECRETS_FILES, ) from esphome.core import CORE, EsphomeError, coroutine -from esphome.helpers import indent, is_ip_address, get_bool_env +from esphome.helpers import get_bool_env, indent, is_ip_address from esphome.log import Fore, color, setup_log from esphome.util import ( get_serial_ports, @@ -378,7 +378,7 @@ def show_logs(config, args, port): port = mqtt.get_esphome_device_ip( config, args.username, args.password, args.client_id - ) + )[0] from esphome.components.api.client import run_logs diff --git a/esphome/espota2.py b/esphome/espota2.py index 580536153a..94b845b246 100644 --- a/esphome/espota2.py +++ b/esphome/espota2.py @@ -10,7 +10,7 @@ import sys import time from esphome.core import EsphomeError -from esphome.helpers import is_ip_address, resolve_ip_address +from esphome.helpers import resolve_ip_address RESPONSE_OK = 0x00 RESPONSE_REQUEST_AUTH = 0x01 @@ -311,44 +311,45 @@ def perform_ota( def run_ota_impl_(remote_host, remote_port, password, filename): - if is_ip_address(remote_host): - _LOGGER.info("Connecting to %s", remote_host) - ip = remote_host - else: - _LOGGER.info("Resolving IP address of %s", remote_host) - try: - ip = resolve_ip_address(remote_host) - except EsphomeError as err: - _LOGGER.error( - "Error resolving IP address of %s. Is it connected to WiFi?", - remote_host, - ) - _LOGGER.error( - "(If this error persists, please set a static IP address: " - "https://esphome.io/components/wifi.html#manual-ips)" - ) - raise OTAError(err) from err - _LOGGER.info(" -> %s", ip) - - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.settimeout(10.0) try: - sock.connect((ip, remote_port)) - except OSError as err: - sock.close() - _LOGGER.error("Connecting to %s:%s failed: %s", remote_host, remote_port, err) - return 1 + res = resolve_ip_address(remote_host, remote_port) + except EsphomeError as err: + _LOGGER.error( + "Error resolving IP address of %s. Is it connected to WiFi?", + remote_host, + ) + _LOGGER.error( + "(If this error persists, please set a static IP address: " + "https://esphome.io/components/wifi.html#manual-ips)" + ) + raise OTAError(err) from err - with open(filename, "rb") as file_handle: + for r in res: + af, socktype, _, _, sa = r + _LOGGER.info("Connecting to %s port %s...", sa[0], sa[1]) + sock = socket.socket(af, socktype) + sock.settimeout(10.0) try: - perform_ota(sock, password, file_handle, filename) - except OTAError as err: - _LOGGER.error(str(err)) - return 1 - finally: + sock.connect(sa) + except OSError as err: sock.close() + _LOGGER.error("Connecting to %s port %s failed: %s", sa[0], sa[1], err) + continue - return 0 + _LOGGER.info("Connected to %s", sa[0]) + with open(filename, "rb") as file_handle: + try: + perform_ota(sock, password, file_handle, filename) + except OTAError as err: + _LOGGER.error(str(err)) + return 1 + finally: + sock.close() + + return 0 + + _LOGGER.error("Connection failed.") + return 1 def run_ota(remote_host, remote_port, password, filename): diff --git a/esphome/helpers.py b/esphome/helpers.py index 2a7e5cd9b6..8aae43c2bb 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -1,5 +1,6 @@ import codecs from contextlib import suppress +import ipaddress import logging import os from pathlib import Path @@ -91,12 +92,8 @@ def mkdir_p(path): def is_ip_address(host): - parts = host.split(".") - if len(parts) != 4: - return False try: - for p in parts: - int(p) + ipaddress.ip_address(host) return True except ValueError: return False @@ -127,25 +124,80 @@ def _resolve_with_zeroconf(host): return info -def resolve_ip_address(host): +def addr_preference_(res): + # Trivial alternative to RFC6724 sorting. Put sane IPv6 first, then + # Legacy IP, then IPv6 link-local addresses without an actual link. + sa = res[4] + ip = ipaddress.ip_address(sa[0]) + if ip.version == 4: + return 2 + if ip.is_link_local and sa[3] == 0: + return 3 + return 1 + + +def resolve_ip_address(host, port): import socket from esphome.core import EsphomeError + # There are five cases here. The host argument could be one of: + # • a *list* of IP addresses discovered by MQTT, + # • a single IP address specified by the user, + # • a .local hostname to be resolved by mDNS, + # • a normal hostname to be resolved in DNS, or + # • A URL from which we should extract the hostname. + # + # In each of the first three cases, we end up with IP addresses in + # string form which need to be converted to a 5-tuple to be used + # for the socket connection attempt. The easiest way to construct + # those is to pass the IP address string to getaddrinfo(). Which, + # coincidentally, is how we do hostname lookups in the other cases + # too. So first build a list which contains either IP addresses or + # a single hostname, then call getaddrinfo() on each element of + # that list. + errs = [] + if isinstance(host, list): + addr_list = host + elif is_ip_address(host): + addr_list = [host] + else: + url = urlparse(host) + if url.scheme != "": + host = url.hostname - if host.endswith(".local"): + addr_list = [] + if host.endswith(".local"): + try: + _LOGGER.info("Resolving IP address of %s in mDNS", host) + addr_list = _resolve_with_zeroconf(host) + except EsphomeError as err: + errs.append(str(err)) + + # If not mDNS, or if mDNS failed, use normal DNS + if not addr_list: + addr_list = [host] + + # Now we have a list containing either IP addresses or a hostname + res = [] + for addr in addr_list: + if not is_ip_address(addr): + _LOGGER.info("Resolving IP address of %s", host) try: - return _resolve_with_zeroconf(host) - except EsphomeError as err: + r = socket.getaddrinfo(addr, port, proto=socket.IPPROTO_TCP) + except OSError as err: errs.append(str(err)) + raise EsphomeError( + f"Error resolving IP address: {', '.join(errs)}" + ) from err - try: - host_url = host if (urlparse(host).scheme != "") else "http://" + host - return socket.gethostbyname(urlparse(host_url).hostname) - except OSError as err: - errs.append(str(err)) - raise EsphomeError(f"Error resolving IP address: {', '.join(errs)}") from err + res = res + r + + # Zeroconf tends to give us link-local IPv6 addresses without specifying + # the link. Put those last in the list to be attempted. + res.sort(key=addr_preference_) + return res def get_bool_env(var, default=False): diff --git a/esphome/mqtt.py b/esphome/mqtt.py index d55fb0202d..2f90c49025 100644 --- a/esphome/mqtt.py +++ b/esphome/mqtt.py @@ -175,8 +175,15 @@ def get_esphome_device_ip( _LOGGER.Warn("Wrong device answer") return - if "ip" in data: - dev_ip = data["ip"] + dev_ip = [] + key = "ip" + n = 0 + while key in data: + dev_ip.append(data[key]) + n = n + 1 + key = "ip" + str(n) + + if dev_ip: client.disconnect() def on_connect(client, userdata, flags, return_code): diff --git a/esphome/zeroconf.py b/esphome/zeroconf.py index b3ee64e259..76049fa776 100644 --- a/esphome/zeroconf.py +++ b/esphome/zeroconf.py @@ -182,8 +182,8 @@ class EsphomeZeroconf(Zeroconf): if ( info.load_from_cache(self) or (timeout and info.request(self, timeout * 1000)) - ) and (addresses := info.ip_addresses_by_version(IPVersion.V4Only)): - return str(addresses[0]) + ) and (addresses := info.parsed_scoped_addresses(IPVersion.All)): + return addresses return None @@ -194,6 +194,6 @@ class AsyncEsphomeZeroconf(AsyncZeroconf): if ( info.load_from_cache(self.zeroconf) or (timeout and await info.async_request(self.zeroconf, timeout * 1000)) - ) and (addresses := info.ip_addresses_by_version(IPVersion.V4Only)): - return str(addresses[0]) + ) and (addresses := info.parsed_scoped_addresses(IPVersion.All)): + return addresses return None From 2ec17eed588f2e0ca2392337034981bdb83fcdcf Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Sun, 27 Oct 2024 13:17:09 +1100 Subject: [PATCH 26/46] [rpi_dpi_rgb] Fix get_width and height (Bugfix) (#7675) Co-authored-by: clydeps --- .../components/rpi_dpi_rgb/rpi_dpi_rgb.cpp | 20 +++++++++++++++++++ esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.h | 5 +++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.cpp b/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.cpp index 655b469b91..ba09171649 100644 --- a/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.cpp +++ b/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.cpp @@ -84,6 +84,26 @@ void RpiDpiRgb::draw_pixels_at(int x_start, int y_start, int w, int h, const uin ESP_LOGE(TAG, "lcd_lcd_panel_draw_bitmap failed: %s", esp_err_to_name(err)); } +int RpiDpiRgb::get_width() { + switch (this->rotation_) { + case display::DISPLAY_ROTATION_90_DEGREES: + case display::DISPLAY_ROTATION_270_DEGREES: + return this->get_height_internal(); + default: + return this->get_width_internal(); + } +} + +int RpiDpiRgb::get_height() { + switch (this->rotation_) { + case display::DISPLAY_ROTATION_90_DEGREES: + case display::DISPLAY_ROTATION_270_DEGREES: + return this->get_width_internal(); + default: + return this->get_height_internal(); + } +} + void RpiDpiRgb::draw_pixel_at(int x, int y, Color color) { if (!this->get_clipping().inside(x, y)) return; // NOLINT diff --git a/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.h b/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.h index 10f77a2624..7525040cd1 100644 --- a/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.h +++ b/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.h @@ -24,6 +24,7 @@ class RpiDpiRgb : public display::Display { void update() override { this->do_update_(); } void setup() override; void loop() override; + float get_setup_priority() const override { return setup_priority::HARDWARE; } void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order, display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override; void draw_pixel_at(int x, int y, Color color) override; @@ -44,8 +45,8 @@ class RpiDpiRgb : public display::Display { this->width_ = width; this->height_ = height; } - int get_width() override { return this->width_; } - int get_height() override { return this->height_; } + int get_width() override; + int get_height() override; void set_hsync_back_porch(uint16_t hsync_back_porch) { this->hsync_back_porch_ = hsync_back_porch; } void set_hsync_front_porch(uint16_t hsync_front_porch) { this->hsync_front_porch_ = hsync_front_porch; } void set_hsync_pulse_width(uint16_t hsync_pulse_width) { this->hsync_pulse_width_ = hsync_pulse_width; } From e85cbf26f881e91c4f02f4612d1b92d556ff56af Mon Sep 17 00:00:00 2001 From: Bonne Eggleston Date: Mon, 28 Oct 2024 20:52:39 -0700 Subject: [PATCH 27/46] Fixes modbus timing error (#7674) --- esphome/components/modbus/modbus.cpp | 36 ++++++++++++++++++---------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/esphome/components/modbus/modbus.cpp b/esphome/components/modbus/modbus.cpp index f8dd4c18b9..8544b50261 100644 --- a/esphome/components/modbus/modbus.cpp +++ b/esphome/components/modbus/modbus.cpp @@ -15,23 +15,33 @@ void Modbus::setup() { void Modbus::loop() { const uint32_t now = millis(); - if (now - this->last_modbus_byte_ > 50) { - this->rx_buffer_.clear(); - this->last_modbus_byte_ = now; - } - // stop blocking new send commands after send_wait_time_ ms regardless if a response has been received since then - if (now - this->last_send_ > send_wait_time_) { - waiting_for_response = 0; - } - while (this->available()) { uint8_t byte; this->read_byte(&byte); if (this->parse_modbus_byte_(byte)) { this->last_modbus_byte_ = now; } else { + size_t at = this->rx_buffer_.size(); + if (at > 0) { + ESP_LOGV(TAG, "Clearing buffer of %d bytes - parse failed", at); + this->rx_buffer_.clear(); + } + } + } + + if (now - this->last_modbus_byte_ > 50) { + size_t at = this->rx_buffer_.size(); + if (at > 0) { + ESP_LOGV(TAG, "Clearing buffer of %d bytes - timeout", at); this->rx_buffer_.clear(); } + + // stop blocking new send commands after sent_wait_time_ ms after response received + if (now - this->last_send_ > send_wait_time_) { + if (waiting_for_response > 0) + ESP_LOGV(TAG, "Stop waiting for response from %d", waiting_for_response); + waiting_for_response = 0; + } } } @@ -39,7 +49,7 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) { size_t at = this->rx_buffer_.size(); this->rx_buffer_.push_back(byte); const uint8_t *raw = &this->rx_buffer_[0]; - ESP_LOGV(TAG, "Modbus received Byte %d (0X%x)", byte, byte); + ESP_LOGVV(TAG, "Modbus received Byte %d (0X%x)", byte, byte); // Byte 0: modbus address (match all) if (at == 0) return true; @@ -144,8 +154,10 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) { ESP_LOGW(TAG, "Got Modbus frame from unknown address 0x%02X! ", address); } - // return false to reset buffer - return false; + // reset buffer + ESP_LOGV(TAG, "Clearing buffer of %d bytes - parse succeeded", at); + this->rx_buffer_.clear(); + return true; } void Modbus::dump_config() { From 3a25eaca3f90345dd45ffc7e03734f8a6c0c3c45 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 5 Nov 2024 11:32:18 +1100 Subject: [PATCH 28/46] [lvgl] Ensure images are configured before using them. (Bugfix) (#7721) --- esphome/components/lvgl/widgets/animimg.py | 7 ++++--- esphome/components/lvgl/widgets/img.py | 2 ++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/esphome/components/lvgl/widgets/animimg.py b/esphome/components/lvgl/widgets/animimg.py index 3b20008c3d..8adea72ad3 100644 --- a/esphome/components/lvgl/widgets/animimg.py +++ b/esphome/components/lvgl/widgets/animimg.py @@ -60,9 +60,10 @@ class AnimimgType(WidgetType): lvgl_components_required.add(CONF_IMAGE) lvgl_components_required.add(CONF_ANIMIMG) if CONF_SRC in config: - for x in config[CONF_SRC]: - await cg.get_variable(x) - srcs = [await lv_image.process(x) for x in config[CONF_SRC]] + srcs = [ + await lv_image.process(await cg.get_variable(x)) + for x in config[CONF_SRC] + ] src_id = cg.static_const_array(config[CONF_SRC_LIST_ID], srcs) count = len(config[CONF_SRC]) lv.animimg_set_src(w.obj, src_id, count) diff --git a/esphome/components/lvgl/widgets/img.py b/esphome/components/lvgl/widgets/img.py index 59b2c97c63..931d0c0b5b 100644 --- a/esphome/components/lvgl/widgets/img.py +++ b/esphome/components/lvgl/widgets/img.py @@ -1,3 +1,4 @@ +import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import CONF_ANGLE, CONF_MODE @@ -64,6 +65,7 @@ class ImgType(WidgetType): async def to_code(self, w: Widget, config): if src := config.get(CONF_SRC): + src = await cg.get_variable(src) lv.img_set_src(w.obj, await lv_image.process(src)) if (cf_angle := config.get(CONF_ANGLE)) is not None: pivot_x = config[CONF_PIVOT_X] From 551ea378824bc18df6523fa8ce810833f31b205e Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 8 Nov 2024 17:02:31 +1300 Subject: [PATCH 29/46] Bump version to 2024.10.3 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 032a4c79a0..c9decd4fd2 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2024.10.2" +__version__ = "2024.10.3" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 335faf858baabee77a8104379e5fcba8da5ac58b Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 11 Nov 2024 08:55:19 +1300 Subject: [PATCH 30/46] Fix dashboard ip resolving (#7747) --- esphome/dashboard/status/mdns.py | 7 +++---- esphome/dashboard/web_server.py | 4 ++-- esphome/zeroconf.py | 6 ++++-- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/esphome/dashboard/status/mdns.py b/esphome/dashboard/status/mdns.py index bd212bc563..9f6399ca8b 100644 --- a/esphome/dashboard/status/mdns.py +++ b/esphome/dashboard/status/mdns.py @@ -26,7 +26,7 @@ class MDNSStatus: self.host_mdns_state: dict[str, bool | None] = {} self._loop = asyncio.get_running_loop() - async def async_resolve_host(self, host_name: str) -> str | None: + async def async_resolve_host(self, host_name: str) -> list[str] | None: """Resolve a host name to an address in a thread-safe manner.""" if aiozc := self.aiozc: return await aiozc.async_resolve_host(host_name) @@ -50,13 +50,12 @@ class MDNSStatus: poll_names.setdefault(entry.name, set()).add(entry) elif (online := host_mdns_state.get(entry.name, SENTINEL)) != SENTINEL: entries.async_set_state(entry, bool_to_entry_state(online)) - if poll_names and self.aiozc: results = await asyncio.gather( *(self.aiozc.async_resolve_host(name) for name in poll_names) ) - for name, address in zip(poll_names, results): - result = bool(address) + for name, address_list in zip(poll_names, results): + result = bool(address_list) host_mdns_state[name] = result for entry in poll_names[name]: entries.async_set_state(entry, bool_to_entry_state(result)) diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py index 9aeece9aab..07f7f019f8 100644 --- a/esphome/dashboard/web_server.py +++ b/esphome/dashboard/web_server.py @@ -320,12 +320,12 @@ class EsphomePortCommandWebSocket(EsphomeCommandWebSocket): and "api" in entry.loaded_integrations ): if (mdns := dashboard.mdns_status) and ( - address := await mdns.async_resolve_host(entry.name) + address_list := await mdns.async_resolve_host(entry.name) ): # Use the IP address if available but only # if the API is loaded and the device is online # since MQTT logging will not work otherwise - port = address + port = address_list[0] elif ( entry.address and ( diff --git a/esphome/zeroconf.py b/esphome/zeroconf.py index 76049fa776..5a92a4ed7c 100644 --- a/esphome/zeroconf.py +++ b/esphome/zeroconf.py @@ -176,7 +176,7 @@ def _make_host_resolver(host: str) -> HostResolver: class EsphomeZeroconf(Zeroconf): - def resolve_host(self, host: str, timeout: float = 3.0) -> str | None: + def resolve_host(self, host: str, timeout: float = 3.0) -> list[str] | None: """Resolve a host name to an IP address.""" info = _make_host_resolver(host) if ( @@ -188,7 +188,9 @@ class EsphomeZeroconf(Zeroconf): class AsyncEsphomeZeroconf(AsyncZeroconf): - async def async_resolve_host(self, host: str, timeout: float = 3.0) -> str | None: + async def async_resolve_host( + self, host: str, timeout: float = 3.0 + ) -> list[str] | None: """Resolve a host name to an IP address.""" info = _make_host_resolver(host) if ( From 7c00c5db7020f09a6e8b3ffc3cb637fa2aa2fdc6 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 11 Nov 2024 09:44:02 +1300 Subject: [PATCH 31/46] [docker] Bump curl, iputils-ping and libssl-dev (#7748) --- docker/Dockerfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 44ee879a12..ed6ce083a8 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -32,9 +32,9 @@ RUN \ python3-setuptools=66.1.1-1 \ python3-venv=3.11.2-1+b1 \ python3-wheel=0.38.4-2 \ - iputils-ping=3:20221126-1 \ + iputils-ping=3:20221126-1+deb12u1 \ git=1:2.39.5-0+deb12u1 \ - curl=7.88.1-10+deb12u7 \ + curl=7.88.1-10+deb12u8 \ openssh-client=1:9.2p1-2+deb12u3 \ python3-cffi=1.15.1-5 \ libcairo2=1.16.0-7 \ @@ -97,7 +97,7 @@ BUILD_DEPS=" zlib1g-dev=1:1.2.13.dfsg-1 libjpeg-dev=1:2.1.5-2 libfreetype-dev=2.12.1+dfsg-5+deb12u3 - libssl-dev=3.0.14-1~deb12u2 + libssl-dev=3.0.15-1~deb12u1 libffi-dev=3.4.4-1 libopenjp2-7=2.5.0-2 libtiff6=4.5.0-6+deb12u1 From c35240ca3207f7efef3cb0dcd146c32c5e33c6c7 Mon Sep 17 00:00:00 2001 From: Kyle Cascade Date: Sun, 10 Nov 2024 17:13:43 -0800 Subject: [PATCH 32/46] Remove the choice for MQTT logging if it is disabled (#7723) --- esphome/__main__.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index 85ab3cc00c..86d529e1bf 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -20,6 +20,8 @@ from esphome.const import ( CONF_DEASSERT_RTS_DTR, CONF_DISABLED, CONF_ESPHOME, + CONF_LEVEL, + CONF_LOG_TOPIC, CONF_LOGGER, CONF_MDNS, CONF_MQTT, @@ -30,6 +32,7 @@ from esphome.const import ( CONF_PLATFORMIO_OPTIONS, CONF_PORT, CONF_SUBSTITUTIONS, + CONF_TOPIC, PLATFORM_BK72XX, PLATFORM_ESP32, PLATFORM_ESP8266, @@ -95,8 +98,12 @@ def choose_upload_log_host( options.append((f"Over The Air ({CORE.address})", CORE.address)) if default == "OTA": return CORE.address - if show_mqtt and CONF_MQTT in CORE.config: - options.append((f"MQTT ({CORE.config['mqtt'][CONF_BROKER]})", "MQTT")) + if ( + show_mqtt + and (mqtt_config := CORE.config.get(CONF_MQTT)) + and mqtt_logging_enabled(mqtt_config) + ): + options.append((f"MQTT ({mqtt_config[CONF_BROKER]})", "MQTT")) if default == "OTA": return "MQTT" if default is not None: @@ -106,6 +113,17 @@ def choose_upload_log_host( return choose_prompt(options, purpose=purpose) +def mqtt_logging_enabled(mqtt_config): + log_topic = mqtt_config[CONF_LOG_TOPIC] + if log_topic is None: + return False + if CONF_TOPIC not in log_topic: + return False + if log_topic.get(CONF_LEVEL, None) == "NONE": + return False + return True + + def get_port_type(port): if port.startswith("/") or port.startswith("COM"): return "SERIAL" From d885d65c9bc7667c07afc1066f4bbc00efe09aff Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 11 Nov 2024 12:18:05 +1100 Subject: [PATCH 33/46] [sensor] Make some values templatable (#7735) --- esphome/components/sensor/__init__.py | 26 +++++++++++++------ esphome/components/sensor/filter.cpp | 37 ++++++++++++++------------- esphome/components/sensor/filter.h | 19 +++++++------- tests/components/template/common.yaml | 19 ++++++++++++++ 4 files changed, 65 insertions(+), 36 deletions(-) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 27338b8608..9dbad27102 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -335,19 +335,28 @@ def sensor_schema( return SENSOR_SCHEMA.extend(schema) -@FILTER_REGISTRY.register("offset", OffsetFilter, cv.float_) +@FILTER_REGISTRY.register("offset", OffsetFilter, cv.templatable(cv.float_)) async def offset_filter_to_code(config, filter_id): - return cg.new_Pvariable(filter_id, config) + template_ = await cg.templatable(config, [], float) + return cg.new_Pvariable(filter_id, template_) -@FILTER_REGISTRY.register("multiply", MultiplyFilter, cv.float_) +@FILTER_REGISTRY.register("multiply", MultiplyFilter, cv.templatable(cv.float_)) async def multiply_filter_to_code(config, filter_id): - return cg.new_Pvariable(filter_id, config) + template_ = await cg.templatable(config, [], float) + return cg.new_Pvariable(filter_id, template_) -@FILTER_REGISTRY.register("filter_out", FilterOutValueFilter, cv.float_) +@FILTER_REGISTRY.register( + "filter_out", + FilterOutValueFilter, + cv.Any(cv.templatable(cv.float_), [cv.templatable(cv.float_)]), +) async def filter_out_filter_to_code(config, filter_id): - return cg.new_Pvariable(filter_id, config) + if not isinstance(config, list): + config = [config] + template_ = [await cg.templatable(x, [], float) for x in config] + return cg.new_Pvariable(filter_id, template_) QUANTILE_SCHEMA = cv.All( @@ -573,7 +582,7 @@ async def heartbeat_filter_to_code(config, filter_id): TIMEOUT_SCHEMA = cv.maybe_simple_value( { cv.Required(CONF_TIMEOUT): cv.positive_time_period_milliseconds, - cv.Optional(CONF_VALUE, default="nan"): cv.float_, + cv.Optional(CONF_VALUE, default="nan"): cv.templatable(cv.float_), }, key=CONF_TIMEOUT, ) @@ -581,7 +590,8 @@ TIMEOUT_SCHEMA = cv.maybe_simple_value( @FILTER_REGISTRY.register("timeout", TimeoutFilter, TIMEOUT_SCHEMA) async def timeout_filter_to_code(config, filter_id): - var = cg.new_Pvariable(filter_id, config[CONF_TIMEOUT], config[CONF_VALUE]) + template_ = await cg.templatable(config[CONF_VALUE], [], float) + var = cg.new_Pvariable(filter_id, config[CONF_TIMEOUT], template_) await cg.register_component(var, {}) return var diff --git a/esphome/components/sensor/filter.cpp b/esphome/components/sensor/filter.cpp index bcf1fc8269..0a8740dd5b 100644 --- a/esphome/components/sensor/filter.cpp +++ b/esphome/components/sensor/filter.cpp @@ -288,36 +288,36 @@ optional LambdaFilter::new_value(float value) { } // OffsetFilter -OffsetFilter::OffsetFilter(float offset) : offset_(offset) {} +OffsetFilter::OffsetFilter(TemplatableValue offset) : offset_(std::move(offset)) {} -optional OffsetFilter::new_value(float value) { return value + this->offset_; } +optional OffsetFilter::new_value(float value) { return value + this->offset_.value(); } // MultiplyFilter -MultiplyFilter::MultiplyFilter(float multiplier) : multiplier_(multiplier) {} +MultiplyFilter::MultiplyFilter(TemplatableValue multiplier) : multiplier_(std::move(multiplier)) {} -optional MultiplyFilter::new_value(float value) { return value * this->multiplier_; } +optional MultiplyFilter::new_value(float value) { return value * this->multiplier_.value(); } // FilterOutValueFilter -FilterOutValueFilter::FilterOutValueFilter(float value_to_filter_out) : value_to_filter_out_(value_to_filter_out) {} +FilterOutValueFilter::FilterOutValueFilter(std::vector> values_to_filter_out) + : values_to_filter_out_(std::move(values_to_filter_out)) {} optional FilterOutValueFilter::new_value(float value) { - if (std::isnan(this->value_to_filter_out_)) { - if (std::isnan(value)) { - return {}; - } else { - return value; + int8_t accuracy = this->parent_->get_accuracy_decimals(); + float accuracy_mult = powf(10.0f, accuracy); + for (auto filter_value : this->values_to_filter_out_) { + if (std::isnan(filter_value.value())) { + if (std::isnan(value)) { + return {}; + } + continue; } - } else { - int8_t accuracy = this->parent_->get_accuracy_decimals(); - float accuracy_mult = powf(10.0f, accuracy); - float rounded_filter_out = roundf(accuracy_mult * this->value_to_filter_out_); + float rounded_filter_out = roundf(accuracy_mult * filter_value.value()); float rounded_value = roundf(accuracy_mult * value); if (rounded_filter_out == rounded_value) { return {}; - } else { - return value; } } + return value; } // ThrottleFilter @@ -383,11 +383,12 @@ void OrFilter::initialize(Sensor *parent, Filter *next) { // TimeoutFilter optional TimeoutFilter::new_value(float value) { - this->set_timeout("timeout", this->time_period_, [this]() { this->output(this->value_); }); + this->set_timeout("timeout", this->time_period_, [this]() { this->output(this->value_.value()); }); return value; } -TimeoutFilter::TimeoutFilter(uint32_t time_period, float new_value) : time_period_(time_period), value_(new_value) {} +TimeoutFilter::TimeoutFilter(uint32_t time_period, TemplatableValue new_value) + : time_period_(time_period), value_(std::move(new_value)) {} float TimeoutFilter::get_setup_priority() const { return setup_priority::HARDWARE; } // DebounceFilter diff --git a/esphome/components/sensor/filter.h b/esphome/components/sensor/filter.h index 92b1d8d240..86586b458d 100644 --- a/esphome/components/sensor/filter.h +++ b/esphome/components/sensor/filter.h @@ -5,6 +5,7 @@ #include #include "esphome/core/component.h" #include "esphome/core/helpers.h" +#include "esphome/core/automation.h" namespace esphome { namespace sensor { @@ -273,34 +274,33 @@ class LambdaFilter : public Filter { /// A simple filter that adds `offset` to each value it receives. class OffsetFilter : public Filter { public: - explicit OffsetFilter(float offset); + explicit OffsetFilter(TemplatableValue offset); optional new_value(float value) override; protected: - float offset_; + TemplatableValue offset_; }; /// A simple filter that multiplies to each value it receives by `multiplier`. class MultiplyFilter : public Filter { public: - explicit MultiplyFilter(float multiplier); - + explicit MultiplyFilter(TemplatableValue multiplier); optional new_value(float value) override; protected: - float multiplier_; + TemplatableValue multiplier_; }; /// A simple filter that only forwards the filter chain if it doesn't receive `value_to_filter_out`. class FilterOutValueFilter : public Filter { public: - explicit FilterOutValueFilter(float value_to_filter_out); + explicit FilterOutValueFilter(std::vector> values_to_filter_out); optional new_value(float value) override; protected: - float value_to_filter_out_; + std::vector> values_to_filter_out_; }; class ThrottleFilter : public Filter { @@ -316,8 +316,7 @@ class ThrottleFilter : public Filter { class TimeoutFilter : public Filter, public Component { public: - explicit TimeoutFilter(uint32_t time_period, float new_value); - void set_value(float new_value) { this->value_ = new_value; } + explicit TimeoutFilter(uint32_t time_period, TemplatableValue new_value); optional new_value(float value) override; @@ -325,7 +324,7 @@ class TimeoutFilter : public Filter, public Component { protected: uint32_t time_period_; - float value_; + TemplatableValue value_; }; class DebounceFilter : public Filter, public Component { diff --git a/tests/components/template/common.yaml b/tests/components/template/common.yaml index 3565926933..79201fbe07 100644 --- a/tests/components/template/common.yaml +++ b/tests/components/template/common.yaml @@ -9,6 +9,25 @@ sensor: return 0.0; } update_interval: 60s + filters: + - offset: 10 + - multiply: 1 + - offset: !lambda return 10; + - multiply: !lambda return 2; + - filter_out: + - 10 + - 20 + - !lambda return 10; + - filter_out: 10 + - filter_out: !lambda return NAN; + - timeout: + timeout: 10s + value: !lambda return 10; + - timeout: + timeout: 1h + value: 20.0 + - timeout: + timeout: 1d esphome: on_boot: From ffee2f0e8878edf6c5feafbd643a7ef6bcc3fc7a Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 11 Nov 2024 14:07:48 +1100 Subject: [PATCH 34/46] [lvgl] Implement keypads (#7719) --- esphome/components/lvgl/__init__.py | 24 +++++++- esphome/components/lvgl/defines.py | 1 + esphome/components/lvgl/encoders.py | 19 +++--- esphome/components/lvgl/keypads.py | 77 +++++++++++++++++++++++++ esphome/components/lvgl/lvgl_esphome.h | 11 +--- esphome/components/lvgl/types.py | 1 + tests/components/lvgl/lvgl-package.yaml | 10 ++++ 7 files changed, 123 insertions(+), 20 deletions(-) create mode 100644 esphome/components/lvgl/keypads.py diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py index 7476c0a09c..d03adc9624 100644 --- a/esphome/components/lvgl/__init__.py +++ b/esphome/components/lvgl/__init__.py @@ -7,6 +7,7 @@ import esphome.config_validation as cv from esphome.const import ( CONF_AUTO_CLEAR_ENABLED, CONF_BUFFER_SIZE, + CONF_GROUP, CONF_ID, CONF_LAMBDA, CONF_ON_IDLE, @@ -23,9 +24,15 @@ from esphome.helpers import write_file_if_changed from . import defines as df, helpers, lv_validation as lvalid from .automation import disp_update, focused_widgets, update_to_code from .defines import add_define -from .encoders import ENCODERS_CONFIG, encoders_to_code, initial_focus_to_code +from .encoders import ( + ENCODERS_CONFIG, + encoders_to_code, + get_default_group, + initial_focus_to_code, +) from .gradient import GRADIENT_SCHEMA, gradients_to_code from .hello_world import get_hello_world +from .keypads import KEYPADS_CONFIG, keypads_to_code from .lv_validation import lv_bool, lv_images_used from .lvcode import LvContext, LvglComponent, lvgl_static from .schemas import ( @@ -158,6 +165,13 @@ def multi_conf_validate(configs: list[dict]): display_list = [disp for disps in displays for disp in disps] if len(display_list) != len(set(display_list)): raise cv.Invalid("A display ID may be used in only one LVGL instance") + for config in configs: + for item in (df.CONF_ENCODERS, df.CONF_KEYPADS): + for enc in config.get(item, ()): + if CONF_GROUP not in enc: + raise cv.Invalid( + f"'{item}' must have an explicit group set when using multiple LVGL instances" + ) base_config = configs[0] for config in configs[1:]: for item in ( @@ -173,7 +187,8 @@ def multi_conf_validate(configs: list[dict]): def final_validation(configs): - multi_conf_validate(configs) + if len(configs) != 1: + multi_conf_validate(configs) global_config = full_config.get() for config in configs: if pages := config.get(CONF_PAGES): @@ -275,6 +290,7 @@ async def to_code(configs): else: add_define("LV_FONT_DEFAULT", await lvalid.lv_font.process(default_font)) cg.add(lvgl_static.esphome_lvgl_init()) + default_group = get_default_group(config_0) for config in configs: frac = config[CONF_BUFFER_SIZE] @@ -303,7 +319,8 @@ async def to_code(configs): lv_scr_act = get_scr_act(lv_component) async with LvContext(): await touchscreens_to_code(lv_component, config) - await encoders_to_code(lv_component, config) + await encoders_to_code(lv_component, config, default_group) + await keypads_to_code(lv_component, config, default_group) await theme_to_code(config) await styles_to_code(config) await gradients_to_code(config) @@ -430,6 +447,7 @@ LVGL_SCHEMA = ( cv.Optional(df.CONF_GRADIENTS): GRADIENT_SCHEMA, cv.Optional(df.CONF_TOUCHSCREENS, default=None): touchscreen_schema, cv.Optional(df.CONF_ENCODERS, default=None): ENCODERS_CONFIG, + cv.Optional(df.CONF_KEYPADS, default=None): KEYPADS_CONFIG, cv.GenerateID(df.CONF_DEFAULT_GROUP): cv.declare_id(lv_group_t), cv.Optional(df.CONF_RESUME_ON_INPUT, default=True): cv.boolean, } diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py index 4d48028611..ea345fa55c 100644 --- a/esphome/components/lvgl/defines.py +++ b/esphome/components/lvgl/defines.py @@ -438,6 +438,7 @@ CONF_HEADER_MODE = "header_mode" CONF_HOME = "home" CONF_INITIAL_FOCUS = "initial_focus" CONF_KEY_CODE = "key_code" +CONF_KEYPADS = "keypads" CONF_LAYOUT = "layout" CONF_LEFT_BUTTON = "left_button" CONF_LINE_WIDTH = "line_width" diff --git a/esphome/components/lvgl/encoders.py b/esphome/components/lvgl/encoders.py index 81bcda95b4..952572df43 100644 --- a/esphome/components/lvgl/encoders.py +++ b/esphome/components/lvgl/encoders.py @@ -17,7 +17,7 @@ from .defines import ( from .helpers import lvgl_components_required, requires_component from .lvcode import lv, lv_add, lv_assign, lv_expr, lv_Pvariable from .schemas import ENCODER_SCHEMA -from .types import lv_group_t, lv_indev_type_t +from .types import lv_group_t, lv_indev_type_t, lv_key_t ENCODERS_CONFIG = cv.ensure_list( ENCODER_SCHEMA.extend( @@ -39,10 +39,13 @@ ENCODERS_CONFIG = cv.ensure_list( ) -async def encoders_to_code(var, config): - default_group = lv_Pvariable(lv_group_t, config[CONF_DEFAULT_GROUP]) - lv_assign(default_group, lv_expr.group_create()) - lv.group_set_default(default_group) +def get_default_group(config): + default_group = cg.Pvariable(config[CONF_DEFAULT_GROUP], lv_expr.group_create()) + cg.add(lv.group_set_default(default_group)) + return default_group + + +async def encoders_to_code(var, config, default_group): for enc_conf in config[CONF_ENCODERS]: lvgl_components_required.add("KEY_LISTENER") lpt = enc_conf[CONF_LONG_PRESS_TIME].total_milliseconds @@ -54,14 +57,14 @@ async def encoders_to_code(var, config): if sensor_config := enc_conf.get(CONF_SENSOR): if isinstance(sensor_config, dict): b_sensor = await cg.get_variable(sensor_config[CONF_LEFT_BUTTON]) - cg.add(listener.set_left_button(b_sensor)) + cg.add(listener.add_button(b_sensor, lv_key_t.LV_KEY_LEFT)) b_sensor = await cg.get_variable(sensor_config[CONF_RIGHT_BUTTON]) - cg.add(listener.set_right_button(b_sensor)) + cg.add(listener.add_button(b_sensor, lv_key_t.LV_KEY_RIGHT)) else: sensor_config = await cg.get_variable(sensor_config) lv_add(listener.set_sensor(sensor_config)) b_sensor = await cg.get_variable(enc_conf[CONF_ENTER_BUTTON]) - cg.add(listener.set_enter_button(b_sensor)) + cg.add(listener.add_button(b_sensor, lv_key_t.LV_KEY_ENTER)) if group := enc_conf.get(CONF_GROUP): group = lv_Pvariable(lv_group_t, group) lv_assign(group, lv_expr.group_create()) diff --git a/esphome/components/lvgl/keypads.py b/esphome/components/lvgl/keypads.py new file mode 100644 index 0000000000..5e2953d57f --- /dev/null +++ b/esphome/components/lvgl/keypads.py @@ -0,0 +1,77 @@ +import esphome.codegen as cg +from esphome.components.binary_sensor import BinarySensor +import esphome.config_validation as cv +from esphome.const import CONF_GROUP, CONF_ID + +from .defines import ( + CONF_ENCODERS, + CONF_INITIAL_FOCUS, + CONF_KEYPADS, + CONF_LONG_PRESS_REPEAT_TIME, + CONF_LONG_PRESS_TIME, + literal, +) +from .helpers import lvgl_components_required +from .lvcode import lv, lv_assign, lv_expr, lv_Pvariable +from .schemas import ENCODER_SCHEMA +from .types import lv_group_t, lv_indev_type_t + +KEYPAD_KEYS = ( + "up", + "down", + "right", + "left", + "esc", + "del", + "backspace", + "enter", + "next", + "prev", + "home", + "end", + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "#", + "*", +) + +KEYPADS_CONFIG = cv.ensure_list( + ENCODER_SCHEMA.extend( + {cv.Optional(key): cv.use_id(BinarySensor) for key in KEYPAD_KEYS} + ) +) + + +async def keypads_to_code(var, config, default_group): + for enc_conf in config[CONF_KEYPADS]: + lvgl_components_required.add("KEY_LISTENER") + lpt = enc_conf[CONF_LONG_PRESS_TIME].total_milliseconds + lprt = enc_conf[CONF_LONG_PRESS_REPEAT_TIME].total_milliseconds + listener = cg.new_Pvariable( + enc_conf[CONF_ID], lv_indev_type_t.LV_INDEV_TYPE_KEYPAD, lpt, lprt + ) + await cg.register_parented(listener, var) + for key in [x for x in enc_conf if x in KEYPAD_KEYS]: + b_sensor = await cg.get_variable(enc_conf[key]) + cg.add(listener.add_button(b_sensor, literal(f"LV_KEY_{key.upper()}"))) + if group := enc_conf.get(CONF_GROUP): + group = lv_Pvariable(lv_group_t, group) + lv_assign(group, lv_expr.group_create()) + else: + group = default_group + lv.indev_set_group(lv_expr.indev_drv_register(listener.get_drv()), group) + + +async def initial_focus_to_code(config): + for enc_conf in config[CONF_ENCODERS]: + if default_focus := enc_conf.get(CONF_INITIAL_FOCUS): + obj = await cg.get_variable(default_focus) + lv.group_focus_obj(obj) diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index dae07d5153..208cb1cbd5 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -256,15 +256,8 @@ class LVEncoderListener : public Parented { LVEncoderListener(lv_indev_type_t type, uint16_t lpt, uint16_t lprt); #ifdef USE_BINARY_SENSOR - void set_left_button(binary_sensor::BinarySensor *left_button) { - left_button->add_on_state_callback([this](bool state) { this->event(LV_KEY_LEFT, state); }); - } - void set_right_button(binary_sensor::BinarySensor *right_button) { - right_button->add_on_state_callback([this](bool state) { this->event(LV_KEY_RIGHT, state); }); - } - - void set_enter_button(binary_sensor::BinarySensor *enter_button) { - enter_button->add_on_state_callback([this](bool state) { this->event(LV_KEY_ENTER, state); }); + void add_button(binary_sensor::BinarySensor *button, lv_key_t key) { + button->add_on_state_callback([this, key](bool state) { this->event(key, state); }); } #endif diff --git a/esphome/components/lvgl/types.py b/esphome/components/lvgl/types.py index b504f24674..40e69119f0 100644 --- a/esphome/components/lvgl/types.py +++ b/esphome/components/lvgl/types.py @@ -40,6 +40,7 @@ void_ptr = cg.void.operator("ptr") lv_coord_t = cg.global_ns.namespace("lv_coord_t") lv_event_code_t = cg.global_ns.enum("lv_event_code_t") lv_indev_type_t = cg.global_ns.enum("lv_indev_type_t") +lv_key_t = cg.global_ns.enum("lv_key_t") FontEngine = lvgl_ns.class_("FontEngine") IdleTrigger = lvgl_ns.class_("IdleTrigger", automation.Trigger.template()) PauseTrigger = lvgl_ns.class_("PauseTrigger", automation.Trigger.template()) diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index 9bfbb5fc95..db0443b3bb 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -11,6 +11,12 @@ substitutions: check: "\U000F012C" arrow_down: "\U000F004B" +binary_sensor: + - id: enter_sensor + platform: template + - id: left_sensor + platform: template + lvgl: log_level: debug resume_on_input: true @@ -93,6 +99,10 @@ lvgl: - touchscreen_id: tft_touch long_press_repeat_time: 200ms long_press_time: 500ms + keypads: + - initial_focus: button_button + enter: enter_sensor + next: left_sensor msgboxes: - id: message_box From a2dccc4730566536b700985310016fe1299ae371 Mon Sep 17 00:00:00 2001 From: Djordje Mandic <6750655+DjordjeMandic@users.noreply.github.com> Date: Mon, 11 Nov 2024 05:14:01 +0100 Subject: [PATCH 35/46] [midea] Add temperature validation in do_follow_me method (bugfix) (#7736) --- esphome/components/midea/air_conditioner.cpp | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/esphome/components/midea/air_conditioner.cpp b/esphome/components/midea/air_conditioner.cpp index b5bf43b64f..a823680d03 100644 --- a/esphome/components/midea/air_conditioner.cpp +++ b/esphome/components/midea/air_conditioner.cpp @@ -3,6 +3,8 @@ #include "esphome/core/log.h" #include "air_conditioner.h" #include "ac_adapter.h" +#include +#include namespace esphome { namespace midea { @@ -121,7 +123,21 @@ void AirConditioner::dump_config() { void AirConditioner::do_follow_me(float temperature, bool beeper) { #ifdef USE_REMOTE_TRANSMITTER - IrFollowMeData data(static_cast(lroundf(temperature)), beeper); + // Check if temperature is finite (not NaN or infinite) + if (!std::isfinite(temperature)) { + ESP_LOGW(Constants::TAG, "Follow me action requires a finite temperature, got: %f", temperature); + return; + } + + // Round and convert temperature to long, then clamp and convert it to uint8_t + uint8_t temp_uint8 = + static_cast(std::max(0L, std::min(static_cast(UINT8_MAX), std::lroundf(temperature)))); + + ESP_LOGD(Constants::TAG, "Follow me action called with temperature: %f °C, rounded to: %u °C", temperature, + temp_uint8); + + // Create and transmit the data + IrFollowMeData data(temp_uint8, beeper); this->transmitter_.transmit(data); #else ESP_LOGW(Constants::TAG, "Action needs remote_transmitter component"); From 58d028ac1375511362fac3bf524b86567ef584b0 Mon Sep 17 00:00:00 2001 From: Oleg Tarasov Date: Tue, 12 Nov 2024 06:19:42 +0300 Subject: [PATCH 36/46] Add OpenTherm component (part 3: rest of the sensors) (#7676) Co-authored-by: FreeBear Co-authored-by: FreeBear-nc <67865163+FreeBear-nc@users.noreply.github.com> Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/opentherm/__init__.py | 30 +- .../opentherm/binary_sensor/__init__.py | 33 ++ esphome/components/opentherm/const.py | 6 + esphome/components/opentherm/generate.py | 2 + esphome/components/opentherm/hub.cpp | 42 +- esphome/components/opentherm/hub.h | 52 ++- esphome/components/opentherm/input.h | 18 + esphome/components/opentherm/input.py | 51 +++ .../components/opentherm/number/__init__.py | 74 ++++ .../components/opentherm/number/number.cpp | 40 ++ esphome/components/opentherm/number/number.h | 31 ++ esphome/components/opentherm/opentherm.h | 33 ++ .../components/opentherm/opentherm_macros.h | 60 +++ .../components/opentherm/output/__init__.py | 47 +++ .../components/opentherm/output/output.cpp | 18 + esphome/components/opentherm/output/output.h | 33 ++ esphome/components/opentherm/schema.py | 378 +++++++++++++++++- .../components/opentherm/sensor/__init__.py | 16 + .../components/opentherm/switch/__init__.py | 43 ++ .../components/opentherm/switch/switch.cpp | 28 ++ esphome/components/opentherm/switch/switch.h | 20 + tests/components/opentherm/common.yaml | 84 ++++ 22 files changed, 1128 insertions(+), 11 deletions(-) create mode 100644 esphome/components/opentherm/binary_sensor/__init__.py create mode 100644 esphome/components/opentherm/input.h create mode 100644 esphome/components/opentherm/input.py create mode 100644 esphome/components/opentherm/number/__init__.py create mode 100644 esphome/components/opentherm/number/number.cpp create mode 100644 esphome/components/opentherm/number/number.h create mode 100644 esphome/components/opentherm/output/__init__.py create mode 100644 esphome/components/opentherm/output/output.cpp create mode 100644 esphome/components/opentherm/output/output.h create mode 100644 esphome/components/opentherm/switch/__init__.py create mode 100644 esphome/components/opentherm/switch/switch.cpp create mode 100644 esphome/components/opentherm/switch/switch.h diff --git a/esphome/components/opentherm/__init__.py b/esphome/components/opentherm/__init__.py index ee19818a29..81cd78af08 100644 --- a/esphome/components/opentherm/__init__.py +++ b/esphome/components/opentherm/__init__.py @@ -3,8 +3,9 @@ from typing import Any import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins +from esphome.components import sensor from esphome.const import CONF_ID, PLATFORM_ESP32, PLATFORM_ESP8266 -from . import generate +from . import const, schema, validate, generate CODEOWNERS = ["@olegtarasov"] MULTI_CONF = True @@ -19,6 +20,7 @@ CONF_CH2_ACTIVE = "ch2_active" CONF_SUMMER_MODE_ACTIVE = "summer_mode_active" CONF_DHW_BLOCK = "dhw_block" CONF_SYNC_MODE = "sync_mode" +CONF_OPENTHERM_VERSION = "opentherm_version" CONFIG_SCHEMA = cv.All( cv.Schema( @@ -34,8 +36,15 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_SUMMER_MODE_ACTIVE, False): cv.boolean, cv.Optional(CONF_DHW_BLOCK, False): cv.boolean, cv.Optional(CONF_SYNC_MODE, False): cv.boolean, + cv.Optional(CONF_OPENTHERM_VERSION): cv.positive_float, } - ).extend(cv.COMPONENT_SCHEMA), + ) + .extend( + validate.create_entities_schema( + schema.INPUTS, (lambda _: cv.use_id(sensor.Sensor)) + ) + ) + .extend(cv.COMPONENT_SCHEMA), cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266]), ) @@ -52,8 +61,23 @@ async def to_code(config: dict[str, Any]) -> None: cg.add(var.set_out_pin(out_pin)) non_sensors = {CONF_ID, CONF_IN_PIN, CONF_OUT_PIN} + input_sensors = [] for key, value in config.items(): if key in non_sensors: continue + if key in schema.INPUTS: + input_sensor = await cg.get_variable(value) + cg.add( + getattr(var, f"set_{key}_{const.INPUT_SENSOR.lower()}")(input_sensor) + ) + input_sensors.append(key) + else: + cg.add(getattr(var, f"set_{key}")(value)) - cg.add(getattr(var, f"set_{key}")(value)) + if len(input_sensors) > 0: + generate.define_has_component(const.INPUT_SENSOR, input_sensors) + generate.define_message_handler( + const.INPUT_SENSOR, input_sensors, schema.INPUTS + ) + generate.define_readers(const.INPUT_SENSOR, input_sensors) + generate.add_messages(var, input_sensors, schema.INPUTS) diff --git a/esphome/components/opentherm/binary_sensor/__init__.py b/esphome/components/opentherm/binary_sensor/__init__.py new file mode 100644 index 0000000000..643734f90c --- /dev/null +++ b/esphome/components/opentherm/binary_sensor/__init__.py @@ -0,0 +1,33 @@ +from typing import Any + +import esphome.config_validation as cv +from esphome.components import binary_sensor +from .. import const, schema, validate, generate + +DEPENDENCIES = [const.OPENTHERM] +COMPONENT_TYPE = const.BINARY_SENSOR + + +def get_entity_validation_schema(entity: schema.BinarySensorSchema) -> cv.Schema: + return binary_sensor.binary_sensor_schema( + device_class=( + entity.device_class + or binary_sensor._UNDEF # pylint: disable=protected-access + ), + icon=(entity.icon or binary_sensor._UNDEF), # pylint: disable=protected-access + ) + + +CONFIG_SCHEMA = validate.create_component_schema( + schema.BINARY_SENSORS, get_entity_validation_schema +) + + +async def to_code(config: dict[str, Any]) -> None: + await generate.component_to_code( + COMPONENT_TYPE, + schema.BINARY_SENSORS, + binary_sensor.BinarySensor, + generate.create_only_conf(binary_sensor.new_binary_sensor), + config, + ) diff --git a/esphome/components/opentherm/const.py b/esphome/components/opentherm/const.py index 1f997c5d9c..a113331585 100644 --- a/esphome/components/opentherm/const.py +++ b/esphome/components/opentherm/const.py @@ -1,5 +1,11 @@ OPENTHERM = "opentherm" CONF_OPENTHERM_ID = "opentherm_id" +CONF_DATA_TYPE = "data_type" SENSOR = "sensor" +BINARY_SENSOR = "binary_sensor" +SWITCH = "switch" +NUMBER = "number" +OUTPUT = "output" +INPUT_SENSOR = "input_sensor" diff --git a/esphome/components/opentherm/generate.py b/esphome/components/opentherm/generate.py index 6a97835a57..9716cab093 100644 --- a/esphome/components/opentherm/generate.py +++ b/esphome/components/opentherm/generate.py @@ -130,6 +130,8 @@ async def component_to_code( id = conf[CONF_ID] if id and id.type == type: entity = await create(conf, key, hub) + if const.CONF_DATA_TYPE in conf: + schemas[key].message_data = conf[const.CONF_DATA_TYPE] cg.add(getattr(hub, f"set_{key}_{component_type.lower()}")(entity)) keys.append(key) diff --git a/esphome/components/opentherm/hub.cpp b/esphome/components/opentherm/hub.cpp index 770bbd82b7..432036d58d 100644 --- a/esphome/components/opentherm/hub.cpp +++ b/esphome/components/opentherm/hub.cpp @@ -29,6 +29,8 @@ uint8_t parse_u8_hb(OpenthermData &data) { return data.valueHB; } int8_t parse_s8_lb(OpenthermData &data) { return (int8_t) data.valueLB; } int8_t parse_s8_hb(OpenthermData &data) { return (int8_t) data.valueHB; } uint16_t parse_u16(OpenthermData &data) { return data.u16(); } +uint16_t parse_u8_lb_60(OpenthermData &data) { return data.valueLB * 60; } +uint16_t parse_u8_hb_60(OpenthermData &data) { return data.valueHB * 60; } int16_t parse_s16(OpenthermData &data) { return data.s16(); } float parse_f88(OpenthermData &data) { return data.f88(); } @@ -87,13 +89,40 @@ OpenthermData OpenthermHub::build_request_(MessageId request_id) const { return data; } + // Another special case is OpenTherm version number which is configured at hub level as a constant + if (request_id == MessageId::OT_VERSION_CONTROLLER) { + data.type = MessageType::WRITE_DATA; + data.id = MessageId::OT_VERSION_CONTROLLER; + data.f88(this->opentherm_version_); + + return data; + } + // Disable incomplete switch statement warnings, because the cases in each // switch are generated based on the configured sensors and inputs. #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wswitch" - switch (request_id) { OPENTHERM_SENSOR_MESSAGE_HANDLERS(OPENTHERM_MESSAGE_READ_MESSAGE, OPENTHERM_IGNORE, , , ) } + // Next, we start with the write requests from switches and other inputs, + // because we would want to write that data if it is available, rather than + // request a read for that type (in the case that both read and write are + // supported). + switch (request_id) { + OPENTHERM_SWITCH_MESSAGE_HANDLERS(OPENTHERM_MESSAGE_WRITE_MESSAGE, OPENTHERM_MESSAGE_WRITE_ENTITY, , + OPENTHERM_MESSAGE_WRITE_POSTSCRIPT, ) + OPENTHERM_NUMBER_MESSAGE_HANDLERS(OPENTHERM_MESSAGE_WRITE_MESSAGE, OPENTHERM_MESSAGE_WRITE_ENTITY, , + OPENTHERM_MESSAGE_WRITE_POSTSCRIPT, ) + OPENTHERM_OUTPUT_MESSAGE_HANDLERS(OPENTHERM_MESSAGE_WRITE_MESSAGE, OPENTHERM_MESSAGE_WRITE_ENTITY, , + OPENTHERM_MESSAGE_WRITE_POSTSCRIPT, ) + OPENTHERM_INPUT_SENSOR_MESSAGE_HANDLERS(OPENTHERM_MESSAGE_WRITE_MESSAGE, OPENTHERM_MESSAGE_WRITE_ENTITY, , + OPENTHERM_MESSAGE_WRITE_POSTSCRIPT, ) + } + // Finally, handle the simple read requests, which only change with the message id. + switch (request_id) { OPENTHERM_SENSOR_MESSAGE_HANDLERS(OPENTHERM_MESSAGE_READ_MESSAGE, OPENTHERM_IGNORE, , , ) } + switch (request_id) { + OPENTHERM_BINARY_SENSOR_MESSAGE_HANDLERS(OPENTHERM_MESSAGE_READ_MESSAGE, OPENTHERM_IGNORE, , , ) + } #pragma GCC diagnostic pop // And if we get here, a message was requested which somehow wasn't handled. @@ -115,6 +144,10 @@ void OpenthermHub::process_response(OpenthermData &data) { OPENTHERM_SENSOR_MESSAGE_HANDLERS(OPENTHERM_MESSAGE_RESPONSE_MESSAGE, OPENTHERM_MESSAGE_RESPONSE_ENTITY, , OPENTHERM_MESSAGE_RESPONSE_POSTSCRIPT, ) } + switch (data.id) { + OPENTHERM_BINARY_SENSOR_MESSAGE_HANDLERS(OPENTHERM_MESSAGE_RESPONSE_MESSAGE, OPENTHERM_MESSAGE_RESPONSE_ENTITY, , + OPENTHERM_MESSAGE_RESPONSE_POSTSCRIPT, ) + } } void OpenthermHub::setup() { @@ -131,6 +164,13 @@ void OpenthermHub::setup() { // good practice anyway. this->add_repeating_message(MessageId::STATUS); + // Also ensure that we start communication with the STATUS message + this->initial_messages_.insert(this->initial_messages_.begin(), MessageId::STATUS); + + if (this->opentherm_version_ > 0.0f) { + this->initial_messages_.insert(this->initial_messages_.begin(), MessageId::OT_VERSION_CONTROLLER); + } + this->current_message_iterator_ = this->initial_messages_.begin(); } diff --git a/esphome/components/opentherm/hub.h b/esphome/components/opentherm/hub.h index 3b90cdf427..1f536653e8 100644 --- a/esphome/components/opentherm/hub.h +++ b/esphome/components/opentherm/hub.h @@ -4,6 +4,7 @@ #include "esphome/core/hal.h" #include "esphome/core/component.h" #include "esphome/core/log.h" +#include #include "opentherm.h" @@ -11,6 +12,22 @@ #include "esphome/components/sensor/sensor.h" #endif +#ifdef OPENTHERM_USE_BINARY_SENSOR +#include "esphome/components/binary_sensor/binary_sensor.h" +#endif + +#ifdef OPENTHERM_USE_SWITCH +#include "esphome/components/opentherm/switch/switch.h" +#endif + +#ifdef OPENTHERM_USE_OUTPUT +#include "esphome/components/opentherm/output/output.h" +#endif + +#ifdef OPENTHERM_USE_NUMBER +#include "esphome/components/opentherm/number/number.h" +#endif + #include #include #include @@ -31,15 +48,25 @@ class OpenthermHub : public Component { OPENTHERM_SENSOR_LIST(OPENTHERM_DECLARE_SENSOR, ) + OPENTHERM_BINARY_SENSOR_LIST(OPENTHERM_DECLARE_BINARY_SENSOR, ) + + OPENTHERM_SWITCH_LIST(OPENTHERM_DECLARE_SWITCH, ) + + OPENTHERM_NUMBER_LIST(OPENTHERM_DECLARE_NUMBER, ) + + OPENTHERM_OUTPUT_LIST(OPENTHERM_DECLARE_OUTPUT, ) + + OPENTHERM_INPUT_SENSOR_LIST(OPENTHERM_DECLARE_INPUT_SENSOR, ) + // The set of initial messages to send on starting communication with the boiler - std::unordered_set initial_messages_; + std::vector initial_messages_; // and the repeating messages which are sent repeatedly to update various sensors // and boiler parameters (like the setpoint). - std::unordered_set repeating_messages_; + std::vector repeating_messages_; // Indicates if we are still working on the initial requests or not bool sending_initial_ = true; // Index for the current request in one of the _requests sets. - std::unordered_set::const_iterator current_message_iterator_; + std::vector::const_iterator current_message_iterator_; uint32_t last_conversation_start_ = 0; uint32_t last_conversation_end_ = 0; @@ -51,6 +78,8 @@ class OpenthermHub : public Component { // Very likely to happen while using Dallas temperature sensors. bool sync_mode_ = false; + float opentherm_version_ = 0.0f; + // Create OpenTherm messages based on the message id OpenthermData build_request_(MessageId request_id) const; void handle_protocol_write_error_(); @@ -88,13 +117,23 @@ class OpenthermHub : public Component { OPENTHERM_SENSOR_LIST(OPENTHERM_SET_SENSOR, ) - // Add a request to the set of initial requests - void add_initial_message(MessageId message_id) { this->initial_messages_.insert(message_id); } + OPENTHERM_BINARY_SENSOR_LIST(OPENTHERM_SET_BINARY_SENSOR, ) + + OPENTHERM_SWITCH_LIST(OPENTHERM_SET_SWITCH, ) + + OPENTHERM_NUMBER_LIST(OPENTHERM_SET_NUMBER, ) + + OPENTHERM_OUTPUT_LIST(OPENTHERM_SET_OUTPUT, ) + + OPENTHERM_INPUT_SENSOR_LIST(OPENTHERM_SET_INPUT_SENSOR, ) + + // Add a request to the vector of initial requests + void add_initial_message(MessageId message_id) { this->initial_messages_.push_back(message_id); } // Add a request to the set of repeating requests. Note that a large number of repeating // requests will slow down communication with the boiler. Each request may take up to 1 second, // so with all sensors enabled, it may take about half a minute before a change in setpoint // will be processed. - void add_repeating_message(MessageId message_id) { this->repeating_messages_.insert(message_id); } + void add_repeating_message(MessageId message_id) { this->repeating_messages_.push_back(message_id); } // There are seven status variables, which can either be set as a simple variable, // or using a switch. ch_enable and dhw_enable default to true, the others to false. @@ -110,6 +149,7 @@ class OpenthermHub : public Component { void set_summer_mode_active(bool value) { this->summer_mode_active = value; } void set_dhw_block(bool value) { this->dhw_block = value; } void set_sync_mode(bool sync_mode) { this->sync_mode_ = sync_mode; } + void set_opentherm_version(float value) { this->opentherm_version_ = value; } float get_setup_priority() const override { return setup_priority::HARDWARE; } diff --git a/esphome/components/opentherm/input.h b/esphome/components/opentherm/input.h new file mode 100644 index 0000000000..3567138792 --- /dev/null +++ b/esphome/components/opentherm/input.h @@ -0,0 +1,18 @@ +#pragma once + +namespace esphome { +namespace opentherm { + +class OpenthermInput { + public: + bool auto_min_value, auto_max_value; + + virtual void set_min_value(float min_value) = 0; + virtual void set_max_value(float max_value) = 0; + + virtual void set_auto_min_value(bool auto_min_value) { this->auto_min_value = auto_min_value; } + virtual void set_auto_max_value(bool auto_max_value) { this->auto_max_value = auto_max_value; } +}; + +} // namespace opentherm +} // namespace esphome diff --git a/esphome/components/opentherm/input.py b/esphome/components/opentherm/input.py new file mode 100644 index 0000000000..7897747be1 --- /dev/null +++ b/esphome/components/opentherm/input.py @@ -0,0 +1,51 @@ +from typing import Any + +import esphome.codegen as cg +import esphome.config_validation as cv +from . import schema, generate + +CONF_min_value = "min_value" +CONF_max_value = "max_value" +CONF_auto_min_value = "auto_min_value" +CONF_auto_max_value = "auto_max_value" +CONF_step = "step" + +OpenthermInput = generate.opentherm_ns.class_("OpenthermInput") + + +def validate_min_value_less_than_max_value(conf): + if ( + CONF_min_value in conf + and CONF_max_value in conf + and conf[CONF_min_value] > conf[CONF_max_value] + ): + raise cv.Invalid(f"{CONF_min_value} must be less than {CONF_max_value}") + return conf + + +def input_schema(entity: schema.InputSchema) -> cv.Schema: + result = cv.Schema( + { + cv.Optional(CONF_min_value, entity.range[0]): cv.float_range( + entity.range[0], entity.range[1] + ), + cv.Optional(CONF_max_value, entity.range[1]): cv.float_range( + entity.range[0], entity.range[1] + ), + } + ) + result = result.add_extra(validate_min_value_less_than_max_value) + result = result.extend({cv.Optional(CONF_step, False): cv.float_}) + if entity.auto_min_value is not None: + result = result.extend({cv.Optional(CONF_auto_min_value, False): cv.boolean}) + if entity.auto_max_value is not None: + result = result.extend({cv.Optional(CONF_auto_max_value, False): cv.boolean}) + + return result + + +def generate_setters(entity: cg.MockObj, conf: dict[str, Any]) -> None: + generate.add_property_set(entity, CONF_min_value, conf) + generate.add_property_set(entity, CONF_max_value, conf) + generate.add_property_set(entity, CONF_auto_min_value, conf) + generate.add_property_set(entity, CONF_auto_max_value, conf) diff --git a/esphome/components/opentherm/number/__init__.py b/esphome/components/opentherm/number/__init__.py new file mode 100644 index 0000000000..bbf3e87586 --- /dev/null +++ b/esphome/components/opentherm/number/__init__.py @@ -0,0 +1,74 @@ +from typing import Any + +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import number +from esphome.const import ( + CONF_ID, + CONF_UNIT_OF_MEASUREMENT, + CONF_STEP, + CONF_INITIAL_VALUE, + CONF_RESTORE_VALUE, +) +from .. import const, schema, validate, input, generate + +DEPENDENCIES = [const.OPENTHERM] +COMPONENT_TYPE = const.NUMBER + +OpenthermNumber = generate.opentherm_ns.class_( + "OpenthermNumber", number.Number, cg.Component, input.OpenthermInput +) + + +async def new_openthermnumber(config: dict[str, Any]) -> cg.Pvariable: + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await number.register_number( + var, + config, + min_value=config[input.CONF_min_value], + max_value=config[input.CONF_max_value], + step=config[input.CONF_step], + ) + input.generate_setters(var, config) + + if CONF_INITIAL_VALUE in config: + cg.add(var.set_initial_value(config[CONF_INITIAL_VALUE])) + if CONF_RESTORE_VALUE in config: + cg.add(var.set_restore_value(config[CONF_RESTORE_VALUE])) + + return var + + +def get_entity_validation_schema(entity: schema.InputSchema) -> cv.Schema: + return ( + number.NUMBER_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(OpenthermNumber), + cv.Optional( + CONF_UNIT_OF_MEASUREMENT, entity.unit_of_measurement + ): cv.string_strict, + cv.Optional(CONF_STEP, entity.step): cv.float_, + cv.Optional(CONF_INITIAL_VALUE): cv.float_, + cv.Optional(CONF_RESTORE_VALUE): cv.boolean, + } + ) + .extend(input.input_schema(entity)) + .extend(cv.COMPONENT_SCHEMA) + ) + + +CONFIG_SCHEMA = validate.create_component_schema( + schema.INPUTS, get_entity_validation_schema +) + + +async def to_code(config: dict[str, Any]) -> None: + keys = await generate.component_to_code( + COMPONENT_TYPE, + schema.INPUTS, + OpenthermNumber, + generate.create_only_conf(new_openthermnumber), + config, + ) + generate.define_readers(COMPONENT_TYPE, keys) diff --git a/esphome/components/opentherm/number/number.cpp b/esphome/components/opentherm/number/number.cpp new file mode 100644 index 0000000000..d02b99ee9c --- /dev/null +++ b/esphome/components/opentherm/number/number.cpp @@ -0,0 +1,40 @@ +#include "number.h" + +namespace esphome { +namespace opentherm { + +static const char *const TAG = "opentherm.number"; + +void OpenthermNumber::control(float value) { + this->publish_state(value); + + if (this->restore_value_) + this->pref_.save(&value); +} + +void OpenthermNumber::setup() { + float value; + if (!this->restore_value_) { + value = this->initial_value_; + } else { + this->pref_ = global_preferences->make_preference(this->get_object_id_hash()); + if (!this->pref_.load(&value)) { + if (!std::isnan(this->initial_value_)) { + value = this->initial_value_; + } else { + value = this->traits.get_min_value(); + } + } + } + this->publish_state(value); +} + +void OpenthermNumber::dump_config() { + LOG_NUMBER("", "OpenTherm Number", this); + ESP_LOGCONFIG(TAG, " Restore value: %d", this->restore_value_); + ESP_LOGCONFIG(TAG, " Initial value: %.2f", this->initial_value_); + ESP_LOGCONFIG(TAG, " Current value: %.2f", this->state); +} + +} // namespace opentherm +} // namespace esphome diff --git a/esphome/components/opentherm/number/number.h b/esphome/components/opentherm/number/number.h new file mode 100644 index 0000000000..6f86072754 --- /dev/null +++ b/esphome/components/opentherm/number/number.h @@ -0,0 +1,31 @@ +#pragma once + +#include "esphome/components/number/number.h" +#include "esphome/core/preferences.h" +#include "esphome/core/log.h" +#include "esphome/components/opentherm/input.h" + +namespace esphome { +namespace opentherm { + +// Just a simple number, which stores the number +class OpenthermNumber : public number::Number, public Component, public OpenthermInput { + protected: + void control(float value) override; + void setup() override; + void dump_config() override; + + float initial_value_{NAN}; + bool restore_value_{false}; + + ESPPreferenceObject pref_; + + public: + void set_min_value(float min_value) override { this->traits.set_min_value(min_value); } + void set_max_value(float max_value) override { this->traits.set_max_value(max_value); } + void set_initial_value(float initial_value) { initial_value_ = initial_value; } + void set_restore_value(bool restore_value) { this->restore_value_ = restore_value; } +}; + +} // namespace opentherm +} // namespace esphome diff --git a/esphome/components/opentherm/opentherm.h b/esphome/components/opentherm/opentherm.h index 23f4b39a1a..85f4611125 100644 --- a/esphome/components/opentherm/opentherm.h +++ b/esphome/components/opentherm/opentherm.h @@ -99,6 +99,8 @@ enum MessageId { EXHAUST_TEMP = 33, FAN_SPEED = 35, FLAME_CURRENT = 36, + ROOM_TEMP_CH2 = 37, + REL_HUMIDITY = 38, DHW_BOUNDS = 48, CH_BOUNDS = 49, OTC_CURVE_BOUNDS = 50, @@ -110,15 +112,46 @@ enum MessageId { HVAC_STATUS = 70, REL_VENT_SETPOINT = 71, DEVICE_VENT = 74, + HVAC_VER_ID = 75, REL_VENTILATION = 77, REL_HUMID_EXHAUST = 78, + EXHAUST_CO2 = 79, SUPPLY_INLET_TEMP = 80, SUPPLY_OUTLET_TEMP = 81, EXHAUST_INLET_TEMP = 82, EXHAUST_OUTLET_TEMP = 83, + EXHAUST_FAN_SPEED = 84, + SUPPLY_FAN_SPEED = 85, + REMOTE_VENTILATION_PARAM = 86, NOM_REL_VENTILATION = 87, + HVAC_NUM_TSP = 88, + HVAC_IDX_TSP = 89, + HVAC_FHB_SIZE = 90, + HVAC_FHB_IDX = 91, + RF_SIGNAL = 98, + DHW_MODE = 99, OVERRIDE_FUNC = 100, + + // Solar Specific Message IDs + SOLAR_MODE_FLAGS = 101, // hb0-2 Controller storage mode + // lb0 Device fault + // lb1-3 Device mode status + // lb4-5 Device status + SOLAR_ASF = 102, + SOLAR_VERSION_ID = 103, + SOLAR_PRODUCT_ID = 104, + SOLAR_NUM_TSP = 105, + SOLAR_IDX_TSP = 106, + SOLAR_FHB_SIZE = 107, + SOLAR_FHB_IDX = 108, + SOLAR_STARTS = 109, + SOLAR_HOURS = 110, + SOLAR_ENERGY = 111, + SOLAR_TOTAL_ENERGY = 112, + + FAILED_BURNER_STARTS = 113, + BURNER_FLAME_LOW = 114, OEM_DIAGNOSTIC = 115, BURNER_STARTS = 116, CH_PUMP_STARTS = 117, diff --git a/esphome/components/opentherm/opentherm_macros.h b/esphome/components/opentherm/opentherm_macros.h index 0389e975ff..8aaec0b48a 100644 --- a/esphome/components/opentherm/opentherm_macros.h +++ b/esphome/components/opentherm/opentherm_macros.h @@ -13,14 +13,49 @@ namespace opentherm { #ifndef OPENTHERM_SENSOR_LIST #define OPENTHERM_SENSOR_LIST(F, sep) #endif +#ifndef OPENTHERM_BINARY_SENSOR_LIST +#define OPENTHERM_BINARY_SENSOR_LIST(F, sep) +#endif +#ifndef OPENTHERM_SWITCH_LIST +#define OPENTHERM_SWITCH_LIST(F, sep) +#endif +#ifndef OPENTHERM_NUMBER_LIST +#define OPENTHERM_NUMBER_LIST(F, sep) +#endif +#ifndef OPENTHERM_OUTPUT_LIST +#define OPENTHERM_OUTPUT_LIST(F, sep) +#endif +#ifndef OPENTHERM_INPUT_SENSOR_LIST +#define OPENTHERM_INPUT_SENSOR_LIST(F, sep) +#endif // Use macros to create fields for every entity specified in the ESPHome configuration #define OPENTHERM_DECLARE_SENSOR(entity) sensor::Sensor *entity; +#define OPENTHERM_DECLARE_BINARY_SENSOR(entity) binary_sensor::BinarySensor *entity; +#define OPENTHERM_DECLARE_SWITCH(entity) OpenthermSwitch *entity; +#define OPENTHERM_DECLARE_NUMBER(entity) OpenthermNumber *entity; +#define OPENTHERM_DECLARE_OUTPUT(entity) OpenthermOutput *entity; +#define OPENTHERM_DECLARE_INPUT_SENSOR(entity) sensor::Sensor *entity; // Setter macros #define OPENTHERM_SET_SENSOR(entity) \ void set_##entity(sensor::Sensor *sensor) { this->entity = sensor; } +#define OPENTHERM_SET_BINARY_SENSOR(entity) \ + void set_##entity(binary_sensor::BinarySensor *binary_sensor) { this->entity = binary_sensor; } + +#define OPENTHERM_SET_SWITCH(entity) \ + void set_##entity(OpenthermSwitch *sw) { this->entity = sw; } + +#define OPENTHERM_SET_NUMBER(entity) \ + void set_##entity(OpenthermNumber *number) { this->entity = number; } + +#define OPENTHERM_SET_OUTPUT(entity) \ + void set_##entity(OpenthermOutput *output) { this->entity = output; } + +#define OPENTHERM_SET_INPUT_SENSOR(entity) \ + void set_##entity(sensor::Sensor *sensor) { this->entity = sensor; } + // ===== hub.cpp macros ===== // *_MESSAGE_HANDLERS are generated in defines.h and look like this: @@ -35,6 +70,31 @@ namespace opentherm { #ifndef OPENTHERM_SENSOR_MESSAGE_HANDLERS #define OPENTHERM_SENSOR_MESSAGE_HANDLERS(MESSAGE, ENTITY, entity_sep, postscript, msg_sep) #endif +#ifndef OPENTHERM_BINARY_SENSOR_MESSAGE_HANDLERS +#define OPENTHERM_BINARY_SENSOR_MESSAGE_HANDLERS(MESSAGE, ENTITY, entity_sep, postscript, msg_sep) +#endif +#ifndef OPENTHERM_SWITCH_MESSAGE_HANDLERS +#define OPENTHERM_SWITCH_MESSAGE_HANDLERS(MESSAGE, ENTITY, entity_sep, postscript, msg_sep) +#endif +#ifndef OPENTHERM_NUMBER_MESSAGE_HANDLERS +#define OPENTHERM_NUMBER_MESSAGE_HANDLERS(MESSAGE, ENTITY, entity_sep, postscript, msg_sep) +#endif +#ifndef OPENTHERM_OUTPUT_MESSAGE_HANDLERS +#define OPENTHERM_OUTPUT_MESSAGE_HANDLERS(MESSAGE, ENTITY, entity_sep, postscript, msg_sep) +#endif +#ifndef OPENTHERM_INPUT_SENSOR_MESSAGE_HANDLERS +#define OPENTHERM_INPUT_SENSOR_MESSAGE_HANDLERS(MESSAGE, ENTITY, entity_sep, postscript, msg_sep) +#endif + +// Write data request builders +#define OPENTHERM_MESSAGE_WRITE_MESSAGE(msg) \ + case MessageId::msg: { \ + data.type = MessageType::WRITE_DATA; \ + data.id = request_id; +#define OPENTHERM_MESSAGE_WRITE_ENTITY(key, msg_data) message_data::write_##msg_data(this->key->state, data); +#define OPENTHERM_MESSAGE_WRITE_POSTSCRIPT \ + return data; \ + } // Read data request builder #define OPENTHERM_MESSAGE_READ_MESSAGE(msg) \ diff --git a/esphome/components/opentherm/output/__init__.py b/esphome/components/opentherm/output/__init__.py new file mode 100644 index 0000000000..3a53c9d4f4 --- /dev/null +++ b/esphome/components/opentherm/output/__init__.py @@ -0,0 +1,47 @@ +from typing import Any + +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import output +from esphome.const import CONF_ID +from .. import const, schema, validate, input, generate + +DEPENDENCIES = [const.OPENTHERM] +COMPONENT_TYPE = const.OUTPUT + +OpenthermOutput = generate.opentherm_ns.class_( + "OpenthermOutput", output.FloatOutput, cg.Component, input.OpenthermInput +) + + +async def new_openthermoutput( + config: dict[str, Any], key: str, _hub: cg.MockObj +) -> cg.Pvariable: + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await output.register_output(var, config) + cg.add(getattr(var, "set_id")(cg.RawExpression(f'"{key}_{config[CONF_ID]}"'))) + input.generate_setters(var, config) + return var + + +def get_entity_validation_schema(entity: schema.InputSchema) -> cv.Schema: + return ( + output.FLOAT_OUTPUT_SCHEMA.extend( + {cv.GenerateID(): cv.declare_id(OpenthermOutput)} + ) + .extend(input.input_schema(entity)) + .extend(cv.COMPONENT_SCHEMA) + ) + + +CONFIG_SCHEMA = validate.create_component_schema( + schema.INPUTS, get_entity_validation_schema +) + + +async def to_code(config: dict[str, Any]) -> None: + keys = await generate.component_to_code( + COMPONENT_TYPE, schema.INPUTS, OpenthermOutput, new_openthermoutput, config + ) + generate.define_readers(COMPONENT_TYPE, keys) diff --git a/esphome/components/opentherm/output/output.cpp b/esphome/components/opentherm/output/output.cpp new file mode 100644 index 0000000000..f820dc76f1 --- /dev/null +++ b/esphome/components/opentherm/output/output.cpp @@ -0,0 +1,18 @@ +#include "esphome/core/helpers.h" // for clamp() and lerp() +#include "output.h" + +namespace esphome { +namespace opentherm { + +static const char *const TAG = "opentherm.output"; + +void opentherm::OpenthermOutput::write_state(float state) { + ESP_LOGD(TAG, "Received state: %.2f. Min value: %.2f, max value: %.2f", state, min_value_, max_value_); + this->state = state < 0.003 && this->zero_means_zero_ + ? 0.0 + : clamp(lerp(state, min_value_, max_value_), min_value_, max_value_); + this->has_state_ = true; + ESP_LOGD(TAG, "Output %s set to %.2f", this->id_, this->state); +} +} // namespace opentherm +} // namespace esphome diff --git a/esphome/components/opentherm/output/output.h b/esphome/components/opentherm/output/output.h new file mode 100644 index 0000000000..8d6a0ee4ba --- /dev/null +++ b/esphome/components/opentherm/output/output.h @@ -0,0 +1,33 @@ +#pragma once + +#include "esphome/components/output/float_output.h" +#include "esphome/components/opentherm/input.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace opentherm { + +class OpenthermOutput : public output::FloatOutput, public Component, public OpenthermInput { + protected: + bool has_state_ = false; + const char *id_ = nullptr; + + float min_value_, max_value_; + + public: + float state; + + void set_id(const char *id) { this->id_ = id; } + + void write_state(float state) override; + + bool has_state() { return this->has_state_; }; + + void set_min_value(float min_value) override { this->min_value_ = min_value; } + void set_max_value(float max_value) override { this->max_value_ = max_value; } + float get_min_value() { return this->min_value_; } + float get_max_value() { return this->max_value_; } +}; + +} // namespace opentherm +} // namespace esphome diff --git a/esphome/components/opentherm/schema.py b/esphome/components/opentherm/schema.py index 6ed0029437..fe0f2a77a3 100644 --- a/esphome/components/opentherm/schema.py +++ b/esphome/components/opentherm/schema.py @@ -11,9 +11,12 @@ from esphome.const import ( UNIT_MICROAMP, UNIT_PERCENT, UNIT_REVOLUTIONS_PER_MINUTE, + DEVICE_CLASS_COLD, DEVICE_CLASS_CURRENT, DEVICE_CLASS_EMPTY, + DEVICE_CLASS_HEAT, DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_PROBLEM, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT, STATE_CLASS_NONE, @@ -188,11 +191,23 @@ SENSORS: dict[str, SensorSchema] = { description="Boiler fan speed", unit_of_measurement=UNIT_REVOLUTIONS_PER_MINUTE, accuracy_decimals=0, + icon="mdi:fan", device_class=DEVICE_CLASS_EMPTY, state_class=STATE_CLASS_MEASUREMENT, message="FAN_SPEED", keep_updated=True, - message_data="u16", + message_data="u8_lb_60", + ), + "fan_speed_setpoint": SensorSchema( + description="Boiler fan speed setpoint", + unit_of_measurement=UNIT_REVOLUTIONS_PER_MINUTE, + accuracy_decimals=0, + icon="mdi:fan", + device_class=DEVICE_CLASS_EMPTY, + state_class=STATE_CLASS_MEASUREMENT, + message="FAN_SPEED", + keep_updated=True, + message_data="u8_hb_60", ), "flame_current": SensorSchema( description="Boiler flame current", @@ -436,3 +451,364 @@ SENSORS: dict[str, SensorSchema] = { message_data="u8_lb", ), } + + +@dataclass +class BinarySensorSchema(EntitySchema): + icon: Optional[str] = None + device_class: Optional[str] = None + + +BINARY_SENSORS: dict[str, BinarySensorSchema] = { + "fault_indication": BinarySensorSchema( + description="Status: Fault indication", + device_class=DEVICE_CLASS_PROBLEM, + message="STATUS", + keep_updated=True, + message_data="flag8_lb_0", + ), + "ch_active": BinarySensorSchema( + description="Status: Central Heating active", + device_class=DEVICE_CLASS_HEAT, + icon="mdi:radiator", + message="STATUS", + keep_updated=True, + message_data="flag8_lb_1", + ), + "dhw_active": BinarySensorSchema( + description="Status: Domestic Hot Water active", + device_class=DEVICE_CLASS_HEAT, + icon="mdi:faucet", + message="STATUS", + keep_updated=True, + message_data="flag8_lb_2", + ), + "flame_on": BinarySensorSchema( + description="Status: Flame on", + device_class=DEVICE_CLASS_HEAT, + icon="mdi:fire", + message="STATUS", + keep_updated=True, + message_data="flag8_lb_3", + ), + "cooling_active": BinarySensorSchema( + description="Status: Cooling active", + device_class=DEVICE_CLASS_COLD, + message="STATUS", + keep_updated=True, + message_data="flag8_lb_4", + ), + "ch2_active": BinarySensorSchema( + description="Status: Central Heating 2 active", + device_class=DEVICE_CLASS_HEAT, + icon="mdi:radiator", + message="STATUS", + keep_updated=True, + message_data="flag8_lb_5", + ), + "diagnostic_indication": BinarySensorSchema( + description="Status: Diagnostic event", + device_class=DEVICE_CLASS_PROBLEM, + message="STATUS", + keep_updated=True, + message_data="flag8_lb_6", + ), + "electricity_production": BinarySensorSchema( + description="Status: Electricity production", + device_class=DEVICE_CLASS_PROBLEM, + message="STATUS", + keep_updated=True, + message_data="flag8_lb_7", + ), + "dhw_present": BinarySensorSchema( + description="Configuration: DHW present", + message="DEVICE_CONFIG", + keep_updated=False, + message_data="flag8_hb_0", + ), + "control_type_on_off": BinarySensorSchema( + description="Configuration: Control type is on/off", + message="DEVICE_CONFIG", + keep_updated=False, + message_data="flag8_hb_1", + ), + "cooling_supported": BinarySensorSchema( + description="Configuration: Cooling supported", + message="DEVICE_CONFIG", + keep_updated=False, + message_data="flag8_hb_2", + ), + "dhw_storage_tank": BinarySensorSchema( + description="Configuration: DHW storage tank", + message="DEVICE_CONFIG", + keep_updated=False, + message_data="flag8_hb_3", + ), + "controller_pump_control_allowed": BinarySensorSchema( + description="Configuration: Controller pump control allowed", + message="DEVICE_CONFIG", + keep_updated=False, + message_data="flag8_hb_4", + ), + "ch2_present": BinarySensorSchema( + description="Configuration: CH2 present", + message="DEVICE_CONFIG", + keep_updated=False, + message_data="flag8_hb_5", + ), + "water_filling": BinarySensorSchema( + description="Configuration: Remote water filling", + message="DEVICE_CONFIG", + keep_updated=False, + message_data="flag8_hb_6", + ), + "heat_mode": BinarySensorSchema( + description="Configuration: Heating or cooling", + message="DEVICE_CONFIG", + keep_updated=False, + message_data="flag8_hb_7", + ), + "dhw_setpoint_transfer_enabled": BinarySensorSchema( + description="Remote boiler parameters: DHW setpoint transfer enabled", + message="REMOTE", + keep_updated=False, + message_data="flag8_hb_0", + ), + "max_ch_setpoint_transfer_enabled": BinarySensorSchema( + description="Remote boiler parameters: CH maximum setpoint transfer enabled", + message="REMOTE", + keep_updated=False, + message_data="flag8_hb_1", + ), + "dhw_setpoint_rw": BinarySensorSchema( + description="Remote boiler parameters: DHW setpoint read/write", + message="REMOTE", + keep_updated=False, + message_data="flag8_lb_0", + ), + "max_ch_setpoint_rw": BinarySensorSchema( + description="Remote boiler parameters: CH maximum setpoint read/write", + message="REMOTE", + keep_updated=False, + message_data="flag8_lb_1", + ), + "service_request": BinarySensorSchema( + description="Service required", + device_class=DEVICE_CLASS_PROBLEM, + message="FAULT_FLAGS", + keep_updated=True, + message_data="flag8_hb_0", + ), + "lockout_reset": BinarySensorSchema( + description="Lockout Reset", + device_class=DEVICE_CLASS_PROBLEM, + message="FAULT_FLAGS", + keep_updated=True, + message_data="flag8_hb_1", + ), + "low_water_pressure": BinarySensorSchema( + description="Low water pressure fault", + device_class=DEVICE_CLASS_PROBLEM, + message="FAULT_FLAGS", + keep_updated=True, + message_data="flag8_hb_2", + ), + "flame_fault": BinarySensorSchema( + description="Flame fault", + device_class=DEVICE_CLASS_PROBLEM, + message="FAULT_FLAGS", + keep_updated=True, + message_data="flag8_hb_3", + ), + "air_pressure_fault": BinarySensorSchema( + description="Air pressure fault", + device_class=DEVICE_CLASS_PROBLEM, + message="FAULT_FLAGS", + keep_updated=True, + message_data="flag8_hb_4", + ), + "water_over_temp": BinarySensorSchema( + description="Water overtemperature", + device_class=DEVICE_CLASS_PROBLEM, + message="FAULT_FLAGS", + keep_updated=True, + message_data="flag8_hb_5", + ), +} + + +@dataclass +class SwitchSchema(EntitySchema): + default_mode: Optional[str] = None + + +SWITCHES: dict[str, SwitchSchema] = { + "ch_enable": SwitchSchema( + description="Central Heating enabled", + message="STATUS", + keep_updated=True, + message_data="flag8_hb_0", + default_mode="restore_default_off", + ), + "dhw_enable": SwitchSchema( + description="Domestic Hot Water enabled", + message="STATUS", + keep_updated=True, + message_data="flag8_hb_1", + default_mode="restore_default_off", + ), + "cooling_enable": SwitchSchema( + description="Cooling enabled", + message="STATUS", + keep_updated=True, + message_data="flag8_hb_2", + default_mode="restore_default_off", + ), + "otc_active": SwitchSchema( + description="Outside temperature compensation active", + message="STATUS", + keep_updated=True, + message_data="flag8_hb_3", + default_mode="restore_default_off", + ), + "ch2_active": SwitchSchema( + description="Central Heating 2 active", + message="STATUS", + keep_updated=True, + message_data="flag8_hb_4", + default_mode="restore_default_off", + ), + "summer_mode_active": SwitchSchema( + description="Summer mode active", + message="STATUS", + keep_updated=True, + message_data="flag8_hb_5", + default_mode="restore_default_off", + ), + "dhw_block": SwitchSchema( + description="DHW blocked", + message="STATUS", + keep_updated=True, + message_data="flag8_hb_6", + default_mode="restore_default_off", + ), +} + + +@dataclass +class AutoConfigure: + message: str + message_data: str + + +@dataclass +class InputSchema(EntitySchema): + unit_of_measurement: str + step: float + range: tuple[int, int] + icon: Optional[str] = None + auto_max_value: Optional[AutoConfigure] = None + auto_min_value: Optional[AutoConfigure] = None + + +INPUTS: dict[str, InputSchema] = { + "t_set": InputSchema( + description="Control setpoint: temperature setpoint for the boiler's supply water", + unit_of_measurement=UNIT_CELSIUS, + step=0.1, + message="CH_SETPOINT", + keep_updated=True, + message_data="f88", + range=(0, 100), + auto_max_value=AutoConfigure(message="MAX_CH_SETPOINT", message_data="f88"), + ), + "t_set_ch2": InputSchema( + description="Control setpoint 2: temperature setpoint for the boiler's supply water on the second heating circuit", + unit_of_measurement=UNIT_CELSIUS, + step=0.1, + message="CH2_SETPOINT", + keep_updated=True, + message_data="f88", + range=(0, 100), + auto_max_value=AutoConfigure(message="MAX_CH_SETPOINT", message_data="f88"), + ), + "cooling_control": InputSchema( + description="Cooling control signal", + unit_of_measurement=UNIT_PERCENT, + step=1.0, + message="COOLING_CONTROL", + keep_updated=True, + message_data="f88", + range=(0, 100), + ), + "t_dhw_set": InputSchema( + description="Domestic hot water temperature setpoint", + unit_of_measurement=UNIT_CELSIUS, + step=0.1, + message="DHW_SETPOINT", + keep_updated=True, + message_data="f88", + range=(0, 127), + auto_min_value=AutoConfigure(message="DHW_BOUNDS", message_data="s8_lb"), + auto_max_value=AutoConfigure(message="DHW_BOUNDS", message_data="s8_hb"), + ), + "max_t_set": InputSchema( + description="Maximum allowable CH water setpoint", + unit_of_measurement=UNIT_CELSIUS, + step=0.1, + message="MAX_CH_SETPOINT", + keep_updated=True, + message_data="f88", + range=(0, 127), + auto_min_value=AutoConfigure(message="CH_BOUNDS", message_data="s8_lb"), + auto_max_value=AutoConfigure(message="CH_BOUNDS", message_data="s8_hb"), + ), + "t_room_set": InputSchema( + description="Current room temperature setpoint (informational)", + unit_of_measurement=UNIT_CELSIUS, + step=0.1, + message="ROOM_SETPOINT", + keep_updated=True, + message_data="f88", + range=(-40, 127), + ), + "t_room_set_ch2": InputSchema( + description="Current room temperature setpoint on CH2 (informational)", + unit_of_measurement=UNIT_CELSIUS, + step=0.1, + message="ROOM_SETPOINT_CH2", + keep_updated=True, + message_data="f88", + range=(-40, 127), + ), + "t_room": InputSchema( + description="Current sensed room temperature (informational)", + unit_of_measurement=UNIT_CELSIUS, + step=0.1, + message="ROOM_TEMP", + keep_updated=True, + message_data="f88", + range=(-40, 127), + ), + "max_rel_mod_level": InputSchema( + description="Maximum relative modulation level", + unit_of_measurement=UNIT_PERCENT, + step=1, + icon="mdi:percent", + message="MAX_MODULATION_LEVEL", + keep_updated=True, + message_data="f88", + range=(0, 100), + ), + "otc_hc_ratio": InputSchema( + description="OTC heat curve ratio", + unit_of_measurement=UNIT_CELSIUS, + step=0.1, + message="OTC_CURVE_RATIO", + keep_updated=True, + message_data="f88", + range=(0, 127), + auto_min_value=AutoConfigure(message="OTC_CURVE_BOUNDS", message_data="u8_lb"), + auto_max_value=AutoConfigure(message="OTC_CURVE_BOUNDS", message_data="u8_hb"), + ), +} diff --git a/esphome/components/opentherm/sensor/__init__.py b/esphome/components/opentherm/sensor/__init__.py index 20224e0eda..546a79054b 100644 --- a/esphome/components/opentherm/sensor/__init__.py +++ b/esphome/components/opentherm/sensor/__init__.py @@ -7,6 +7,18 @@ from .. import const, schema, validate, generate DEPENDENCIES = [const.OPENTHERM] COMPONENT_TYPE = const.SENSOR +MSG_DATA_TYPES = { + "u8_lb", + "u8_hb", + "s8_lb", + "s8_hb", + "u8_lb_60", + "u8_hb_60", + "u16", + "s16", + "f88", +} + def get_entity_validation_schema(entity: schema.SensorSchema) -> cv.Schema: return sensor.sensor_schema( @@ -17,6 +29,10 @@ def get_entity_validation_schema(entity: schema.SensorSchema) -> cv.Schema: or sensor._UNDEF, # pylint: disable=protected-access icon=entity.icon or sensor._UNDEF, # pylint: disable=protected-access state_class=entity.state_class, + ).extend( + { + cv.Optional(const.CONF_DATA_TYPE): cv.one_of(*MSG_DATA_TYPES), + } ) diff --git a/esphome/components/opentherm/switch/__init__.py b/esphome/components/opentherm/switch/__init__.py new file mode 100644 index 0000000000..94ec25e36c --- /dev/null +++ b/esphome/components/opentherm/switch/__init__.py @@ -0,0 +1,43 @@ +from typing import Any + +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import switch +from esphome.const import CONF_ID +from .. import const, schema, validate, generate + +DEPENDENCIES = [const.OPENTHERM] +COMPONENT_TYPE = const.SWITCH + +OpenthermSwitch = generate.opentherm_ns.class_( + "OpenthermSwitch", switch.Switch, cg.Component +) + + +async def new_openthermswitch(config: dict[str, Any]) -> cg.Pvariable: + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await switch.register_switch(var, config) + return var + + +def get_entity_validation_schema(entity: schema.SwitchSchema) -> cv.Schema: + return switch.SWITCH_SCHEMA.extend( + {cv.GenerateID(): cv.declare_id(OpenthermSwitch)} + ).extend(cv.COMPONENT_SCHEMA) + + +CONFIG_SCHEMA = validate.create_component_schema( + schema.SWITCHES, get_entity_validation_schema +) + + +async def to_code(config: dict[str, Any]) -> None: + keys = await generate.component_to_code( + COMPONENT_TYPE, + schema.SWITCHES, + OpenthermSwitch, + generate.create_only_conf(new_openthermswitch), + config, + ) + generate.define_readers(COMPONENT_TYPE, keys) diff --git a/esphome/components/opentherm/switch/switch.cpp b/esphome/components/opentherm/switch/switch.cpp new file mode 100644 index 0000000000..228d9ac8f3 --- /dev/null +++ b/esphome/components/opentherm/switch/switch.cpp @@ -0,0 +1,28 @@ +#include "switch.h" + +namespace esphome { +namespace opentherm { + +static const char *const TAG = "opentherm.switch"; + +void OpenthermSwitch::write_state(bool state) { this->publish_state(state); } + +void OpenthermSwitch::setup() { + auto restored = this->get_initial_state_with_restore_mode(); + bool state = false; + if (!restored.has_value()) { + ESP_LOGD(TAG, "Couldn't restore state for OpenTherm switch '%s'", this->get_name().c_str()); + } else { + ESP_LOGD(TAG, "Restored state for OpenTherm switch '%s': %d", this->get_name().c_str(), restored.value()); + state = restored.value(); + } + this->write_state(state); +} + +void OpenthermSwitch::dump_config() { + LOG_SWITCH("", "OpenTherm Switch", this); + ESP_LOGCONFIG(TAG, " Current state: %d", this->state); +} + +} // namespace opentherm +} // namespace esphome diff --git a/esphome/components/opentherm/switch/switch.h b/esphome/components/opentherm/switch/switch.h new file mode 100644 index 0000000000..0c20a0d9ed --- /dev/null +++ b/esphome/components/opentherm/switch/switch.h @@ -0,0 +1,20 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/switch/switch.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace opentherm { + +class OpenthermSwitch : public switch_::Switch, public Component { + protected: + void write_state(bool state) override; + + public: + void setup() override; + void dump_config() override; +}; + +} // namespace opentherm +} // namespace esphome diff --git a/tests/components/opentherm/common.yaml b/tests/components/opentherm/common.yaml index 27cbae280a..744580f18b 100644 --- a/tests/components/opentherm/common.yaml +++ b/tests/components/opentherm/common.yaml @@ -12,10 +12,41 @@ opentherm: cooling_enable: false otc_active: false ch2_active: true + t_room: boiler_sensor summer_mode_active: true dhw_block: true sync_mode: true +output: + - platform: opentherm + t_set: + id: t_set + min_value: 20 + auto_max_value: true + zero_means_zero: true + t_set_ch2: + id: t_set_ch2 + min_value: 20 + max_value: 40 + zero_means_zero: true + +number: + - platform: opentherm + cooling_control: + name: "Boiler Cooling control signal" + t_dhw_set: + name: "Boiler DHW Setpoint" + max_t_set: + name: "Boiler Max Setpoint" + t_room_set: + name: "Boiler Room Setpoint" + t_room_set_ch2: + name: "Boiler Room Setpoint CH2" + max_rel_mod_level: + name: "Maximum relative modulation level" + otc_hc_ratio: + name: "OTC heat curve ratio" + sensor: - platform: opentherm rel_mod_level: @@ -25,6 +56,7 @@ sensor: dhw_flow_rate: name: "Boiler Water flow rate in DHW circuit" t_boiler: + id: "boiler_sensor" name: "Boiler water temperature" t_dhw: name: "Boiler DHW temperature" @@ -74,3 +106,55 @@ sensor: name: "OTC heat curve ratio upper bound" otc_hc_ratio_lb: name: "OTC heat curve ratio lower bound" + +binary_sensor: + - platform: opentherm + fault_indication: + name: "Boiler Fault indication" + ch_active: + name: "Boiler Central Heating active" + dhw_active: + name: "Boiler Domestic Hot Water active" + flame_on: + name: "Boiler Flame on" + cooling_active: + name: "Boiler Cooling active" + ch2_active: + name: "Boiler Central Heating 2 active" + diagnostic_indication: + name: "Boiler Diagnostic event" + dhw_present: + name: "Boiler DHW present" + control_type_on_off: + name: "Boiler Control type is on/off" + cooling_supported: + name: "Boiler Cooling supported" + dhw_storage_tank: + name: "Boiler DHW storage tank" + controller_pump_control_allowed: + name: "Boiler Controller pump control allowed" + ch2_present: + name: "Boiler CH2 present" + dhw_setpoint_transfer_enabled: + name: "Boiler DHW setpoint transfer enabled" + max_ch_setpoint_transfer_enabled: + name: "Boiler CH maximum setpoint transfer enabled" + dhw_setpoint_rw: + name: "Boiler DHW setpoint read/write" + max_ch_setpoint_rw: + name: "Boiler CH maximum setpoint read/write" + +switch: + - platform: opentherm + ch_enable: + name: "Boiler Central Heating enabled" + restore_mode: RESTORE_DEFAULT_ON + dhw_enable: + name: "Boiler Domestic Hot Water enabled" + cooling_enable: + name: "Boiler Cooling enabled" + restore_mode: ALWAYS_OFF + otc_active: + name: "Boiler Outside temperature compensation active" + ch2_active: + name: "Boiler Central Heating 2 active" From 928b39f4950536ff2d6501da8c35c139ea650b8b Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Tue, 12 Nov 2024 13:20:12 -0500 Subject: [PATCH 37/46] [i2s_audio] I2S speaker improvements (#7749) --- .../components/i2s_audio/speaker/__init__.py | 13 ++- .../i2s_audio/speaker/i2s_audio_speaker.cpp | 106 +++++++++--------- .../i2s_audio/speaker/i2s_audio_speaker.h | 12 +- 3 files changed, 72 insertions(+), 59 deletions(-) diff --git a/esphome/components/i2s_audio/speaker/__init__.py b/esphome/components/i2s_audio/speaker/__init__.py index dd43d6cb39..0355c16321 100644 --- a/esphome/components/i2s_audio/speaker/__init__.py +++ b/esphome/components/i2s_audio/speaker/__init__.py @@ -24,9 +24,10 @@ I2SAudioSpeaker = i2s_audio_ns.class_( "I2SAudioSpeaker", cg.Component, speaker.Speaker, I2SAudioOut ) - +CONF_BUFFER_DURATION = "buffer_duration" CONF_DAC_TYPE = "dac_type" CONF_I2S_COMM_FMT = "i2s_comm_fmt" +CONF_NEVER = "never" i2s_dac_mode_t = cg.global_ns.enum("i2s_dac_mode_t") INTERNAL_DAC_OPTIONS = { @@ -73,8 +74,12 @@ BASE_SCHEMA = ( .extend( { cv.Optional( - CONF_TIMEOUT, default="500ms" + CONF_BUFFER_DURATION, default="500ms" ): cv.positive_time_period_milliseconds, + cv.Optional(CONF_TIMEOUT, default="500ms"): cv.Any( + cv.positive_time_period_milliseconds, + cv.one_of(CONF_NEVER, lower=True), + ), } ) .extend(cv.COMPONENT_SCHEMA) @@ -116,4 +121,6 @@ async def to_code(config): else: cg.add(var.set_dout_pin(config[CONF_I2S_DOUT_PIN])) cg.add(var.set_i2s_comm_fmt(config[CONF_I2S_COMM_FMT])) - cg.add(var.set_timeout(config[CONF_TIMEOUT])) + if config[CONF_TIMEOUT] != CONF_NEVER: + cg.add(var.set_timeout(config[CONF_TIMEOUT])) + cg.add(var.set_buffer_duration(config[CONF_BUFFER_DURATION])) diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp index cf6c3bbbba..c3f4566411 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp @@ -13,21 +13,22 @@ namespace esphome { namespace i2s_audio { -static const size_t DMA_BUFFER_SIZE = 512; +static const uint8_t DMA_BUFFER_DURATION_MS = 15; static const size_t DMA_BUFFERS_COUNT = 4; -static const size_t FRAMES_IN_ALL_DMA_BUFFERS = DMA_BUFFER_SIZE * DMA_BUFFERS_COUNT; -static const size_t RING_BUFFER_SAMPLES = 8192; -static const size_t TASK_DELAY_MS = 10; + +static const size_t TASK_DELAY_MS = DMA_BUFFER_DURATION_MS * DMA_BUFFERS_COUNT / 2; + static const size_t TASK_STACK_SIZE = 4096; static const ssize_t TASK_PRIORITY = 23; +static const size_t I2S_EVENT_QUEUE_COUNT = DMA_BUFFERS_COUNT + 1; + static const char *const TAG = "i2s_audio.speaker"; enum SpeakerEventGroupBits : uint32_t { - COMMAND_START = (1 << 0), // Starts the main task purpose - COMMAND_STOP = (1 << 1), // stops the main task - COMMAND_STOP_GRACEFULLY = (1 << 2), // Stops the task once all data has been written - MESSAGE_RING_BUFFER_AVAILABLE_TO_WRITE = (1 << 5), // Locks the ring buffer when not set + COMMAND_START = (1 << 0), // starts the speaker task + COMMAND_STOP = (1 << 1), // stops the speaker task + COMMAND_STOP_GRACEFULLY = (1 << 2), // Stops the speaker task once all data has been written STATE_STARTING = (1 << 10), STATE_RUNNING = (1 << 11), STATE_STOPPING = (1 << 12), @@ -91,15 +92,21 @@ static const std::vector Q15_VOLUME_SCALING_FACTORS = { void I2SAudioSpeaker::setup() { ESP_LOGCONFIG(TAG, "Setting up I2S Audio Speaker..."); - if (this->event_group_ == nullptr) { - this->event_group_ = xEventGroupCreate(); - } + this->event_group_ = xEventGroupCreate(); if (this->event_group_ == nullptr) { ESP_LOGE(TAG, "Failed to create event group"); this->mark_failed(); return; } + + this->i2s_event_queue_ = xQueueCreate(I2S_EVENT_QUEUE_COUNT, sizeof(i2s_event_t)); + + if (this->i2s_event_queue_ == nullptr) { + ESP_LOGE(TAG, "Failed to create I2S event queue"); + this->mark_failed(); + return; + } } void I2SAudioSpeaker::loop() { @@ -199,23 +206,17 @@ size_t I2SAudioSpeaker::play(const uint8_t *data, size_t length, TickType_t tick this->start(); } - // Wait for the ring buffer to be available - uint32_t event_bits = - xEventGroupWaitBits(this->event_group_, SpeakerEventGroupBits::MESSAGE_RING_BUFFER_AVAILABLE_TO_WRITE, pdFALSE, - pdFALSE, pdMS_TO_TICKS(TASK_DELAY_MS)); + size_t bytes_written = 0; + if ((this->state_ == speaker::STATE_RUNNING) && (this->audio_ring_buffer_.use_count() == 1)) { + // Only one owner of the ring buffer (the speaker task), so the ring buffer is allocated and no other components are + // attempting to write to it. - if (event_bits & SpeakerEventGroupBits::MESSAGE_RING_BUFFER_AVAILABLE_TO_WRITE) { - // Ring buffer is available to write - - // Lock the ring buffer, write to it, then unlock it - xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::MESSAGE_RING_BUFFER_AVAILABLE_TO_WRITE); - size_t bytes_written = this->audio_ring_buffer_->write_without_replacement((void *) data, length, ticks_to_wait); - xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::MESSAGE_RING_BUFFER_AVAILABLE_TO_WRITE); - - return bytes_written; + // Temporarily share ownership of the ring buffer so it won't be deallocated while writing + std::shared_ptr temp_ring_buffer = this->audio_ring_buffer_; + bytes_written = temp_ring_buffer->write_without_replacement((void *) data, length, ticks_to_wait); } - return 0; + return bytes_written; } bool I2SAudioSpeaker::has_buffered_data() const { @@ -246,10 +247,12 @@ void I2SAudioSpeaker::speaker_task(void *params) { const ssize_t bytes_per_sample = audio_stream_info.get_bytes_per_sample(); const uint8_t number_of_channels = audio_stream_info.channels; - const size_t dma_buffers_size = FRAMES_IN_ALL_DMA_BUFFERS * bytes_per_sample * number_of_channels; + const size_t dma_buffers_size = DMA_BUFFERS_COUNT * DMA_BUFFER_DURATION_MS * this_speaker->sample_rate_ / 1000 * + bytes_per_sample * number_of_channels; + const size_t ring_buffer_size = + this_speaker->buffer_duration_ms_ * this_speaker->sample_rate_ / 1000 * bytes_per_sample * number_of_channels; - if (this_speaker->send_esp_err_to_event_group_( - this_speaker->allocate_buffers_(dma_buffers_size, RING_BUFFER_SAMPLES * bytes_per_sample))) { + if (this_speaker->send_esp_err_to_event_group_(this_speaker->allocate_buffers_(dma_buffers_size, ring_buffer_size))) { // Failed to allocate buffers xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::ERR_ESP_NO_MEM); this_speaker->delete_task_(dma_buffers_size); @@ -258,9 +261,6 @@ void I2SAudioSpeaker::speaker_task(void *params) { if (this_speaker->send_esp_err_to_event_group_(this_speaker->start_i2s_driver_())) { // Failed to start I2S driver this_speaker->delete_task_(dma_buffers_size); - } else { - // Ring buffer is allocated, so indicate its can be written to - xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::MESSAGE_RING_BUFFER_AVAILABLE_TO_WRITE); } if (!this_speaker->send_esp_err_to_event_group_(this_speaker->reconfigure_i2s_stream_info_(audio_stream_info))) { @@ -270,8 +270,10 @@ void I2SAudioSpeaker::speaker_task(void *params) { bool stop_gracefully = false; uint32_t last_data_received_time = millis(); + bool tx_dma_underflow = false; - while ((millis() - last_data_received_time) <= this_speaker->timeout_) { + while (!this_speaker->timeout_.has_value() || + (millis() - last_data_received_time) <= this_speaker->timeout_.value()) { event_group_bits = xEventGroupGetBits(this_speaker->event_group_); if (event_group_bits & SpeakerEventGroupBits::COMMAND_STOP) { @@ -281,12 +283,18 @@ void I2SAudioSpeaker::speaker_task(void *params) { stop_gracefully = true; } + i2s_event_t i2s_event; + while (xQueueReceive(this_speaker->i2s_event_queue_, &i2s_event, 0)) { + if (i2s_event.type == I2S_EVENT_TX_Q_OVF) { + tx_dma_underflow = true; + } + } + size_t bytes_to_read = dma_buffers_size; size_t bytes_read = this_speaker->audio_ring_buffer_->read((void *) this_speaker->data_buffer_, bytes_to_read, pdMS_TO_TICKS(TASK_DELAY_MS)); if (bytes_read > 0) { - last_data_received_time = millis(); size_t bytes_written = 0; if ((audio_stream_info.bits_per_sample == 16) && (this_speaker->q15_volume_factor_ < INT16_MAX)) { @@ -307,15 +315,13 @@ void I2SAudioSpeaker::speaker_task(void *params) { if (bytes_written != bytes_read) { xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::ERR_ESP_INVALID_SIZE); } - + tx_dma_underflow = false; + last_data_received_time = millis(); } else { // No data received - - if (stop_gracefully) { + if (stop_gracefully && tx_dma_underflow) { break; } - - i2s_zero_dma_buffer(this_speaker->parent_->get_port()); } } } else { @@ -326,7 +332,6 @@ void I2SAudioSpeaker::speaker_task(void *params) { xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::STATE_STOPPING); - i2s_stop(this_speaker->parent_->get_port()); i2s_driver_uninstall(this_speaker->parent_->get_port()); this_speaker->parent_->unlock(); @@ -402,8 +407,8 @@ esp_err_t I2SAudioSpeaker::allocate_buffers_(size_t data_buffer_size, size_t rin return ESP_ERR_NO_MEM; } - if (this->audio_ring_buffer_ == nullptr) { - // Allocate ring buffer + if (this->audio_ring_buffer_.use_count() == 0) { + // Allocate ring buffer. Uses a shared_ptr to ensure it isn't improperly deallocated. this->audio_ring_buffer_ = RingBuffer::create(ring_buffer_size); } @@ -419,6 +424,8 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver_() { return ESP_ERR_INVALID_STATE; } + int dma_buffer_length = DMA_BUFFER_DURATION_MS * this->sample_rate_ / 1000; + i2s_driver_config_t config = { .mode = (i2s_mode_t) (this->i2s_mode_ | I2S_MODE_TX), .sample_rate = this->sample_rate_, @@ -427,7 +434,7 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver_() { .communication_format = this->i2s_comm_fmt_, .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, .dma_buf_count = DMA_BUFFERS_COUNT, - .dma_buf_len = DMA_BUFFER_SIZE, + .dma_buf_len = dma_buffer_length, .use_apll = this->use_apll_, .tx_desc_auto_clear = true, .fixed_mclk = I2S_PIN_NO_CHANGE, @@ -448,7 +455,8 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver_() { } #endif - esp_err_t err = i2s_driver_install(this->parent_->get_port(), &config, 0, nullptr); + esp_err_t err = + i2s_driver_install(this->parent_->get_port(), &config, I2S_EVENT_QUEUE_COUNT, &this->i2s_event_queue_); if (err != ESP_OK) { // Failed to install the driver, so unlock the I2S port this->parent_->unlock(); @@ -502,16 +510,7 @@ esp_err_t I2SAudioSpeaker::reconfigure_i2s_stream_info_(audio::AudioStreamInfo & } void I2SAudioSpeaker::delete_task_(size_t buffer_size) { - if (this->audio_ring_buffer_ != nullptr) { - xEventGroupWaitBits(this->event_group_, - MESSAGE_RING_BUFFER_AVAILABLE_TO_WRITE, // Bit message to read - pdFALSE, // Don't clear the bits on exit - pdTRUE, // Don't wait for all the bits, - portMAX_DELAY); // Block indefinitely until a command bit is set - - this->audio_ring_buffer_.reset(); // Deallocates the ring buffer stored in the unique_ptr - this->audio_ring_buffer_ = nullptr; - } + this->audio_ring_buffer_.reset(); // Releases onwership of the shared_ptr if (this->data_buffer_ != nullptr) { ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); @@ -520,6 +519,7 @@ void I2SAudioSpeaker::delete_task_(size_t buffer_size) { } xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::STATE_STOPPED); + xQueueReset(this->i2s_event_queue_); this->task_created_ = false; vTaskDelete(nullptr); diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h index 3c512d4d4d..8b7386ba58 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h @@ -7,6 +7,7 @@ #include #include +#include #include #include "esphome/components/audio/audio.h" @@ -27,6 +28,7 @@ class I2SAudioSpeaker : public I2SAudioOut, public speaker::Speaker, public Comp void setup() override; void loop() override; + void set_buffer_duration(uint32_t buffer_duration_ms) { this->buffer_duration_ms_ = buffer_duration_ms; } void set_timeout(uint32_t ms) { this->timeout_ = ms; } void set_dout_pin(uint8_t pin) { this->dout_pin_ = pin; } #if SOC_I2S_SUPPORTS_DAC @@ -117,10 +119,14 @@ class I2SAudioSpeaker : public I2SAudioOut, public speaker::Speaker, public Comp TaskHandle_t speaker_task_handle_{nullptr}; EventGroupHandle_t event_group_{nullptr}; - uint8_t *data_buffer_; - std::unique_ptr audio_ring_buffer_; + QueueHandle_t i2s_event_queue_; - uint32_t timeout_; + uint8_t *data_buffer_; + std::shared_ptr audio_ring_buffer_; + + uint32_t buffer_duration_ms_; + + optional timeout_; uint8_t dout_pin_; bool task_created_{false}; From 1e80c4807eaab36d92c732a303664d29576d9e06 Mon Sep 17 00:00:00 2001 From: FreeBear-nc <67865163+FreeBear-nc@users.noreply.github.com> Date: Tue, 12 Nov 2024 18:20:48 +0000 Subject: [PATCH 38/46] Message to string extend (#7755) --- esphome/components/opentherm/hub.cpp | 4 ++-- esphome/components/opentherm/opentherm.cpp | 27 ++++++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/esphome/components/opentherm/hub.cpp b/esphome/components/opentherm/hub.cpp index 432036d58d..dfa8ea95c5 100644 --- a/esphome/components/opentherm/hub.cpp +++ b/esphome/components/opentherm/hub.cpp @@ -371,11 +371,11 @@ void OpenthermHub::dump_config() { ESP_LOGCONFIG(TAG, " Numbers: %s", SHOW(OPENTHERM_NUMBER_LIST(ID, ))); ESP_LOGCONFIG(TAG, " Initial requests:"); for (auto type : this->initial_messages_) { - ESP_LOGCONFIG(TAG, " - %d", type); + ESP_LOGCONFIG(TAG, " - %d (%s)", type, this->opentherm_->message_id_to_str((type))); } ESP_LOGCONFIG(TAG, " Repeating requests:"); for (auto type : this->repeating_messages_) { - ESP_LOGCONFIG(TAG, " - %d", type); + ESP_LOGCONFIG(TAG, " - %d (%s)", type, this->opentherm_->message_id_to_str((type))); } } diff --git a/esphome/components/opentherm/opentherm.cpp b/esphome/components/opentherm/opentherm.cpp index 4a23bb94cf..26c707f9a0 100644 --- a/esphome/components/opentherm/opentherm.cpp +++ b/esphome/components/opentherm/opentherm.cpp @@ -483,6 +483,8 @@ const char *OpenTherm::message_id_to_str(MessageId id) { TO_STRING_MEMBER(EXHAUST_TEMP) TO_STRING_MEMBER(FAN_SPEED) TO_STRING_MEMBER(FLAME_CURRENT) + TO_STRING_MEMBER(ROOM_TEMP_CH2) + TO_STRING_MEMBER(REL_HUMIDITY) TO_STRING_MEMBER(DHW_BOUNDS) TO_STRING_MEMBER(CH_BOUNDS) TO_STRING_MEMBER(OTC_CURVE_BOUNDS) @@ -492,14 +494,39 @@ const char *OpenTherm::message_id_to_str(MessageId id) { TO_STRING_MEMBER(HVAC_STATUS) TO_STRING_MEMBER(REL_VENT_SETPOINT) TO_STRING_MEMBER(DEVICE_VENT) + TO_STRING_MEMBER(HVAC_VER_ID) TO_STRING_MEMBER(REL_VENTILATION) TO_STRING_MEMBER(REL_HUMID_EXHAUST) + TO_STRING_MEMBER(EXHAUST_CO2) TO_STRING_MEMBER(SUPPLY_INLET_TEMP) TO_STRING_MEMBER(SUPPLY_OUTLET_TEMP) TO_STRING_MEMBER(EXHAUST_INLET_TEMP) TO_STRING_MEMBER(EXHAUST_OUTLET_TEMP) + TO_STRING_MEMBER(EXHAUST_FAN_SPEED) + TO_STRING_MEMBER(SUPPLY_FAN_SPEED) + TO_STRING_MEMBER(REMOTE_VENTILATION_PARAM) TO_STRING_MEMBER(NOM_REL_VENTILATION) + TO_STRING_MEMBER(HVAC_NUM_TSP) + TO_STRING_MEMBER(HVAC_IDX_TSP) + TO_STRING_MEMBER(HVAC_FHB_SIZE) + TO_STRING_MEMBER(HVAC_FHB_IDX) + TO_STRING_MEMBER(RF_SIGNAL) + TO_STRING_MEMBER(DHW_MODE) TO_STRING_MEMBER(OVERRIDE_FUNC) + TO_STRING_MEMBER(SOLAR_MODE_FLAGS) + TO_STRING_MEMBER(SOLAR_ASF) + TO_STRING_MEMBER(SOLAR_VERSION_ID) + TO_STRING_MEMBER(SOLAR_PRODUCT_ID) + TO_STRING_MEMBER(SOLAR_NUM_TSP) + TO_STRING_MEMBER(SOLAR_IDX_TSP) + TO_STRING_MEMBER(SOLAR_FHB_SIZE) + TO_STRING_MEMBER(SOLAR_FHB_IDX) + TO_STRING_MEMBER(SOLAR_STARTS) + TO_STRING_MEMBER(SOLAR_HOURS) + TO_STRING_MEMBER(SOLAR_ENERGY) + TO_STRING_MEMBER(SOLAR_TOTAL_ENERGY) + TO_STRING_MEMBER(FAILED_BURNER_STARTS) + TO_STRING_MEMBER(BURNER_FLAME_LOW) TO_STRING_MEMBER(OEM_DIAGNOSTIC) TO_STRING_MEMBER(BURNER_STARTS) TO_STRING_MEMBER(CH_PUMP_STARTS) From e6a1254e65d69ae0f362891409e7085768b6a479 Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Tue, 12 Nov 2024 19:23:00 +0100 Subject: [PATCH 39/46] [sun] Implements `is_above_horizon()` (#7754) --- esphome/components/sun/sun.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/sun/sun.h b/esphome/components/sun/sun.h index de4801a655..77d62d34c3 100644 --- a/esphome/components/sun/sun.h +++ b/esphome/components/sun/sun.h @@ -59,6 +59,9 @@ class Sun { void set_latitude(double latitude) { location_.latitude = latitude; } void set_longitude(double longitude) { location_.longitude = longitude; } + // Check if the sun is above the horizon, with a default elevation angle of -0.83333 (standard for sunrise/set). + bool is_above_horizon(double elevation = -0.83333) { return this->elevation() > elevation; } + optional sunrise(double elevation); optional sunset(double elevation); optional sunrise(ESPTime date, double elevation); From b367c01b4b27ac19c75d75774c7ce162894d6035 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Tue, 12 Nov 2024 13:48:03 -0500 Subject: [PATCH 40/46] [core] Ring buffer write functions use const pointer parameter (#7750) --- esphome/core/ring_buffer.cpp | 4 ++-- esphome/core/ring_buffer.h | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/core/ring_buffer.cpp b/esphome/core/ring_buffer.cpp index f97c686684..6152ada314 100644 --- a/esphome/core/ring_buffer.cpp +++ b/esphome/core/ring_buffer.cpp @@ -46,7 +46,7 @@ size_t RingBuffer::read(void *data, size_t len, TickType_t ticks_to_wait) { return bytes_read; } -size_t RingBuffer::write(void *data, size_t len) { +size_t RingBuffer::write(const void *data, size_t len) { size_t free = this->free(); if (free < len) { size_t needed = len - free; @@ -56,7 +56,7 @@ size_t RingBuffer::write(void *data, size_t len) { return xStreamBufferSend(this->handle_, data, len, 0); } -size_t RingBuffer::write_without_replacement(void *data, size_t len, TickType_t ticks_to_wait) { +size_t RingBuffer::write_without_replacement(const void *data, size_t len, TickType_t ticks_to_wait) { return xStreamBufferSend(this->handle_, data, len, ticks_to_wait); } diff --git a/esphome/core/ring_buffer.h b/esphome/core/ring_buffer.h index c0511fb52e..aade1b5f49 100644 --- a/esphome/core/ring_buffer.h +++ b/esphome/core/ring_buffer.h @@ -37,7 +37,7 @@ class RingBuffer { * @param len Number of bytes to write * @return Number of bytes written */ - size_t write(void *data, size_t len); + size_t write(const void *data, size_t len); /** * @brief Writes to the ring buffer without overwriting oldest data. @@ -50,7 +50,7 @@ class RingBuffer { * @param ticks_to_wait Maximum number of FreeRTOS ticks to wait (default: 0) * @return Number of bytes written */ - size_t write_without_replacement(void *data, size_t len, TickType_t ticks_to_wait = 0); + size_t write_without_replacement(const void *data, size_t len, TickType_t ticks_to_wait = 0); /** * @brief Returns the number of available bytes in the ring buffer. From 7d75c9157bd9ddaa7c56389d6b3126a461b1e52b Mon Sep 17 00:00:00 2001 From: TFGF Date: Tue, 12 Nov 2024 17:48:40 -0300 Subject: [PATCH 41/46] [Modbus Controller] Added `on_online` and `on_offline` automation (#7417) --- .../components/modbus_controller/__init__.py | 32 ++++++++++++++++++- .../components/modbus_controller/automation.h | 16 ++++++++++ esphome/components/modbus_controller/const.py | 2 ++ .../modbus_controller/modbus_controller.cpp | 16 ++++++++-- .../modbus_controller/modbus_controller.h | 9 ++++++ .../modbus_controller/test.esp32-ard.yaml | 3 ++ .../modbus_controller/test.esp32-idf.yaml | 3 ++ 7 files changed, 78 insertions(+), 3 deletions(-) diff --git a/esphome/components/modbus_controller/__init__.py b/esphome/components/modbus_controller/__init__.py index 488baa245a..5c407d6fff 100644 --- a/esphome/components/modbus_controller/__init__.py +++ b/esphome/components/modbus_controller/__init__.py @@ -25,6 +25,8 @@ from .const import ( CONF_MODBUS_CONTROLLER_ID, CONF_OFFLINE_SKIP_UPDATES, CONF_ON_COMMAND_SENT, + CONF_ON_ONLINE, + CONF_ON_OFFLINE, CONF_REGISTER_COUNT, CONF_REGISTER_TYPE, CONF_RESPONSE_SIZE, @@ -114,6 +116,14 @@ ModbusCommandSentTrigger = modbus_controller_ns.class_( "ModbusCommandSentTrigger", automation.Trigger.template(cg.int_, cg.int_) ) +ModbusOnlineTrigger = modbus_controller_ns.class_( + "ModbusOnlineTrigger", automation.Trigger.template(cg.int_, cg.int_) +) + +ModbusOfflineTrigger = modbus_controller_ns.class_( + "ModbusOfflineTrigger", automation.Trigger.template(cg.int_, cg.int_) +) + _LOGGER = logging.getLogger(__name__) ModbusServerRegisterSchema = cv.Schema( @@ -146,6 +156,16 @@ CONFIG_SCHEMA = cv.All( ), } ), + cv.Optional(CONF_ON_ONLINE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ModbusOnlineTrigger), + } + ), + cv.Optional(CONF_ON_OFFLINE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ModbusOnlineTrigger), + } + ), } ) .extend(cv.polling_component_schema("60s")) @@ -284,7 +304,17 @@ async def to_code(config): for conf in config.get(CONF_ON_COMMAND_SENT, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation( - trigger, [(int, "function_code"), (int, "address")], conf + trigger, [(cg.int_, "function_code"), (cg.int_, "address")], conf + ) + for conf in config.get(CONF_ON_ONLINE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation( + trigger, [(cg.int_, "function_code"), (cg.int_, "address")], conf + ) + for conf in config.get(CONF_ON_OFFLINE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation( + trigger, [(cg.int_, "function_code"), (cg.int_, "address")], conf ) diff --git a/esphome/components/modbus_controller/automation.h b/esphome/components/modbus_controller/automation.h index ad8de4b05d..b3338192cc 100644 --- a/esphome/components/modbus_controller/automation.h +++ b/esphome/components/modbus_controller/automation.h @@ -15,5 +15,21 @@ class ModbusCommandSentTrigger : public Trigger { } }; +class ModbusOnlineTrigger : public Trigger { + public: + ModbusOnlineTrigger(ModbusController *a_modbuscontroller) { + a_modbuscontroller->add_on_online_callback( + [this](int function_code, int address) { this->trigger(function_code, address); }); + } +}; + +class ModbusOfflineTrigger : public Trigger { + public: + ModbusOfflineTrigger(ModbusController *a_modbuscontroller) { + a_modbuscontroller->add_on_offline_callback( + [this](int function_code, int address) { this->trigger(function_code, address); }); + } +}; + } // namespace modbus_controller } // namespace esphome diff --git a/esphome/components/modbus_controller/const.py b/esphome/components/modbus_controller/const.py index 5cf7d230f1..4d39e48dcd 100644 --- a/esphome/components/modbus_controller/const.py +++ b/esphome/components/modbus_controller/const.py @@ -9,6 +9,8 @@ CONF_MAX_CMD_RETRIES = "max_cmd_retries" CONF_MODBUS_CONTROLLER_ID = "modbus_controller_id" CONF_MODBUS_FUNCTIONCODE = "modbus_functioncode" CONF_ON_COMMAND_SENT = "on_command_sent" +CONF_ON_ONLINE = "on_online" +CONF_ON_OFFLINE = "on_offline" CONF_RAW_ENCODE = "raw_encode" CONF_REGISTER_COUNT = "register_count" CONF_REGISTER_TYPE = "register_type" diff --git a/esphome/components/modbus_controller/modbus_controller.cpp b/esphome/components/modbus_controller/modbus_controller.cpp index 1dcb533629..e1102516ca 100644 --- a/esphome/components/modbus_controller/modbus_controller.cpp +++ b/esphome/components/modbus_controller/modbus_controller.cpp @@ -32,8 +32,10 @@ bool ModbusController::send_next_command_() { r.skip_updates_counter = this->offline_skip_updates_; } } + + this->module_offline_ = true; + this->offline_callback_.call((int) command->function_code, command->register_address); } - this->module_offline_ = true; ESP_LOGD(TAG, "Modbus command to device=%d register=0x%02X no response received - removed from send queue", this->address_, command->register_address); this->command_queue_.pop_front(); @@ -68,8 +70,10 @@ void ModbusController::on_modbus_data(const std::vector &data) { r.skip_updates_counter = 0; } } + // Restore module online state + this->module_offline_ = false; + this->online_callback_.call((int) current_command->function_code, current_command->register_address); } - this->module_offline_ = false; // Move the commandItem to the response queue current_command->payload = data; @@ -670,5 +674,13 @@ void ModbusController::add_on_command_sent_callback(std::functioncommand_sent_callback_.add(std::move(callback)); } +void ModbusController::add_on_online_callback(std::function &&callback) { + this->online_callback_.add(std::move(callback)); +} + +void ModbusController::add_on_offline_callback(std::function &&callback) { + this->offline_callback_.add(std::move(callback)); +} + } // namespace modbus_controller } // namespace esphome diff --git a/esphome/components/modbus_controller/modbus_controller.h b/esphome/components/modbus_controller/modbus_controller.h index 1fa35e1535..2a0b936bf5 100644 --- a/esphome/components/modbus_controller/modbus_controller.h +++ b/esphome/components/modbus_controller/modbus_controller.h @@ -468,6 +468,10 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice { bool get_module_offline() { return module_offline_; } /// Set callback for commands void add_on_command_sent_callback(std::function &&callback); + /// Set callback for online changes + void add_on_online_callback(std::function &&callback); + /// Set callback for offline changes + void add_on_offline_callback(std::function &&callback); /// called by esphome generated code to set the max_cmd_retries. void set_max_cmd_retries(uint8_t max_cmd_retries) { this->max_cmd_retries_ = max_cmd_retries; } /// get how many times a command will be (re)sent if no response is received @@ -508,7 +512,12 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice { uint16_t offline_skip_updates_; /// How many times we will retry a command if we get no response uint8_t max_cmd_retries_{4}; + /// Command sent callback CallbackManager command_sent_callback_{}; + /// Server online callback + CallbackManager online_callback_{}; + /// Server offline callback + CallbackManager offline_callback_{}; }; /** Convert vector response payload to float. diff --git a/tests/components/modbus_controller/test.esp32-ard.yaml b/tests/components/modbus_controller/test.esp32-ard.yaml index cd95d149cb..f5c5c10125 100644 --- a/tests/components/modbus_controller/test.esp32-ard.yaml +++ b/tests/components/modbus_controller/test.esp32-ard.yaml @@ -21,6 +21,9 @@ modbus_controller: address: 0x2 modbus_id: mod_bus1 allow_duplicate_commands: false + on_online: + then: + logger.log: "Module Online" - id: modbus_controller2 address: 0x2 modbus_id: mod_bus2 diff --git a/tests/components/modbus_controller/test.esp32-idf.yaml b/tests/components/modbus_controller/test.esp32-idf.yaml index ba28e94d73..0e1849dd88 100644 --- a/tests/components/modbus_controller/test.esp32-idf.yaml +++ b/tests/components/modbus_controller/test.esp32-idf.yaml @@ -13,4 +13,7 @@ modbus_controller: address: 0x2 modbus_id: mod_bus1 allow_duplicate_commands: true + on_offline: + then: + logger.log: "Module Offline" max_cmd_retries: 10 From 053465d3f627809fc890eb94271419df0368f369 Mon Sep 17 00:00:00 2001 From: Kyle Cascade Date: Tue, 12 Nov 2024 14:54:25 -0800 Subject: [PATCH 42/46] Updated dfplayer logging to be more user-friendly (#7740) --- esphome/components/dfplayer/dfplayer.cpp | 138 ++++++++++++++++++++++- esphome/components/dfplayer/dfplayer.h | 72 ++++-------- 2 files changed, 151 insertions(+), 59 deletions(-) diff --git a/esphome/components/dfplayer/dfplayer.cpp b/esphome/components/dfplayer/dfplayer.cpp index aa2dc260e0..98c3e91e46 100644 --- a/esphome/components/dfplayer/dfplayer.cpp +++ b/esphome/components/dfplayer/dfplayer.cpp @@ -6,7 +6,104 @@ namespace dfplayer { static const char *const TAG = "dfplayer"; +void DFPlayer::next() { + this->ack_set_is_playing_ = true; + ESP_LOGD(TAG, "Playing next track"); + this->send_cmd_(0x01); +} + +void DFPlayer::previous() { + this->ack_set_is_playing_ = true; + ESP_LOGD(TAG, "Playing previous track"); + this->send_cmd_(0x02); +} +void DFPlayer::play_mp3(uint16_t file) { + this->ack_set_is_playing_ = true; + ESP_LOGD(TAG, "Playing file %d in mp3 folder", file); + this->send_cmd_(0x12, file); +} + +void DFPlayer::play_file(uint16_t file) { + this->ack_set_is_playing_ = true; + ESP_LOGD(TAG, "Playing file %d", file); + this->send_cmd_(0x03, file); +} + +void DFPlayer::play_file_loop(uint16_t file) { + this->ack_set_is_playing_ = true; + ESP_LOGD(TAG, "Playing file %d in loop", file); + this->send_cmd_(0x08, file); +} + +void DFPlayer::play_folder_loop(uint16_t folder) { + this->ack_set_is_playing_ = true; + ESP_LOGD(TAG, "Playing folder %d in loop", folder); + this->send_cmd_(0x17, folder); +} + +void DFPlayer::volume_up() { + ESP_LOGD(TAG, "Increasing volume"); + this->send_cmd_(0x04); +} + +void DFPlayer::volume_down() { + ESP_LOGD(TAG, "Decreasing volume"); + this->send_cmd_(0x05); +} + +void DFPlayer::set_device(Device device) { + ESP_LOGD(TAG, "Setting device to %d", device); + this->send_cmd_(0x09, device); +} + +void DFPlayer::set_volume(uint8_t volume) { + ESP_LOGD(TAG, "Setting volume to %d", volume); + this->send_cmd_(0x06, volume); +} + +void DFPlayer::set_eq(EqPreset preset) { + ESP_LOGD(TAG, "Setting EQ to %d", preset); + this->send_cmd_(0x07, preset); +} + +void DFPlayer::sleep() { + this->ack_reset_is_playing_ = true; + ESP_LOGD(TAG, "Putting DFPlayer to sleep"); + this->send_cmd_(0x0A); +} + +void DFPlayer::reset() { + this->ack_reset_is_playing_ = true; + ESP_LOGD(TAG, "Resetting DFPlayer"); + this->send_cmd_(0x0C); +} + +void DFPlayer::start() { + this->ack_set_is_playing_ = true; + ESP_LOGD(TAG, "Starting playback"); + this->send_cmd_(0x0D); +} + +void DFPlayer::pause() { + this->ack_reset_is_playing_ = true; + ESP_LOGD(TAG, "Pausing playback"); + this->send_cmd_(0x0E); +} + +void DFPlayer::stop() { + this->ack_reset_is_playing_ = true; + ESP_LOGD(TAG, "Stopping playback"); + this->send_cmd_(0x16); +} + +void DFPlayer::random() { + this->ack_set_is_playing_ = true; + ESP_LOGD(TAG, "Playing random file"); + this->send_cmd_(0x18); +} + void DFPlayer::play_folder(uint16_t folder, uint16_t file) { + ESP_LOGD(TAG, "Playing file %d in folder %d", file, folder); if (folder < 100 && file < 256) { this->ack_set_is_playing_ = true; this->send_cmd_(0x0F, (uint8_t) folder, (uint8_t) file); @@ -29,7 +126,7 @@ void DFPlayer::send_cmd_(uint8_t cmd, uint16_t argument) { this->sent_cmd_ = cmd; - ESP_LOGD(TAG, "Send Command %#02x arg %#04x", cmd, argument); + ESP_LOGV(TAG, "Send Command %#02x arg %#04x", cmd, argument); this->write_array(buffer, 10); } @@ -101,9 +198,37 @@ void DFPlayer::loop() { ESP_LOGV(TAG, "Nack"); this->ack_set_is_playing_ = false; this->ack_reset_is_playing_ = false; - if (argument == 6) { - ESP_LOGV(TAG, "File not found"); - this->is_playing_ = false; + switch (argument) { + case 0x01: + ESP_LOGE(TAG, "Module is busy or uninitialized"); + break; + case 0x02: + ESP_LOGE(TAG, "Module is in sleep mode"); + break; + case 0x03: + ESP_LOGE(TAG, "Serial receive error"); + break; + case 0x04: + ESP_LOGE(TAG, "Checksum incorrect"); + break; + case 0x05: + ESP_LOGE(TAG, "Specified track is out of current track scope"); + this->is_playing_ = false; + break; + case 0x06: + ESP_LOGE(TAG, "Specified track is not found"); + this->is_playing_ = false; + break; + case 0x07: + ESP_LOGE(TAG, "Insertion error (an inserting operation only can be done when a track is being played)"); + break; + case 0x08: + ESP_LOGE(TAG, "SD card reading failed (SD card pulled out or damaged)"); + break; + case 0x09: + ESP_LOGE(TAG, "Entered into sleep mode"); + this->is_playing_ = false; + break; } break; case 0x41: @@ -113,12 +238,13 @@ void DFPlayer::loop() { this->ack_set_is_playing_ = false; this->ack_reset_is_playing_ = false; break; - case 0x3D: // Playback finished + case 0x3D: + ESP_LOGV(TAG, "Playback finished"); this->is_playing_ = false; this->on_finished_playback_callback_.call(); break; default: - ESP_LOGD(TAG, "Command %#02x arg %#04x", cmd, argument); + ESP_LOGV(TAG, "Received unknown cmd %#02x arg %#04x", cmd, argument); } this->sent_cmd_ = 0; this->read_pos_ = 0; diff --git a/esphome/components/dfplayer/dfplayer.h b/esphome/components/dfplayer/dfplayer.h index 26e90fd410..d2ec0a2310 100644 --- a/esphome/components/dfplayer/dfplayer.h +++ b/esphome/components/dfplayer/dfplayer.h @@ -23,64 +23,30 @@ enum Device { TF_CARD = 2, }; +// See the datasheet here: +// https://github.com/DFRobot/DFRobotDFPlayerMini/blob/master/doc/FN-M16P%2BEmbedded%2BMP3%2BAudio%2BModule%2BDatasheet.pdf class DFPlayer : public uart::UARTDevice, public Component { public: void loop() override; - void next() { - this->ack_set_is_playing_ = true; - this->send_cmd_(0x01); - } - void previous() { - this->ack_set_is_playing_ = true; - this->send_cmd_(0x02); - } - void play_mp3(uint16_t file) { - this->ack_set_is_playing_ = true; - this->send_cmd_(0x12, file); - } - void play_file(uint16_t file) { - this->ack_set_is_playing_ = true; - this->send_cmd_(0x03, file); - } - void play_file_loop(uint16_t file) { - this->ack_set_is_playing_ = true; - this->send_cmd_(0x08, file); - } + void next(); + void previous(); + void play_mp3(uint16_t file); + void play_file(uint16_t file); + void play_file_loop(uint16_t file); void play_folder(uint16_t folder, uint16_t file); - void play_folder_loop(uint16_t folder) { - this->ack_set_is_playing_ = true; - this->send_cmd_(0x17, folder); - } - void volume_up() { this->send_cmd_(0x04); } - void volume_down() { this->send_cmd_(0x05); } - void set_device(Device device) { this->send_cmd_(0x09, device); } - void set_volume(uint8_t volume) { this->send_cmd_(0x06, volume); } - void set_eq(EqPreset preset) { this->send_cmd_(0x07, preset); } - void sleep() { - this->ack_reset_is_playing_ = true; - this->send_cmd_(0x0A); - } - void reset() { - this->ack_reset_is_playing_ = true; - this->send_cmd_(0x0C); - } - void start() { - this->ack_set_is_playing_ = true; - this->send_cmd_(0x0D); - } - void pause() { - this->ack_reset_is_playing_ = true; - this->send_cmd_(0x0E); - } - void stop() { - this->ack_reset_is_playing_ = true; - this->send_cmd_(0x16); - } - void random() { - this->ack_set_is_playing_ = true; - this->send_cmd_(0x18); - } + void play_folder_loop(uint16_t folder); + void volume_up(); + void volume_down(); + void set_device(Device device); + void set_volume(uint8_t volume); + void set_eq(EqPreset preset); + void sleep(); + void reset(); + void start(); + void pause(); + void stop(); + void random(); bool is_playing() { return is_playing_; } void dump_config() override; From 80226694d5d0c5d44f0bb8c2c39b070802c4b073 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 13 Nov 2024 13:16:13 +1300 Subject: [PATCH 43/46] Bump version to 2024.11.0b1 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 5645c9eaab..c3c8712677 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2024.11.0-dev" +__version__ = "2024.11.0b1" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 1f7f03f563e3500beeda9a3c163ee48e309e7617 Mon Sep 17 00:00:00 2001 From: luar123 <49960470+luar123@users.noreply.github.com> Date: Wed, 13 Nov 2024 01:18:10 +0100 Subject: [PATCH 44/46] Fix temperature and humidity for bme680 with bsec2 (#7728) --- .../components/bme68x_bsec2/bme68x_bsec2.cpp | 95 ++++++++++--------- .../components/bme68x_bsec2/bme68x_bsec2.h | 2 - 2 files changed, 49 insertions(+), 48 deletions(-) diff --git a/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp b/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp index 5425bbd5b7..f83f20f1a5 100644 --- a/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp +++ b/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp @@ -204,11 +204,11 @@ void BME68xBSEC2Component::update_subscription_() { } void BME68xBSEC2Component::run_() { + this->op_mode_ = this->bsec_settings_.op_mode; int64_t curr_time_ns = this->get_time_ns_(); - if (curr_time_ns < this->next_call_ns_) { + if (curr_time_ns < this->bsec_settings_.next_call) { return; } - this->op_mode_ = this->bsec_settings_.op_mode; uint8_t status; ESP_LOGV(TAG, "Performing sensor run"); @@ -219,57 +219,60 @@ void BME68xBSEC2Component::run_() { ESP_LOGW(TAG, "Failed to fetch sensor control settings (BSEC2 error code %d)", this->bsec_status_); return; } - this->next_call_ns_ = this->bsec_settings_.next_call; - if (this->bsec_settings_.trigger_measurement) { - bme68x_get_conf(&bme68x_conf, &this->bme68x_); + switch (this->bsec_settings_.op_mode) { + case BME68X_FORCED_MODE: + bme68x_get_conf(&bme68x_conf, &this->bme68x_); - bme68x_conf.os_hum = this->bsec_settings_.humidity_oversampling; - bme68x_conf.os_temp = this->bsec_settings_.temperature_oversampling; - bme68x_conf.os_pres = this->bsec_settings_.pressure_oversampling; - bme68x_set_conf(&bme68x_conf, &this->bme68x_); + bme68x_conf.os_hum = this->bsec_settings_.humidity_oversampling; + bme68x_conf.os_temp = this->bsec_settings_.temperature_oversampling; + bme68x_conf.os_pres = this->bsec_settings_.pressure_oversampling; + bme68x_set_conf(&bme68x_conf, &this->bme68x_); + this->bme68x_heatr_conf_.enable = BME68X_ENABLE; + this->bme68x_heatr_conf_.heatr_temp = this->bsec_settings_.heater_temperature; + this->bme68x_heatr_conf_.heatr_dur = this->bsec_settings_.heater_duration; + + // status = bme68x_set_op_mode(this->bsec_settings_.op_mode, &this->bme68x_); + status = bme68x_set_heatr_conf(BME68X_FORCED_MODE, &this->bme68x_heatr_conf_, &this->bme68x_); + status = bme68x_set_op_mode(BME68X_FORCED_MODE, &this->bme68x_); + this->op_mode_ = BME68X_FORCED_MODE; + ESP_LOGV(TAG, "Using forced mode"); + + break; + case BME68X_PARALLEL_MODE: + if (this->op_mode_ != this->bsec_settings_.op_mode) { + bme68x_get_conf(&bme68x_conf, &this->bme68x_); + + bme68x_conf.os_hum = this->bsec_settings_.humidity_oversampling; + bme68x_conf.os_temp = this->bsec_settings_.temperature_oversampling; + bme68x_conf.os_pres = this->bsec_settings_.pressure_oversampling; + bme68x_set_conf(&bme68x_conf, &this->bme68x_); - switch (this->bsec_settings_.op_mode) { - case BME68X_FORCED_MODE: this->bme68x_heatr_conf_.enable = BME68X_ENABLE; - this->bme68x_heatr_conf_.heatr_temp = this->bsec_settings_.heater_temperature; - this->bme68x_heatr_conf_.heatr_dur = this->bsec_settings_.heater_duration; + this->bme68x_heatr_conf_.heatr_temp_prof = this->bsec_settings_.heater_temperature_profile; + this->bme68x_heatr_conf_.heatr_dur_prof = this->bsec_settings_.heater_duration_profile; + this->bme68x_heatr_conf_.profile_len = this->bsec_settings_.heater_profile_len; + this->bme68x_heatr_conf_.shared_heatr_dur = + BSEC_TOTAL_HEAT_DUR - + (bme68x_get_meas_dur(BME68X_PARALLEL_MODE, &bme68x_conf, &this->bme68x_) / INT64_C(1000)); - status = bme68x_set_op_mode(this->bsec_settings_.op_mode, &this->bme68x_); - status = bme68x_set_heatr_conf(BME68X_FORCED_MODE, &this->bme68x_heatr_conf_, &this->bme68x_); - status = bme68x_set_op_mode(BME68X_FORCED_MODE, &this->bme68x_); - this->op_mode_ = BME68X_FORCED_MODE; - this->sleep_mode_ = false; - ESP_LOGV(TAG, "Using forced mode"); + status = bme68x_set_heatr_conf(BME68X_PARALLEL_MODE, &this->bme68x_heatr_conf_, &this->bme68x_); - break; - case BME68X_PARALLEL_MODE: - if (this->op_mode_ != this->bsec_settings_.op_mode) { - this->bme68x_heatr_conf_.enable = BME68X_ENABLE; - this->bme68x_heatr_conf_.heatr_temp_prof = this->bsec_settings_.heater_temperature_profile; - this->bme68x_heatr_conf_.heatr_dur_prof = this->bsec_settings_.heater_duration_profile; - this->bme68x_heatr_conf_.profile_len = this->bsec_settings_.heater_profile_len; - this->bme68x_heatr_conf_.shared_heatr_dur = - BSEC_TOTAL_HEAT_DUR - - (bme68x_get_meas_dur(BME68X_PARALLEL_MODE, &bme68x_conf, &this->bme68x_) / INT64_C(1000)); - - status = bme68x_set_heatr_conf(BME68X_PARALLEL_MODE, &this->bme68x_heatr_conf_, &this->bme68x_); - - status = bme68x_set_op_mode(BME68X_PARALLEL_MODE, &this->bme68x_); - this->op_mode_ = BME68X_PARALLEL_MODE; - this->sleep_mode_ = false; - ESP_LOGV(TAG, "Using parallel mode"); - } - break; - case BME68X_SLEEP_MODE: - if (!this->sleep_mode_) { - bme68x_set_op_mode(BME68X_SLEEP_MODE, &this->bme68x_); - this->sleep_mode_ = true; - ESP_LOGV(TAG, "Using sleep mode"); - } - break; - } + status = bme68x_set_op_mode(BME68X_PARALLEL_MODE, &this->bme68x_); + this->op_mode_ = BME68X_PARALLEL_MODE; + ESP_LOGV(TAG, "Using parallel mode"); + } + break; + case BME68X_SLEEP_MODE: + if (this->op_mode_ != this->bsec_settings_.op_mode) { + bme68x_set_op_mode(BME68X_SLEEP_MODE, &this->bme68x_); + this->op_mode_ = BME68X_SLEEP_MODE; + ESP_LOGV(TAG, "Using sleep mode"); + } + break; + } + if (this->bsec_settings_.trigger_measurement && this->bsec_settings_.op_mode != BME68X_SLEEP_MODE) { uint32_t meas_dur = 0; meas_dur = bme68x_get_meas_dur(this->op_mode_, &bme68x_conf, &this->bme68x_); ESP_LOGV(TAG, "Queueing read in %uus", meas_dur); diff --git a/esphome/components/bme68x_bsec2/bme68x_bsec2.h b/esphome/components/bme68x_bsec2/bme68x_bsec2.h index 7b9db2b7bf..86d3e5dfbf 100644 --- a/esphome/components/bme68x_bsec2/bme68x_bsec2.h +++ b/esphome/components/bme68x_bsec2/bme68x_bsec2.h @@ -113,13 +113,11 @@ class BME68xBSEC2Component : public Component { struct bme68x_heatr_conf bme68x_heatr_conf_; uint8_t op_mode_; // operating mode of sensor - bool sleep_mode_; bsec_library_return_t bsec_status_{BSEC_OK}; int8_t bme68x_status_{BME68X_OK}; int64_t last_time_ms_{0}; uint32_t millis_overflow_counter_{0}; - int64_t next_call_ns_{0}; std::queue> queue_; From a2cab960a9d2f138fe736ce8147b383f13146475 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 13 Nov 2024 13:16:13 +1300 Subject: [PATCH 45/46] Bump version to 2024.12.0-dev --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 5645c9eaab..d42ee5ee72 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2024.11.0-dev" +__version__ = "2024.12.0-dev" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From c7c8711c9c8380d9d90875973e2e8c0e70678667 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Wed, 13 Nov 2024 12:39:02 -0500 Subject: [PATCH 46/46] [i2s_audio] Bugfix: Adjust I2S speaker setup priority (#7759) --- .../i2s_audio/speaker/i2s_audio_speaker.cpp | 11 +---------- .../components/i2s_audio/speaker/i2s_audio_speaker.h | 2 +- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp index c3f4566411..53b3cc8dc0 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp @@ -99,14 +99,6 @@ void I2SAudioSpeaker::setup() { this->mark_failed(); return; } - - this->i2s_event_queue_ = xQueueCreate(I2S_EVENT_QUEUE_COUNT, sizeof(i2s_event_t)); - - if (this->i2s_event_queue_ == nullptr) { - ESP_LOGE(TAG, "Failed to create I2S event queue"); - this->mark_failed(); - return; - } } void I2SAudioSpeaker::loop() { @@ -339,7 +331,7 @@ void I2SAudioSpeaker::speaker_task(void *params) { } void I2SAudioSpeaker::start() { - if (this->is_failed() || this->status_has_error()) + if (!this->is_ready() || this->is_failed() || this->status_has_error()) return; if ((this->state_ == speaker::STATE_STARTING) || (this->state_ == speaker::STATE_RUNNING)) return; @@ -519,7 +511,6 @@ void I2SAudioSpeaker::delete_task_(size_t buffer_size) { } xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::STATE_STOPPED); - xQueueReset(this->i2s_event_queue_); this->task_created_ = false; vTaskDelete(nullptr); diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h index 8b7386ba58..2b90f39399 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h @@ -23,7 +23,7 @@ namespace i2s_audio { class I2SAudioSpeaker : public I2SAudioOut, public speaker::Speaker, public Component { public: - float get_setup_priority() const override { return esphome::setup_priority::LATE; } + float get_setup_priority() const override { return esphome::setup_priority::PROCESSOR; } void setup() override; void loop() override;