From dc5b4087486c8d707c7b89379c7bf8bdd367c86a Mon Sep 17 00:00:00 2001 From: Peter Zich Date: Sun, 5 Jan 2025 17:50:35 -0800 Subject: [PATCH 01/86] Initialize esp32_rmt_led_strip buffer (#8036) --- esphome/components/esp32_rmt_led_strip/led_strip.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/esp32_rmt_led_strip/led_strip.cpp b/esphome/components/esp32_rmt_led_strip/led_strip.cpp index 8ee890ec10..4e8c862c23 100644 --- a/esphome/components/esp32_rmt_led_strip/led_strip.cpp +++ b/esphome/components/esp32_rmt_led_strip/led_strip.cpp @@ -33,6 +33,7 @@ void ESP32RMTLEDStripLightOutput::setup() { this->mark_failed(); return; } + memset(this->buf_, 0, buffer_size); this->effect_data_ = allocator.allocate(this->num_leds_); if (this->effect_data_ == nullptr) { From a0615a92f0124b4ee863e8d0f95f39d49c0d2a6e Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Thu, 9 Jan 2025 07:25:10 +1100 Subject: [PATCH 02/86] [addressable_light] Remove rmt channel from idf tests (#7987) --- tests/components/addressable_light/test.esp32-c3-idf.yaml | 1 - tests/components/addressable_light/test.esp32-idf.yaml | 1 - 2 files changed, 2 deletions(-) diff --git a/tests/components/addressable_light/test.esp32-c3-idf.yaml b/tests/components/addressable_light/test.esp32-c3-idf.yaml index f587113fac..7b3516345d 100644 --- a/tests/components/addressable_light/test.esp32-c3-idf.yaml +++ b/tests/components/addressable_light/test.esp32-c3-idf.yaml @@ -6,7 +6,6 @@ light: rgb_order: GRB num_leds: 256 pin: 2 - rmt_channel: 0 display: - platform: addressable_light diff --git a/tests/components/addressable_light/test.esp32-idf.yaml b/tests/components/addressable_light/test.esp32-idf.yaml index f587113fac..7b3516345d 100644 --- a/tests/components/addressable_light/test.esp32-idf.yaml +++ b/tests/components/addressable_light/test.esp32-idf.yaml @@ -6,7 +6,6 @@ light: rgb_order: GRB num_leds: 256 pin: 2 - rmt_channel: 0 display: - platform: addressable_light From 5e72b7196bee468a75a3b8b9cb6312de0eb2f04e Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Thu, 9 Jan 2025 08:14:08 +1100 Subject: [PATCH 03/86] Remove rmt channel from idf tests (#8054) --- tests/components/e131/test.esp32-c3-idf.yaml | 1 - tests/components/e131/test.esp32-idf.yaml | 1 - tests/components/partition/test.esp32-c3-idf.yaml | 1 - tests/components/partition/test.esp32-idf.yaml | 1 - 4 files changed, 4 deletions(-) diff --git a/tests/components/e131/test.esp32-c3-idf.yaml b/tests/components/e131/test.esp32-c3-idf.yaml index 25304cd3b4..a27e62c1fb 100644 --- a/tests/components/e131/test.esp32-c3-idf.yaml +++ b/tests/components/e131/test.esp32-c3-idf.yaml @@ -12,7 +12,6 @@ light: rgb_order: GRB num_leds: 256 pin: 2 - rmt_channel: 0 effects: - e131: universe: 1 diff --git a/tests/components/e131/test.esp32-idf.yaml b/tests/components/e131/test.esp32-idf.yaml index 25304cd3b4..a27e62c1fb 100644 --- a/tests/components/e131/test.esp32-idf.yaml +++ b/tests/components/e131/test.esp32-idf.yaml @@ -12,7 +12,6 @@ light: rgb_order: GRB num_leds: 256 pin: 2 - rmt_channel: 0 effects: - e131: universe: 1 diff --git a/tests/components/partition/test.esp32-c3-idf.yaml b/tests/components/partition/test.esp32-c3-idf.yaml index 77cfc5ad44..397e1b0642 100644 --- a/tests/components/partition/test.esp32-c3-idf.yaml +++ b/tests/components/partition/test.esp32-c3-idf.yaml @@ -6,7 +6,6 @@ light: rgb_order: GRB num_leds: 256 pin: 2 - rmt_channel: 0 - platform: partition name: Partition Light segments: diff --git a/tests/components/partition/test.esp32-idf.yaml b/tests/components/partition/test.esp32-idf.yaml index 77cfc5ad44..397e1b0642 100644 --- a/tests/components/partition/test.esp32-idf.yaml +++ b/tests/components/partition/test.esp32-idf.yaml @@ -6,7 +6,6 @@ light: rgb_order: GRB num_leds: 256 pin: 2 - rmt_channel: 0 - platform: partition name: Partition Light segments: From 78543e1e159c7043ce51336924747e6067125e10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samu=20N=C3=A9meth?= Date: Wed, 8 Jan 2025 23:37:52 +0100 Subject: [PATCH 04/86] Fixed comment typo in light_color_values.h (#8050) Co-authored-by: Keith Burzinski --- esphome/components/light/light_color_values.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/light/light_color_values.h b/esphome/components/light/light_color_values.h index bad180ce6d..ca32b9c571 100644 --- a/esphome/components/light/light_color_values.h +++ b/esphome/components/light/light_color_values.h @@ -36,7 +36,7 @@ inline static uint8_t to_uint8_scale(float x) { return static_cast(roun * range as set in the traits, so the output needs to do this. * * For COLD_WARM_WHITE capability: - * - cold_white, warm_white: The brightness of the cald and warm white channels of the light. + * - cold_white, warm_white: The brightness of the light's cold and warm white channels. * * All values (except color temperature) are represented using floats in the range 0.0 (off) to 1.0 (on), and are * automatically clamped to this range. Properties not used in the current color mode can still have (invalid) values From a498fb5dcfa3d5999f87817d2b728dbb4972aad9 Mon Sep 17 00:00:00 2001 From: Peter Zich Date: Wed, 8 Jan 2025 22:47:30 -0800 Subject: [PATCH 05/86] Fix braceless else statements (#7799) --- esphome/components/climate_ir/climate_ir.cpp | 3 +- esphome/components/coolix/coolix.cpp | 3 +- .../dfrobot_sen0395/dfrobot_sen0395.cpp | 3 +- esphome/components/dht/dht.cpp | 3 +- esphome/components/display/display.cpp | 3 +- esphome/components/display/rect.cpp | 3 +- .../esp32_improv/esp32_improv_component.cpp | 2 +- esphome/components/gcja5/gcja5.cpp | 3 +- esphome/components/haier/haier_base.cpp | 3 +- esphome/components/haier/hon_climate.cpp | 3 +- esphome/components/heatpumpir/heatpumpir.cpp | 3 +- .../microphone/i2s_audio_microphone.cpp | 12 ++++---- .../micronova/switch/micronova_switch.cpp | 6 ++-- esphome/components/toshiba/toshiba.cpp | 3 +- esphome/components/tuya/light/tuya_light.cpp | 6 ++-- esphome/components/yashima/yashima.cpp | 3 +- esphome/core/helpers.cpp | 28 ++++++++++--------- 17 files changed, 55 insertions(+), 35 deletions(-) diff --git a/esphome/components/climate_ir/climate_ir.cpp b/esphome/components/climate_ir/climate_ir.cpp index 76adfb42bb..8175383627 100644 --- a/esphome/components/climate_ir/climate_ir.cpp +++ b/esphome/components/climate_ir/climate_ir.cpp @@ -37,8 +37,9 @@ void ClimateIR::setup() { this->publish_state(); }); this->current_temperature = this->sensor_->state; - } else + } else { this->current_temperature = NAN; + } // restore set points auto restore = this->restore_state_(); if (restore.has_value()) { diff --git a/esphome/components/coolix/coolix.cpp b/esphome/components/coolix/coolix.cpp index 22b3431c3e..5c6bfd7740 100644 --- a/esphome/components/coolix/coolix.cpp +++ b/esphome/components/coolix/coolix.cpp @@ -131,8 +131,9 @@ bool CoolixClimate::on_coolix(climate::Climate *parent, remote_base::RemoteRecei } else { parent->mode = climate::CLIMATE_MODE_FAN_ONLY; } - } else + } else { parent->mode = climate::CLIMATE_MODE_COOL; + } // Fan Speed if ((remote_state & COOLIX_FAN_AUTO) == COOLIX_FAN_AUTO || parent->mode == climate::CLIMATE_MODE_HEAT_COOL || diff --git a/esphome/components/dfrobot_sen0395/dfrobot_sen0395.cpp b/esphome/components/dfrobot_sen0395/dfrobot_sen0395.cpp index f8ef6c7138..f47025698b 100644 --- a/esphome/components/dfrobot_sen0395/dfrobot_sen0395.cpp +++ b/esphome/components/dfrobot_sen0395/dfrobot_sen0395.cpp @@ -118,8 +118,9 @@ std::unique_ptr CircularCommandQueue::dequeue() { if (front_ == rear_) { front_ = -1; rear_ = -1; - } else + } else { front_ = (front_ + 1) % COMMAND_QUEUE_SIZE; + } return dequeued_cmd; } diff --git a/esphome/components/dht/dht.cpp b/esphome/components/dht/dht.cpp index 3f9f9c57f4..5a18f6f36e 100644 --- a/esphome/components/dht/dht.cpp +++ b/esphome/components/dht/dht.cpp @@ -157,8 +157,9 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r if (bit == 0) { bit = 7; byte++; - } else + } else { bit--; + } } } if (!report_errors && error_code != 0) diff --git a/esphome/components/display/display.cpp b/esphome/components/display/display.cpp index f00c2936a8..202c64ef14 100644 --- a/esphome/components/display/display.cpp +++ b/esphome/components/display/display.cpp @@ -266,8 +266,9 @@ void Display::filled_gauge(int center_x, int center_y, int radius1, int radius2, if (dymax < float(-dxmax) * tan_a) { upd_dxmax = ceil(float(dymax) / tan_a); hline_width = -dxmax - upd_dxmax + 1; - } else + } else { hline_width = 0; + } } if (hline_width > 0) this->horizontal_line(center_x + dxmax, center_y - dymax, hline_width, color); diff --git a/esphome/components/display/rect.cpp b/esphome/components/display/rect.cpp index 34b611191f..49bb7d025f 100644 --- a/esphome/components/display/rect.cpp +++ b/esphome/components/display/rect.cpp @@ -90,8 +90,9 @@ void Rect::info(const std::string &prefix) { if (this->is_set()) { ESP_LOGI(TAG, "%s [%3d,%3d,%3d,%3d] (%3d,%3d)", prefix.c_str(), this->x, this->y, this->w, this->h, this->x2(), this->y2()); - } else + } else { ESP_LOGI(TAG, "%s ** IS NOT SET **", prefix.c_str()); + } } } // namespace display diff --git a/esphome/components/esp32_improv/esp32_improv_component.cpp b/esphome/components/esp32_improv/esp32_improv_component.cpp index d36b50feb0..c67431077c 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.cpp +++ b/esphome/components/esp32_improv/esp32_improv_component.cpp @@ -112,7 +112,7 @@ void ESP32ImprovComponent::loop() { this->set_state_(improv::STATE_AUTHORIZED); } else #else - this->set_state_(improv::STATE_AUTHORIZED); + { this->set_state_(improv::STATE_AUTHORIZED); } #endif { if (!this->check_identify_()) diff --git a/esphome/components/gcja5/gcja5.cpp b/esphome/components/gcja5/gcja5.cpp index 7f980ca0ad..b1db58654b 100644 --- a/esphome/components/gcja5/gcja5.cpp +++ b/esphome/components/gcja5/gcja5.cpp @@ -97,8 +97,9 @@ void GCJA5Component::parse_data_() { if (this->rx_message_[0] != 0x02 || this->rx_message_[31] != 0x03 || !this->calculate_checksum_()) { ESP_LOGVV(TAG, "Discarding bad packet - failed checks."); return; - } else + } else { ESP_LOGVV(TAG, "Good packet found."); + } this->have_good_data_ = true; uint8_t status = this->rx_message_[29]; diff --git a/esphome/components/haier/haier_base.cpp b/esphome/components/haier/haier_base.cpp index ba80c1ca1b..f8c0a7587e 100644 --- a/esphome/components/haier/haier_base.cpp +++ b/esphome/components/haier/haier_base.cpp @@ -342,8 +342,9 @@ bool HaierClimateBase::prepare_pending_action() { this->action_request_.reset(); return false; } - } else + } else { return false; + } } ClimateTraits HaierClimateBase::traits() { return traits_; } diff --git a/esphome/components/haier/hon_climate.cpp b/esphome/components/haier/hon_climate.cpp index c95a87223d..9b59dd0c10 100644 --- a/esphome/components/haier/hon_climate.cpp +++ b/esphome/components/haier/hon_climate.cpp @@ -710,8 +710,9 @@ void HonClimate::process_alarm_message_(const uint8_t *packet, uint8_t size, boo alarm_code++; } active_alarms_[i] = packet[2 + i]; - } else + } else { alarm_code += 8; + } } } else { float alarm_count = 0.0f; diff --git a/esphome/components/heatpumpir/heatpumpir.cpp b/esphome/components/heatpumpir/heatpumpir.cpp index 144dcc9bfa..55f0599cba 100644 --- a/esphome/components/heatpumpir/heatpumpir.cpp +++ b/esphome/components/heatpumpir/heatpumpir.cpp @@ -87,8 +87,9 @@ void HeatpumpIRClimate::setup() { this->publish_state(); }); this->current_temperature = this->sensor_->state; - } else + } else { this->current_temperature = NAN; + } } void HeatpumpIRClimate::transmit_state() { diff --git a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp index 23689afb91..4dbc9dcdac 100644 --- a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp +++ b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp @@ -25,11 +25,13 @@ void I2SAudioMicrophone::setup() { } } else #endif - if (this->pdm_) { - if (this->parent_->get_port() != I2S_NUM_0) { - ESP_LOGE(TAG, "PDM only works on I2S0!"); - this->mark_failed(); - return; + { + if (this->pdm_) { + if (this->parent_->get_port() != I2S_NUM_0) { + ESP_LOGE(TAG, "PDM only works on I2S0!"); + this->mark_failed(); + return; + } } } } diff --git a/esphome/components/micronova/switch/micronova_switch.cpp b/esphome/components/micronova/switch/micronova_switch.cpp index dcc96102db..28674acd96 100644 --- a/esphome/components/micronova/switch/micronova_switch.cpp +++ b/esphome/components/micronova/switch/micronova_switch.cpp @@ -11,15 +11,17 @@ void MicroNovaSwitch::write_state(bool state) { if (this->micronova_->get_current_stove_state() == 0) { this->micronova_->write_address(this->memory_location_, this->memory_address_, this->memory_data_on_); this->publish_state(true); - } else + } else { ESP_LOGW(TAG, "Unable to turn stove on, invalid state: %d", micronova_->get_current_stove_state()); + } } else { // don't send power-off when status is Off or Final cleaning if (this->micronova_->get_current_stove_state() != 0 && micronova_->get_current_stove_state() != 6) { this->micronova_->write_address(this->memory_location_, this->memory_address_, this->memory_data_off_); this->publish_state(false); - } else + } else { ESP_LOGW(TAG, "Unable to turn stove off, invalid state: %d", micronova_->get_current_stove_state()); + } } this->micronova_->update(); break; diff --git a/esphome/components/toshiba/toshiba.cpp b/esphome/components/toshiba/toshiba.cpp index 33d36d6a69..ff4241a81f 100644 --- a/esphome/components/toshiba/toshiba.cpp +++ b/esphome/components/toshiba/toshiba.cpp @@ -106,8 +106,9 @@ void ToshibaClimate::setup() { this->publish_state(); }); this->current_temperature = this->sensor_->state; - } else + } else { this->current_temperature = NAN; + } // restore set points auto restore = this->restore_state_(); if (restore.has_value()) { diff --git a/esphome/components/tuya/light/tuya_light.cpp b/esphome/components/tuya/light/tuya_light.cpp index 66931767b2..815a089d9f 100644 --- a/esphome/components/tuya/light/tuya_light.cpp +++ b/esphome/components/tuya/light/tuya_light.cpp @@ -120,8 +120,9 @@ light::LightTraits TuyaLight::get_traits() { traits.set_supported_color_modes( {light::ColorMode::RGB_COLOR_TEMPERATURE, light::ColorMode::COLOR_TEMPERATURE}); } - } else + } else { traits.set_supported_color_modes({light::ColorMode::COLOR_TEMPERATURE}); + } traits.set_min_mireds(this->cold_white_temperature_); traits.set_max_mireds(this->warm_white_temperature_); } else if (this->color_id_.has_value()) { @@ -131,8 +132,9 @@ light::LightTraits TuyaLight::get_traits() { } else { traits.set_supported_color_modes({light::ColorMode::RGB_WHITE}); } - } else + } else { traits.set_supported_color_modes({light::ColorMode::RGB}); + } } else if (this->dimmer_id_.has_value()) { traits.set_supported_color_modes({light::ColorMode::BRIGHTNESS}); } else { diff --git a/esphome/components/yashima/yashima.cpp b/esphome/components/yashima/yashima.cpp index 493c689b42..a3cf53ff66 100644 --- a/esphome/components/yashima/yashima.cpp +++ b/esphome/components/yashima/yashima.cpp @@ -104,8 +104,9 @@ void YashimaClimate::setup() { this->publish_state(); }); this->current_temperature = this->sensor_->state; - } else + } else { this->current_temperature = NAN; + } // restore set points auto restore = this->restore_state_(); if (restore.has_value()) { diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index b11615204e..2d2c88b844 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -126,19 +126,21 @@ uint16_t crc16(const uint8_t *data, uint16_t len, uint16_t crc, uint16_t reverse } } else #endif - if (reverse_poly == 0xa001) { - while (len--) { - uint8_t combo = crc ^ (uint8_t) *data++; - crc = (crc >> 8) ^ CRC16_A001_LE_LUT_L[combo & 0x0F] ^ CRC16_A001_LE_LUT_H[combo >> 4]; - } - } else { - while (len--) { - crc ^= *data++; - for (uint8_t i = 0; i < 8; i++) { - if (crc & 0x0001) { - crc = (crc >> 1) ^ reverse_poly; - } else { - crc >>= 1; + { + if (reverse_poly == 0xa001) { + while (len--) { + uint8_t combo = crc ^ (uint8_t) *data++; + crc = (crc >> 8) ^ CRC16_A001_LE_LUT_L[combo & 0x0F] ^ CRC16_A001_LE_LUT_H[combo >> 4]; + } + } else { + while (len--) { + crc ^= *data++; + for (uint8_t i = 0; i < 8; i++) { + if (crc & 0x0001) { + crc = (crc >> 1) ^ reverse_poly; + } else { + crc >>= 1; + } } } } From de603c756500a5fe0dac805d498c3cb870e1b04b Mon Sep 17 00:00:00 2001 From: Jimmy Hedman Date: Fri, 10 Jan 2025 22:10:19 +0100 Subject: [PATCH 06/86] Enable udp to work (on ipv4) when ipv6 is enabled (#8060) Co-authored-by: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> --- esphome/components/udp/udp_component.cpp | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/esphome/components/udp/udp_component.cpp b/esphome/components/udp/udp_component.cpp index b8727ec423..e29620fa9a 100644 --- a/esphome/components/udp/udp_component.cpp +++ b/esphome/components/udp/udp_component.cpp @@ -245,13 +245,9 @@ void UDPComponent::setup() { } struct sockaddr_in server {}; - socklen_t sl = socket::set_sockaddr_any((struct sockaddr *) &server, sizeof(server), this->port_); - if (sl == 0) { - ESP_LOGE(TAG, "Socket unable to set sockaddr: errno %d", errno); - this->mark_failed(); - this->status_set_error("Unable to set sockaddr"); - return; - } + server.sin_family = AF_INET; + server.sin_addr.s_addr = ESPHOME_INADDR_ANY; + server.sin_port = htons(this->port_); err = this->listen_socket_->bind((struct sockaddr *) &server, sizeof(server)); if (err != 0) { From 4d7c6b28e1260be7217960ae61b74bc1f903d1ae Mon Sep 17 00:00:00 2001 From: Juan Jose Restrepo <40721479+jotaj91@users.noreply.github.com> Date: Fri, 10 Jan 2025 18:22:30 -0500 Subject: [PATCH 07/86] Update sprinkler.cpp (#7996) --- esphome/components/sprinkler/sprinkler.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/components/sprinkler/sprinkler.cpp b/esphome/components/sprinkler/sprinkler.cpp index 5384d29871..3cfb5ccdee 100644 --- a/esphome/components/sprinkler/sprinkler.cpp +++ b/esphome/components/sprinkler/sprinkler.cpp @@ -184,11 +184,13 @@ void SprinklerValveOperator::set_controller(Sprinkler *controller) { void SprinklerValveOperator::set_valve(SprinklerValve *valve) { if (valve != nullptr) { + if (this->state_ != IDLE) { // Only kill if not already idle + this->kill_(); // ensure everything is off before we let go! + } this->state_ = IDLE; // reset state this->run_duration_ = 0; // reset to ensure the valve isn't started without updating it this->start_millis_ = 0; // reset because (new) valve has not been started yet this->stop_millis_ = 0; // reset because (new) valve has not been started yet - this->kill_(); // ensure everything is off before we let go! this->valve_ = valve; // finally, set the pointer to the new valve } } From 4530e4d60fedc3557e2ad52980a1968d0a47ec79 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 13 Jan 2025 05:40:50 +1100 Subject: [PATCH 08/86] [lvgl] remove default state (#8038) --- esphome/components/lvgl/defines.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py index 02323f9655..7587c336bb 100644 --- a/esphome/components/lvgl/defines.py +++ b/esphome/components/lvgl/defines.py @@ -215,7 +215,7 @@ LV_LONG_MODES = LvConstant( ) STATES = ( - "default", + # default state not included here "checked", "focused", "focus_key", From 8a98b69a576bf11dd919e990b91b80127db9e486 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 13 Jan 2025 05:42:03 +1100 Subject: [PATCH 09/86] [lvgl] fix bg_image_src (#8005) Co-authored-by: clydeps --- esphome/components/lvgl/lvgl_esphome.h | 10 ++++++++++ esphome/components/lvgl/widgets/meter.py | 3 ++- esphome/core/defines.h | 1 + tests/components/lvgl/lvgl-package.yaml | 1 + 4 files changed, 14 insertions(+), 1 deletion(-) diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index 56413ad77e..8e89b02db9 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -59,6 +59,16 @@ inline void lv_img_set_src(lv_obj_t *obj, esphome::image::Image *image) { inline void lv_disp_set_bg_image(lv_disp_t *disp, esphome::image::Image *image) { lv_disp_set_bg_image(disp, image->get_lv_img_dsc()); } + +inline void lv_obj_set_style_bg_img_src(lv_obj_t *obj, esphome::image::Image *image, lv_style_selector_t selector) { + lv_obj_set_style_bg_img_src(obj, image->get_lv_img_dsc(), selector); +} +#ifdef USE_LVGL_METER +inline lv_meter_indicator_t *lv_meter_add_needle_img(lv_obj_t *obj, lv_meter_scale_t *scale, esphome::image::Image *src, + lv_coord_t pivot_x, lv_coord_t pivot_y) { + return lv_meter_add_needle_img(obj, scale, src->get_lv_img_dsc(), pivot_x, pivot_y); +} +#endif // USE_LVGL_METER #endif // USE_LVGL_IMAGE #ifdef USE_LVGL_ANIMIMG inline void lv_animimg_set_src(lv_obj_t *img, std::vector images) { diff --git a/esphome/components/lvgl/widgets/meter.py b/esphome/components/lvgl/widgets/meter.py index cd61d1c775..29a382f7cf 100644 --- a/esphome/components/lvgl/widgets/meter.py +++ b/esphome/components/lvgl/widgets/meter.py @@ -27,7 +27,7 @@ from ..defines import ( CONF_START_VALUE, CONF_TICKS, ) -from ..helpers import add_lv_use +from ..helpers import add_lv_use, lvgl_components_required from ..lv_validation import ( angle, get_end_value, @@ -182,6 +182,7 @@ class MeterType(WidgetType): async def to_code(self, w: Widget, config): """For a meter object, create and set parameters""" + lvgl_components_required.add(CONF_METER) var = w.obj for scale_conf in config.get(CONF_SCALES, ()): rotation = 90 + (360 - scale_conf[CONF_ANGLE_RANGE]) / 2 diff --git a/esphome/core/defines.h b/esphome/core/defines.h index eb3b20d007..c38a26c6a8 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -49,6 +49,7 @@ #define USE_LVGL_IMAGE #define USE_LVGL_KEY_LISTENER #define USE_LVGL_KEYBOARD +#define USE_LVGL_METER #define USE_LVGL_ROLLER #define USE_LVGL_ROTARY_ENCODER #define USE_LVGL_TOUCHSCREEN diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index b1b89adfe0..68f91884b2 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -132,6 +132,7 @@ lvgl: pages: - id: page1 + bg_image_src: cat_image on_load: - logger.log: page loaded - lvgl.widget.focus: From 0df6a913b3df34c9d6d7229dd802bc22a75db808 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 13 Jan 2025 05:46:17 +1100 Subject: [PATCH 10/86] [lgvl] disp_bg_image and disp_bg_opa changes (#8025) --- esphome/components/lvgl/automation.py | 16 +++++++++++++--- esphome/components/lvgl/defines.py | 1 + esphome/components/lvgl/schemas.py | 7 +++++-- tests/components/lvgl/lvgl-package.yaml | 3 ++- 4 files changed, 21 insertions(+), 6 deletions(-) diff --git a/esphome/components/lvgl/automation.py b/esphome/components/lvgl/automation.py index c26ae54892..a987cf4097 100644 --- a/esphome/components/lvgl/automation.py +++ b/esphome/components/lvgl/automation.py @@ -10,13 +10,14 @@ from esphome.cpp_types import nullptr from .defines import ( CONF_DISP_BG_COLOR, CONF_DISP_BG_IMAGE, + CONF_DISP_BG_OPA, CONF_EDITING, CONF_FREEZE, CONF_LVGL_ID, CONF_SHOW_SNOW, literal, ) -from .lv_validation import lv_bool, lv_color, lv_image +from .lv_validation import lv_bool, lv_color, lv_image, opacity from .lvcode import ( LVGL_COMP_ARG, UPDATE_EVENT, @@ -119,13 +120,22 @@ async def lvgl_is_idle(config, condition_id, template_arg, args): async def disp_update(disp, config: dict): - if CONF_DISP_BG_COLOR not in config and CONF_DISP_BG_IMAGE not in config: + if ( + CONF_DISP_BG_COLOR not in config + and CONF_DISP_BG_IMAGE not in config + and CONF_DISP_BG_OPA not in config + ): return with LocalVariable("lv_disp_tmp", lv_disp_t, disp) as disp_temp: if (bg_color := config.get(CONF_DISP_BG_COLOR)) is not None: lv.disp_set_bg_color(disp_temp, await lv_color.process(bg_color)) if bg_image := config.get(CONF_DISP_BG_IMAGE): - lv.disp_set_bg_image(disp_temp, await lv_image.process(bg_image)) + if bg_image == "none": + lv.disp_set_bg_image(disp_temp, static_cast("void *", "nullptr")) + else: + lv.disp_set_bg_image(disp_temp, await lv_image.process(bg_image)) + if (bg_opa := config.get(CONF_DISP_BG_OPA)) is not None: + lv.disp_set_bg_opa(disp_temp, await opacity.process(bg_opa)) @automation.register_action( diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py index 7587c336bb..733a6bc180 100644 --- a/esphome/components/lvgl/defines.py +++ b/esphome/components/lvgl/defines.py @@ -403,6 +403,7 @@ CONF_COLUMN = "column" CONF_DIGITS = "digits" CONF_DISP_BG_COLOR = "disp_bg_color" CONF_DISP_BG_IMAGE = "disp_bg_image" +CONF_DISP_BG_OPA = "disp_bg_opa" CONF_BODY = "body" CONF_BUTTONS = "buttons" CONF_BYTE_ORDER = "byte_order" diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py index 3f56b3345f..271dbea19f 100644 --- a/esphome/components/lvgl/schemas.py +++ b/esphome/components/lvgl/schemas.py @@ -19,7 +19,7 @@ from esphome.schema_extractors import SCHEMA_EXTRACT from . import defines as df, lv_validation as lvalid from .defines import CONF_TIME_FORMAT, LV_GRAD_DIR from .helpers import add_lv_use, requires_component, validate_printf -from .lv_validation import lv_color, lv_font, lv_gradient, lv_image +from .lv_validation import lv_color, lv_font, lv_gradient, lv_image, opacity from .lvcode import LvglComponent, lv_event_t_ptr from .types import ( LVEncoderListener, @@ -344,8 +344,11 @@ FLEX_OBJ_SCHEMA = { DISP_BG_SCHEMA = cv.Schema( { - cv.Optional(df.CONF_DISP_BG_IMAGE): lv_image, + cv.Optional(df.CONF_DISP_BG_IMAGE): cv.Any( + cv.one_of("none", lower=True), lv_image + ), cv.Optional(df.CONF_DISP_BG_COLOR): lv_color, + cv.Optional(df.CONF_DISP_BG_OPA): opacity, } ) diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index 68f91884b2..512045d748 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -27,6 +27,7 @@ lvgl: bg_color: light_blue disp_bg_color: color_id disp_bg_image: cat_image + disp_bg_opa: cover theme: obj: border_width: 1 @@ -207,7 +208,7 @@ lvgl: - lvgl.animimg.stop: anim_img - lvgl.update: disp_bg_color: 0xffff00 - disp_bg_image: cat_image + disp_bg_image: none - lvgl.widget.show: message_box - label: text: "Hello shiny day" From f1712cffa8c15037d5f1b23c862c904fac6d7318 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 13 Jan 2025 05:49:05 +1100 Subject: [PATCH 11/86] [spi_led_strip] Fix priority (#8021) --- esphome/components/spi_led_strip/light.py | 8 +-- .../spi_led_strip/spi_led_strip.cpp | 67 ++++++++++++++++++ .../components/spi_led_strip/spi_led_strip.h | 68 +++---------------- 3 files changed, 78 insertions(+), 65 deletions(-) create mode 100644 esphome/components/spi_led_strip/spi_led_strip.cpp diff --git a/esphome/components/spi_led_strip/light.py b/esphome/components/spi_led_strip/light.py index 78642935de..ca320265a9 100644 --- a/esphome/components/spi_led_strip/light.py +++ b/esphome/components/spi_led_strip/light.py @@ -1,8 +1,7 @@ import esphome.codegen as cg +from esphome.components import light, spi import esphome.config_validation as cv -from esphome.components import light -from esphome.components import spi -from esphome.const import CONF_OUTPUT_ID, CONF_NUM_LEDS +from esphome.const import CONF_NUM_LEDS, CONF_OUTPUT_ID spi_led_strip_ns = cg.esphome_ns.namespace("spi_led_strip") SpiLedStrip = spi_led_strip_ns.class_( @@ -18,8 +17,7 @@ CONFIG_SCHEMA = light.ADDRESSABLE_LIGHT_SCHEMA.extend( async def to_code(config): - var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) - cg.add(var.set_num_leds(config[CONF_NUM_LEDS])) + var = cg.new_Pvariable(config[CONF_OUTPUT_ID], config[CONF_NUM_LEDS]) await light.register_light(var, config) await spi.register_spi_device(var, config) await cg.register_component(var, config) diff --git a/esphome/components/spi_led_strip/spi_led_strip.cpp b/esphome/components/spi_led_strip/spi_led_strip.cpp new file mode 100644 index 0000000000..46243c0686 --- /dev/null +++ b/esphome/components/spi_led_strip/spi_led_strip.cpp @@ -0,0 +1,67 @@ +#include "spi_led_strip.h" + +namespace esphome { +namespace spi_led_strip { + +SpiLedStrip::SpiLedStrip(uint16_t num_leds) { + this->num_leds_ = num_leds; + ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + this->buffer_size_ = num_leds * 4 + 8; + this->buf_ = allocator.allocate(this->buffer_size_); + if (this->buf_ == nullptr) { + ESP_LOGE(TAG, "Failed to allocate buffer of size %u", this->buffer_size_); + return; + } + + this->effect_data_ = allocator.allocate(num_leds); + if (this->effect_data_ == nullptr) { + ESP_LOGE(TAG, "Failed to allocate effect data of size %u", num_leds); + return; + } + memset(this->buf_, 0xFF, this->buffer_size_); + memset(this->buf_, 0, 4); +} +void SpiLedStrip::setup() { + if (this->effect_data_ == nullptr || this->buf_ == nullptr) { + this->mark_failed(); + return; + } + this->spi_setup(); +} +light::LightTraits SpiLedStrip::get_traits() { + auto traits = light::LightTraits(); + traits.set_supported_color_modes({light::ColorMode::RGB}); + return traits; +} +void SpiLedStrip::dump_config() { + esph_log_config(TAG, "SPI LED Strip:"); + esph_log_config(TAG, " LEDs: %d", this->num_leds_); + if (this->data_rate_ >= spi::DATA_RATE_1MHZ) { + esph_log_config(TAG, " Data rate: %uMHz", (unsigned) (this->data_rate_ / 1000000)); + } else { + esph_log_config(TAG, " Data rate: %ukHz", (unsigned) (this->data_rate_ / 1000)); + } +} +void SpiLedStrip::write_state(light::LightState *state) { + if (this->is_failed()) + return; + if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE) { + char strbuf[49]; + size_t len = std::min(this->buffer_size_, (size_t) (sizeof(strbuf) - 1) / 3); + memset(strbuf, 0, sizeof(strbuf)); + for (size_t i = 0; i != len; i++) { + sprintf(strbuf + i * 3, "%02X ", this->buf_[i]); + } + esph_log_v(TAG, "write_state: buf = %s", strbuf); + } + this->enable(); + this->write_array(this->buf_, this->buffer_size_); + this->disable(); +} +light::ESPColorView SpiLedStrip::get_view_internal(int32_t index) const { + size_t pos = index * 4 + 5; + return {this->buf_ + pos + 2, this->buf_ + pos + 1, this->buf_ + pos + 0, nullptr, + this->effect_data_ + index, &this->correction_}; +} +} // namespace spi_led_strip +} // namespace esphome diff --git a/esphome/components/spi_led_strip/spi_led_strip.h b/esphome/components/spi_led_strip/spi_led_strip.h index 1b317cdd69..14c5627ac3 100644 --- a/esphome/components/spi_led_strip/spi_led_strip.h +++ b/esphome/components/spi_led_strip/spi_led_strip.h @@ -13,74 +13,22 @@ class SpiLedStrip : public light::AddressableLight, public spi::SPIDevice { public: - void setup() override { this->spi_setup(); } + SpiLedStrip(uint16_t num_leds); + void setup() override; + float get_setup_priority() const override { return setup_priority::IO; } int32_t size() const override { return this->num_leds_; } - light::LightTraits get_traits() override { - auto traits = light::LightTraits(); - traits.set_supported_color_modes({light::ColorMode::RGB}); - return traits; - } - void set_num_leds(uint16_t num_leds) { - this->num_leds_ = num_leds; - ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); - this->buffer_size_ = num_leds * 4 + 8; - this->buf_ = allocator.allocate(this->buffer_size_); - if (this->buf_ == nullptr) { - esph_log_e(TAG, "Failed to allocate buffer of size %u", this->buffer_size_); - this->mark_failed(); - return; - } + light::LightTraits get_traits() override; - this->effect_data_ = allocator.allocate(num_leds); - if (this->effect_data_ == nullptr) { - esph_log_e(TAG, "Failed to allocate effect data of size %u", num_leds); - this->mark_failed(); - return; - } - memset(this->buf_, 0xFF, this->buffer_size_); - memset(this->buf_, 0, 4); - } + void dump_config() override; - void dump_config() override { - esph_log_config(TAG, "SPI LED Strip:"); - esph_log_config(TAG, " LEDs: %d", this->num_leds_); - if (this->data_rate_ >= spi::DATA_RATE_1MHZ) { - esph_log_config(TAG, " Data rate: %uMHz", (unsigned) (this->data_rate_ / 1000000)); - } else { - esph_log_config(TAG, " Data rate: %ukHz", (unsigned) (this->data_rate_ / 1000)); - } - } + void write_state(light::LightState *state) override; - void write_state(light::LightState *state) override { - if (this->is_failed()) - return; - if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE) { - char strbuf[49]; - size_t len = std::min(this->buffer_size_, (size_t) (sizeof(strbuf) - 1) / 3); - memset(strbuf, 0, sizeof(strbuf)); - for (size_t i = 0; i != len; i++) { - sprintf(strbuf + i * 3, "%02X ", this->buf_[i]); - } - esph_log_v(TAG, "write_state: buf = %s", strbuf); - } - this->enable(); - this->write_array(this->buf_, this->buffer_size_); - this->disable(); - } - - void clear_effect_data() override { - for (int i = 0; i < this->size(); i++) - this->effect_data_[i] = 0; - } + void clear_effect_data() override { memset(this->effect_data_, 0, this->num_leds_ * sizeof(this->effect_data_[0])); } protected: - light::ESPColorView get_view_internal(int32_t index) const override { - size_t pos = index * 4 + 5; - return {this->buf_ + pos + 2, this->buf_ + pos + 1, this->buf_ + pos + 0, nullptr, - this->effect_data_ + index, &this->correction_}; - } + light::ESPColorView get_view_internal(int32_t index) const override; size_t buffer_size_{}; uint8_t *effect_data_{nullptr}; From bd17ee8e3304b311bfac699ce49e6f16bc91fa1e Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 13 Jan 2025 05:50:13 +1100 Subject: [PATCH 12/86] [config] Early check for required version (#8000) --- esphome/config.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/esphome/config.py b/esphome/config.py index 7d48569d2d..65e9ac29bc 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -18,6 +18,7 @@ from esphome.const import ( CONF_ESPHOME, CONF_EXTERNAL_COMPONENTS, CONF_ID, + CONF_MIN_VERSION, CONF_PACKAGES, CONF_PLATFORM, CONF_SUBSTITUTIONS, @@ -839,6 +840,10 @@ def validate_config( # Remove temporary esphome config path again, it will be reloaded later result.remove_output_path([CONF_ESPHOME], CONF_ESPHOME) + # Check version number now to avoid loading components that are not supported + if min_version := config[CONF_ESPHOME].get(CONF_MIN_VERSION): + cv.All(cv.version_number, cv.validate_esphome_version)(min_version) + # First run platform validation steps for key in TARGET_PLATFORMS: if key in config: From 109d737d5d7fbe8d55c2fb3c74179c43e9f8f92a Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 13 Jan 2025 05:53:26 +1100 Subject: [PATCH 13/86] [lvgl] Implement `lvgl.page.is_showing:` condition (#8055) --- esphome/components/lvgl/automation.py | 15 +++++++-- esphome/components/lvgl/lvgl_esphome.cpp | 3 ++ esphome/components/lvgl/lvgl_esphome.h | 15 ++++++--- esphome/components/lvgl/widgets/page.py | 39 ++++++++++++++++++++++-- tests/components/lvgl/lvgl-package.yaml | 5 +++ 5 files changed, 67 insertions(+), 10 deletions(-) diff --git a/esphome/components/lvgl/automation.py b/esphome/components/lvgl/automation.py index a987cf4097..7db6e1f045 100644 --- a/esphome/components/lvgl/automation.py +++ b/esphome/components/lvgl/automation.py @@ -4,7 +4,7 @@ from esphome import automation import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import CONF_ACTION, CONF_GROUP, CONF_ID, CONF_TIMEOUT -from esphome.cpp_generator import get_variable +from esphome.cpp_generator import TemplateArguments, get_variable from esphome.cpp_types import nullptr from .defines import ( @@ -23,6 +23,7 @@ from .lvcode import ( UPDATE_EVENT, LambdaContext, LocalVariable, + LvglComponent, ReturnStatement, add_line_marks, lv, @@ -93,7 +94,11 @@ async def lvgl_is_paused(config, condition_id, template_arg, args): lvgl = config[CONF_LVGL_ID] async with LambdaContext(LVGL_COMP_ARG, return_type=cg.bool_) as context: lv_add(ReturnStatement(lvgl_comp.is_paused())) - var = cg.new_Pvariable(condition_id, template_arg, await context.get_lambda()) + var = cg.new_Pvariable( + condition_id, + TemplateArguments(LvglComponent, *template_arg), + await context.get_lambda(), + ) await cg.register_parented(var, lvgl) return var @@ -114,7 +119,11 @@ async def lvgl_is_idle(config, condition_id, template_arg, args): timeout = await cg.templatable(config[CONF_TIMEOUT], [], cg.uint32) async with LambdaContext(LVGL_COMP_ARG, return_type=cg.bool_) as context: lv_add(ReturnStatement(lvgl_comp.is_idle(timeout))) - var = cg.new_Pvariable(condition_id, template_arg, await context.get_lambda()) + var = cg.new_Pvariable( + condition_id, + TemplateArguments(LvglComponent, *template_arg), + await context.get_lambda(), + ) await cg.register_parented(var, lvgl) return var diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index 61bdfe9755..5abeead9d8 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -119,6 +119,7 @@ void LvglComponent::add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_ev } void LvglComponent::add_page(LvPageType *page) { this->pages_.push_back(page); + page->set_parent(this); page->setup(this->pages_.size() - 1); } void LvglComponent::show_page(size_t index, lv_scr_load_anim_t anim, uint32_t time) { @@ -143,6 +144,8 @@ void LvglComponent::show_prev_page(lv_scr_load_anim_t anim, uint32_t time) { } while (this->pages_[this->current_page_]->skip); // skip empty pages() this->show_page(this->current_page_, anim, time); } +size_t LvglComponent::get_current_page() const { return this->current_page_; } +bool LvPageType::is_showing() const { return this->parent_->get_current_page() == this->index; } void LvglComponent::draw_buffer_(const lv_area_t *area, lv_color_t *ptr) { auto width = lv_area_get_width(area); auto height = lv_area_get_height(area); diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index 8e89b02db9..69fa808d53 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -94,7 +94,9 @@ class LvCompound { lv_obj_t *obj{}; }; -class LvPageType { +class LvglComponent; + +class LvPageType : public Parented { public: LvPageType(bool skip) : skip(skip) {} @@ -102,6 +104,9 @@ class LvPageType { this->index = index; this->obj = lv_obj_create(nullptr); } + + bool is_showing() const; + lv_obj_t *obj{}; size_t index{}; bool skip; @@ -188,6 +193,7 @@ class LvglComponent : public PollingComponent { void show_next_page(lv_scr_load_anim_t anim, uint32_t time); void show_prev_page(lv_scr_load_anim_t anim, uint32_t time); void set_page_wrap(bool wrap) { this->page_wrap_ = wrap; } + size_t get_current_page() const; void set_focus_mark(lv_group_t *group) { this->focus_marks_[group] = lv_group_get_focused(group); } void restore_focus_mark(lv_group_t *group) { auto *mark = this->focus_marks_[group]; @@ -251,14 +257,13 @@ template class LvglAction : public Action, public Parente std::function action_{}; }; -template class LvglCondition : public Condition, public Parented { +template class LvglCondition : public Condition, public Parented { public: - LvglCondition(std::function &&condition_lambda) - : condition_lambda_(std::move(condition_lambda)) {} + LvglCondition(std::function &&condition_lambda) : condition_lambda_(std::move(condition_lambda)) {} bool check(Ts... x) override { return this->condition_lambda_(this->parent_); } protected: - std::function condition_lambda_{}; + std::function condition_lambda_{}; }; #ifdef USE_LVGL_TOUCHSCREEN diff --git a/esphome/components/lvgl/widgets/page.py b/esphome/components/lvgl/widgets/page.py index a754a9cb9a..23c162e010 100644 --- a/esphome/components/lvgl/widgets/page.py +++ b/esphome/components/lvgl/widgets/page.py @@ -2,6 +2,7 @@ from esphome import automation, codegen as cg from esphome.automation import Trigger import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_PAGES, CONF_TIME, CONF_TRIGGER_ID +from esphome.cpp_generator import MockObj, TemplateArguments from ..defines import ( CONF_ANIMATION, @@ -17,18 +18,28 @@ from ..lvcode import ( EVENT_ARG, LVGL_COMP_ARG, LambdaContext, + ReturnStatement, add_line_marks, lv_add, lvgl_comp, lvgl_static, ) from ..schemas import LVGL_SCHEMA -from ..types import LvglAction, lv_page_t -from . import Widget, WidgetType, add_widgets, get_widgets, set_obj_properties +from ..types import LvglAction, LvglCondition, lv_page_t +from . import ( + Widget, + WidgetType, + add_widgets, + get_widgets, + set_obj_properties, + wait_for_widgets, +) CONF_ON_LOAD = "on_load" CONF_ON_UNLOAD = "on_unload" +PAGE_ARG = "_page" + PAGE_SCHEMA = cv.Schema( { cv.Optional(CONF_SKIP, default=False): lv_bool, @@ -86,6 +97,30 @@ async def page_next_to_code(config, action_id, template_arg, args): return var +@automation.register_condition( + "lvgl.page.is_showing", + LvglCondition, + cv.maybe_simple_value( + cv.Schema({cv.Required(CONF_ID): cv.use_id(lv_page_t)}), + key=CONF_ID, + ), +) +async def page_is_showing_to_code(config, condition_id, template_arg, args): + await wait_for_widgets() + page = await cg.get_variable(config[CONF_ID]) + async with LambdaContext( + [(lv_page_t.operator("ptr"), PAGE_ARG)], return_type=cg.bool_ + ) as context: + lv_add(ReturnStatement(MockObj(PAGE_ARG, "->").is_showing())) + var = cg.new_Pvariable( + condition_id, + TemplateArguments(lv_page_t, *template_arg), + await context.get_lambda(), + ) + await cg.register_parented(var, page) + return var + + @automation.register_action( "lvgl.page.previous", LvglAction, diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index 512045d748..234fd78678 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -138,6 +138,11 @@ lvgl: - logger.log: page loaded - lvgl.widget.focus: action: restore + - if: + condition: + lvgl.page.is_showing: page1 + then: + logger.log: "Yes, page1 showing" on_unload: - logger.log: page unloaded - lvgl.widget.focus: mark From fe80750743e2eeddd1a5631aed30b81c2f252d62 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 13 Jan 2025 05:56:54 +1100 Subject: [PATCH 14/86] [display] auto_clear_enabled defaults (#7986) --- esphome/components/display/__init__.py | 23 ++++++++++++++++++----- esphome/components/lvgl/__init__.py | 6 +++--- tests/components/lvgl/test.host.yaml | 2 -- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/esphome/components/display/__init__.py b/esphome/components/display/__init__.py index 32a8b3b090..99224df7b3 100644 --- a/esphome/components/display/__init__.py +++ b/esphome/components/display/__init__.py @@ -39,6 +39,7 @@ DisplayOnPageChangeTrigger = display_ns.class_( CONF_ON_PAGE_CHANGE = "on_page_change" CONF_SHOW_TEST_CARD = "show_test_card" +CONF_UNSPECIFIED = "unspecified" DISPLAY_ROTATIONS = { 0: display_ns.DISPLAY_ROTATION_0_DEGREES, @@ -55,16 +56,22 @@ def validate_rotation(value): return cv.enum(DISPLAY_ROTATIONS, int=True)(value) +def validate_auto_clear(value): + if value == CONF_UNSPECIFIED: + return value + return cv.boolean(value) + + BASIC_DISPLAY_SCHEMA = cv.Schema( { - cv.Optional(CONF_LAMBDA): cv.lambda_, + cv.Exclusive(CONF_LAMBDA, CONF_LAMBDA): cv.lambda_, } ).extend(cv.polling_component_schema("1s")) FULL_DISPLAY_SCHEMA = BASIC_DISPLAY_SCHEMA.extend( { cv.Optional(CONF_ROTATION): validate_rotation, - cv.Optional(CONF_PAGES): cv.All( + cv.Exclusive(CONF_PAGES, CONF_LAMBDA): cv.All( cv.ensure_list( { cv.GenerateID(): cv.declare_id(DisplayPage), @@ -82,7 +89,9 @@ FULL_DISPLAY_SCHEMA = BASIC_DISPLAY_SCHEMA.extend( cv.Optional(CONF_TO): cv.use_id(DisplayPage), } ), - cv.Optional(CONF_AUTO_CLEAR_ENABLED, default=True): cv.boolean, + cv.Optional( + CONF_AUTO_CLEAR_ENABLED, default=CONF_UNSPECIFIED + ): validate_auto_clear, cv.Optional(CONF_SHOW_TEST_CARD): cv.boolean, } ) @@ -92,8 +101,12 @@ async def setup_display_core_(var, config): if CONF_ROTATION in config: cg.add(var.set_rotation(DISPLAY_ROTATIONS[config[CONF_ROTATION]])) - if CONF_AUTO_CLEAR_ENABLED in config: - cg.add(var.set_auto_clear(config[CONF_AUTO_CLEAR_ENABLED])) + if auto_clear := config.get(CONF_AUTO_CLEAR_ENABLED): + # Default to true if pages or lambda is specified. Ideally this would be done during validation, but + # the possible schemas are too complex to do this easily. + if auto_clear == CONF_UNSPECIFIED: + auto_clear = CONF_LAMBDA in config or CONF_PAGES in config + cg.add(var.set_auto_clear(auto_clear)) if CONF_PAGES in config: pages = [] diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py index b858e8df01..c64ffcb5f2 100644 --- a/esphome/components/lvgl/__init__.py +++ b/esphome/components/lvgl/__init__.py @@ -197,11 +197,11 @@ def final_validation(configs): 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: + if CONF_LAMBDA in display or CONF_PAGES in display: raise cv.Invalid( - "Using lambda: in display config not compatible with LVGL" + "Using lambda: or pages: in display config is not compatible with LVGL" ) - if display[CONF_AUTO_CLEAR_ENABLED]: + if display.get(CONF_AUTO_CLEAR_ENABLED) is True: raise cv.Invalid( "Using auto_clear_enabled: true in display config not compatible with LVGL" ) diff --git a/tests/components/lvgl/test.host.yaml b/tests/components/lvgl/test.host.yaml index 34918cb113..39d9a0ebf3 100644 --- a/tests/components/lvgl/test.host.yaml +++ b/tests/components/lvgl/test.host.yaml @@ -7,7 +7,6 @@ display: height: 320 - platform: sdl id: sdl1 - auto_clear_enabled: false dimensions: width: 480 height: 480 @@ -40,4 +39,3 @@ lvgl: text: Click ME on_click: logger.log: Clicked - From d69926485c6ed33b476aef8994b41ac50b31ece9 Mon Sep 17 00:00:00 2001 From: Jimmy Hedman Date: Sun, 12 Jan 2025 20:12:38 +0100 Subject: [PATCH 15/86] Convert IPAddress to use Pythonmodule ipaddress (#8072) --- esphome/components/ethernet/__init__.py | 20 ++++++------- esphome/components/udp/__init__.py | 2 +- esphome/components/wifi/__init__.py | 12 ++++---- esphome/components/wireguard/__init__.py | 4 +-- esphome/config_validation.py | 33 +++++++++++----------- esphome/core/__init__.py | 10 ------- esphome/yaml_util.py | 4 +-- tests/unit_tests/test_config_validation.py | 30 +++++++++++++++++--- tests/unit_tests/test_core.py | 25 ++-------------- 9 files changed, 65 insertions(+), 75 deletions(-) diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index dca37b8dc2..ab760a9b6c 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -94,11 +94,11 @@ CLK_MODES = { MANUAL_IP_SCHEMA = cv.Schema( { - cv.Required(CONF_STATIC_IP): cv.ipv4, - cv.Required(CONF_GATEWAY): cv.ipv4, - cv.Required(CONF_SUBNET): cv.ipv4, - cv.Optional(CONF_DNS1, default="0.0.0.0"): cv.ipv4, - cv.Optional(CONF_DNS2, default="0.0.0.0"): cv.ipv4, + cv.Required(CONF_STATIC_IP): cv.ipv4address, + cv.Required(CONF_GATEWAY): cv.ipv4address, + cv.Required(CONF_SUBNET): cv.ipv4address, + cv.Optional(CONF_DNS1, default="0.0.0.0"): cv.ipv4address, + cv.Optional(CONF_DNS2, default="0.0.0.0"): cv.ipv4address, } ) @@ -255,11 +255,11 @@ FINAL_VALIDATE_SCHEMA = _final_validate def manual_ip(config): return cg.StructInitializer( ManualIP, - ("static_ip", IPAddress(*config[CONF_STATIC_IP].args)), - ("gateway", IPAddress(*config[CONF_GATEWAY].args)), - ("subnet", IPAddress(*config[CONF_SUBNET].args)), - ("dns1", IPAddress(*config[CONF_DNS1].args)), - ("dns2", IPAddress(*config[CONF_DNS2].args)), + ("static_ip", IPAddress(str(config[CONF_STATIC_IP]))), + ("gateway", IPAddress(str(config[CONF_GATEWAY]))), + ("subnet", IPAddress(str(config[CONF_SUBNET]))), + ("dns1", IPAddress(str(config[CONF_DNS1]))), + ("dns2", IPAddress(str(config[CONF_DNS2]))), ) diff --git a/esphome/components/udp/__init__.py b/esphome/components/udp/__init__.py index ca15be2a80..e189975ade 100644 --- a/esphome/components/udp/__init__.py +++ b/esphome/components/udp/__init__.py @@ -85,7 +85,7 @@ CONFIG_SCHEMA = cv.All( cv.GenerateID(): cv.declare_id(UDPComponent), cv.Optional(CONF_PORT, default=18511): cv.port, cv.Optional(CONF_ADDRESSES, default=["255.255.255.255"]): cv.ensure_list( - cv.ipv4 + cv.ipv4address, ), cv.Optional(CONF_ROLLING_CODE_ENABLE, default=False): cv.boolean, cv.Optional(CONF_PING_PONG_ENABLE, default=False): cv.boolean, diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index ad1a4f5262..582b826de0 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -93,16 +93,16 @@ def validate_channel(value): AP_MANUAL_IP_SCHEMA = cv.Schema( { - cv.Required(CONF_STATIC_IP): cv.ipv4, - cv.Required(CONF_GATEWAY): cv.ipv4, - cv.Required(CONF_SUBNET): cv.ipv4, + cv.Required(CONF_STATIC_IP): cv.ipv4address, + cv.Required(CONF_GATEWAY): cv.ipv4address, + cv.Required(CONF_SUBNET): cv.ipv4address, } ) STA_MANUAL_IP_SCHEMA = AP_MANUAL_IP_SCHEMA.extend( { - cv.Optional(CONF_DNS1, default="0.0.0.0"): cv.ipv4, - cv.Optional(CONF_DNS2, default="0.0.0.0"): cv.ipv4, + cv.Optional(CONF_DNS1, default="0.0.0.0"): cv.ipv4address, + cv.Optional(CONF_DNS2, default="0.0.0.0"): cv.ipv4address, } ) @@ -364,7 +364,7 @@ def eap_auth(config): def safe_ip(ip): if ip is None: return IPAddress(0, 0, 0, 0) - return IPAddress(*ip.args) + return IPAddress(str(ip)) def manual_ip(config): diff --git a/esphome/components/wireguard/__init__.py b/esphome/components/wireguard/__init__.py index 5e34a8a19b..fc0e4e0538 100644 --- a/esphome/components/wireguard/__init__.py +++ b/esphome/components/wireguard/__init__.py @@ -67,8 +67,8 @@ CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(Wireguard), cv.GenerateID(CONF_TIME_ID): cv.use_id(time.RealTimeClock), - cv.Required(CONF_ADDRESS): cv.ipv4, - cv.Optional(CONF_NETMASK, default="255.255.255.255"): cv.ipv4, + cv.Required(CONF_ADDRESS): cv.ipv4address, + cv.Optional(CONF_NETMASK, default="255.255.255.255"): cv.ipv4address, cv.Required(CONF_PRIVATE_KEY): _wireguard_key, cv.Required(CONF_PEER_ENDPOINT): cv.string, cv.Required(CONF_PEER_PUBLIC_KEY): _wireguard_key, diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 38fd677a2a..20a0774ccb 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -3,6 +3,7 @@ from contextlib import contextmanager from dataclasses import dataclass from datetime import datetime +from ipaddress import AddressValueError, IPv4Address, ip_address import logging import os import re @@ -67,7 +68,6 @@ from esphome.const import ( from esphome.core import ( CORE, HexInt, - IPAddress, Lambda, TimePeriod, TimePeriodMicroseconds, @@ -1130,7 +1130,7 @@ def domain(value): if re.match(vol.DOMAIN_REGEX, value) is not None: return value try: - return str(ipv4(value)) + return str(ipaddress(value)) except Invalid as err: raise Invalid(f"Invalid domain: {value}") from err @@ -1160,21 +1160,20 @@ def ssid(value): return value -def ipv4(value): - if isinstance(value, list): - parts = value - elif isinstance(value, str): - parts = value.split(".") - elif isinstance(value, IPAddress): - return value - else: - raise Invalid("IPv4 address must consist of either string or integer list") - if len(parts) != 4: - raise Invalid("IPv4 address must consist of four point-separated integers") - parts_ = list(map(int, parts)) - if not all(0 <= x < 256 for x in parts_): - raise Invalid("IPv4 address parts must be in range from 0 to 255") - return IPAddress(*parts_) +def ipv4address(value): + try: + address = IPv4Address(value) + except AddressValueError as exc: + raise Invalid(f"{value} is not a valid IPv4 address") from exc + return address + + +def ipaddress(value): + try: + address = ip_address(value) + except ValueError as exc: + raise Invalid(f"{value} is not a valid IP address") from exc + return address def _valid_topic(value): diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index a97c3b18c9..f26c3da483 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -54,16 +54,6 @@ class HexInt(int): return f"{sign}0x{value:X}" -class IPAddress: - def __init__(self, *args): - if len(args) != 4: - raise ValueError("IPAddress must consist of 4 items") - self.args = args - - def __str__(self): - return ".".join(str(x) for x in self.args) - - class MACAddress: def __init__(self, *parts): if len(parts) != 6: diff --git a/esphome/yaml_util.py b/esphome/yaml_util.py index d67511dfec..b27ce4c3e3 100644 --- a/esphome/yaml_util.py +++ b/esphome/yaml_util.py @@ -4,6 +4,7 @@ import fnmatch import functools import inspect from io import TextIOWrapper +from ipaddress import _BaseAddress import logging import math import os @@ -25,7 +26,6 @@ from esphome.core import ( CORE, DocumentRange, EsphomeError, - IPAddress, Lambda, MACAddress, TimePeriod, @@ -576,7 +576,7 @@ ESPHomeDumper.add_multi_representer(bool, ESPHomeDumper.represent_bool) ESPHomeDumper.add_multi_representer(str, ESPHomeDumper.represent_stringify) ESPHomeDumper.add_multi_representer(int, ESPHomeDumper.represent_int) ESPHomeDumper.add_multi_representer(float, ESPHomeDumper.represent_float) -ESPHomeDumper.add_multi_representer(IPAddress, ESPHomeDumper.represent_stringify) +ESPHomeDumper.add_multi_representer(_BaseAddress, ESPHomeDumper.represent_stringify) ESPHomeDumper.add_multi_representer(MACAddress, ESPHomeDumper.represent_stringify) ESPHomeDumper.add_multi_representer(TimePeriod, ESPHomeDumper.represent_stringify) ESPHomeDumper.add_multi_representer(Lambda, ESPHomeDumper.represent_lambda) diff --git a/tests/unit_tests/test_config_validation.py b/tests/unit_tests/test_config_validation.py index 34f70be2fb..93ae67754a 100644 --- a/tests/unit_tests/test_config_validation.py +++ b/tests/unit_tests/test_config_validation.py @@ -1,12 +1,12 @@ -import pytest import string -from hypothesis import given, example -from hypothesis.strategies import one_of, text, integers, builds +from hypothesis import example, given +from hypothesis.strategies import builds, integers, ip_addresses, one_of, text +import pytest from esphome import config_validation from esphome.config_validation import Invalid -from esphome.core import CORE, Lambda, HexInt +from esphome.core import CORE, HexInt, Lambda def test_check_not_templatable__invalid(): @@ -145,6 +145,28 @@ def test_boolean__invalid(value): config_validation.boolean(value) +@given(value=ip_addresses(v=4).map(str)) +def test_ipv4__valid(value): + config_validation.ipv4address(value) + + +@pytest.mark.parametrize("value", ("127.0.0", "localhost", "")) +def test_ipv4__invalid(value): + with pytest.raises(Invalid, match="is not a valid IPv4 address"): + config_validation.ipv4address(value) + + +@given(value=ip_addresses(v=6).map(str)) +def test_ipv6__valid(value): + config_validation.ipaddress(value) + + +@pytest.mark.parametrize("value", ("127.0.0", "localhost", "", "2001:db8::2::3")) +def test_ipv6__invalid(value): + with pytest.raises(Invalid, match="is not a valid IP address"): + config_validation.ipaddress(value) + + # TODO: ensure_list @given(integers()) def hex_int__valid(value): diff --git a/tests/unit_tests/test_core.py b/tests/unit_tests/test_core.py index 2860486efe..4f2a6453b4 100644 --- a/tests/unit_tests/test_core.py +++ b/tests/unit_tests/test_core.py @@ -1,10 +1,8 @@ -import pytest - from hypothesis import given -from hypothesis.strategies import ip_addresses +import pytest from strategies import mac_addr_strings -from esphome import core, const +from esphome import const, core class TestHexInt: @@ -26,25 +24,6 @@ class TestHexInt: assert actual == expected -class TestIPAddress: - @given(value=ip_addresses(v=4).map(str)) - def test_init__valid(self, value): - core.IPAddress(*value.split(".")) - - @pytest.mark.parametrize("value", ("127.0.0", "localhost", "")) - def test_init__invalid(self, value): - with pytest.raises(ValueError, match="IPAddress must consist of 4 items"): - core.IPAddress(*value.split(".")) - - @given(value=ip_addresses(v=4).map(str)) - def test_str(self, value): - target = core.IPAddress(*value.split(".")) - - actual = str(target) - - assert actual == value - - class TestMACAddress: @given(value=mac_addr_strings()) def test_init__valid(self, value): From 40bee2a854a0cd82872c83d5c81f812199df5495 Mon Sep 17 00:00:00 2001 From: Brian Whicheloe Date: Sun, 12 Jan 2025 11:15:22 -0800 Subject: [PATCH 16/86] Add log level env var (#7604) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/__main__.py | 19 ++++++++++++++++--- esphome/log.py | 12 +++++------- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index dce041e5ac..2a0bd8f2b3 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -758,6 +758,14 @@ def parse_args(argv): options_parser.add_argument( "-q", "--quiet", help="Disable all ESPHome logs.", action="store_true" ) + options_parser.add_argument( + "-l", + "--log-level", + help="Set the log level.", + default=os.getenv("ESPHOME_LOG_LEVEL", "INFO"), + action="store", + choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], + ) options_parser.add_argument( "--dashboard", help=argparse.SUPPRESS, action="store_true" ) @@ -987,11 +995,16 @@ def run_esphome(argv): args = parse_args(argv) CORE.dashboard = args.dashboard + # Override log level if verbose is set + if args.verbose: + args.log_level = "DEBUG" + elif args.quiet: + args.log_level = "CRITICAL" + setup_log( - args.verbose, - args.quiet, + log_level=args.log_level, # Show timestamp for dashboard access logs - args.command == "dashboard", + include_timestamp=args.command == "dashboard", ) if args.command in PRE_CONFIG_ACTIONS: diff --git a/esphome/log.py b/esphome/log.py index 23dc453d32..835cd6b44d 100644 --- a/esphome/log.py +++ b/esphome/log.py @@ -67,20 +67,18 @@ class ESPHomeLogFormatter(logging.Formatter): def setup_log( - debug: bool = False, quiet: bool = False, include_timestamp: bool = False + log_level=logging.INFO, + include_timestamp: bool = False, ) -> None: import colorama colorama.init() - if debug: - log_level = logging.DEBUG + if log_level == logging.DEBUG: CORE.verbose = True - elif quiet: - log_level = logging.CRITICAL + elif log_level == logging.CRITICAL: CORE.quiet = True - else: - log_level = logging.INFO + logging.basicConfig(level=log_level) logging.getLogger("urllib3").setLevel(logging.WARNING) From 731fb1d172f83d15a3f03152477dbd5f482cd19f Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Sun, 12 Jan 2025 23:15:39 +0100 Subject: [PATCH 17/86] [spi] relay on KEY_TARGET_PLATFORM as the other platforms does (#8066) --- esphome/components/spi/__init__.py | 6 +----- script/build_codeowners.py | 4 ++-- script/build_language_schema.py | 4 ++-- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/esphome/components/spi/__init__.py b/esphome/components/spi/__init__.py index 52afbf365e..3e6d680b89 100644 --- a/esphome/components/spi/__init__.py +++ b/esphome/components/spi/__init__.py @@ -97,11 +97,7 @@ RP_SPI_PINSETS = [ def get_target_platform(): - return ( - CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] - if KEY_TARGET_PLATFORM in CORE.data[KEY_CORE] - else "" - ) + return CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] def get_target_variant(): diff --git a/script/build_codeowners.py b/script/build_codeowners.py index db34ad7702..523fe8ac7f 100755 --- a/script/build_codeowners.py +++ b/script/build_codeowners.py @@ -5,7 +5,7 @@ from pathlib import Path import sys from esphome.config import get_component, get_platform -from esphome.const import KEY_CORE, KEY_TARGET_FRAMEWORK +from esphome.const import KEY_CORE, KEY_TARGET_FRAMEWORK, KEY_TARGET_PLATFORM from esphome.core import CORE from esphome.helpers import write_file_if_changed @@ -39,7 +39,7 @@ parts = [BASE] # Fake some directory so that get_component works CORE.config_path = str(root) -CORE.data[KEY_CORE] = {KEY_TARGET_FRAMEWORK: None} +CORE.data[KEY_CORE] = {KEY_TARGET_FRAMEWORK: None, KEY_TARGET_PLATFORM: None} codeowners = defaultdict(list) diff --git a/script/build_language_schema.py b/script/build_language_schema.py index 2023dc0402..07093e179a 100644 --- a/script/build_language_schema.py +++ b/script/build_language_schema.py @@ -85,12 +85,12 @@ def load_components(): # pylint: disable=wrong-import-position -from esphome.const import CONF_TYPE, KEY_CORE +from esphome.const import CONF_TYPE, KEY_CORE, KEY_TARGET_PLATFORM from esphome.core import CORE # pylint: enable=wrong-import-position -CORE.data[KEY_CORE] = {} +CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: None} load_components() # Import esphome after loading components (so schema is tracked) From 7c3942269200e43f81391cc0e63d636ab63b6761 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Jan 2025 12:54:44 +1300 Subject: [PATCH 18/86] Bump actions/upload-artifact from 4.5.0 to 4.6.0 (#8058) 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 0304cd2304..e791f666d4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -141,7 +141,7 @@ jobs: echo name=$(cat /tmp/platform) >> $GITHUB_OUTPUT - name: Upload digests - uses: actions/upload-artifact@v4.5.0 + uses: actions/upload-artifact@v4.6.0 with: name: digests-${{ steps.sanitize.outputs.name }} path: /tmp/digests From 571935fb3b97cd031390bb5b7ac4b4da5f62575b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Jan 2025 12:55:00 +1300 Subject: [PATCH 19/86] Bump peter-evans/create-pull-request from 7.0.5 to 7.0.6 (#8024) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/sync-device-classes.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sync-device-classes.yml b/.github/workflows/sync-device-classes.yml index 7a46d596a1..9160ab4a1b 100644 --- a/.github/workflows/sync-device-classes.yml +++ b/.github/workflows/sync-device-classes.yml @@ -36,7 +36,7 @@ jobs: python ./script/sync-device_class.py - name: Commit changes - uses: peter-evans/create-pull-request@v7.0.5 + uses: peter-evans/create-pull-request@v7.0.6 with: commit-message: "Synchronise Device Classes from Home Assistant" committer: esphomebot From f25f3334d11d5a508aad6cdf5056b4cc33213fe7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Jan 2025 12:55:37 +1300 Subject: [PATCH 20/86] Bump docker/setup-qemu-action from 3.2.0 to 3.3.0 in the docker-actions group (#8052) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci-docker.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-docker.yml b/.github/workflows/ci-docker.yml index cf7b08f3d4..b994cfaf17 100644 --- a/.github/workflows/ci-docker.yml +++ b/.github/workflows/ci-docker.yml @@ -48,7 +48,7 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3.8.0 - name: Set up QEMU - uses: docker/setup-qemu-action@v3.2.0 + uses: docker/setup-qemu-action@v3.3.0 - name: Set TAG run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e791f666d4..962bc66e94 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -93,7 +93,7 @@ jobs: uses: docker/setup-buildx-action@v3.8.0 - name: Set up QEMU if: matrix.platform != 'linux/amd64' - uses: docker/setup-qemu-action@v3.2.0 + uses: docker/setup-qemu-action@v3.3.0 - name: Log in to docker hub uses: docker/login-action@v3.3.0 From 739edce268f83657ce5ef7d823008260a7c424db Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Jan 2025 12:55:53 +1300 Subject: [PATCH 21/86] Bump docker/build-push-action from 6.10.0 to 6.11.0 in /.github/actions/build-image (#8053) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/actions/build-image/action.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/actions/build-image/action.yaml b/.github/actions/build-image/action.yaml index cc9894a657..e6b6da4177 100644 --- a/.github/actions/build-image/action.yaml +++ b/.github/actions/build-image/action.yaml @@ -46,7 +46,7 @@ runs: - name: Build and push to ghcr by digest id: build-ghcr - uses: docker/build-push-action@v6.10.0 + uses: docker/build-push-action@v6.11.0 env: DOCKER_BUILD_SUMMARY: false DOCKER_BUILD_RECORD_UPLOAD: false @@ -72,7 +72,7 @@ runs: - name: Build and push to dockerhub by digest id: build-dockerhub - uses: docker/build-push-action@v6.10.0 + uses: docker/build-push-action@v6.11.0 env: DOCKER_BUILD_SUMMARY: false DOCKER_BUILD_RECORD_UPLOAD: false From 4409471cd1c20a43a45bcfddead959c9d58c3419 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 13 Jan 2025 14:32:10 +1300 Subject: [PATCH 22/86] Bump python3-setuptools to 66.1.1-1+deb12u1 (#8074) --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 0bb558d35e..429f5c4a1f 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -29,7 +29,7 @@ RUN \ # Use pinned versions so that we get updates with build caching && apt-get install -y --no-install-recommends \ python3-pip=23.0.1+dfsg-1 \ - python3-setuptools=66.1.1-1 \ + python3-setuptools=66.1.1-1+deb12u1 \ python3-venv=3.11.2-1+b1 \ python3-wheel=0.38.4-2 \ iputils-ping=3:20221126-1+deb12u1 \ From fb87a1c0bc565447785f90367f28bb0e74584f49 Mon Sep 17 00:00:00 2001 From: Mischa Siekmann <45062894+gnumpi@users.noreply.github.com> Date: Mon, 13 Jan 2025 02:42:03 +0100 Subject: [PATCH 23/86] Allow CONF_RMT_CHANNEL parameter for IDF 4.X (#8035) --- .../components/esp32_rmt_led_strip/light.py | 54 +++++++++++++++++-- 1 file changed, 49 insertions(+), 5 deletions(-) diff --git a/esphome/components/esp32_rmt_led_strip/light.py b/esphome/components/esp32_rmt_led_strip/light.py index 67a0e31461..64104bb6de 100644 --- a/esphome/components/esp32_rmt_led_strip/light.py +++ b/esphome/components/esp32_rmt_led_strip/light.py @@ -1,4 +1,5 @@ from dataclasses import dataclass +import logging from esphome import pins import esphome.codegen as cg @@ -15,6 +16,9 @@ from esphome.const import ( CONF_RMT_CHANNEL, CONF_RMT_SYMBOLS, ) +from esphome.core import CORE + +_LOGGER = logging.getLogger(__name__) CODEOWNERS = ["@jesserockz"] DEPENDENCIES = ["esp32"] @@ -64,13 +68,53 @@ CONF_RESET_HIGH = "reset_high" CONF_RESET_LOW = "reset_low" +class OptionalForIDF5(cv.SplitDefault): + @property + def default(self): + if not esp32_rmt.use_new_rmt_driver(): + return cv.UNDEFINED + return super().default + + @default.setter + def default(self, value): + # Ignore default set from vol.Optional + pass + + +def only_with_new_rmt_driver(obj): + if not esp32_rmt.use_new_rmt_driver(): + raise cv.Invalid( + "This feature is only available for the IDF framework version 5." + ) + return obj + + +def not_with_new_rmt_driver(obj): + if esp32_rmt.use_new_rmt_driver(): + raise cv.Invalid( + "This feature is not available for the IDF framework version 5." + ) + return obj + + def final_validation(config): - if not esp32_rmt.use_new_rmt_driver() and CONF_RMT_CHANNEL not in config: - raise cv.Invalid("rmt_channel is a required option.") + if not esp32_rmt.use_new_rmt_driver(): + if CONF_RMT_CHANNEL not in config: + if CORE.using_esp_idf: + raise cv.Invalid( + "rmt_channel is a required option for IDF version < 5." + ) + raise cv.Invalid( + "rmt_channel is a required option for the Arduino framework." + ) + _LOGGER.warning( + "RMT_LED_STRIP support for IDF version < 5 is deprecated and will be removed soon." + ) FINAL_VALIDATE_SCHEMA = final_validation + CONFIG_SCHEMA = cv.All( light.ADDRESSABLE_LIGHT_SCHEMA.extend( { @@ -79,9 +123,9 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_NUM_LEDS): cv.positive_not_null_int, cv.Required(CONF_RGB_ORDER): cv.enum(RGB_ORDERS, upper=True), cv.Optional(CONF_RMT_CHANNEL): cv.All( - cv.only_with_arduino, esp32_rmt.validate_rmt_channel(tx=True) + not_with_new_rmt_driver, esp32_rmt.validate_rmt_channel(tx=True) ), - cv.SplitDefault( + OptionalForIDF5( CONF_RMT_SYMBOLS, esp32_idf=64, esp32_s2_idf=64, @@ -89,7 +133,7 @@ CONFIG_SCHEMA = cv.All( esp32_c3_idf=48, esp32_c6_idf=48, esp32_h2_idf=48, - ): cv.All(cv.only_with_esp_idf, cv.int_range(min=2)), + ): cv.All(only_with_new_rmt_driver, cv.int_range(min=2)), cv.Optional(CONF_MAX_REFRESH_RATE): cv.positive_time_period_microseconds, cv.Optional(CONF_CHIPSET): cv.one_of(*CHIPSETS, upper=True), cv.Optional(CONF_IS_RGBW, default=False): cv.boolean, From aac38419915486fff121651bf6a4d063f6f4b312 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sun, 12 Jan 2025 20:45:35 -0500 Subject: [PATCH 24/86] [esp32] Fix arch_get_cpu_freq_hz (#8047) Co-authored-by: Jonathan Swoboda --- esphome/components/esp32/core.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/esphome/components/esp32/core.cpp b/esphome/components/esp32/core.cpp index 48c8b2b04d..ff8e663ec1 100644 --- a/esphome/components/esp32/core.cpp +++ b/esphome/components/esp32/core.cpp @@ -58,7 +58,11 @@ uint32_t arch_get_cpu_cycle_count() { return esp_cpu_get_cycle_count(); } #else uint32_t arch_get_cpu_cycle_count() { return cpu_hal_get_cycle_count(); } #endif -uint32_t arch_get_cpu_freq_hz() { return rtc_clk_apb_freq_get(); } +uint32_t arch_get_cpu_freq_hz() { + rtc_cpu_freq_config_t config; + rtc_clk_cpu_freq_get_config(&config); + return config.freq_mhz * 1000000U; +} #ifdef USE_ESP_IDF TaskHandle_t loop_task_handle = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) From dd3ffc7f29d8436171af8ce070ab68f7e226d570 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20=C5=BBbik?= Date: Mon, 13 Jan 2025 03:55:30 +0100 Subject: [PATCH 25/86] Fix Waveshare 7in5bv3bwr image quality in BWR mode (#8043) Co-authored-by: zbikmarc --- .../waveshare_epaper/waveshare_epaper.cpp | 27 +++++++------------ 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.cpp b/esphome/components/waveshare_epaper/waveshare_epaper.cpp index cb3b19aa1a..fb9e8ff6e5 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.cpp +++ b/esphome/components/waveshare_epaper/waveshare_epaper.cpp @@ -2425,28 +2425,21 @@ void WaveshareEPaper7P5InBV3BWR::init_display_() { this->command(0x01); // 1-0=11: internal power - this->data(0x07); - this->data(0x17); // VGH&VGL - this->data(0x3F); // VSH - this->data(0x26); // VSL - this->data(0x11); // VSHR + this->data(0x07); // VRS_EN=1, VS_EN=1, VG_EN=1 + this->data(0x17); // VGH&VGL ??? VCOM_SLEW=1 but this is fixed, VG_LVL[2:0]=111 => VGH=20V VGL=-20V, it could be 0x07 + this->data(0x3F); // VSH=15V? + this->data(0x26); // VSL=-9.4V? + this->data(0x11); // VSHR=5.8V? // VCOM DC Setting this->command(0x82); - this->data(0x24); // VCOM - - // Booster Setting - this->command(0x06); - this->data(0x27); - this->data(0x27); - this->data(0x2F); - this->data(0x17); + this->data(0x24); // VCOM=-1.9V // POWER ON this->command(0x04); - delay(100); // NOLINT this->wait_until_idle_(); + // COMMAND PANEL SETTING this->command(0x00); this->data(0x0F); // KW-3f KWR-2F BWROTP 0f BWOTP 1f @@ -2457,16 +2450,16 @@ void WaveshareEPaper7P5InBV3BWR::init_display_() { this->data(0x20); this->data(0x01); // gate 480 this->data(0xE0); - // COMMAND ...? - this->command(0x15); - this->data(0x00); + // COMMAND VCOM AND DATA INTERVAL SETTING this->command(0x50); this->data(0x20); this->data(0x00); + // COMMAND TCON SETTING this->command(0x60); this->data(0x22); + // Resolution setting this->command(0x65); this->data(0x00); From 92a8ebe1f8c15db118dac1269eb309b83f0ed9ee Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 13 Jan 2025 13:56:42 +1100 Subject: [PATCH 26/86] [json] use correct formatting (#8039) --- esphome/components/json/json_util.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/json/json_util.cpp b/esphome/components/json/json_util.cpp index 87b1cc6d2d..d50b2b483c 100644 --- a/esphome/components/json/json_util.cpp +++ b/esphome/components/json/json_util.cpp @@ -17,11 +17,11 @@ std::string build_json(const json_build_t &f) { auto free_heap = ALLOCATOR.get_max_free_block_size(); size_t request_size = std::min(free_heap, (size_t) 512); while (true) { - ESP_LOGV(TAG, "Attempting to allocate %u bytes for JSON serialization", request_size); + ESP_LOGV(TAG, "Attempting to allocate %zu bytes for JSON serialization", request_size); DynamicJsonDocument json_document(request_size); if (json_document.capacity() == 0) { ESP_LOGE(TAG, - "Could not allocate memory for JSON document! Requested %u bytes, largest free heap block: %u bytes", + "Could not allocate memory for JSON document! Requested %zu bytes, largest free heap block: %zu bytes", request_size, free_heap); return "{}"; } @@ -29,7 +29,7 @@ std::string build_json(const json_build_t &f) { f(root); if (json_document.overflowed()) { if (request_size == free_heap) { - ESP_LOGE(TAG, "Could not allocate memory for JSON document! Overflowed largest free heap block: %u bytes", + ESP_LOGE(TAG, "Could not allocate memory for JSON document! Overflowed largest free heap block: %zu bytes", free_heap); return "{}"; } @@ -37,7 +37,7 @@ std::string build_json(const json_build_t &f) { continue; } json_document.shrinkToFit(); - ESP_LOGV(TAG, "Size after shrink %u bytes", json_document.capacity()); + ESP_LOGV(TAG, "Size after shrink %zu bytes", json_document.capacity()); std::string output; serializeJson(json_document, output); return output; From aa87c607173d9ef65e047a5e6b73d7ab4fac649d Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Sun, 12 Jan 2025 21:12:54 -0600 Subject: [PATCH 27/86] [nextion] Brightness control tweaks (#8027) --- esphome/components/nextion/automation.h | 17 +++++++++ esphome/components/nextion/display.py | 51 ++++++++++++++++++------- esphome/components/nextion/nextion.cpp | 4 +- esphome/components/nextion/nextion.h | 2 +- tests/components/nextion/common.yaml | 2 + 5 files changed, 61 insertions(+), 15 deletions(-) diff --git a/esphome/components/nextion/automation.h b/esphome/components/nextion/automation.h index 65f1fd0058..c718355af8 100644 --- a/esphome/components/nextion/automation.h +++ b/esphome/components/nextion/automation.h @@ -49,6 +49,23 @@ class TouchTrigger : public Trigger { } }; +template class NextionSetBrightnessAction : public Action { + public: + explicit NextionSetBrightnessAction(Nextion *component) : component_(component) {} + + TEMPLATABLE_VALUE(float, brightness) + + void play(Ts... x) override { + this->component_->set_brightness(this->brightness_.value(x...)); + this->component_->set_backlight_brightness(this->brightness_.value(x...)); + } + + void set_brightness(std::function brightness) { this->brightness_ = brightness; } + + protected: + Nextion *component_; +}; + template class NextionPublishFloatAction : public Action { public: explicit NextionPublishFloatAction(NextionComponent *component) : component_(component) {} diff --git a/esphome/components/nextion/display.py b/esphome/components/nextion/display.py index f6bd863d42..60f26e5234 100644 --- a/esphome/components/nextion/display.py +++ b/esphome/components/nextion/display.py @@ -1,30 +1,30 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import automation -from esphome.components import display, uart -from esphome.components import esp32 +import esphome.codegen as cg +from esphome.components import display, esp32, uart +import esphome.config_validation as cv from esphome.const import ( + CONF_BRIGHTNESS, CONF_ID, CONF_LAMBDA, - CONF_BRIGHTNESS, - CONF_TRIGGER_ID, CONF_ON_TOUCH, + CONF_TRIGGER_ID, ) from esphome.core import CORE + from . import Nextion, nextion_ns, nextion_ref from .base_component import ( + CONF_AUTO_WAKE_ON_TOUCH, + CONF_EXIT_REPARSE_ON_START, CONF_ON_BUFFER_OVERFLOW, + CONF_ON_PAGE, + CONF_ON_SETUP, CONF_ON_SLEEP, CONF_ON_WAKE, - CONF_ON_SETUP, - CONF_ON_PAGE, + CONF_SKIP_CONNECTION_HANDSHAKE, + CONF_START_UP_PAGE, CONF_TFT_URL, CONF_TOUCH_SLEEP_TIMEOUT, CONF_WAKE_UP_PAGE, - CONF_START_UP_PAGE, - CONF_AUTO_WAKE_ON_TOUCH, - CONF_EXIT_REPARSE_ON_START, - CONF_SKIP_CONNECTION_HANDSHAKE, ) CODEOWNERS = ["@senexcrenshaw", "@edwardtfn"] @@ -32,6 +32,9 @@ CODEOWNERS = ["@senexcrenshaw", "@edwardtfn"] DEPENDENCIES = ["uart"] AUTO_LOAD = ["binary_sensor", "switch", "sensor", "text_sensor"] +NextionSetBrightnessAction = nextion_ns.class_( + "NextionSetBrightnessAction", automation.Action +) SetupTrigger = nextion_ns.class_("SetupTrigger", automation.Trigger.template()) SleepTrigger = nextion_ns.class_("SleepTrigger", automation.Trigger.template()) WakeTrigger = nextion_ns.class_("WakeTrigger", automation.Trigger.template()) @@ -46,7 +49,7 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(Nextion), cv.Optional(CONF_TFT_URL): cv.url, - cv.Optional(CONF_BRIGHTNESS, default=1.0): cv.percentage, + cv.Optional(CONF_BRIGHTNESS): cv.percentage, cv.Optional(CONF_ON_SETUP): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SetupTrigger), @@ -92,12 +95,34 @@ CONFIG_SCHEMA = ( ) +@automation.register_action( + "display.nextion.set_brightness", + NextionSetBrightnessAction, + cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(Nextion), + cv.Required(CONF_BRIGHTNESS): cv.templatable(cv.percentage), + }, + key=CONF_BRIGHTNESS, + ), +) +async def nextion_set_brightness_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + + template_ = await cg.templatable(config[CONF_BRIGHTNESS], args, float) + cg.add(var.set_brightness(template_)) + + return var + + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await uart.register_uart_device(var, config) if CONF_BRIGHTNESS in config: cg.add(var.set_brightness(config[CONF_BRIGHTNESS])) + if CONF_LAMBDA in config: lambda_ = await cg.process_lambda( config[CONF_LAMBDA], [(nextion_ref, "it")], return_type=cg.void diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index e5df13c64e..67f08f68f8 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -273,7 +273,9 @@ void Nextion::loop() { this->sent_setup_commands_ = true; this->send_command_("bkcmd=3"); // Always, returns 0x00 to 0x23 result of serial command. - this->set_backlight_brightness(this->brightness_); + if (this->brightness_.has_value()) { + this->set_backlight_brightness(this->brightness_.value()); + } // Check if a startup page has been set and send the command if (this->start_up_page_ != -1) { diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index c293f80aee..b2404e1f0d 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -1339,7 +1339,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe CallbackManager buffer_overflow_callback_{}; optional writer_; - float brightness_{1.0}; + optional brightness_; std::string device_model_; std::string firmware_version_; diff --git a/tests/components/nextion/common.yaml b/tests/components/nextion/common.yaml index 73fc8484c0..589afcfefb 100644 --- a/tests/components/nextion/common.yaml +++ b/tests/components/nextion/common.yaml @@ -2,6 +2,8 @@ esphome: on_boot: - lambda: 'ESP_LOGD("display","is_connected(): %s", YESNO(id(main_lcd).is_connected()));' + - display.nextion.set_brightness: 80% + # Binary sensor publish action tests - binary_sensor.nextion.publish: id: r0_sensor From f1c0570e3b51062f12c8ebf0fcefeb6fc81e22b7 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 13 Jan 2025 14:21:42 +1100 Subject: [PATCH 28/86] [image] Transparency changes; code refactor (#7908) --- CODEOWNERS | 2 +- esphome/components/animation/__init__.py | 284 +------ esphome/components/animation/animation.cpp | 4 +- esphome/components/animation/animation.h | 3 +- esphome/components/image/__init__.py | 691 +++++++++++------- esphome/components/image/image.cpp | 143 ++-- esphome/components/image/image.h | 39 +- esphome/components/online_image/__init__.py | 118 +-- .../components/online_image/image_decoder.h | 11 +- .../components/online_image/online_image.cpp | 92 +-- .../components/online_image/online_image.h | 3 +- esphome/components/online_image/png_image.h | 1 + script/ci-custom.py | 17 +- tests/components/animation/.gitattributes | 4 + tests/components/animation/anim.apng | Bin 0 -> 12626 bytes tests/components/animation/anim.gif | Bin 0 -> 9735 bytes tests/components/animation/anim.webp | Bin 0 -> 8244 bytes tests/components/animation/common.yaml | 23 + .../components/animation/test.esp32-ard.yaml | 10 +- .../animation/test.esp32-c3-ard.yaml | 11 +- .../animation/test.esp32-c3-idf.yaml | 11 +- .../components/animation/test.esp32-idf.yaml | 11 +- .../animation/test.esp8266-ard.yaml | 11 +- .../components/animation/test.rp2040-ard.yaml | 11 +- tests/components/image/common.yaml | 49 +- tests/components/image/test.host.yaml | 42 +- tests/components/online_image/common.yaml | 41 +- 27 files changed, 845 insertions(+), 787 deletions(-) create mode 100644 tests/components/animation/.gitattributes create mode 100644 tests/components/animation/anim.apng create mode 100644 tests/components/animation/anim.gif create mode 100644 tests/components/animation/anim.webp create mode 100644 tests/components/animation/common.yaml diff --git a/CODEOWNERS b/CODEOWNERS index 404ad35efc..088e350f5d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -302,7 +302,7 @@ esphome/components/noblex/* @AGalfra esphome/components/npi19/* @bakerkj esphome/components/number/* @esphome/core esphome/components/one_wire/* @ssieb -esphome/components/online_image/* @guillempages +esphome/components/online_image/* @clydebarrow @guillempages esphome/components/opentherm/* @olegtarasov esphome/components/ota/* @esphome/core esphome/components/output/* @esphome/core diff --git a/esphome/components/animation/__init__.py b/esphome/components/animation/__init__.py index 21a82649f0..f73b8ef08f 100644 --- a/esphome/components/animation/__init__.py +++ b/esphome/components/animation/__init__.py @@ -1,28 +1,10 @@ import logging -from esphome import automation, core +from esphome import automation import esphome.codegen as cg import esphome.components.image as espImage -from esphome.components.image import ( - CONF_USE_TRANSPARENCY, - LOCAL_SCHEMA, - SOURCE_LOCAL, - SOURCE_WEB, - WEB_SCHEMA, -) import esphome.config_validation as cv -from esphome.const import ( - CONF_FILE, - CONF_ID, - CONF_PATH, - CONF_RAW_DATA_ID, - CONF_REPEAT, - CONF_RESIZE, - CONF_SOURCE, - CONF_TYPE, - CONF_URL, -) -from esphome.core import CORE, HexInt +from esphome.const import CONF_ID, CONF_REPEAT _LOGGER = logging.getLogger(__name__) @@ -30,6 +12,7 @@ AUTO_LOAD = ["image"] CODEOWNERS = ["@syndlex"] DEPENDENCIES = ["display"] MULTI_CONF = True +MULTI_CONF_NO_DEFAULT = True CONF_LOOP = "loop" CONF_START_FRAME = "start_frame" @@ -51,86 +34,19 @@ SetFrameAction = animation_ns.class_( "AnimationSetFrameAction", automation.Action, cg.Parented.template(Animation_) ) -TYPED_FILE_SCHEMA = cv.typed_schema( +CONFIG_SCHEMA = espImage.IMAGE_SCHEMA.extend( { - SOURCE_LOCAL: LOCAL_SCHEMA, - SOURCE_WEB: WEB_SCHEMA, - }, - key=CONF_SOURCE, -) - - -def _file_schema(value): - if isinstance(value, str): - return validate_file_shorthand(value) - return TYPED_FILE_SCHEMA(value) - - -FILE_SCHEMA = cv.Schema(_file_schema) - - -def validate_file_shorthand(value): - value = cv.string_strict(value) - if value.startswith("http://") or value.startswith("https://"): - return FILE_SCHEMA( + cv.Required(CONF_ID): cv.declare_id(Animation_), + cv.Optional(CONF_LOOP): cv.All( { - CONF_SOURCE: SOURCE_WEB, - CONF_URL: value, + cv.Optional(CONF_START_FRAME, default=0): cv.positive_int, + cv.Optional(CONF_END_FRAME): cv.positive_int, + cv.Optional(CONF_REPEAT): cv.positive_int, } - ) - return FILE_SCHEMA( - { - CONF_SOURCE: SOURCE_LOCAL, - CONF_PATH: value, - } - ) - - -def validate_cross_dependencies(config): - """ - Validate fields whose possible values depend on other fields. - For example, validate that explicitly transparent image types - have "use_transparency" set to True. - Also set the default value for those kind of dependent fields. - """ - image_type = config[CONF_TYPE] - is_transparent_type = image_type in ["TRANSPARENT_BINARY", "RGBA"] - # If the use_transparency option was not specified, set the default depending on the image type - if CONF_USE_TRANSPARENCY not in config: - config[CONF_USE_TRANSPARENCY] = is_transparent_type - - if is_transparent_type and not config[CONF_USE_TRANSPARENCY]: - raise cv.Invalid(f"Image type {image_type} must always be transparent.") - - return config - - -ANIMATION_SCHEMA = cv.Schema( - cv.All( - { - cv.Required(CONF_ID): cv.declare_id(Animation_), - cv.Required(CONF_FILE): FILE_SCHEMA, - cv.Optional(CONF_RESIZE): cv.dimensions, - cv.Optional(CONF_TYPE, default="BINARY"): cv.enum( - espImage.IMAGE_TYPE, upper=True - ), - # Not setting default here on purpose; the default depends on the image type, - # and thus will be set in the "validate_cross_dependencies" validator. - cv.Optional(CONF_USE_TRANSPARENCY): cv.boolean, - cv.Optional(CONF_LOOP): cv.All( - { - cv.Optional(CONF_START_FRAME, default=0): cv.positive_int, - cv.Optional(CONF_END_FRAME): cv.positive_int, - cv.Optional(CONF_REPEAT): cv.positive_int, - } - ), - cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), - }, - validate_cross_dependencies, - ) + ), + }, ) -CONFIG_SCHEMA = ANIMATION_SCHEMA NEXT_FRAME_SCHEMA = automation.maybe_simple_id( { @@ -164,180 +80,26 @@ async def animation_action_to_code(config, action_id, template_arg, args): async def to_code(config): - from PIL import Image + ( + prog_arr, + width, + height, + image_type, + trans_value, + frame_count, + ) = await espImage.write_image(config, all_frames=True) - conf_file = config[CONF_FILE] - if conf_file[CONF_SOURCE] == SOURCE_LOCAL: - path = CORE.relative_config_path(conf_file[CONF_PATH]) - elif conf_file[CONF_SOURCE] == SOURCE_WEB: - path = espImage.compute_local_image_path(conf_file).as_posix() - else: - raise core.EsphomeError(f"Unknown animation source: {conf_file[CONF_SOURCE]}") - - try: - image = Image.open(path) - except Exception as e: - raise core.EsphomeError(f"Could not load image file {path}: {e}") - - width, height = image.size - frames = image.n_frames - if CONF_RESIZE in config: - new_width_max, new_height_max = config[CONF_RESIZE] - ratio = min(new_width_max / width, new_height_max / height) - width, height = int(width * ratio), int(height * ratio) - elif width > 500 or height > 500: - _LOGGER.warning( - 'The image "%s" you requested is very big. Please consider' - " using the resize parameter.", - path, - ) - - transparent = config[CONF_USE_TRANSPARENCY] - - if config[CONF_TYPE] == "GRAYSCALE": - data = [0 for _ in range(height * width * frames)] - pos = 0 - for frameIndex in range(frames): - image.seek(frameIndex) - frame = image.convert("LA", dither=Image.Dither.NONE) - if CONF_RESIZE in config: - frame = frame.resize([width, height]) - pixels = list(frame.getdata()) - if len(pixels) != height * width: - raise core.EsphomeError( - f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height * width})" - ) - for pix, a in pixels: - if transparent: - if pix == 1: - pix = 0 - if a < 0x80: - pix = 1 - - data[pos] = pix - pos += 1 - - elif config[CONF_TYPE] == "RGBA": - data = [0 for _ in range(height * width * 4 * frames)] - pos = 0 - for frameIndex in range(frames): - image.seek(frameIndex) - frame = image.convert("RGBA") - if CONF_RESIZE in config: - frame = frame.resize([width, height]) - pixels = list(frame.getdata()) - if len(pixels) != height * width: - raise core.EsphomeError( - f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height * width})" - ) - for pix in pixels: - data[pos] = pix[0] - pos += 1 - data[pos] = pix[1] - pos += 1 - data[pos] = pix[2] - pos += 1 - data[pos] = pix[3] - pos += 1 - - elif config[CONF_TYPE] == "RGB24": - data = [0 for _ in range(height * width * 3 * frames)] - pos = 0 - for frameIndex in range(frames): - image.seek(frameIndex) - frame = image.convert("RGBA") - if CONF_RESIZE in config: - frame = frame.resize([width, height]) - pixels = list(frame.getdata()) - if len(pixels) != height * width: - raise core.EsphomeError( - f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height * width})" - ) - for r, g, b, a in pixels: - if transparent: - if r == 0 and g == 0 and b == 1: - b = 0 - if a < 0x80: - r = 0 - g = 0 - b = 1 - - data[pos] = r - pos += 1 - data[pos] = g - pos += 1 - data[pos] = b - pos += 1 - - elif config[CONF_TYPE] in ["RGB565", "TRANSPARENT_IMAGE"]: - bytes_per_pixel = 3 if transparent else 2 - data = [0 for _ in range(height * width * bytes_per_pixel * frames)] - pos = 0 - for frameIndex in range(frames): - image.seek(frameIndex) - frame = image.convert("RGBA") - if CONF_RESIZE in config: - frame = frame.resize([width, height]) - pixels = list(frame.getdata()) - if len(pixels) != height * width: - raise core.EsphomeError( - f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height * width})" - ) - for r, g, b, a in pixels: - R = r >> 3 - G = g >> 2 - B = b >> 3 - rgb = (R << 11) | (G << 5) | B - data[pos] = rgb >> 8 - pos += 1 - data[pos] = rgb & 0xFF - pos += 1 - if transparent: - data[pos] = a - pos += 1 - - elif config[CONF_TYPE] in ["BINARY", "TRANSPARENT_BINARY"]: - width8 = ((width + 7) // 8) * 8 - data = [0 for _ in range((height * width8 // 8) * frames)] - for frameIndex in range(frames): - image.seek(frameIndex) - if transparent: - alpha = image.split()[-1] - has_alpha = alpha.getextrema()[0] < 0xFF - else: - has_alpha = False - frame = image.convert("1", dither=Image.Dither.NONE) - if CONF_RESIZE in config: - frame = frame.resize([width, height]) - if transparent: - alpha = alpha.resize([width, height]) - for x, y in [(i, j) for i in range(width) for j in range(height)]: - if transparent and has_alpha: - if not alpha.getpixel((x, y)): - continue - elif frame.getpixel((x, y)): - continue - - pos = x + y * width8 + (height * width8 * frameIndex) - data[pos // 8] |= 0x80 >> (pos % 8) - else: - raise core.EsphomeError( - f"Animation f{config[CONF_ID]} has not supported type {config[CONF_TYPE]}." - ) - - rhs = [HexInt(x) for x in data] - prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) var = cg.new_Pvariable( config[CONF_ID], prog_arr, width, height, - frames, - espImage.IMAGE_TYPE[config[CONF_TYPE]], + frame_count, + image_type, + trans_value, ) - cg.add(var.set_transparency(transparent)) if loop_config := config.get(CONF_LOOP): start = loop_config[CONF_START_FRAME] - end = loop_config.get(CONF_END_FRAME, frames) + end = loop_config.get(CONF_END_FRAME, frame_count) count = loop_config.get(CONF_REPEAT, -1) cg.add(var.set_loop(start, end, count)) diff --git a/esphome/components/animation/animation.cpp b/esphome/components/animation/animation.cpp index 1375dfe07e..6db6f1a7bd 100644 --- a/esphome/components/animation/animation.cpp +++ b/esphome/components/animation/animation.cpp @@ -6,8 +6,8 @@ namespace esphome { namespace animation { Animation::Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, - image::ImageType type) - : Image(data_start, width, height, type), + image::ImageType type, image::Transparency transparent) + : Image(data_start, width, height, type, transparent), animation_data_start_(data_start), current_frame_(0), animation_frame_count_(animation_frame_count), diff --git a/esphome/components/animation/animation.h b/esphome/components/animation/animation.h index 272c5153d1..c44e0060af 100644 --- a/esphome/components/animation/animation.h +++ b/esphome/components/animation/animation.h @@ -8,7 +8,8 @@ namespace animation { class Animation : public image::Image { public: - Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, image::ImageType type); + Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, image::ImageType type, + image::Transparency transparent); uint32_t get_animation_frame_count() const; int get_current_frame() const; diff --git a/esphome/components/image/__init__.py b/esphome/components/image/__init__.py index 4669a3418a..801b05e160 100644 --- a/esphome/components/image/__init__.py +++ b/esphome/components/image/__init__.py @@ -6,7 +6,7 @@ import logging from pathlib import Path import re -import puremagic +from PIL import Image, UnidentifiedImageError from esphome import core, external_files import esphome.codegen as cg @@ -29,21 +29,236 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = "image" DEPENDENCIES = ["display"] -MULTI_CONF = True -MULTI_CONF_NO_DEFAULT = True image_ns = cg.esphome_ns.namespace("image") ImageType = image_ns.enum("ImageType") + +CONF_OPAQUE = "opaque" +CONF_CHROMA_KEY = "chroma_key" +CONF_ALPHA_CHANNEL = "alpha_channel" +CONF_INVERT_ALPHA = "invert_alpha" + +TRANSPARENCY_TYPES = ( + CONF_OPAQUE, + CONF_CHROMA_KEY, + CONF_ALPHA_CHANNEL, +) + + +def get_image_type_enum(type): + return getattr(ImageType, f"IMAGE_TYPE_{type.upper()}") + + +def get_transparency_enum(transparency): + return getattr(TransparencyType, f"TRANSPARENCY_{transparency.upper()}") + + +class ImageEncoder: + """ + Superclass of image type encoders + """ + + # Control which transparency options are available for a given type + allow_config = {CONF_ALPHA_CHANNEL, CONF_CHROMA_KEY, CONF_OPAQUE} + + # All imageencoder types are valid + @staticmethod + def validate(value): + return value + + def __init__(self, width, height, transparency, dither, invert_alpha): + """ + :param width: The image width in pixels + :param height: The image height in pixels + :param transparency: Transparency type + :param dither: Dither method + :param invert_alpha: True if the alpha channel should be inverted; for monochrome formats inverts the colours. + """ + self.transparency = transparency + self.width = width + self.height = height + self.data = [0 for _ in range(width * height)] + self.dither = dither + self.index = 0 + self.invert_alpha = invert_alpha + + def convert(self, image): + """ + Convert the image format + :param image: Input image + :return: converted image + """ + return image + + def encode(self, pixel): + """ + Encode a single pixel + """ + + def end_row(self): + """ + Marks the end of a pixel row + :return: + """ + + +class ImageBinary(ImageEncoder): + allow_config = {CONF_OPAQUE, CONF_INVERT_ALPHA, CONF_CHROMA_KEY} + + def __init__(self, width, height, transparency, dither, invert_alpha): + self.width8 = (width + 7) // 8 + super().__init__(self.width8, height, transparency, dither, invert_alpha) + self.bitno = 0 + + def convert(self, image): + return image.convert("1", dither=self.dither) + + def encode(self, pixel): + if self.invert_alpha: + pixel = not pixel + if pixel: + self.data[self.index] |= 0x80 >> (self.bitno % 8) + self.bitno += 1 + if self.bitno == 8: + self.bitno = 0 + self.index += 1 + + def end_row(self): + """ + Pad rows to a byte boundary + """ + if self.bitno != 0: + self.bitno = 0 + self.index += 1 + + +class ImageGrayscale(ImageEncoder): + allow_config = {CONF_ALPHA_CHANNEL, CONF_CHROMA_KEY, CONF_INVERT_ALPHA, CONF_OPAQUE} + + def convert(self, image): + return image.convert("LA") + + def encode(self, pixel): + b, a = pixel + if self.transparency == CONF_CHROMA_KEY: + if b == 1: + b = 0 + if a != 0xFF: + b = 1 + if self.invert_alpha: + b ^= 0xFF + if self.transparency == CONF_ALPHA_CHANNEL: + if a != 0xFF: + b = a + self.data[self.index] = b + self.index += 1 + + +class ImageRGB565(ImageEncoder): + def __init__(self, width, height, transparency, dither, invert_alpha): + stride = 3 if transparency == CONF_ALPHA_CHANNEL else 2 + super().__init__( + width * stride, + height, + transparency, + dither, + invert_alpha, + ) + + def convert(self, image): + return image.convert("RGBA") + + def encode(self, pixel): + r, g, b, a = pixel + r = r >> 3 + g = g >> 2 + b = b >> 3 + if self.transparency == CONF_CHROMA_KEY: + if r == 0 and g == 1 and b == 0: + g = 0 + elif a < 128: + r = 0 + g = 1 + b = 0 + rgb = (r << 11) | (g << 5) | b + self.data[self.index] = rgb >> 8 + self.index += 1 + self.data[self.index] = rgb & 0xFF + self.index += 1 + if self.transparency == CONF_ALPHA_CHANNEL: + if self.invert_alpha: + a ^= 0xFF + self.data[self.index] = a + self.index += 1 + + +class ImageRGB(ImageEncoder): + def __init__(self, width, height, transparency, dither, invert_alpha): + stride = 4 if transparency == CONF_ALPHA_CHANNEL else 3 + super().__init__( + width * stride, + height, + transparency, + dither, + invert_alpha, + ) + + def convert(self, image): + return image.convert("RGBA") + + def encode(self, pixel): + r, g, b, a = pixel + if self.transparency == CONF_CHROMA_KEY: + if r == 0 and g == 1 and b == 0: + g = 0 + elif a < 128: + r = 0 + g = 1 + b = 0 + self.data[self.index] = r + self.index += 1 + self.data[self.index] = g + self.index += 1 + self.data[self.index] = b + self.index += 1 + if self.transparency == CONF_ALPHA_CHANNEL: + if self.invert_alpha: + a ^= 0xFF + self.data[self.index] = a + self.index += 1 + + +class ReplaceWith: + """ + Placeholder class to provide feedback on deprecated features + """ + + allow_config = {CONF_ALPHA_CHANNEL, CONF_CHROMA_KEY, CONF_OPAQUE} + + def __init__(self, replace_with): + self.replace_with = replace_with + + def validate(self, value): + raise cv.Invalid( + f"Image type {value} is removed; replace with {self.replace_with}" + ) + + IMAGE_TYPE = { - "BINARY": ImageType.IMAGE_TYPE_BINARY, - "TRANSPARENT_BINARY": ImageType.IMAGE_TYPE_BINARY, - "GRAYSCALE": ImageType.IMAGE_TYPE_GRAYSCALE, - "RGB565": ImageType.IMAGE_TYPE_RGB565, - "RGB24": ImageType.IMAGE_TYPE_RGB24, - "RGBA": ImageType.IMAGE_TYPE_RGBA, + "BINARY": ImageBinary, + "GRAYSCALE": ImageGrayscale, + "RGB565": ImageRGB565, + "RGB": ImageRGB, + "TRANSPARENT_BINARY": ReplaceWith( + "'type: BINARY' and 'use_transparency: chroma_key'" + ), + "RGB24": ReplaceWith("'type: RGB'"), + "RGBA": ReplaceWith("'type: RGB' and 'use_transparency: alpha_channel'"), } +TransparencyType = image_ns.enum("TransparencyType") + CONF_USE_TRANSPARENCY = "use_transparency" # If the MDI file cannot be downloaded within this time, abort. @@ -53,17 +268,11 @@ SOURCE_LOCAL = "local" SOURCE_MDI = "mdi" SOURCE_WEB = "web" - Image_ = image_ns.class_("Image") -def _compute_local_icon_path(value: dict) -> Path: - base_dir = external_files.compute_local_file_dir(DOMAIN) / "mdi" - return base_dir / f"{value[CONF_ICON]}.svg" - - -def compute_local_image_path(value: dict) -> Path: - url = value[CONF_URL] +def compute_local_image_path(value) -> Path: + url = value[CONF_URL] if isinstance(value, dict) else value h = hashlib.new("sha256") h.update(url.encode()) key = h.hexdigest()[:8] @@ -71,30 +280,38 @@ def compute_local_image_path(value: dict) -> Path: return base_dir / key -def download_mdi(value): - validate_cairosvg_installed(value) +def local_path(value): + value = value[CONF_PATH] if isinstance(value, dict) else value + return str(CORE.relative_config_path(value)) - mdi_id = value[CONF_ICON] - path = _compute_local_icon_path(value) + +def download_file(url, path): + external_files.download_content(url, path, IMAGE_DOWNLOAD_TIMEOUT) + return str(path) + + +def download_mdi(value): + mdi_id = value[CONF_ICON] if isinstance(value, dict) else value + base_dir = external_files.compute_local_file_dir(DOMAIN) / "mdi" + path = base_dir / f"{mdi_id}.svg" url = f"https://raw.githubusercontent.com/Templarian/MaterialDesign/master/svg/{mdi_id}.svg" - - external_files.download_content(url, path, IMAGE_DOWNLOAD_TIMEOUT) - - return value + return download_file(url, path) def download_image(value): - url = value[CONF_URL] - path = compute_local_image_path(value) - - external_files.download_content(url, path, IMAGE_DOWNLOAD_TIMEOUT) - - return value + value = value[CONF_URL] if isinstance(value, dict) else value + return download_file(value, compute_local_image_path(value)) -def validate_cairosvg_installed(value): - """Validate that cairosvg is installed""" +def is_svg_file(file): + if not file: + return False + with open(file, "rb") as f: + return " 500 or height > 500): + if not resize and (width > 500 or height > 500): _LOGGER.warning( 'The image "%s" you requested is very big. Please consider' " using the resize parameter.", path, ) - transparent = config[CONF_USE_TRANSPARENCY] - dither = ( Image.Dither.NONE if config[CONF_DITHER] == "NONE" else Image.Dither.FLOYDSTEINBERG ) - if config[CONF_TYPE] == "GRAYSCALE": - image = image.convert("LA", dither=dither) - pixels = list(image.getdata()) - data = [0 for _ in range(height * width)] - pos = 0 - for g, a in pixels: - if transparent: - if g == 1: - g = 0 - if a < 0x80: - g = 1 + type = config[CONF_TYPE] + transparency = config[CONF_USE_TRANSPARENCY] + invert_alpha = config[CONF_INVERT_ALPHA] + frame_count = 1 + if all_frames: + try: + frame_count = image.n_frames + except AttributeError: + pass + if frame_count <= 1: + _LOGGER.warning("Image file %s has no animation frames", path) - data[pos] = g - pos += 1 + total_rows = height * frame_count + encoder = IMAGE_TYPE[type](width, total_rows, transparency, dither, invert_alpha) + for frame_index in range(frame_count): + image.seek(frame_index) + pixels = encoder.convert(image.resize((width, height))).getdata() + for row in range(height): + for col in range(width): + encoder.encode(pixels[row * width + col]) + encoder.end_row() - elif config[CONF_TYPE] == "RGBA": - image = image.convert("RGBA") - pixels = list(image.getdata()) - data = [0 for _ in range(height * width * 4)] - pos = 0 - for r, g, b, a in pixels: - data[pos] = r - pos += 1 - data[pos] = g - pos += 1 - data[pos] = b - pos += 1 - data[pos] = a - pos += 1 - - elif config[CONF_TYPE] == "RGB24": - image = image.convert("RGBA") - pixels = list(image.getdata()) - data = [0 for _ in range(height * width * 3)] - pos = 0 - for r, g, b, a in pixels: - if transparent: - if r == 0 and g == 0 and b == 1: - b = 0 - if a < 0x80: - r = 0 - g = 0 - b = 1 - - data[pos] = r - pos += 1 - data[pos] = g - pos += 1 - data[pos] = b - pos += 1 - - elif config[CONF_TYPE] in ["RGB565"]: - image = image.convert("RGBA") - pixels = list(image.getdata()) - bytes_per_pixel = 3 if transparent else 2 - data = [0 for _ in range(height * width * bytes_per_pixel)] - pos = 0 - for r, g, b, a in pixels: - R = r >> 3 - G = g >> 2 - B = b >> 3 - rgb = (R << 11) | (G << 5) | B - data[pos] = rgb >> 8 - pos += 1 - data[pos] = rgb & 0xFF - pos += 1 - if transparent: - data[pos] = a - pos += 1 - - elif config[CONF_TYPE] in ["BINARY", "TRANSPARENT_BINARY"]: - if transparent: - alpha = image.split()[-1] - has_alpha = alpha.getextrema()[0] < 0xFF - _LOGGER.debug("%s Has alpha: %s", config[CONF_ID], has_alpha) - image = image.convert("1", dither=dither) - width8 = ((width + 7) // 8) * 8 - data = [0 for _ in range(height * width8 // 8)] - for y in range(height): - for x in range(width): - if transparent and has_alpha: - a = alpha.getpixel((x, y)) - if not a: - continue - elif image.getpixel((x, y)): - continue - pos = x + y * width8 - data[pos // 8] |= 0x80 >> (pos % 8) - else: - raise core.EsphomeError( - f"Image f{config[CONF_ID]} has an unsupported type: {config[CONF_TYPE]}." - ) - - rhs = [HexInt(x) for x in data] + rhs = [HexInt(x) for x in encoder.data] prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) - var = cg.new_Pvariable( - config[CONF_ID], prog_arr, width, height, IMAGE_TYPE[config[CONF_TYPE]] - ) - cg.add(var.set_transparency(transparent)) + image_type = get_image_type_enum(type) + trans_value = get_transparency_enum(transparency) + + return prog_arr, width, height, image_type, trans_value, frame_count + + +async def to_code(config): + if isinstance(config, list): + for entry in config: + await to_code(entry) + elif CONF_ID not in config: + for entry in config.values(): + await to_code(entry) + else: + prog_arr, width, height, image_type, trans_value, _ = await write_image(config) + cg.new_Pvariable( + config[CONF_ID], prog_arr, width, height, image_type, trans_value + ) diff --git a/esphome/components/image/image.cpp b/esphome/components/image/image.cpp index ca2f659fb0..e380112050 100644 --- a/esphome/components/image/image.cpp +++ b/esphome/components/image/image.cpp @@ -12,7 +12,7 @@ void Image::draw(int x, int y, display::Display *display, Color color_on, Color for (int img_y = 0; img_y < height_; img_y++) { if (this->get_binary_pixel_(img_x, img_y)) { display->draw_pixel_at(x + img_x, y + img_y, color_on); - } else if (!this->transparent_) { + } else if (!this->transparency_) { display->draw_pixel_at(x + img_x, y + img_y, color_off); } } @@ -39,20 +39,10 @@ void Image::draw(int x, int y, display::Display *display, Color color_on, Color } } break; - case IMAGE_TYPE_RGB24: + case IMAGE_TYPE_RGB: for (int img_x = 0; img_x < width_; img_x++) { for (int img_y = 0; img_y < height_; img_y++) { - auto color = this->get_rgb24_pixel_(img_x, img_y); - if (color.w >= 0x80) { - display->draw_pixel_at(x + img_x, y + img_y, color); - } - } - } - break; - case IMAGE_TYPE_RGBA: - for (int img_x = 0; img_x < width_; img_x++) { - for (int img_y = 0; img_y < height_; img_y++) { - auto color = this->get_rgba_pixel_(img_x, img_y); + auto color = this->get_rgb_pixel_(img_x, img_y); if (color.w >= 0x80) { display->draw_pixel_at(x + img_x, y + img_y, color); } @@ -61,20 +51,20 @@ void Image::draw(int x, int y, display::Display *display, Color color_on, Color break; } } -Color Image::get_pixel(int x, int y, Color color_on, Color color_off) const { +Color Image::get_pixel(int x, int y, const Color color_on, const Color color_off) const { if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) return color_off; switch (this->type_) { case IMAGE_TYPE_BINARY: - return this->get_binary_pixel_(x, y) ? color_on : color_off; + if (this->get_binary_pixel_(x, y)) + return color_on; + return color_off; case IMAGE_TYPE_GRAYSCALE: return this->get_grayscale_pixel_(x, y); case IMAGE_TYPE_RGB565: return this->get_rgb565_pixel_(x, y); - case IMAGE_TYPE_RGB24: - return this->get_rgb24_pixel_(x, y); - case IMAGE_TYPE_RGBA: - return this->get_rgba_pixel_(x, y); + case IMAGE_TYPE_RGB: + return this->get_rgb_pixel_(x, y); default: return color_off; } @@ -98,23 +88,40 @@ lv_img_dsc_t *Image::get_lv_img_dsc() { this->dsc_.header.cf = LV_IMG_CF_ALPHA_8BIT; break; - case IMAGE_TYPE_RGB24: - this->dsc_.header.cf = LV_IMG_CF_RGB888; + case IMAGE_TYPE_RGB: +#if LV_COLOR_DEPTH == 32 + switch (this->transparent_) { + case TRANSPARENCY_ALPHA_CHANNEL: + this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR_ALPHA; + break; + case TRANSPARENCY_CHROMA_KEY: + this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR_CHROMA_KEYED; + break; + default: + this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR; + break; + } +#else + this->dsc_.header.cf = + this->transparency_ == TRANSPARENCY_ALPHA_CHANNEL ? LV_IMG_CF_RGBA8888 : LV_IMG_CF_RGB888; +#endif break; case IMAGE_TYPE_RGB565: #if LV_COLOR_DEPTH == 16 - this->dsc_.header.cf = this->has_transparency() ? LV_IMG_CF_TRUE_COLOR_ALPHA : LV_IMG_CF_TRUE_COLOR; + switch (this->transparency_) { + case TRANSPARENCY_ALPHA_CHANNEL: + this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR_ALPHA; + break; + case TRANSPARENCY_CHROMA_KEY: + this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR_CHROMA_KEYED; + break; + default: + this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR; + break; + } #else - this->dsc_.header.cf = LV_IMG_CF_RGB565; -#endif - break; - - case IMAGE_TYPE_RGBA: -#if LV_COLOR_DEPTH == 32 - this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR; -#else - this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR_ALPHA; + this->dsc_.header.cf = this->transparent_ == TRANSPARENCY_ALPHA_CHANNEL ? LV_IMG_CF_RGB565A8 : LV_IMG_CF_RGB565; #endif break; } @@ -128,51 +135,73 @@ bool Image::get_binary_pixel_(int x, int y) const { const uint32_t pos = x + y * width_8; return progmem_read_byte(this->data_start_ + (pos / 8u)) & (0x80 >> (pos % 8u)); } -Color Image::get_rgba_pixel_(int x, int y) const { - const uint32_t pos = (x + y * this->width_) * 4; - return Color(progmem_read_byte(this->data_start_ + pos + 0), progmem_read_byte(this->data_start_ + pos + 1), - progmem_read_byte(this->data_start_ + pos + 2), progmem_read_byte(this->data_start_ + pos + 3)); -} -Color Image::get_rgb24_pixel_(int x, int y) const { - const uint32_t pos = (x + y * this->width_) * 3; +Color Image::get_rgb_pixel_(int x, int y) const { + const uint32_t pos = (x + y * this->width_) * this->bpp_ / 8; Color color = Color(progmem_read_byte(this->data_start_ + pos + 0), progmem_read_byte(this->data_start_ + pos + 1), - progmem_read_byte(this->data_start_ + pos + 2)); - if (color.b == 1 && color.r == 0 && color.g == 0 && transparent_) { - // (0, 0, 1) has been defined as transparent color for non-alpha images. - // putting blue == 1 as a first condition for performance reasons (least likely value to short-cut the if) - color.w = 0; - } else { - color.w = 0xFF; + progmem_read_byte(this->data_start_ + pos + 2), 0xFF); + + switch (this->transparency_) { + case TRANSPARENCY_CHROMA_KEY: + if (color.g == 1 && color.r == 0 && color.b == 0) { + // (0, 1, 0) has been defined as transparent color for non-alpha images. + color.w = 0; + } + break; + case TRANSPARENCY_ALPHA_CHANNEL: + color.w = progmem_read_byte(this->data_start_ + (pos + 3)); + break; + default: + break; } return color; } Color Image::get_rgb565_pixel_(int x, int y) const { - const uint8_t *pos = this->data_start_; - if (this->transparent_) { - pos += (x + y * this->width_) * 3; - } else { - pos += (x + y * this->width_) * 2; - } + const uint8_t *pos = this->data_start_ + (x + y * this->width_) * this->bpp_ / 8; uint16_t rgb565 = encode_uint16(progmem_read_byte(pos), progmem_read_byte(pos + 1)); auto r = (rgb565 & 0xF800) >> 11; auto g = (rgb565 & 0x07E0) >> 5; auto b = rgb565 & 0x001F; - auto a = this->transparent_ ? progmem_read_byte(pos + 2) : 0xFF; - Color color = Color((r << 3) | (r >> 2), (g << 2) | (g >> 4), (b << 3) | (b >> 2), a); - return color; + auto a = 0xFF; + switch (this->transparency_) { + case TRANSPARENCY_ALPHA_CHANNEL: + a = progmem_read_byte(pos + 2); + break; + case TRANSPARENCY_CHROMA_KEY: + if (rgb565 == 0x0020) + a = 0; + break; + default: + break; + } + return Color((r << 3) | (r >> 2), (g << 2) | (g >> 4), (b << 3) | (b >> 2), a); } Color Image::get_grayscale_pixel_(int x, int y) const { const uint32_t pos = (x + y * this->width_); const uint8_t gray = progmem_read_byte(this->data_start_ + pos); - uint8_t alpha = (gray == 1 && transparent_) ? 0 : 0xFF; + uint8_t alpha = (gray == 1 && this->transparency_ == TRANSPARENCY_CHROMA_KEY) ? 0 : 0xFF; return Color(gray, gray, gray, alpha); } int Image::get_width() const { return this->width_; } int Image::get_height() const { return this->height_; } ImageType Image::get_type() const { return this->type_; } -Image::Image(const uint8_t *data_start, int width, int height, ImageType type) - : width_(width), height_(height), type_(type), data_start_(data_start) {} +Image::Image(const uint8_t *data_start, int width, int height, ImageType type, Transparency transparency) + : width_(width), height_(height), type_(type), data_start_(data_start), transparency_(transparency) { + switch (this->type_) { + case IMAGE_TYPE_BINARY: + this->bpp_ = 1; + break; + case IMAGE_TYPE_GRAYSCALE: + this->bpp_ = 8; + break; + case IMAGE_TYPE_RGB565: + this->bpp_ = transparency == TRANSPARENCY_ALPHA_CHANNEL ? 24 : 16; + break; + case IMAGE_TYPE_RGB: + this->bpp_ = this->transparency_ == TRANSPARENCY_ALPHA_CHANNEL ? 32 : 24; + break; + } +} } // namespace image } // namespace esphome diff --git a/esphome/components/image/image.h b/esphome/components/image/image.h index 40370d18da..4024ab1357 100644 --- a/esphome/components/image/image.h +++ b/esphome/components/image/image.h @@ -12,51 +12,40 @@ namespace image { enum ImageType { IMAGE_TYPE_BINARY = 0, IMAGE_TYPE_GRAYSCALE = 1, - IMAGE_TYPE_RGB24 = 2, + IMAGE_TYPE_RGB = 2, IMAGE_TYPE_RGB565 = 3, - IMAGE_TYPE_RGBA = 4, +}; + +enum Transparency { + TRANSPARENCY_OPAQUE = 0, + TRANSPARENCY_CHROMA_KEY = 1, + TRANSPARENCY_ALPHA_CHANNEL = 2, }; class Image : public display::BaseImage { public: - Image(const uint8_t *data_start, int width, int height, ImageType type); + Image(const uint8_t *data_start, int width, int height, ImageType type, Transparency transparency); Color get_pixel(int x, int y, Color color_on = display::COLOR_ON, Color color_off = display::COLOR_OFF) const; int get_width() const override; int get_height() const override; const uint8_t *get_data_start() const { return this->data_start_; } ImageType get_type() const; - int get_bpp() const { - switch (this->type_) { - case IMAGE_TYPE_BINARY: - return 1; - case IMAGE_TYPE_GRAYSCALE: - return 8; - case IMAGE_TYPE_RGB565: - return this->transparent_ ? 24 : 16; - case IMAGE_TYPE_RGB24: - return 24; - case IMAGE_TYPE_RGBA: - return 32; - } - return 0; - } + int get_bpp() const { return this->bpp_; } /// Return the stride of the image in bytes, that is, the distance in bytes /// between two consecutive rows of pixels. - uint32_t get_width_stride() const { return (this->width_ * this->get_bpp() + 7u) / 8u; } + size_t get_width_stride() const { return (this->width_ * this->get_bpp() + 7u) / 8u; } void draw(int x, int y, display::Display *display, Color color_on, Color color_off) override; - void set_transparency(bool transparent) { transparent_ = transparent; } - bool has_transparency() const { return transparent_; } + bool has_transparency() const { return this->transparency_ != TRANSPARENCY_OPAQUE; } #ifdef USE_LVGL lv_img_dsc_t *get_lv_img_dsc(); #endif protected: bool get_binary_pixel_(int x, int y) const; - Color get_rgb24_pixel_(int x, int y) const; - Color get_rgba_pixel_(int x, int y) const; + Color get_rgb_pixel_(int x, int y) const; Color get_rgb565_pixel_(int x, int y) const; Color get_grayscale_pixel_(int x, int y) const; @@ -64,7 +53,9 @@ class Image : public display::BaseImage { int height_; ImageType type_; const uint8_t *data_start_; - bool transparent_; + Transparency transparency_; + size_t bpp_{}; + size_t stride_{}; #ifdef USE_LVGL lv_img_dsc_t dsc_{}; #endif diff --git a/esphome/components/online_image/__init__.py b/esphome/components/online_image/__init__.py index be1bfb4a00..d1915c7364 100644 --- a/esphome/components/online_image/__init__.py +++ b/esphome/components/online_image/__init__.py @@ -4,14 +4,18 @@ from esphome import automation import esphome.codegen as cg from esphome.components.http_request import CONF_HTTP_REQUEST_ID, HttpRequestComponent from esphome.components.image import ( + CONF_INVERT_ALPHA, CONF_USE_TRANSPARENCY, - IMAGE_TYPE, + IMAGE_SCHEMA, Image_, - validate_cross_dependencies, + get_image_type_enum, + get_transparency_enum, ) import esphome.config_validation as cv from esphome.const import ( CONF_BUFFER_SIZE, + CONF_DITHER, + CONF_FILE, CONF_FORMAT, CONF_ID, CONF_ON_ERROR, @@ -23,7 +27,7 @@ from esphome.const import ( AUTO_LOAD = ["image"] DEPENDENCIES = ["display", "http_request"] -CODEOWNERS = ["@guillempages"] +CODEOWNERS = ["@guillempages", "@clydebarrow"] MULTI_CONF = True CONF_ON_DOWNLOAD_FINISHED = "on_download_finished" @@ -35,9 +39,30 @@ online_image_ns = cg.esphome_ns.namespace("online_image") ImageFormat = online_image_ns.enum("ImageFormat") -FORMAT_PNG = "PNG" -IMAGE_FORMAT = {FORMAT_PNG: ImageFormat.PNG} # Add new supported formats here +class Format: + def __init__(self, image_type): + self.image_type = image_type + + @property + def enum(self): + return getattr(ImageFormat, self.image_type) + + def actions(self): + pass + + +class PNGFormat(Format): + def __init__(self): + super().__init__("PNG") + + def actions(self): + cg.add_define("USE_ONLINE_IMAGE_PNG_SUPPORT") + cg.add_library("pngle", "1.0.2") + + +# New formats can be added here. +IMAGE_FORMATS = {x.image_type: x for x in (PNGFormat(),)} OnlineImage = online_image_ns.class_("OnlineImage", cg.PollingComponent, Image_) @@ -57,48 +82,54 @@ DownloadErrorTrigger = online_image_ns.class_( "DownloadErrorTrigger", automation.Trigger.template() ) -ONLINE_IMAGE_SCHEMA = cv.Schema( - { - cv.Required(CONF_ID): cv.declare_id(OnlineImage), - cv.GenerateID(CONF_HTTP_REQUEST_ID): cv.use_id(HttpRequestComponent), - # - # Common image options - # - cv.Optional(CONF_RESIZE): cv.dimensions, - cv.Optional(CONF_TYPE, default="BINARY"): cv.enum(IMAGE_TYPE, upper=True), - # Not setting default here on purpose; the default depends on the image type, - # and thus will be set in the "validate_cross_dependencies" validator. - cv.Optional(CONF_USE_TRANSPARENCY): cv.boolean, - # - # Online Image specific options - # - cv.Required(CONF_URL): cv.url, - cv.Required(CONF_FORMAT): cv.enum(IMAGE_FORMAT, upper=True), - cv.Optional(CONF_PLACEHOLDER): cv.use_id(Image_), - cv.Optional(CONF_BUFFER_SIZE, default=2048): cv.int_range(256, 65536), - cv.Optional(CONF_ON_DOWNLOAD_FINISHED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DownloadFinishedTrigger), - } - ), - cv.Optional(CONF_ON_ERROR): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DownloadErrorTrigger), - } - ), + +def remove_options(*options): + return { + cv.Optional(option): cv.invalid( + f"{option} is an invalid option for online_image" + ) + for option in options } -).extend(cv.polling_component_schema("never")) + + +ONLINE_IMAGE_SCHEMA = ( + IMAGE_SCHEMA.extend(remove_options(CONF_FILE, CONF_INVERT_ALPHA, CONF_DITHER)) + .extend( + { + cv.Required(CONF_ID): cv.declare_id(OnlineImage), + cv.GenerateID(CONF_HTTP_REQUEST_ID): cv.use_id(HttpRequestComponent), + # Online Image specific options + cv.Required(CONF_URL): cv.url, + cv.Required(CONF_FORMAT): cv.one_of(*IMAGE_FORMATS, upper=True), + cv.Optional(CONF_PLACEHOLDER): cv.use_id(Image_), + cv.Optional(CONF_BUFFER_SIZE, default=2048): cv.int_range(256, 65536), + cv.Optional(CONF_ON_DOWNLOAD_FINISHED): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + DownloadFinishedTrigger + ), + } + ), + cv.Optional(CONF_ON_ERROR): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DownloadErrorTrigger), + } + ), + } + ) + .extend(cv.polling_component_schema("never")) +) CONFIG_SCHEMA = cv.Schema( cv.All( ONLINE_IMAGE_SCHEMA, - validate_cross_dependencies, cv.require_framework_version( # esp8266 not supported yet; if enabled in the future, minimum version of 2.7.0 is needed # esp8266_arduino=cv.Version(2, 7, 0), esp32_arduino=cv.Version(0, 0, 0), esp_idf=cv.Version(4, 0, 0), rp2040_arduino=cv.Version(0, 0, 0), + host=cv.Version(0, 0, 0), ), ) ) @@ -132,29 +163,26 @@ async def online_image_action_to_code(config, action_id, template_arg, args): async def to_code(config): - format = config[CONF_FORMAT] - if format in [FORMAT_PNG]: - cg.add_define("USE_ONLINE_IMAGE_PNG_SUPPORT") - cg.add_library("pngle", "1.0.2") + image_format = IMAGE_FORMATS[config[CONF_FORMAT]] + image_format.actions() url = config[CONF_URL] width, height = config.get(CONF_RESIZE, (0, 0)) - transparent = config[CONF_USE_TRANSPARENCY] + transparent = get_transparency_enum(config[CONF_USE_TRANSPARENCY]) var = cg.new_Pvariable( config[CONF_ID], url, width, height, - format, - config[CONF_TYPE], + image_format.enum, + get_image_type_enum(config[CONF_TYPE]), + transparent, config[CONF_BUFFER_SIZE], ) await cg.register_component(var, config) await cg.register_parented(var, config[CONF_HTTP_REQUEST_ID]) - cg.add(var.set_transparency(transparent)) - if placeholder_id := config.get(CONF_PLACEHOLDER): placeholder = await cg.get_variable(placeholder_id) cg.add(var.set_placeholder(placeholder)) diff --git a/esphome/components/online_image/image_decoder.h b/esphome/components/online_image/image_decoder.h index 908efab987..cde7f572e3 100644 --- a/esphome/components/online_image/image_decoder.h +++ b/esphome/components/online_image/image_decoder.h @@ -1,5 +1,4 @@ #pragma once -#include "esphome/core/defines.h" #include "esphome/core/color.h" namespace esphome { @@ -23,7 +22,7 @@ class ImageDecoder { /** * @brief Initialize the decoder. * - * @param download_size The total number of bytes that need to be download for the image. + * @param download_size The total number of bytes that need to be downloaded for the image. */ virtual void prepare(uint32_t download_size) { this->download_size_ = download_size; } @@ -38,7 +37,7 @@ class ImageDecoder { * @return int The amount of bytes read. It can be 0 if the buffer does not have enough content to meaningfully * decode anything, or negative in case of a decoding error. */ - virtual int decode(uint8_t *buffer, size_t size); + virtual int decode(uint8_t *buffer, size_t size) = 0; /** * @brief Request the image to be resized once the actual dimensions are known. @@ -50,7 +49,7 @@ class ImageDecoder { void set_size(int width, int height); /** - * @brief Draw a rectangle on the display_buffer using the defined color. + * @brief Fill a rectangle on the display_buffer using the defined color. * Will check the given coordinates for out-of-bounds, and clip the rectangle accordingly. * In case of binary displays, the color will be converted to binary as well. * Called by the callback functions, to be able to access the parent Image class. @@ -59,7 +58,7 @@ class ImageDecoder { * @param y The top-most coordinate of the rectangle. * @param w The width of the rectangle. * @param h The height of the rectangle. - * @param color The color to draw the rectangle with. + * @param color The fill color */ void draw(int x, int y, int w, int h, const Color &color); @@ -67,7 +66,7 @@ class ImageDecoder { protected: OnlineImage *image_; - // Initializing to 1, to ensure it is different than initial "decoded_bytes_". + // Initializing to 1, to ensure it is distinguishable from initial "decoded_bytes_". // Will be overwritten anyway once the download size is known. uint32_t download_size_ = 1; uint32_t decoded_bytes_ = 0; diff --git a/esphome/components/online_image/online_image.cpp b/esphome/components/online_image/online_image.cpp index 8c4669cba5..93d070c6a9 100644 --- a/esphome/components/online_image/online_image.cpp +++ b/esphome/components/online_image/online_image.cpp @@ -25,8 +25,8 @@ inline bool is_color_on(const Color &color) { } OnlineImage::OnlineImage(const std::string &url, int width, int height, ImageFormat format, ImageType type, - uint32_t download_buffer_size) - : Image(nullptr, 0, 0, type), + image::Transparency transparency, uint32_t download_buffer_size) + : Image(nullptr, 0, 0, type, transparency), buffer_(nullptr), download_buffer_(download_buffer_size), format_(format), @@ -45,7 +45,7 @@ void OnlineImage::draw(int x, int y, display::Display *display, Color color_on, void OnlineImage::release() { if (this->buffer_) { - ESP_LOGD(TAG, "Deallocating old buffer..."); + ESP_LOGV(TAG, "Deallocating old buffer..."); this->allocator_.deallocate(this->buffer_, this->get_buffer_size_()); this->data_start_ = nullptr; this->buffer_ = nullptr; @@ -70,20 +70,19 @@ bool OnlineImage::resize_(int width_in, int height_in) { if (this->buffer_) { return false; } - auto new_size = this->get_buffer_size_(width, height); - ESP_LOGD(TAG, "Allocating new buffer of %d Bytes...", new_size); - delay_microseconds_safe(2000); + size_t new_size = this->get_buffer_size_(width, height); + ESP_LOGD(TAG, "Allocating new buffer of %zu bytes", new_size); this->buffer_ = this->allocator_.allocate(new_size); - if (this->buffer_) { - this->buffer_width_ = width; - this->buffer_height_ = height; - this->width_ = width; - ESP_LOGD(TAG, "New size: (%d, %d)", width, height); - } else { - ESP_LOGE(TAG, "allocation failed. Biggest block in heap: %zu Bytes", this->allocator_.get_max_free_block_size()); + if (this->buffer_ == nullptr) { + ESP_LOGE(TAG, "allocation of %zu bytes failed. Biggest block in heap: %zu Bytes", new_size, + this->allocator_.get_max_free_block_size()); this->end_connection_(); return false; } + this->buffer_width_ = width; + this->buffer_height_ = height; + this->width_ = width; + ESP_LOGV(TAG, "New size: (%d, %d)", width, height); return true; } @@ -91,9 +90,8 @@ void OnlineImage::update() { if (this->decoder_) { ESP_LOGW(TAG, "Image already being updated."); return; - } else { - ESP_LOGI(TAG, "Updating image"); } + ESP_LOGI(TAG, "Updating image %s", this->url_.c_str()); this->downloader_ = this->parent_->get(this->url_); @@ -142,10 +140,11 @@ void OnlineImage::loop() { return; } if (!this->downloader_ || this->decoder_->is_finished()) { - ESP_LOGD(TAG, "Image fully downloaded"); this->data_start_ = buffer_; this->width_ = buffer_width_; this->height_ = buffer_height_; + ESP_LOGD(TAG, "Image fully downloaded, read %zu bytes, width/height = %d/%d", this->downloader_->get_bytes_read(), + this->width_, this->height_); this->end_connection_(); this->download_finished_callback_.call(); return; @@ -171,6 +170,19 @@ void OnlineImage::loop() { } } +void OnlineImage::map_chroma_key(Color &color) { + if (this->transparency_ == image::TRANSPARENCY_CHROMA_KEY) { + if (color.g == 1 && color.r == 0 && color.b == 0) { + color.g = 0; + } + if (color.w < 0x80) { + color.r = 0; + color.g = this->type_ == ImageType::IMAGE_TYPE_RGB565 ? 4 : 1; + color.b = 0; + } + } +} + void OnlineImage::draw_pixel_(int x, int y, Color color) { if (!this->buffer_) { ESP_LOGE(TAG, "Buffer not allocated!"); @@ -184,57 +196,53 @@ void OnlineImage::draw_pixel_(int x, int y, Color color) { switch (this->type_) { case ImageType::IMAGE_TYPE_BINARY: { const uint32_t width_8 = ((this->width_ + 7u) / 8u) * 8u; - const uint32_t pos = x + y * width_8; - if ((this->has_transparency() && color.w > 127) || is_color_on(color)) { - this->buffer_[pos / 8u] |= (0x80 >> (pos % 8u)); + pos = x + y * width_8; + auto bitno = 0x80 >> (pos % 8u); + pos /= 8u; + auto on = is_color_on(color); + if (this->has_transparency() && color.w < 0x80) + on = false; + if (on) { + this->buffer_[pos] |= bitno; } else { - this->buffer_[pos / 8u] &= ~(0x80 >> (pos % 8u)); + this->buffer_[pos] &= ~bitno; } break; } case ImageType::IMAGE_TYPE_GRAYSCALE: { uint8_t gray = static_cast(0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b); - if (this->has_transparency()) { + if (this->transparency_ == image::TRANSPARENCY_CHROMA_KEY) { if (gray == 1) { gray = 0; } if (color.w < 0x80) { gray = 1; } + } else if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) { + if (color.w != 0xFF) + gray = color.w; } this->buffer_[pos] = gray; break; } case ImageType::IMAGE_TYPE_RGB565: { + this->map_chroma_key(color); uint16_t col565 = display::ColorUtil::color_to_565(color); this->buffer_[pos + 0] = static_cast((col565 >> 8) & 0xFF); this->buffer_[pos + 1] = static_cast(col565 & 0xFF); - if (this->has_transparency()) + if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) { this->buffer_[pos + 2] = color.w; - break; - } - case ImageType::IMAGE_TYPE_RGBA: { - this->buffer_[pos + 0] = color.r; - this->buffer_[pos + 1] = color.g; - this->buffer_[pos + 2] = color.b; - this->buffer_[pos + 3] = color.w; - break; - } - case ImageType::IMAGE_TYPE_RGB24: - default: { - if (this->has_transparency()) { - if (color.b == 1 && color.r == 0 && color.g == 0) { - color.b = 0; - } - if (color.w < 0x80) { - color.r = 0; - color.g = 0; - color.b = 1; - } } + break; + } + case ImageType::IMAGE_TYPE_RGB: { + this->map_chroma_key(color); this->buffer_[pos + 0] = color.r; this->buffer_[pos + 1] = color.g; this->buffer_[pos + 2] = color.b; + if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) { + this->buffer_[pos + 3] = color.w; + } break; } } diff --git a/esphome/components/online_image/online_image.h b/esphome/components/online_image/online_image.h index 017402a088..e044b4f390 100644 --- a/esphome/components/online_image/online_image.h +++ b/esphome/components/online_image/online_image.h @@ -48,12 +48,13 @@ class OnlineImage : public PollingComponent, * @param buffer_size Size of the buffer used to download the image. */ OnlineImage(const std::string &url, int width, int height, ImageFormat format, image::ImageType type, - uint32_t buffer_size); + image::Transparency transparency, uint32_t buffer_size); void draw(int x, int y, display::Display *display, Color color_on, Color color_off) override; void update() override; void loop() override; + void map_chroma_key(Color &color); /** Set the URL to download the image from. */ void set_url(const std::string &url) { diff --git a/esphome/components/online_image/png_image.h b/esphome/components/online_image/png_image.h index a928276dcc..d82ff93149 100644 --- a/esphome/components/online_image/png_image.h +++ b/esphome/components/online_image/png_image.h @@ -1,6 +1,7 @@ #pragma once #include "image_decoder.h" +#include "esphome/core/defines.h" #ifdef USE_ONLINE_IMAGE_PNG_SUPPORT #include diff --git a/script/ci-custom.py b/script/ci-custom.py index 81e3da311a..d5d3ab88c8 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -58,7 +58,19 @@ file_types = ( ) cpp_include = ("*.h", "*.c", "*.cpp", "*.tcc") py_include = ("*.py",) -ignore_types = (".ico", ".png", ".woff", ".woff2", "", ".ttf", ".otf", ".pcf") +ignore_types = ( + ".ico", + ".png", + ".woff", + ".woff2", + "", + ".ttf", + ".otf", + ".pcf", + ".apng", + ".gif", + ".webp", +) LINT_FILE_CHECKS = [] LINT_CONTENT_CHECKS = [] @@ -669,8 +681,7 @@ def main(): ) args = parser.parse_args() - global EXECUTABLE_BIT - EXECUTABLE_BIT = git_ls_files() + EXECUTABLE_BIT.update(git_ls_files()) files = list(EXECUTABLE_BIT.keys()) # Match against re file_name_re = re.compile("|".join(args.files)) diff --git a/tests/components/animation/.gitattributes b/tests/components/animation/.gitattributes new file mode 100644 index 0000000000..ff9fc6f1f1 --- /dev/null +++ b/tests/components/animation/.gitattributes @@ -0,0 +1,4 @@ +*.apng -text +*.webp -text +*.gif -text + diff --git a/tests/components/animation/anim.apng b/tests/components/animation/anim.apng new file mode 100644 index 0000000000000000000000000000000000000000..927af5eb05a94ea8b1cdab493d2bfd8feffb7eac GIT binary patch literal 12626 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

m9s*zMUB&9i)Vj-aXg1Rr|O2;S{r^I>B3s zz|%^>Kl#^gf{z)aHz1{GeF6b`w<|Yck3ybz6$J0wY%CXDPDxebWUIu+dCk>Olj563 zM{m*l@$tnpf!jO-6?}+CVSp|fkZw@gycSj_iH>th88{hqaN`~k^XVrJJH@OuL>1!O zF?54D)7y&+ov4H#G1mbQA!)YCu;Om=*Yu60&48w^;ow7ETLWz71I95}^wD|Kn%;o! z78%;U670w;S&GH6;_Q2=CFD{mI5Rq8WtHpKD_OLNl=UHKGms6&!V?yY5%sw|W82te zYTQGkJ&xBYD@Z^?vOOD&fwnVE5%B~Ymm1Mwv?#h=JY_~1YSE}>U&MiY)2*<|db9Sy z8zY`?K*%_*~QZsy0EmGhBtqLB!|drFOS+b3VR0&+UF$m!+NIle@hZ0I_nGha|SDaMF-@+ zRiP3I?9AvyG`?+VDUN>meJGiHYk*G7Ka8Fg@{yc>P5r~ThtbQ|UW(`8TNTQ}?)cXU z{#moSTjeM9pnG-nU2L<2=R`skZuBspAY4>|`$?ke#5Z4QQR@4x9=4O;xQEElFy;yD zu;QpukqOf8GIvjeCSOu5w$A9ct9=dCl<-U(6x^zO$fGe%cxH8yx8R-!A?0U~K6WPk zBKq`G+$OegUQKEx<#m3UAjySH_DEN4$iXJv#>;4X<)<>V*nE=Q6qfSn;4Z^Gqd70L z7q7y?0hsB32qPZeD2Mm$ydIR zRi3=I&P0Dt7sp;CPrGqIT;J|+9kM3p9mGZq7 ze!M5VA9_nHUEkeiE1yTl{4KTXb2Ulnhb6d~eu?*td@+#iA$&!EGE10DhKzWf;c6k)VwU)<^*E2d??dGHkEa544Y9LXrc@C+*J1d-UyZg^69e0O(lB@ zFZ8TQmD3#k$@!B>=V~O;GQyS4Amu_Is0S;R+qHZidC#|fR!brYu_h!E`REe;Er(n2 zixw3M9|eNHzSz>;VyF~Y8($DZBYwgY!|cFRAUQNb$ml;5zl>mP1a%t`y&>-H2<$6n zh_8HETh?_cDXD&^I7@6~XQ66>nXE8J-ozo8<648`NmHfp^w;iA^r!;(zxSNa0 z3Fd=$iSIU`+%oSGMvkA27;($e0Q+@izd^n1ifMZdoUj!cqLT zuU*ssKAw=S$UByDY7uV3tzUJ)tp(^jWzq@yT*1;rT>> zMe2woKrs>CTCntht}@UG(xwFRTx81GgVL7Lm=zUAH?IEF8@IjcdNc2m<3%!F#c9b? zDXE70y=E-S`@|o3zO*pG^EF z>rRDY(a;Y-qepYgN|Vr3QsA(HJ|DWuIucHSmzTh?ME`Z5?1%D!m_r=K91f;8Ob$$& zmpofNwsa-uzDUai@5I&_=@|}~JN3d@YC0%7(P`Lu*n>VBLvOw#jR0856Q2+-!vyS@ z=EtDC>`o*rreRuaATCXoZ{Gd_h`6J^YOB^Pkdr-I`?x#lYW5-f}93P4t;t0C4TZYYWbVK!D0FX(dZ@1 z9XxH5{uf8#DsZBg-T>p|5kM*TzD#VarZCu=l5oYjFtpf}IzY|*7=v%H-!af&PX6<8 z8UF#J?ejBQ;A=`-`@*b-e-6(7>+GQXTmN62o%?C-J^%psgPo1+e|8<}f9}qb7uf*O zk03h&FdF8-k9F=nMhM2%AHrx#ok;GT#%(NJTO6wnE)N%e1I@HiVNEMqP%;P{%qQG@RX&GudC32ZJ}?5E5nO?Qv;1GDn&{9%X53FXpOxgH(o z(WaTO1cvr~Zui8dvGh%P_o@>Qp)?hLqj(3=DtdR@1ECBoH2&u6$l<`(eEelF+{*nFe4~%tu&9X^<7y?w83AaJwS^p~iZ6%a`3C++yEN=O~<3k4q)^@k- zO?jX5YVto@1!Iyfsv=*X9=beww z&aR#*7lXPXs?xPlOud-9wZq^akaxwH9|W&6*0!YUeKIC8__ZH4zVr$A-u+0DCP#5M zr&rvTPar5ElrC>-y1Xd^6K$x&HtPzE?*NLh2Smw4io!D(!~y^}METDGha}Q&Lu7wb zz1vi{X!BVkeZNET-@g3xkTu8G_%!%e$}PWf;;A$$WS{h>u)m7WM3_4(`zOC|JL^<1 zMtTpQtWs%K~-;v6nTA@=)UqqdnXgl1HmNWtL2aN&PE`vyriX1bMp1JUCXTt(c}o&D95M*diG z?)+EGq_4POL*FtwHhsCOKz_=56J$wpO&S{Sbs}olq5!YftXT_s5)w{(bR~q91E=XCkuKig% zb*H{0lqC3gvdJ2X>V);Ot1?1%fCmtSlltelQ2E-_}&HT}~7j z>#TO1EAlSzkN0@Yqa)!TYCnyVA6p-Czy{g>{T`36bytr+bcDt<;4pKycQ+Lir;4@Q z>b4lUyu=3wZz1YT185tM)Ck_3Cm7R3>lbW8e|Vr+<-hp@J*-^hMNx$iBhXp?2WmM{ z_A`kQiU0plLCI{0DyRs49onM~DFxWf=Nph9uu&Z^{7%q|ts!jKtp6L+M!|v@!3!AT z(#EU^G*^TCIN|hMwJTr&r-YM8mXqtE3bJw-%X6qheiVzC=~4_6BhFA9@mNu^m=M)p zv5?6yZ$gz;HWVs8L}@))f>>oe>y0YpEx_XnqKltZ7!5*gvZcH%d|9XkL896cCP1b2 zP#f_$m><1~A$q8S)Evz1yK&8YC<}?Q%0Oe7O(A-*&WxwIVohEh=Ae_CC*+|!7ijY? z&tT5DV4HCTHCfIOH5mrlt9eZwqe0S91qU6x8i26P9IvcEqP~3-7iRX!s^mn;B1K<& zHgze42pXjM!v`nX)z)lm0L6g^H}J^tN(UCY~J9D`h_iC^Gk)>)4{`Jd4D~uDOZO)mOMtYGgs!4MASE|IhG0C^f zDNj6N`}P!^c_{ToTtg_)i$NzKd)~PSGt9|Gss8@wi~_o44wLyvV@ zukJ+qVg6~ziX>J5OPFm(G!VFvY^bYe^-R*|N9mf{*Vm`5Wo;mVdOrb{Tj zaNwdhlC%~geC95U*h!6xj_@Qaxg}0;=@E>{<9zrw1wjhVAE{uvd}Nr!13a(=4E45 z>gaR}%9WtpotO&JdE~dw=jU6)Uck)2+=oV%)pP~Lg-)#QcU%+0zb9{;m~m_`qzkhf z1I%#OTCNdBUFii%9;T$}oeq|VYIPhh?sD5pL8PbWAD6w7e-=|9V|$amtE%5W-_%}7 z{irj$>?|IS>kl>}Y28j>cwbS^Vr;3Zy2r8iUXbfW>}GC_BunKb=he5Q`SHu*iP{-o z(?* z!^}s3P}(@~?Psww$GR2s8SgHe8KUlLv4(h@t9HIUMYEjNv_{N-l;gD9L2WE^7_C1{ z*v)S@Dv`j?*tw57_c!}dfLy`DGU?OxtY%@`A#pw;)5$Zqf2}{iZ)rN8Emtz+vh7ki z9=0@UvBv8Nq3-pXO*?&3fHK)SQGA&Zx3FRFV!N=Wu03bEk@o^hem|&ScEMI@D|d;g%To4H$j0vGgSZ?!Cqa;_3YXHE_0f#3*z#+F(!$kmi8mJRAJu zr}Iz(EkG)mB9u_JD^-^!b12ynIYi~v;IIfEibpMj3W{PEtIG0EaBSeU6dR{ey*hvQ ze3kvR&?n6CL1tZ08NSk#jeg`LkOkj**6hvHp1$)v8(0;4uvX6$crW9x-NBsAW<37? zk?;RCYyU6uom}yj>tACQ{r|{!^#A0$bM-p|vVq-4Nuis`>+9hA)-FH0lbVm2;EEEf zgb4JMMIbBnSIR=hFOb`Hw6Bz8&W!K*Lzsq{zYybL>yU)$>u+M+7Q)asw{XE6l!{5n z<)t9Ueczhr#rw|RKeOhtjIs#0Z^m-6gzW3q%z|o~Z^VPXTQy`-t4kzL5bv}tPmN-4 zg&U4Js7TVEk{}wq8D;s~F{k#5bB7DkX407CSG6MFkB3&LF(j6$3L^X^>q_=gbg?#N z2Fg{rp(82yUR?CdE8nLO0H7;&6r4{GC{2|smQ=6*Lg4e}kZQ6cnO3#Y*^E-scIjCE z`_P%-%GynG@9DQM<0AjO4aXKTh($%nv3lXNgFiBr(gDhD_a5iaAT0s%>dv`Et97zVBbdk2Y+H>u>B1%C!;`zSyvr z&^ek1HR-i%ya_x|ni|0?F5&~y9y+}-WV&YzV@gM5&@e;|ouUN>bEx7=iicZrWtpaa zj@A4rg(4#o{AsxQxqaC6wsaUORw^hoVBq_k*F=!!q(3Mt%bKg>sbiyYL!yY;yQ{Qo z>UT#;7tLw%oWppFzTJvwLD6M@hKl`MM**Uw*e`yXo!gNP_`$1ThV2$aUur?M<7{A; zyjOQm&CAuYkHyt-7tPgjg_5@RP%!NF*pN=b)w07f6wc)RK9!Z`@+lo_QgizEvdGHs znw!Zr23b*na&9_L)aT-Oqa?Ssgo2-ac#wgkL%kDH6( z?E92-iv26x^VO4G_lK$E-3bv_=9bwnGR&DkkR%+j|Kb<=}KMB)z zutYA}Exvn)5HY!UCN!y~jrD9`S2-a`GmIxYjPlpF&?pRA|knJ4AbuO%P5#HR#x&bpMLjg^|8)YHiB z)a&koqYUz%alQNhr9n4VdpIAXDI3l`OTM~WvPzW!ILCu2v9$FQ+@h5!8Z*DAbfgOf z#~$G9WOXK`(&j6hR^@!TDlSgW=Ae->?)_p$*2%uPu`t#4OHJo(ZFT{Y+<7-wWXo?& z9|OrWMRZS#jw0j-wy`r+`(lu!NdWfIGYSE-@)4@}i;b_>Cl+yvxl_w8*pwJm69X|g zc{uMw0hL`Q?4UZjjSDpYZ*8x{%}JSZwHA$QO}!V*rTAVka$C~v4Bj=35fW>yRi_Ma zBf46!SlY174>9B?!ff-$Zry*xmDJSplpQdk2SV@1m1ja;E^_QFKd zG?UEDWbHMR=Q%CSUcSt(lqc8!XgEXlM$PZ-!Mh{+l?7-sC{d60DW0v3j6e+_hRnm8 zVl{b9tHX8dInh#U8E?U&sJ*@Uwz+T8MD;Dl<0sN+fQQ78!g_=xEC&IM-|v%4zfuh3 z`}x+>BK1+Kb!C&hk$$&U4_btyvNhEz>dM?PM zk&YA5rmdv((oxG8d zGJhE`7zgFFqGCHOTrN9z6Bc+T{=oEAnLc^Nlg}>ho*+Wb1Z%KzM6lh+j55L!U6&dY z%%-)+j(F;$e2G|2qeJOAyb(qi1wt8><>2$5oj$@|hE16h)}UFgToM2uRXQN;Ixquq zA@FxUtp4nKd40&G3m@$0lyI>7MFN2dFa9roHylxwFBgG?0C(D6odm28CW=de3Pgi7Oil95_w~SfHhR?Z~;ug z2uLA($_OlQN_;}8f(G72g;jrmf5BzRUc*M)%zsBK2>!)-IHL@Tc|nTDOK>J1Bl6&b z);6yNzMw8GZf`fk_1#}RD$5K$J$d0u&f??Foq{- zOJf4uVRHJk4@+S!!@zr&@iol5GNM4@H6}8_8P@7*dQ5}(@*U#Z0N=K^eN8tGd7(|; zc&oV*XUPnP%9n%UeR6ys8M+|`1l!US2>HeaNGkahCjx%Cou}dlNr<%Q_)rcbz1{e6 zmjMO;c2x|K?!~jA{54aQ|JIQ}B}82cVNx9=dL=w<}{VM@7YgoYtWk__;Mt9vcvL4}3UeTwm_oxVq%Up=q_Z3PgeU|0?7}a06Jf7DO1!{=D7qtxGoP0h=!I$@#0)evTvJ%9B%JN5`dy(q#jkgzADl6m$P+>~x1g?>D z#Q#CifApq-E2Z2OETK7z5^E!cuDRkeKEsAsbxC@uqF&L0=tjdf@AZNClRHOE$c4dD z$;W0QS2k`EhOz_25Jm%#_6EY|duwnwGC2|Ut{(n}+@P^5M#zTDPl{x3W&pBSpX&NP zh5_iyk$Ytv0)-@7`SidZ1d8=!=l2@@nYEs^#DJ*n=K;+hA{M1{0N;#&`CNR}dTC zc=+~!Jo$(tkB(`$xVbZASYjD+QV&<~-nGL0G@gkoRArBbH6_7`bxXKR+3H>jk6fNi zjC{HJKB_Ekg(lPX__49`lSSZ3NDGe*9*vSx1Cj0F)cf-Nr}ra`9hJ@sgkaAmVYYVx z2mKiR9o+kHd|2jVlc;idr+&x77f$s4nHLGvCHh@Nt?;tC~S)t(rVNe zIA%*l0VRTPH~R4YZp<5a#L`UN0mRyyEVqcBU~R6dWQlIgNtk^4kXkdmR4WV*JUbm( zav)e9E4~Q!qk$f|iMZfXU`4f%n5A0Ad+RPQJ?O`3oB%AuFhwdQ3)Us;zpv0%8R zUT)zCb9WTuL(KW~>$owV3Wzzv*^shjjX6p!STndt_#*Y%Yo)BnNux>-$n;FndZ~@ z%MHX)jMMB@?gym>xbFvlr{s;^CPmLf)aXFQAe*V9r67h#&#%3SpNOx!OPCQCcqi{q zUlQ!Uti~IsbN_@dW1%lm^fp*w?CY}g*iIr+gC7LS@EDxo@wfOZ*F)mPrfFh zKhLMwQ|hXKq0WmIy%;HxpX_AwjuB<(GcS(a+JyVtO^>|0ZCn>H8t3Krwv=@2xo1~S zqqSZE4N}Y)QpuntSMH}qTlY^Ky zJs!7}uQ zmg7(R9p1UL!nzC(_myJ~cCPQSr<31fwF2Wvys{YdGAT8A6ar1(QD%8kgtc+x&ktxv z+%c*?sbD#f97aTOzE~1WsNk#W@3(A7e6ZEkPmW*l^4L_GE!;XQ4oN~4Cbdv57RD-! z0Rh#*K4+ejAHAPBvowb78Tx!vH+ddouNxBt9RF1}J2Ldt2=Qptd zkJ4JvV6?fCPN|)awlCrYdd1vPj5{4Sj)(Ku47nOnfHRPBLxPmcXG#ePKkA$MD)}=m zW#4?If6dvV|KkT>xWR)ODe-qb^+Y@3y4FB#q;U&a*hW%mNV@OdF^(;cd?x6${`+2k z&%(8^LAAuA?KX!FUEie8RO6dsGxp;T4PG?oYQBsnX&gEei>VwS4c@@ITQxuAKPMYI zD0#W;bN-|oqX<#aAxk>qQt^}Z1s0& z`|Ye?-R7iX?ZyySN6U8wmE(Wop^DG}}K7byo2xEm2e5X>wH{Wi+*P-Bhi}Lfu0i0dvVzYpf zSUj}ug`}k?Ox;I8{(usd&nA;C;RatLPuqb+Rc{tM(3tt;{ zZbG!cXUQ7pS~uEwa!DdvGiN%+FC{vdx!I3`?_`zwRPHWMj0L<|P6Bd0qxdr^jqz}E zGvk}si7_7KtvB-cx_dgu)blL8m6@cOOMGaZZ5YkK$Lgk5Lg}wWC(@ zZ~#BRN!gCnmIrif@tT;GK@FaTz!3`4<(Va;4C2|B4 zhyf(xIU?ySi4;yG#uJHQBw`GSc%Dd#AdzCoq)Z|)g+$CG5w8)+sbtbs5-Ep7C?yl~ z$w(fFoJ%H^lE@V#(oHhCf=sR<5o-`aHJMyXCe1R zc(7WsDMosGRkihv6tE<;0`O9+v*das9QQE+RN+TM$Y~Qlwa)^~jJ}SJsh+l;qJp9l z8vxksxV^_IyaE6oo<81KgP4WoVKL$eKr!1mfCoyp_NTmdnwS{=^?lF3Tb&2!+_+-v zulxNUTPO~Wr|bcMNLZ_;y_dHShWB9D(BH>v1z*N6ue}TQ7ywWahUL7m31axoir(%| zJhFnF|HR8H*xSrh2fH^e42wDcH*ELcu)T{n+XL%_-8zu;IPQy0Vl}O}6m|4GcmOLU zu)h#E1x$e+&<0{aAM64;Km{lQH7r*F8-Wtogw?=bj9;-W1dPE6Y^*(&dx1dU4P1dU zZ~;C*4%7N#)*Z3Z3DY}bzvCcqWdX2pSo*hPPdONj{!h2KP-@!YGdyZPXb%t<6F3Ggg?26BLKi1gim$f&0AD$VOP_+T& zpArz91yx{cV;BcbVsDn=-KwuI)0Ew_mQ@>VnqgS7=CD{r*fSow#p`TwoI8b2;aQf^ zy_(6cx>LQ8t%sZ4C~N7u=F{m~BabwxVyfF4n>aWnbkovA8tUi9KUBN-wlyryeruPv zV5jwk)IqQt4@1O6Jv4ks#4s5cXA2zeZc>b zrVt)!X!vCy!4NMyn5fI=I(TW1$ihI9p-^QrDt#n7J1)1Kt`#x0zi%g^Dx|oJ-}aWc zbecno>rlE&LGe(AL#&z;{{f*;9YmZ|n|<SlJ^!_le49(tF*mnmBp6hTr3Uv*9|;fop=dnNd1f)6M;o zP%Z5MLSdA~HL2Cf0MIREQrG0YX}6`xHOg^bjnzYLW2J=}XVWKgAD1x3A4ZE%WE!Mh z6363$7RQ_)hPRE4JmNjH?N-j6w!Y8{gM3Vdo5Az4P4Qu|8BwKtg=JNl+);|I`GSKP zH?QDLE;JTOCDhO4+D^3;wQV}UfGY-_460jI_2Noi86H4qU+T!8uF&F=pt`itx0c_@ z-A6InTp+qvunM=;y4RUhRJ7@mr z!&QIrd$TL*;?xIU`-aV5+-s>@x?>Nt2J~6#RBS1UI-t1)98?B%AFJF%?hYQ?k@$1? zV5e|hU%`pCg~2nTlj@`DPkUy2qQhGkzt%Zd1i!c+JXQPFuUYf{t5UyX_qv~Hh>Xq{ zzM7h2iLpNiyh=E5vuf`2&9lps&pF4pvnti)A57jz?ftc2d_x3B5?azcyVTLlZdjhR zm#>*m4ao<>_?(hGzIC{iTfRTDr}9S*;0xxIt0qb-U&A@+k&v_+wz*zjoF<;Aj} z&FhCey*!ZQb%hp8kt)SF2NvLHPuVIvRS|#Mo+->OWF9}`<>6-EEQk(2{TbNqJ|K{X zr^Gxemzs%lH5(X|h|$uHt@QCa-6<++DJ$6XE&9r%E}?ckgT0~c$yzPBeC>70J?S&C z!Er~|E4!+a9(X5Rzbr*69x>2YsZ2a!*pC#x*OtzB9b-RuLy7Xf#ae`wTx^)T@fulY zE7BP@v%yIwl>F$p8&6{Y=`Uo~RwJpJnG_E+dvn)aBX`Z@q~15Z63t}xzHLFtamq&q zpQBqitOZzEA6kZ|U1U{jeY2y#OI}l(bAF;A#~te~G`z*W*yPtv#L#-1%hs=IuUu!6 zGwoTOB6}H+f?|CRyb0nj;(OL%3wt2@42HkW`<5%vFE|z*KA7i*m}$0ETM)yOn$!!7 zvuY?--(yS6dB^EgzaopG|TisA!2ut)OrE)(nc%nXEX|0}|$>c3md;1)s zaF_NTzYP!zcRjt4_9P@DuOnmr_U^?sj+qV{WNlLnGIuc}xP!&1?3^Oqy4G^fojww@ z;%IzI*qY;Ov@>vZ&hWZKV}#%%%7-7h`}$(VxipRNWua&s!Z<{Rv;@8?JcZYhLVXI9 zVIHkQjrR}~^!|eI++k<@*YhU=o*c;HA9dRtXGwE2OAh`0+X+|SNIgz4?1P&NR0z_y zjuR#_-~Nn*n~LSbH`lAyZYC%thcca1TE5UuI6+zIx5D34izr|kUy41$a6^d_5?ABiAJx_PBjZI1w;+5X|Saw$oa#6I3 zCv8^uh1=bjl-d@@EQgO$;yHT^o)``(Wd=$L>sM#IZy8d#w?Jz3im$#pAjI_-bd9}2 z*Km_7FHFD%R)`M-;D45@Z8NKF2mtKWguT&M2p(ZoO9UjW#Mt1fZUwJcS+N0Ntv~<& z=ugG80HbpN1g!Su{Z}1Yt*`r6U1YT`zFIfJatJd9IKfWrb$$Zat&$ZJjJ}+}2+RqL zysRLv|EnJVnC1VZrsDtNrmL5wuU>W~)PQ$PuVRen{>@o2cdRU{*x#J>KhIgCR(1~x z;I$AW1VLgDBnLrTAV>`YdN{BP#`r(BJ>flAlmY{6e;7i*1Of*jupfr^!{7i8m;rDQ zKnF2q3;}Zh9RiR!1RsLIemt;%U<(Mcg1|8VtRZj|hL6C&1^`5O9Zp2LPS`+j0;lo)90mB|R$P))0$FJTWIKom9#4@KZXGypLGL_<&v z1jS+i1~D)cgM(w?e>@TXF8-4q_}^o;${SA;IC0Vch}jO59C@z9)J=lz{*2i&RZopQ z(pSpcla34H<~R6f%=W^^<#o2Ap8B$^mP=@u5qXGih+ZY%|MaJWfH6v`h^%R%73h-u5#dM;`+VE;(ku|7PuOF*C0ipci>_5LkkYdIVJ-uCk>;@@`S<1-8_Jpe z24z>C%*2+J=W^@1MLbpsE-6{BEjUrs{NP2|ogz)av6>FQ((&3pZ^oulhm1ha`pSx6 z_tKY5f>Sp?F~-Xq-uG)%Joq+qulRmDZF70-2+}RxI4E#@Qxmcy)}v~6O0e=eibKCY zKI3%cBvYt*^yIy}oVI`Fbx01WI4Lnmy?XG&Y z8{SxUPnzcpt3-`>^k&B{f!2*3IyxI~y!qO8Z5;f$b z?TyJ&^yBZPlO3K37f&@h?l|rD`NVrRqqdD}rsYSO|JJKpKWt}}&)*K?^xWDRaMQzc zAl}NN?n&^5vkTqn?;GlRJnmha={2@G64=X&Gxq4uI#HqiweXy=?}$r+2&?^W)3Lh~ zcl|fjkLTY!TmLmtaMQv>8T;6_q1zW)mpYqwoCum=4$!vGc@>Im`y9WQQ)&KWujZZC zt*1PKJKybl7(6#%t!|s)>%36h+wXjhp5ZF}{7!~-N5a`9%?=)R_Pn(9g1k4enKKb7 z+Dv#s#``Q@|79wvrM0FtG%QRDzeZJ#VYKI*XR>HA&1GAS)J;E$rb{u;Hir<)r>uly zb5Q3ZamsmmFhXBLv~mF)+nAd8`q$^4YH!Y#5Kz?8zP$#osB?|m+|j9f)Hr~4Ig4@l zd+eZPf`7Mt=~@v)BfHV)WQy|cp|nahXUQ909Ra&X=yc^{Wp8}l26Wzi_#Pk7a>Ko^ zF1^}}BhEw3&{10FnYO@;;+1uh$82~62g*I-!}n9HrnH|M98OAdzl&H4Ed0S-zC^br z=hOA{4RRCn6R7L-ZL&Hpo7c}=sW=^L=rF*uZ)z~5t|fnyb4POY?ZH$gTA;8OPO=kV zr9Cq&P)dE?;-t<>e;u($>+dh5e}5tU|NaXpMuxBmivr=XldW|yQ5q&NV3Hh;v;{}r zhNtYpqlS2@DF@AxgVTY)>BhljPgvtl;`Si$oFecB6W4~5)NY)RTh&Uob-8`>^zN*fge=*EJb#KBD;jre^O-^ zIAwp*=nLqEpESl#Dt(Dlb`fPP(in@J8yC}WwE@rx$=8{NRBZeVjV*fcpd zO?HV(hP5K}MJ~n?*M{F~7;LT$Y%a#{|0JdQ+xB02;D2YE|MP_4fB|+w;P?{4P6#fK zRu#0au+1_xWhbLV&K(bgw6a~=VeEv!$RMP+z0l-_!8L}jYiPkfTLjdb&<$Y%O-M_t zrtD+2OJ~W^mnj}$c*n;QQVQ9QSV)5B+Nhus8paWVqlg&R-TKlT%|kH@< zpAk#nKiJ-=?awJ1AqJE1d_NmQ#e|Cb!cdiwzHlyvMPCFl=vF8PYaGcsI+gyX|Twg;Z=|@~RPD7QCc}d&k ziU%+6GrlXCY;J1lm~!ZF%-1W%yVr%2#eA|GPb13#ht6tCWCJ{p!!$&epSIEE3vjB8 zShHL1%kcFO0%IgQf^X6J`q}xILf(K)jm;Fv(}|&O0zL0-GbvgM5=n~Pflmsvhh*LO z;^MKxfue#8Mp4ct#UY}g85``(*Wxo1hAFyQJBi|N(m6_*gbi|~=av<*69R&j+%3y# zEhi#x3u0A{=TKmt<+6AkN_xNuj;wqhBsf(!sIsJ;&bMbTxP!C2P+lYU6WMg@@g_=1 z!z{Ok{N3d$w@vjOe9Q{1seYu9L(i}4TNCUqYcP8MyAA-C0#C^E%N&NA#b)pG}$BKTpU1b&LAnp;>jxP+{AJ-ha>iSap z!-8UUiOQK0-mMx9E?IF(<0a%<$VFzo?jiThc;4e7lAL?xCKX#v3(IRde+ztfBXFO% zSoOl@#PrS%N9Bh3p3@iDpXR$xp8EXdy=||s&PT=I?jQZZ?2B8v$J&hQKI=<7P##Fz zI$4(&x;NhA%azRXfT6tcvbwGW{~q6a$%0w6J+6937fT9pA`4@9%Cy>7hfI%&36+;^ zKZX)H&x}s_CvV(!i+OO#+Qu8F_={WsL)m?I2U$878QwDqCp(#56Vxu0*V^_g?`gOARJ_&=PY?mKrg%$glGyMraQFXTq% zQRv}Q75oGFvnaj~H}3p!>CUULW#ugPX8!tF{pmp7GMmyzg<5NzUWZEEPsL|G-KKZc zE3~G`?DLUfjoUF}fn6`HOWEAfjY2A3QHl}ioZ-=GrEAtiz3mX67vtY4eU>X?<7L4q zQ|ZU7{U>A0w~6sx;D5Y6tis;_Z_odDL{Tu(_jX!n}Dz>QXKPM+?f6qr%LgZ{| zyq`lie@@-g_4A7QS&Qatad&HW*1wJ>XgCz=7VbWnuMpkdWzEO2waHLl=~AkF-Y2DW zJ?){kL^~ZxVy&~*PKI%E&^AZOSHu^)2%OQC&An>{=N{>$d=K(A%_pC$dvIhd>0;2B zl#m9w#>Ut?t(#@jDP5wn z{5;I0Wh(VUPnntq^xbyQ)Vs@W`r+!4l{hQ8@tbSOTsco_vG=e>po;FfB$oNm0Ixa^ zqt$Qf!1;c|z8%9)o|+I6Jt&G`=1Ht*v+ zM{j6k*2s^#&gKZe9bz;WnFIt)I6Gt%%Tv_$=G0%lqVQJ4bZ7P(cLHzB2(EZ7CBVc? z(Y@p#$B4Xb+FR&TW!lqOtF90}^Y|C-3Hv6+5Ub+qpoKR%d#^}YKBT|!uOXE))vrRYA}yDY|HulQcy%l|Rr;Lb>;OMyFr8@>0Xm^Q`u2M&JS zRP{4Zt``}%9WG%!Cj8j+-Bif|&<^!$b5zofh}mQ{e+b64^zE1_jo+>M$8E0e9{7l~ z)$d&%;ewGjZ9_LWg>CfeMoc=Fxi}8-xTH5Vt?hK!^}cNI^F)Kwa@6eZxTa+e8^1vJ z!^@4e7XJA?j|`4i5;U{DBrQxo>OL-%*7#^*B7Hfz@P45zYt-p<5HER~LNT#>{$AlW za7j8llv_&H(0YdZO5Nq5O&-PUy_QW$X&<`tuJ0P;`@wS=$1%%n*9Z-fzn8c$|W94Y=3}dcPE?sd>A2A&Nj1r78 z#Z``ut1ss;{pts@s1AMa>K@z)@@pMG*7-@J_DRU;k%9GleV-k+sib=P3<&m_X{kMn z@$=3ynsl>T`t<4n)5}|D;$V1SLU5&#lS=2hH)%`VBgR|Z0grHfcASlfn}PN3+_a#s zrJj(SAGW;H8909C=bt&o_rGpWso`JlBkilRT>f>!|KYu(+U=D6@m0Jf7R&wouj&p~ znDSot;*b~n81qcecqZwa?gkYRvZ}6W(@e^}<*$;X(Jju#tcWHz&sCKSObnz;M4#@173tKVQ%mMFr?`xh!ECHtzKnoD$#>-xt6;dbE?} zY0xPbDC?s2c?5rcxR_#<^G;xGMqGuvvT5oqvOQwWshsmfKwiQoB~n-qeXcqq720Bo zOOx5E?s#QdNGMiAA(WS)U_53PGBD6k@!S3dUnE!s;GSfUOf_HvX;7;em||yYtNg+- z?Xdh6QjCS^!o@u!O{CGr7O# ze7Ipky0U5%%RSyp1|RmLPyTxgf-P$=_krZ`8Bx36cU;PUgo2M=eUJn z)~*Z5k^~7`obTHGUcrg(6Aq>_026330RS?_yAUdD)>F|MFaRzD;5|X2neiBy1c#wq zHD`jipBV}MS`tg}B*E4><0D28&RJ|igiv+GH!UIp5$8DOCkf&X&!yBaSWrqbDLy9&CCm)MCd}lGx)ybHt>ag(n9b^P zlFPovTm$4W8JV^fcRYaye-2?x&FFTBC;mUt(T!gJVR+Cf5H;gCNJ%YF9la0o20`hq zNE*BH>rCvW$9Z)KI7qoh6s5Q_qPP{HY_iLN!7dQx&3pwD$gmJ)oCI)UMV-Sry z^&QpLhPmdes~b&tC)o;4`<)m6~}|e)Z%x4te#19_E-j%tv-?*xd=R81R>eN$DyzVa^K~nhf z^aEgCh*<4!`$CLb^1n!b;0&|y3LisAhD3lG49)!d1q5XNwg$l7)>N2q2Q*-kJ(cUY zd|n1HsFChJBGUPB&L$===zG$ehHg19BhMdBzf(*X*NLXjbo-&9a7x) z1Cp}c+>_$L%5fiEtDrj|>6_|Ls|xpNJ?tOfrwwV@iYiy#`dCO(A55Na%Di+m5{^2! z{Ncc2SWufEIv>GaOpA>Utf*yt?f}OZ6M0Un&NUBMc+0p*jCeQ$2EG8e*$a`NoIdnQ z!(?Jj0AC1a1${nAx%}CYR({I0Ox)aXn%;b7hAOtr8=I^WOoNCgF6U^Nc>$eIDH(qRHw{ZdzotA0_R_C zqVw?AZS%_xNV0bqz=j53OXp-nBzL003E*I=!p_v!P%f+!p)blHl*Cz2LWZ%v5*=NE zyRaCc6N3@_6Bstn@9z@vRl*4%yL%bod>&aHzzd&nHDlxWLy(>N5oFfAJ6O z(KvW}-oZE0hoH`P%953v;Zq8L3Id2-h?27w6a;pAUkq>|CK%t3qQOsf6*<-~X?h(I z*ucIO`$b2-3Gs1|H)=L3h8my&9J{P$&aRKc!G$0K+F=(r24p@ zR?lJWev|AjTOs4Xa5wK=@I5zLOvlS=q(HxG5p~KQq@qpFDtSJ>KAWQ>`VQZKYEr}o zYe$^smjhtH$Bnc4xkpTtnj4YTaljYRH;xD(7V9e`gZd|)1DPOb53MBrX;#N*Fvl9O zZR0=8;gvGH%`OPTvIOBUXkF#)VF1;$V%v?7fg?a^_(Ey7CJVoNOe0?svmXwE0&sZ8 zMQ?xu1ptGC844V^xS&8=n6-k1)tM(I9OMrH(BY@FPynd8$HJdHELC&{vt6{=?UGH4 z;)&yq=z&Mn00WMKp8c`eZ9G`<92hjY-b8tUP2=qS*~{5Y;J8t;FKa-f`H~t?<0d*A ztOhJ~$Cr<*e1q;{bNulOKRp4!Y;SdM#2lIHlolD9n^_-U=Q%pyONdquYB_oC=9|zv zKcg4y&MVUo#JG^I`U3{jxj9lfp^@{8`9&Hp{MFGwLx?tKqmgA_R?>mJVbFOcW(E<$ zuoNE?zZr7c`lsci#)LWx*w@iJR~_D|>3DdHeME3M9w9@sR%DLzfiab(LzfS%exV11 zM@)@&Y;M(z4s8JKd)70G;5wG=fw-X%==CoU;8gmI7OF(_Yp@7y@hyflT&b~8ze^t<5o+oYUNM&+e=9I{AWqlyM*)I3u zD}H&n#VY68&*FQ(WV2w=VI}~|0CWO{VVQAaVQxQS8FlB_c$1$v=h^Wr3lM5)!HiG_ z9T)oXZxtoewJm;lzkXdl)JUc64(EvIfHhVCXD?7!WSwUh4x1}S;K;fCs`I1hk(`7B zB*TK8k#ek0e>ePl({f|Pn*$!-0cF)^K?%p%ycQ*j5x+$XfziHY3aP_@LfAY$-KR4h zgVdiRZ;y=DuW6imI;}jVY{w0sy99!FL-u z%%UaS-4V3@l{0}iPF?rGAzk-X`jQIey$+x#mCTU2y&U_`B$7)t()D(#*H?vx;9TiBS1Ga(ERfu=4%ovP;_#0u3TMqpb%_S^3dLdtuO-svvu zP?><>5=+^x(|OBhZ5DNJZNtA}P(elFm81Lc;Fx6lA2RYdtP$mXc;;>ay{8AaNSxH2 ztz_ilsh0=q?*$D>+_2`VN&+m15RMbFGrSMJ;y?^s04T4}*)Smqg+cS;4nLuz`VVk^ zQZLq+X5Rsy#&=NKUsmGY5ssH5+SmyJXuv+$=H~LP%Y?^lEu-7!1kU2hJYS>t)k&{g z(79$9uo(0Q#y{~?5r84lpMRQRhiW{$YR$^isuY*8p$Z?sjceZu@_sT{i;sE+00jK% zS6+4wq-mgDiTKQ3mxcv1$q0br-vXU805Ar>ikCKGaFCN8V6M>7EbRYgmomc7@qbzR zGZyfF`hP49{D-Bp|M~x6X(DV)SJo%f+sB9Fzd1XilvEx$nQCr&d^gaLjktwE*RlCd zTIO=98*JcU`m&9Ly~+or6;oU_SGFf-YObhDY&-08!`TtM-fQgbz91+h}kqIZ8D`7|O;q ztR6XJdvvex)ipP(=;Xfp1*PY1kASg;bkgD+EL$p)WA*~yn^prWfPt%HGge-wI>1sY zXCyUi`$o-XaTXs%hSn>9s&`!ydUp9Z{E;2$%BGRB-F1idu*OZTvBcrm#e0Gtn~Dm9 zG7)Tov=FSKX%~;q0p=;P{OmlcY*<`jXK7yrQ~9VX!TKDc>OY+|UE+d`MWBxvc@B5* zI`%ZUX&f<29!<(*XjcN;Ff~!;SaOItSXPxnFkFZC8v3kgFPxYQVE#+ipgBsek-VWH zrb|_Yx4@t?e9N?T$$ks84KN!k?4_FJcx6dZ<#w z{3}#BhB$pp`~0iRe!rcpQDf1;)fCLB(4lD{q-WL2G_x@^8%dLt$Z$7Vo0{Ztkv8YA z`_mVb@p_Qbq3BATR!vcw%AfDu++5Wl?Z>l|o#9s_Sd6NnmE~?U zPX$#Ig*zcS2y~s;K+$d>n}Ktr@Tqe?^##*lviUZdrgn*&b$0oME~gJNRQ=(sFzl= zo?oNUvs9Hq&zQfTU8o>}6>2Zk7xsF)I*{@=I${@l3ncol#g7IO{T%kj?aTC~eHf$@ zGD-BcS>}wcg{+<>Wl$zsnk4%C$jn+$&&*HQ^p0yOVW9aU+FUf_jK8;QQ^V=%P5l#o zd>|D^GBjIX`>C!1otM%*ud&Ye!hVBtmXzOpXLG}{Lo~x(EYf#hr%2bqF<*n>DR-L$ ztz?zGEyu^PMV4%B*6Hngc&T}^jvxyE?*)dJvi#ePd=vkV_N#X`0%&t-&Q)`i=v!iL z^x?_4{8o`q6~z8LOL=_ubhM=KLr6!5;;7j-kQFt;S^Ud*TjIw7%!bB8$auJdv~>12 z3hhH$c#B|U6*JXuBs?o2wM(c-oZkK!F5_MA6e6&#;*pxKh2mcKUiP&8dv$&e%`65pq&z;PPFDA@;4(f@pqwA*VQ=n0{!% zrTKo6)rxXKx?j}lpBKGQT<&ot^jt4&87C%0zDx>%M+5{b&aQr)4o%vdSqPP<-#D)r ztsf?IN(b-eg+Ztom}1~ww{a1D(fy#?9*_iG6Ff7xz^O}!`lt;jkCCC#HNbcSsjh0(;pH%M_rCIYqRHl3=rFv^ zge7EN&8eE|?9|DrbAB4&uAy&E9|nlIkjnNAvncd=Awo#fztvPJooaXIR&!rsn&EVT zv&p~Nsipc=Iy*UR1S)`7#Ac=z+Oqkx;^d0bb&~*cUiciSw+57bU?%^^^4Rs}(#|ei ztcB-!#`4g46j$BnabQKX(BT)M+X(}#HNHrq)XkvEX0%+py3eecgD)OOE^_}Henfy@G0Jm513U(~<^^3Bl@Ta62BvA+Qt;9? z+Exm!>mgYSG$z!0$Hd1Hwv-?i-x8dZiu_QbYE?>D zRo-JcHt*4r(5({WUdd97h3BCMu3P?M0ypDI`erNU%2atmAWH!G!7%%`rcCT}73%it z6R{&B_4j+5yFYn!pV>5=TBXqz*_JPHp2zSTunWx;lCDH)Rvk=bsjn2SWf3ae419BF zUK;6pT@+m+7M8I*=1zuj#JcV1JMY%?x=S#65R1^ieRSJ36L7y8R3SlG${cw(w!FQ0 zd#k1`OwweatyG8?G3?2&Xz7rGET6%?IxFCA!z?bbrhS0_EoE%ND_BduoHQ%O+H6`F zLU1X<*4F$TB?p$y|4wPwqL^fV*gxFLRJn?IkGxIZX3N&7e&=dT6hS_CK}VbAmqMU8 z#Uy0&);;9z_VG^eCxgYXO7*+S~ETt0J zhLcIU9>O5BU5~z1(WBO_D7F+fO>A0ZWODt3@zFhGMJ2_Rm;MZb%?f(tLDZADY0N)j z0|uw#AVANOqTsl-hEeLb#!;o(ozHss0^G()VW8A@oi-6b2i}2*K^&$%Bast#gZ6Y1 zLYVk?2q`+uL8`uA@LjY)ARUjR;}9}QReVHQB<2>73Dm?+Yk`m!eJa4@4=Id?i|BBq zYiXv!@hu@#MF*OIm#J;-2z7Q@o&M-Lu_QHde*f5=d-5XfQ*vW>lZcKdv?62(cNg1E zCGFkCOM=x44FXin%W&bY*-5P#w`CI}D;lgtWMcby%-xT$C8P}-@5=GR<4XJBe_UKw z^$>?FAifS+>soqs-*^=|@aIavx=+%yzHG zCCrrV1Ins$E2)>Ld+OMO3BgaH?){NLEy6lr{HmC467}?dv@uZk^#*}+A zTDOOZ8Z{oFZ+UP4MxisN?TCBLk?A~~3ZzQHFOn!Sz5PTNW`bc)hS_a`$GGqHe{lMK zB#1GqY-sOI?0h{^Pt>CG8Tc*CjxJEb!kH3AJk?rI6#%Jrk#0EVSZN;HR{4M>Dg1$@ z;og!pE{hd*Vnz@KV-_E;J9H&w$gF1d@K#|cKh1AxrXI(H9n7?c(Kpo7~f5<)Q?SXVCs(0^7FypGVAPUz|!4Lr8m2{WpZi_3SCa zBaKKh#+et-KPF3D=Z-c-mD-j7O*TVB8%wjtZeUE80%;6R381L5%JpP4Mq_5H= z^+@mbMr(QUf@dV@n^PUX92d?j>s*##QbI!l@r@iEw-AFpjZOP&_m7n1#78F? z$M|m?*Hv*{9L1ZIBq&+E7lCJepMSC*ALEO~@^Y<~Phus9!8)4Zr9Q2$ch|=Hdn}8n zZ`iR)+{W=pju!)R1vD*T3oAL&k*CHz_Ry(eZv6uWF33p*ys5P1xIEMDbu}pFa$}M! zBJkR%au~l>){AzE9?Zdd!LQX;B>ino8c?U6tU}?Y70S!9=I^gwSpAi;+C>~0wjHks z{SZQGNy(a*p3lCKBQv>MI56DfM}afIrgrZmUmz&x4#PezbxK7TzhtG{vANGHo06N{ zYAO9m$KB}*>hh9S>wn}Tu3X~BosJjgUtk` zUvXn}u1&qcS`}$GB3-`{b)-+^V{P`%?-&#MGW*oyVzX6O4^P_107>@A#wPhdGPO;6 zB5bXhNNp(&!rhTh^E3;53vQXA@dyOhOD@8ZktsuZYwqR_7CdRa63GgFrHlHPLa zRl~G;-`K)Bjs|zKVafK9kwTvUI;k${`5$nd68{W*gh-lkn#kSJWnoD=s*O>l=z`pr z*s#So;;ksjEqW^@EH`i%Q>4MGcFi&gpJoqdzZ0$)O~Peh_)#*-Oj3Gmo1$k0SBkY8 zKJY?6Yq`z`bErwz0B#9Ch>xb$s1>2lP@@-Aw4Ssd)aMro`3Z}Svob0f>#+PY9YC#u zon|2Qrt^^f3st%O1YbQ4>OHLZ&9N4p6+#-kUh0?-oKRL5t}lwT9{2Ay$}y`zbS*-e9TimO zbmroDm{U^XS@4P^Gh&~~o>5dkUOCk~2IjNg(WW0s1=F(Mbc*KIBA(8HA7$$6dMx6V>pyjO);c$uwH0NQAdM(nH+7D!=^!6q7 z`|DVAEic1wsS&t$IxTemac_RXCZiP6B@Bc%kI^yYEAGjJV0`yJuMl82INHO~5IdDl zHtgF=bJ3n_*{s>Vb9t?P8gk(FfRPe5F+~U0dDW=lkwidh-BKyy#!#1bLC~0Jce-ho z>XTYF=ZXK1&Pxk+qr!Io9z@57qylbAoCqLu-XnczoRAhxLSZp2cB-Q(K((F-NvrK4 zy3o(4S$ZL{O?wI+w#hA9Y>DmLFu?y+8fzI@gRVx`|JjO7%0N&JMiYR$%=N@SC`v6` zIWeB$r>1XaQKx4FMo%S=pQ8M-m!GzjreoB_q5Ss^iob7!`Im9;a!KLrc$9o)umsVA zmv*UBvwloii^M3t+|+7emuqh3_K1daxrx!U`n%}!~rW5;>n zZ>r(6yXcMQ);w~ARsZ1y85eQzH)Dy7dV?)=NAXIocXGjGT4CdyE$LvU)`*C1v`V2o zm@W72WU;4U7Hl6qw?lr>4Y4e+cS10m_rv*mEBSX}!13uS%2?!BK*)8n8+W--1ysol zQW{n@-ilCAuha_K7Dj7Uk&_z=iea+ddE?r8H(4(MX*BY?x?I0c1Sf((`=|-rMJpq@d7}8gI{pKkha7tH(9l4%PjZv7{_F$~#E=(x#cZ?M&`3 z!}OJ^khK!4Qji3CzlrCR{9^kVI9oGwxnTGba{K8mO`<^u$s}Suoz|ygl{E|VbGLx4 z+@#UY$8jk`%lakxq+c=?Qxa%g%t6nd*0a;dQ+L0(Z-YPgjrx=K<18$IMqlVn7gy&^ zjh2V)aFP8omKb8Xqm+$IB!BnmVuuE3Bx$lbQAjio!Lx{-&f6$1yqU>F|6^#YBr|d@FnT z?`19-y=OfyT=9qEDUo9v3=8COm7^1)u3s%AIGN_}^)#CPnA>nG;^c?5_Rfk-I5~@f z%IiP_q5kN0ee@nzmr2uqD)=`CmY(M%$}&k537Wby&fQv!T2Ft@7VxS!eNz%cR&qN% zf3`3w2SY=z%{ZJJWj~sDXjvRyDrk2mct*y{zzgAxygzxZ9yO08P)Nk@#l)xbU1 Date: Sun, 12 Jan 2025 19:23:35 -0800 Subject: [PATCH 29/86] Added VERY_VERBOSE dfplayer printing (#8026) --- esphome/components/dfplayer/dfplayer.cpp | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/esphome/components/dfplayer/dfplayer.cpp b/esphome/components/dfplayer/dfplayer.cpp index 98c3e91e46..70bd42e1a5 100644 --- a/esphome/components/dfplayer/dfplayer.cpp +++ b/esphome/components/dfplayer/dfplayer.cpp @@ -159,6 +159,15 @@ void DFPlayer::loop() { } break; case 9: // End byte +#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE + char byte_sequence[100]; + byte_sequence[0] = '\0'; + for (size_t i = 0; i < this->read_pos_ + 1; ++i) { + snprintf(byte_sequence + strlen(byte_sequence), sizeof(byte_sequence) - strlen(byte_sequence), "%02X ", + this->read_buffer_[i]); + } + ESP_LOGVV(TAG, "Received byte sequence: %s", byte_sequence); +#endif if (byte != 0xEF) { ESP_LOGW(TAG, "Expected end byte 0xEF, got %#02x", byte); this->read_pos_ = 0; @@ -238,13 +247,17 @@ void DFPlayer::loop() { this->ack_set_is_playing_ = false; this->ack_reset_is_playing_ = false; break; + case 0x3C: + ESP_LOGV(TAG, "Playback finished (USB drive)"); + this->is_playing_ = false; + this->on_finished_playback_callback_.call(); case 0x3D: - ESP_LOGV(TAG, "Playback finished"); + ESP_LOGV(TAG, "Playback finished (SD card)"); this->is_playing_ = false; this->on_finished_playback_callback_.call(); break; default: - ESP_LOGV(TAG, "Received unknown cmd %#02x arg %#04x", cmd, argument); + ESP_LOGE(TAG, "Received unknown cmd %#02x arg %#04x", cmd, argument); } this->sent_cmd_ = 0; this->read_pos_ = 0; From d8c943972b2a3e68ec126922cd8601fba290b6c0 Mon Sep 17 00:00:00 2001 From: Djordje Mandic <6750655+DjordjeMandic@users.noreply.github.com> Date: Mon, 13 Jan 2025 05:05:53 +0100 Subject: [PATCH 30/86] [core] fix comment for crc8 function in helpers.h (#8016) --- esphome/core/helpers.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index c823439fb3..82b0fe07f8 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -163,7 +163,7 @@ template T remap(U value, U min, U max, T min_out, T max return (value - min) * (max_out - min_out) / (max - min) + min_out; } -/// Calculate a CRC-8 checksum of \p data with size \p len. +/// Calculate a CRC-8 checksum of \p data with size \p len using the CRC-8-Dallas/Maxim polynomial. uint8_t crc8(const uint8_t *data, uint8_t len); /// Calculate a CRC-16 checksum of \p data with size \p len. From aa1879082c67dd893591d854ea8e057e2ad99fa6 Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Mon, 13 Jan 2025 05:06:44 +0100 Subject: [PATCH 31/86] [debug] Add framework type to debug info (#8013) --- esphome/components/debug/debug_esp32.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/esphome/components/debug/debug_esp32.cpp b/esphome/components/debug/debug_esp32.cpp index 5f7b9cdbb0..b0631f2b61 100644 --- a/esphome/components/debug/debug_esp32.cpp +++ b/esphome/components/debug/debug_esp32.cpp @@ -276,6 +276,19 @@ void DebugComponent::get_device_info_(std::string &device_info) { device_info += " Cores:" + to_string(info.cores); device_info += " Revision:" + to_string(info.revision); + // Framework detection + device_info += "|Framework: "; +#ifdef USE_ARDUINO + ESP_LOGD(TAG, "Framework: Arduino"); + device_info += "Arduino"; +#elif defined(USE_ESP_IDF) + ESP_LOGD(TAG, "Framework: ESP-IDF"); + device_info += "ESP-IDF"; +#else + ESP_LOGW(TAG, "Framework: UNKNOWN"); + device_info += "UNKNOWN"; +#endif + ESP_LOGD(TAG, "ESP-IDF Version: %s", esp_get_idf_version()); device_info += "|ESP-IDF: "; device_info += esp_get_idf_version(); From fef50afef8bbf333de3d8f0fc3754d231f7bf165 Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Mon, 13 Jan 2025 05:08:20 +0100 Subject: [PATCH 32/86] [debug] Add ESP32 partition table logging to `dump_config` (#8012) --- esphome/components/debug/debug_component.cpp | 4 ++++ esphome/components/debug/debug_component.h | 14 ++++++++++++++ esphome/components/debug/debug_esp32.cpp | 14 ++++++++++++++ tests/components/debug/test.esp32-s2-ard.yaml | 1 + tests/components/debug/test.esp32-s2-idf.yaml | 1 + tests/components/debug/test.esp32-s3-ard.yaml | 1 + tests/components/debug/test.esp32-s3-idf.yaml | 1 + 7 files changed, 36 insertions(+) create mode 100644 tests/components/debug/test.esp32-s2-ard.yaml create mode 100644 tests/components/debug/test.esp32-s2-idf.yaml create mode 100644 tests/components/debug/test.esp32-s3-ard.yaml create mode 100644 tests/components/debug/test.esp32-s3-idf.yaml diff --git a/esphome/components/debug/debug_component.cpp b/esphome/components/debug/debug_component.cpp index cbd4249d92..7d25bf5472 100644 --- a/esphome/components/debug/debug_component.cpp +++ b/esphome/components/debug/debug_component.cpp @@ -50,6 +50,10 @@ void DebugComponent::dump_config() { this->reset_reason_->publish_state(get_reset_reason_()); } #endif // USE_TEXT_SENSOR + +#ifdef USE_ESP32 + this->log_partition_info_(); // Log partition information for ESP32 +#endif // USE_ESP32 } void DebugComponent::loop() { diff --git a/esphome/components/debug/debug_component.h b/esphome/components/debug/debug_component.h index 2b54406603..608addb4a3 100644 --- a/esphome/components/debug/debug_component.h +++ b/esphome/components/debug/debug_component.h @@ -55,6 +55,20 @@ class DebugComponent : public PollingComponent { #endif // USE_ESP32 #endif // USE_SENSOR +#ifdef USE_ESP32 + /** + * @brief Logs information about the device's partition table. + * + * This function iterates through the ESP32's partition table and logs details + * about each partition, including its name, type, subtype, starting address, + * and size. The information is useful for diagnosing issues related to flash + * memory or verifying the partition configuration dynamically at runtime. + * + * Only available when compiled for ESP32 platforms. + */ + void log_partition_info_(); +#endif // USE_ESP32 + #ifdef USE_TEXT_SENSOR text_sensor::TextSensor *device_info_{nullptr}; text_sensor::TextSensor *reset_reason_{nullptr}; diff --git a/esphome/components/debug/debug_esp32.cpp b/esphome/components/debug/debug_esp32.cpp index b0631f2b61..69ae7e3678 100644 --- a/esphome/components/debug/debug_esp32.cpp +++ b/esphome/components/debug/debug_esp32.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #if defined(USE_ESP32_VARIANT_ESP32) #include @@ -28,6 +29,19 @@ namespace debug { static const char *const TAG = "debug"; +void DebugComponent::log_partition_info_() { + ESP_LOGCONFIG(TAG, "Partition table:"); + ESP_LOGCONFIG(TAG, " %-12s %-4s %-8s %-10s %-10s", "Name", "Type", "Subtype", "Address", "Size"); + esp_partition_iterator_t it = esp_partition_find(ESP_PARTITION_TYPE_ANY, ESP_PARTITION_SUBTYPE_ANY, NULL); + while (it != NULL) { + const esp_partition_t *partition = esp_partition_get(it); + ESP_LOGCONFIG(TAG, " %-12s %-4d %-8d 0x%08X 0x%08X", partition->label, partition->type, partition->subtype, + partition->address, partition->size); + it = esp_partition_next(it); + } + esp_partition_iterator_release(it); +} + std::string DebugComponent::get_reset_reason_() { std::string reset_reason; switch (esp_reset_reason()) { diff --git a/tests/components/debug/test.esp32-s2-ard.yaml b/tests/components/debug/test.esp32-s2-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/debug/test.esp32-s2-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/debug/test.esp32-s2-idf.yaml b/tests/components/debug/test.esp32-s2-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/debug/test.esp32-s2-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/debug/test.esp32-s3-ard.yaml b/tests/components/debug/test.esp32-s3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/debug/test.esp32-s3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/debug/test.esp32-s3-idf.yaml b/tests/components/debug/test.esp32-s3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/debug/test.esp32-s3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml From 528d3672b499650185427297cfb7399fedd8fcf4 Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Mon, 13 Jan 2025 05:11:48 +0100 Subject: [PATCH 33/86] [psram] Improve total PSRAM display in logs by using rounded KB values (#8008) Co-authored-by: Djordje Mandic <6750655+DjordjeMandic@users.noreply.github.com> --- esphome/components/psram/psram.cpp | 9 ++++++++- tests/components/psram/test.esp32-s2-ard.yaml | 1 + tests/components/psram/test.esp32-s2-idf.yaml | 1 + tests/components/psram/test.esp32-s3-ard.yaml | 1 + tests/components/psram/test.esp32-s3-idf.yaml | 1 + 5 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 tests/components/psram/test.esp32-s2-ard.yaml create mode 100644 tests/components/psram/test.esp32-s2-idf.yaml create mode 100644 tests/components/psram/test.esp32-s3-ard.yaml create mode 100644 tests/components/psram/test.esp32-s3-idf.yaml diff --git a/esphome/components/psram/psram.cpp b/esphome/components/psram/psram.cpp index 68d8dfd697..d9a5bd101f 100644 --- a/esphome/components/psram/psram.cpp +++ b/esphome/components/psram/psram.cpp @@ -21,7 +21,14 @@ void PsramComponent::dump_config() { ESP_LOGCONFIG(TAG, " Available: %s", YESNO(available)); #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 1, 0) if (available) { - ESP_LOGCONFIG(TAG, " Size: %d KB", heap_caps_get_total_size(MALLOC_CAP_SPIRAM) / 1024); + const size_t psram_total_size_bytes = heap_caps_get_total_size(MALLOC_CAP_SPIRAM); + const float psram_total_size_kb = psram_total_size_bytes / 1024.0f; + + if (abs(std::round(psram_total_size_kb) - psram_total_size_kb) < 0.05f) { + ESP_LOGCONFIG(TAG, " Size: %.0f KB", psram_total_size_kb); + } else { + ESP_LOGCONFIG(TAG, " Size: %zu bytes", psram_total_size_bytes); + } } #endif } diff --git a/tests/components/psram/test.esp32-s2-ard.yaml b/tests/components/psram/test.esp32-s2-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/psram/test.esp32-s2-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/psram/test.esp32-s2-idf.yaml b/tests/components/psram/test.esp32-s2-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/psram/test.esp32-s2-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/psram/test.esp32-s3-ard.yaml b/tests/components/psram/test.esp32-s3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/psram/test.esp32-s3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/psram/test.esp32-s3-idf.yaml b/tests/components/psram/test.esp32-s3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/psram/test.esp32-s3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml From 8fbd512952e516b5b4a64d9592d81cbeb9349c38 Mon Sep 17 00:00:00 2001 From: Douglas <31328123+dougiteixeira@users.noreply.github.com> Date: Mon, 13 Jan 2025 01:16:43 -0300 Subject: [PATCH 34/86] Use ESPHome logo on readme page according to theme (light/dark) (#7992) --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index da1b2b3650..8e3d8f71aa 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,11 @@ # ESPHome [![Discord Chat](https://img.shields.io/discord/429907082951524364.svg)](https://discord.gg/KhAMKrd) [![GitHub release](https://img.shields.io/github/release/esphome/esphome.svg)](https://GitHub.com/esphome/esphome/releases/) -[![ESPHome Logo](https://esphome.io/_images/logo-text.png)](https://esphome.io/) + + + + ESPHome Logo + + **Documentation:** https://esphome.io/ From 3fa67fad32c9515b1bf876bb7aef02b6d311c1b3 Mon Sep 17 00:00:00 2001 From: Ryan Henderson Date: Sun, 12 Jan 2025 20:17:28 -0800 Subject: [PATCH 35/86] Fix compile errors with pioarduino/platform-espressif32: wifi_component_esp32_arduino.cpp (#7998) --- .../components/wifi/wifi_component_esp32_arduino.cpp | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/esphome/components/wifi/wifi_component_esp32_arduino.cpp b/esphome/components/wifi/wifi_component_esp32_arduino.cpp index bc10bbd1e5..76d0b7d96c 100644 --- a/esphome/components/wifi/wifi_component_esp32_arduino.cpp +++ b/esphome/components/wifi/wifi_component_esp32_arduino.cpp @@ -11,6 +11,11 @@ #ifdef USE_WIFI_WPA2_EAP #include #endif + +#ifdef USE_WIFI_AP +#include "dhcpserver/dhcpserver.h" +#endif // USE_WIFI_AP + #include "lwip/apps/sntp.h" #include "lwip/dns.h" #include "lwip/err.h" @@ -638,7 +643,12 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ } WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() { - auto status = WiFiClass::status(); +#if USE_ARDUINO_VERSION_CODE < VERSION_CODE(3, 1, 0) + const auto status = WiFiClass::status(); +#else + const auto status = WiFi.status(); +#endif + if (status == WL_CONNECT_FAILED || status == WL_CONNECTION_LOST) { return WiFiSTAConnectStatus::ERROR_CONNECT_FAILED; } From df50e57409d80e8e43c9d0366ad4bbbdd42c07e1 Mon Sep 17 00:00:00 2001 From: Ryan Henderson Date: Sun, 12 Jan 2025 20:18:20 -0800 Subject: [PATCH 36/86] Include esp_mac.h and C++20 str_startswith/str_ends (#7999) --- esphome/core/helpers.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 2d2c88b844..439bb2ccb0 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -45,7 +45,9 @@ #endif #ifdef USE_ESP32 #include "esp32/rom/crc.h" - +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 2) +#include "esp_mac.h" +#endif #include "esp_efuse.h" #include "esp_efuse_table.h" #endif @@ -261,7 +263,7 @@ bool random_bytes(uint8_t *data, size_t len) { bool str_equals_case_insensitive(const std::string &a, const std::string &b) { return strcasecmp(a.c_str(), b.c_str()) == 0; } -#if ESP_IDF_VERSION_MAJOR >= 5 +#if __cplusplus >= 202002L bool str_startswith(const std::string &str, const std::string &start) { return str.starts_with(start); } bool str_endswith(const std::string &str, const std::string &end) { return str.ends_with(end); } #else From 13909b7994de2c25a62714bfc44c20b4fb845958 Mon Sep 17 00:00:00 2001 From: Ryan Henderson Date: Sun, 12 Jan 2025 20:26:23 -0800 Subject: [PATCH 37/86] [esp32_wifi] Enhance WiFi component with TCPIP core locking. (#7997) --- .../wifi/wifi_component_esp32_arduino.cpp | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/esphome/components/wifi/wifi_component_esp32_arduino.cpp b/esphome/components/wifi/wifi_component_esp32_arduino.cpp index 76d0b7d96c..b7a77fcdc9 100644 --- a/esphome/components/wifi/wifi_component_esp32_arduino.cpp +++ b/esphome/components/wifi/wifi_component_esp32_arduino.cpp @@ -20,6 +20,10 @@ #include "lwip/dns.h" #include "lwip/err.h" +#ifdef CONFIG_LWIP_TCPIP_CORE_LOCKING +#include "lwip/priv/tcpip_priv.h" +#endif + #include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" @@ -291,11 +295,26 @@ bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { } if (!manual_ip.has_value()) { +// sntp_servermode_dhcp lwip/sntp.c (Required to lock TCPIP core functionality!) +// https://github.com/esphome/issues/issues/6591 +// https://github.com/espressif/arduino-esp32/issues/10526 +#ifdef CONFIG_LWIP_TCPIP_CORE_LOCKING + if (!sys_thread_tcpip(LWIP_CORE_LOCK_QUERY_HOLDER)) { + LOCK_TCPIP_CORE(); + } +#endif + // lwIP starts the SNTP client if it gets an SNTP server from DHCP. We don't need the time, and more importantly, // the built-in SNTP client has a memory leak in certain situations. Disable this feature. // https://github.com/esphome/issues/issues/2299 sntp_servermode_dhcp(false); +#ifdef CONFIG_LWIP_TCPIP_CORE_LOCKING + if (sys_thread_tcpip(LWIP_CORE_LOCK_QUERY_HOLDER)) { + UNLOCK_TCPIP_CORE(); + } +#endif + // No manual IP is set; use DHCP client if (dhcp_status != ESP_NETIF_DHCP_STARTED) { err = esp_netif_dhcpc_start(s_sta_netif); From 9874d17613147fd617841d7eae66505a5c024b56 Mon Sep 17 00:00:00 2001 From: NP v/d Spek Date: Mon, 13 Jan 2025 05:29:38 +0100 Subject: [PATCH 38/86] add missing include in base_automation.h (#8001) --- esphome/core/base_automation.h | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index dcf7da2f21..13179b90bb 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -2,6 +2,7 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" +#include "esphome/core/hal.h" #include "esphome/core/defines.h" #include "esphome/core/preferences.h" From 30bb806f26853e00c053f1fc69751a61f105b8ee Mon Sep 17 00:00:00 2001 From: Piotr Szulc Date: Mon, 13 Jan 2025 05:31:01 +0100 Subject: [PATCH 39/86] Fixed libretiny preference wrongly detecting change in the data to store (#7990) --- esphome/components/libretiny/preferences.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/libretiny/preferences.cpp b/esphome/components/libretiny/preferences.cpp index ceeb30baf5..a090f42aa7 100644 --- a/esphome/components/libretiny/preferences.cpp +++ b/esphome/components/libretiny/preferences.cpp @@ -147,7 +147,7 @@ class LibreTinyPreferences : public ESPPreferences { ESP_LOGV(TAG, "fdb_kv_get_obj('%s'): nullptr - the key might not be set yet", to_save.key.c_str()); return true; } - stored_data.data.reserve(kv.value_len); + stored_data.data.resize(kv.value_len); fdb_blob_make(&blob, stored_data.data.data(), kv.value_len); size_t actual_len = fdb_kv_get_blob(db, to_save.key.c_str(), &blob); if (actual_len != kv.value_len) { From b4a2b50ee092abd0eabbbce6db0c8da1d825464e Mon Sep 17 00:00:00 2001 From: Dusan Cervenka Date: Mon, 13 Jan 2025 05:34:07 +0100 Subject: [PATCH 40/86] Fixed topic when mac is used (#7988) --- esphome/components/mqtt/__init__.py | 2 +- esphome/components/mqtt/mqtt_client.cpp | 8 +++++++- esphome/components/mqtt/mqtt_client.h | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index 2b0d941220..e1002478a1 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -373,7 +373,7 @@ async def to_code(config): ) ) - cg.add(var.set_topic_prefix(config[CONF_TOPIC_PREFIX])) + cg.add(var.set_topic_prefix(config[CONF_TOPIC_PREFIX], CORE.name)) if config[CONF_USE_ABBREVIATIONS]: cg.add_define("USE_MQTT_ABBREVIATIONS") diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index c7ace505a8..9afa3a588d 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -606,7 +606,13 @@ void MQTTClientComponent::set_log_level(int level) { this->log_level_ = level; } void MQTTClientComponent::set_keep_alive(uint16_t keep_alive_s) { this->mqtt_backend_.set_keep_alive(keep_alive_s); } void MQTTClientComponent::set_log_message_template(MQTTMessage &&message) { this->log_message_ = std::move(message); } const MQTTDiscoveryInfo &MQTTClientComponent::get_discovery_info() const { return this->discovery_info_; } -void MQTTClientComponent::set_topic_prefix(const std::string &topic_prefix) { this->topic_prefix_ = topic_prefix; } +void MQTTClientComponent::set_topic_prefix(const std::string &topic_prefix, const std::string &check_topic_prefix) { + if (App.is_name_add_mac_suffix_enabled() && (topic_prefix == check_topic_prefix)) { + this->topic_prefix_ = str_sanitize(App.get_name()); + } else { + this->topic_prefix_ = topic_prefix; + } +} const std::string &MQTTClientComponent::get_topic_prefix() const { return this->topic_prefix_; } void MQTTClientComponent::set_publish_nan_as_none(bool publish_nan_as_none) { this->publish_nan_as_none_ = publish_nan_as_none; diff --git a/esphome/components/mqtt/mqtt_client.h b/esphome/components/mqtt/mqtt_client.h index 34eac29464..c68b3c62eb 100644 --- a/esphome/components/mqtt/mqtt_client.h +++ b/esphome/components/mqtt/mqtt_client.h @@ -165,7 +165,7 @@ class MQTTClientComponent : public Component { * * @param topic_prefix The topic prefix. The last "/" is appended automatically. */ - void set_topic_prefix(const std::string &topic_prefix); + void set_topic_prefix(const std::string &topic_prefix, const std::string &check_topic_prefix); /// Get the topic prefix of this device, using default if necessary const std::string &get_topic_prefix() const; From f319472066a45c11f4d71e96b39a201f07e7f0dd Mon Sep 17 00:00:00 2001 From: Nate Clark Date: Sun, 12 Jan 2025 23:35:29 -0500 Subject: [PATCH 41/86] web_server: Adds REST API POST endpoints to arm and disarm (#7985) --- esphome/components/web_server/web_server.cpp | 26 +++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 0467023039..ed0cb3db2c 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -1415,6 +1415,30 @@ void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *reques request->send(200, "application/json", data.c_str()); return; } + + auto call = obj->make_call(); + if (request->hasParam("code")) { + call.set_code(request->getParam("code")->value().c_str()); + } + + if (match.method == "disarm") { + call.disarm(); + } else if (match.method == "arm_away") { + call.arm_away(); + } else if (match.method == "arm_home") { + call.arm_home(); + } else if (match.method == "arm_night") { + call.arm_night(); + } else if (match.method == "arm_vacation") { + call.arm_vacation(); + } else { + request->send(404); + return; + } + + this->schedule_([call]() mutable { call.perform(); }); + request->send(200); + return; } request->send(404); } @@ -1664,7 +1688,7 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) { #endif #ifdef USE_ALARM_CONTROL_PANEL - if (request->method() == HTTP_GET && match.domain == "alarm_control_panel") + if ((request->method() == HTTP_GET || request->method() == HTTP_POST) && match.domain == "alarm_control_panel") return true; #endif From 6262fb8fcf9538d161d94512a857755c839eb48d Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 14 Jan 2025 08:32:54 +1100 Subject: [PATCH 42/86] [lvgl] fix tests (#8075) --- tests/components/lvgl/lvgl-package.yaml | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index 234fd78678..7c59cfa171 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -795,6 +795,19 @@ lvgl: color: 0xA0A0A0 r_mod: -20 opa: 0% + - id: page3 + widgets: + - keyboard: + id: lv_keyboard + align: bottom_mid + on_value: + then: + - logger.log: + format: "keyboard value %s" + args: [text.c_str()] + - keyboard: + id: lv_keyboard1 + mode: special font: - file: "gfonts://Roboto" @@ -805,10 +818,13 @@ image: - id: cat_image resize: 256x48 file: $component_dir/logo-text.svg + type: RGB565 + use_transparency: alpha_channel - id: dog_image file: $component_dir/logo-text.svg resize: 256x48 - type: TRANSPARENT_BINARY + type: BINARY + use_transparency: chroma_key color: - id: light_blue From bdb1094b477baad40cfcf9d9bc86665c14069a55 Mon Sep 17 00:00:00 2001 From: Stefan Rado <628587+kroimon@users.noreply.github.com> Date: Tue, 14 Jan 2025 04:20:52 +0100 Subject: [PATCH 43/86] Allow external libraries to use ESP_LOGx macros (#8078) --- esphome/components/lvgl/lvgl_esphome.cpp | 20 +++++--------------- esphome/core/log.h | 14 +++++++------- 2 files changed, 12 insertions(+), 22 deletions(-) diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index 5abeead9d8..a9fe56fb32 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -501,9 +501,7 @@ size_t lv_millis(void) { return esphome::millis(); } void *lv_custom_mem_alloc(size_t size) { auto *ptr = malloc(size); // NOLINT if (ptr == nullptr) { -#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_ERROR - esphome::ESP_LOGE(esphome::lvgl::TAG, "Failed to allocate %zu bytes", size); -#endif + ESP_LOGE(esphome::lvgl::TAG, "Failed to allocate %zu bytes", size); } return ptr; } @@ -520,30 +518,22 @@ void *lv_custom_mem_alloc(size_t size) { ptr = heap_caps_malloc(size, cap_bits); } if (ptr == nullptr) { -#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_ERROR - esphome::ESP_LOGE(esphome::lvgl::TAG, "Failed to allocate %zu bytes", size); -#endif + ESP_LOGE(esphome::lvgl::TAG, "Failed to allocate %zu bytes", size); return nullptr; } -#ifdef ESPHOME_LOG_HAS_VERBOSE - esphome::ESP_LOGV(esphome::lvgl::TAG, "allocate %zu - > %p", size, ptr); -#endif + ESP_LOGV(esphome::lvgl::TAG, "allocate %zu - > %p", size, ptr); return ptr; } void lv_custom_mem_free(void *ptr) { -#ifdef ESPHOME_LOG_HAS_VERBOSE - esphome::ESP_LOGV(esphome::lvgl::TAG, "free %p", ptr); -#endif + ESP_LOGV(esphome::lvgl::TAG, "free %p", ptr); if (ptr == nullptr) return; heap_caps_free(ptr); } void *lv_custom_mem_realloc(void *ptr, size_t size) { -#ifdef ESPHOME_LOG_HAS_VERBOSE - esphome::ESP_LOGV(esphome::lvgl::TAG, "realloc %p: %zu", ptr, size); -#endif + ESP_LOGV(esphome::lvgl::TAG, "realloc %p: %zu", ptr, size); return heap_caps_realloc(ptr, size, cap_bits); } #endif diff --git a/esphome/core/log.h b/esphome/core/log.h index 86af534f98..99a68024c5 100644 --- a/esphome/core/log.h +++ b/esphome/core/log.h @@ -74,7 +74,7 @@ int esp_idf_log_vprintf_(const char *format, va_list args); // NOLINT #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE #define esph_log_vv(tag, format, ...) \ - esp_log_printf_(ESPHOME_LOG_LEVEL_VERY_VERBOSE, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__) + ::esphome::esp_log_printf_(ESPHOME_LOG_LEVEL_VERY_VERBOSE, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__) #define ESPHOME_LOG_HAS_VERY_VERBOSE #else @@ -83,7 +83,7 @@ int esp_idf_log_vprintf_(const char *format, va_list args); // NOLINT #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE #define esph_log_v(tag, format, ...) \ - esp_log_printf_(ESPHOME_LOG_LEVEL_VERBOSE, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__) + ::esphome::esp_log_printf_(ESPHOME_LOG_LEVEL_VERBOSE, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__) #define ESPHOME_LOG_HAS_VERBOSE #else @@ -92,9 +92,9 @@ int esp_idf_log_vprintf_(const char *format, va_list args); // NOLINT #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG #define esph_log_d(tag, format, ...) \ - esp_log_printf_(ESPHOME_LOG_LEVEL_DEBUG, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__) + ::esphome::esp_log_printf_(ESPHOME_LOG_LEVEL_DEBUG, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__) #define esph_log_config(tag, format, ...) \ - esp_log_printf_(ESPHOME_LOG_LEVEL_CONFIG, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__) + ::esphome::esp_log_printf_(ESPHOME_LOG_LEVEL_CONFIG, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__) #define ESPHOME_LOG_HAS_DEBUG #define ESPHOME_LOG_HAS_CONFIG @@ -105,7 +105,7 @@ int esp_idf_log_vprintf_(const char *format, va_list args); // NOLINT #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_INFO #define esph_log_i(tag, format, ...) \ - esp_log_printf_(ESPHOME_LOG_LEVEL_INFO, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__) + ::esphome::esp_log_printf_(ESPHOME_LOG_LEVEL_INFO, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__) #define ESPHOME_LOG_HAS_INFO #else @@ -114,7 +114,7 @@ int esp_idf_log_vprintf_(const char *format, va_list args); // NOLINT #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_WARN #define esph_log_w(tag, format, ...) \ - esp_log_printf_(ESPHOME_LOG_LEVEL_WARN, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__) + ::esphome::esp_log_printf_(ESPHOME_LOG_LEVEL_WARN, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__) #define ESPHOME_LOG_HAS_WARN #else @@ -123,7 +123,7 @@ int esp_idf_log_vprintf_(const char *format, va_list args); // NOLINT #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_ERROR #define esph_log_e(tag, format, ...) \ - esp_log_printf_(ESPHOME_LOG_LEVEL_ERROR, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__) + ::esphome::esp_log_printf_(ESPHOME_LOG_LEVEL_ERROR, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__) #define ESPHOME_LOG_HAS_ERROR #else From fc2b15e307833359f079aec46ceffd30b9398abe Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 15 Jan 2025 09:27:47 +1100 Subject: [PATCH 44/86] [uptime] Add text_sensor (#8028) --- .../uptime/{sensor.py => sensor/__init__.py} | 6 +-- .../{ => sensor}/uptime_seconds_sensor.cpp | 0 .../{ => sensor}/uptime_seconds_sensor.h | 0 .../{ => sensor}/uptime_timestamp_sensor.cpp | 0 .../{ => sensor}/uptime_timestamp_sensor.h | 0 .../components/uptime/text_sensor/__init__.py | 19 ++++++++ .../uptime/text_sensor/uptime_text_sensor.cpp | 46 +++++++++++++++++++ .../uptime/text_sensor/uptime_text_sensor.h | 25 ++++++++++ tests/components/uptime/common.yaml | 4 ++ 9 files changed, 97 insertions(+), 3 deletions(-) rename esphome/components/uptime/{sensor.py => sensor/__init__.py} (100%) rename esphome/components/uptime/{ => sensor}/uptime_seconds_sensor.cpp (100%) rename esphome/components/uptime/{ => sensor}/uptime_seconds_sensor.h (100%) rename esphome/components/uptime/{ => sensor}/uptime_timestamp_sensor.cpp (100%) rename esphome/components/uptime/{ => sensor}/uptime_timestamp_sensor.h (100%) create mode 100644 esphome/components/uptime/text_sensor/__init__.py create mode 100644 esphome/components/uptime/text_sensor/uptime_text_sensor.cpp create mode 100644 esphome/components/uptime/text_sensor/uptime_text_sensor.h diff --git a/esphome/components/uptime/sensor.py b/esphome/components/uptime/sensor/__init__.py similarity index 100% rename from esphome/components/uptime/sensor.py rename to esphome/components/uptime/sensor/__init__.py index 30220751b6..e2a7aee1a2 100644 --- a/esphome/components/uptime/sensor.py +++ b/esphome/components/uptime/sensor/__init__.py @@ -1,14 +1,14 @@ import esphome.codegen as cg -import esphome.config_validation as cv from esphome.components import sensor, time +import esphome.config_validation as cv from esphome.const import ( CONF_TIME_ID, + DEVICE_CLASS_DURATION, DEVICE_CLASS_TIMESTAMP, ENTITY_CATEGORY_DIAGNOSTIC, + ICON_TIMER, STATE_CLASS_TOTAL_INCREASING, UNIT_SECOND, - ICON_TIMER, - DEVICE_CLASS_DURATION, ) uptime_ns = cg.esphome_ns.namespace("uptime") diff --git a/esphome/components/uptime/uptime_seconds_sensor.cpp b/esphome/components/uptime/sensor/uptime_seconds_sensor.cpp similarity index 100% rename from esphome/components/uptime/uptime_seconds_sensor.cpp rename to esphome/components/uptime/sensor/uptime_seconds_sensor.cpp diff --git a/esphome/components/uptime/uptime_seconds_sensor.h b/esphome/components/uptime/sensor/uptime_seconds_sensor.h similarity index 100% rename from esphome/components/uptime/uptime_seconds_sensor.h rename to esphome/components/uptime/sensor/uptime_seconds_sensor.h diff --git a/esphome/components/uptime/uptime_timestamp_sensor.cpp b/esphome/components/uptime/sensor/uptime_timestamp_sensor.cpp similarity index 100% rename from esphome/components/uptime/uptime_timestamp_sensor.cpp rename to esphome/components/uptime/sensor/uptime_timestamp_sensor.cpp diff --git a/esphome/components/uptime/uptime_timestamp_sensor.h b/esphome/components/uptime/sensor/uptime_timestamp_sensor.h similarity index 100% rename from esphome/components/uptime/uptime_timestamp_sensor.h rename to esphome/components/uptime/sensor/uptime_timestamp_sensor.h diff --git a/esphome/components/uptime/text_sensor/__init__.py b/esphome/components/uptime/text_sensor/__init__.py new file mode 100644 index 0000000000..996d983e71 --- /dev/null +++ b/esphome/components/uptime/text_sensor/__init__.py @@ -0,0 +1,19 @@ +import esphome.codegen as cg +from esphome.components import text_sensor +import esphome.config_validation as cv +from esphome.const import ENTITY_CATEGORY_DIAGNOSTIC, ICON_TIMER + +uptime_ns = cg.esphome_ns.namespace("uptime") +UptimeTextSensor = uptime_ns.class_( + "UptimeTextSensor", text_sensor.TextSensor, cg.PollingComponent +) +CONFIG_SCHEMA = text_sensor.text_sensor_schema( + UptimeTextSensor, + icon=ICON_TIMER, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, +).extend(cv.polling_component_schema("60s")) + + +async def to_code(config): + var = await text_sensor.new_text_sensor(config) + await cg.register_component(var, config) diff --git a/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp b/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp new file mode 100644 index 0000000000..0fa5e199f3 --- /dev/null +++ b/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp @@ -0,0 +1,46 @@ +#include "uptime_text_sensor.h" + +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace uptime { + +static const char *const TAG = "uptime.sensor"; + +void UptimeTextSensor::setup() { this->last_ms_ = millis(); } + +void UptimeTextSensor::update() { + const auto now = millis(); + // get whole seconds since last update. Note that even if the millis count has overflowed between updates, + // the difference will still be correct due to the way twos-complement arithmetic works. + const uint32_t delta = (now - this->last_ms_) / 1000; + if (delta == 0) + return; + // set last_ms_ to the last second boundary + this->last_ms_ = now - (now % 1000); + this->uptime_ += delta; + auto uptime = this->uptime_; + unsigned days = uptime / (24 * 3600); + unsigned seconds = uptime % (24 * 3600); + unsigned hours = seconds / 3600; + seconds %= 3600; + unsigned minutes = seconds / 60; + seconds %= 60; + if (days != 0) { + this->publish_state(str_sprintf("%dd%dh%dm%ds", days, hours, minutes, seconds)); + } else if (hours != 0) { + this->publish_state(str_sprintf("%dh%dm%ds", hours, minutes, seconds)); + } else if (minutes != 0) { + this->publish_state(str_sprintf("%dm%ds", minutes, seconds)); + } else { + this->publish_state(str_sprintf("%ds", seconds)); + } +} + +float UptimeTextSensor::get_setup_priority() const { return setup_priority::HARDWARE; } +void UptimeTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Uptime Text Sensor", this); } + +} // namespace uptime +} // namespace esphome diff --git a/esphome/components/uptime/text_sensor/uptime_text_sensor.h b/esphome/components/uptime/text_sensor/uptime_text_sensor.h new file mode 100644 index 0000000000..4baf1039b6 --- /dev/null +++ b/esphome/components/uptime/text_sensor/uptime_text_sensor.h @@ -0,0 +1,25 @@ +#pragma once + +#include "esphome/core/defines.h" + +#include "esphome/components/text_sensor/text_sensor.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace uptime { + +class UptimeTextSensor : public text_sensor::TextSensor, public PollingComponent { + public: + void update() override; + void dump_config() override; + void setup() override; + + float get_setup_priority() const override; + + protected: + uint64_t uptime_{0}; + uint64_t last_ms_{0}; +}; + +} // namespace uptime +} // namespace esphome diff --git a/tests/components/uptime/common.yaml b/tests/components/uptime/common.yaml index f63f80b050..d78ef8eca9 100644 --- a/tests/components/uptime/common.yaml +++ b/tests/components/uptime/common.yaml @@ -13,3 +13,7 @@ sensor: - platform: uptime name: Uptime Sensor Timestamp type: timestamp + +text_sensor: + - platform: uptime + name: Uptime Text From c3412df169a0fc742b5bf854245d6a07adef836d Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 15 Jan 2025 09:29:27 +1100 Subject: [PATCH 45/86] [image] Fix mdi images (#8082) --- esphome/components/image/__init__.py | 39 ++++++++++++++++++++++------ esphome/components/image/image.cpp | 35 +++++++++++++++++++++---- 2 files changed, 61 insertions(+), 13 deletions(-) diff --git a/esphome/components/image/__init__.py b/esphome/components/image/__init__.py index 801b05e160..a503e8f471 100644 --- a/esphome/components/image/__init__.py +++ b/esphome/components/image/__init__.py @@ -82,11 +82,13 @@ class ImageEncoder: self.dither = dither self.index = 0 self.invert_alpha = invert_alpha + self.path = "" - def convert(self, image): + def convert(self, image, path): """ Convert the image format :param image: Input image + :param path: Path to the image file :return: converted image """ return image @@ -103,6 +105,16 @@ class ImageEncoder: """ +def is_alpha_only(image: Image): + """ + Check if an image (assumed to be RGBA) is only alpha + """ + # Any alpha data? + if image.split()[-1].getextrema()[0] == 0xFF: + return False + return all(b.getextrema()[1] == 0 for b in image.split()[:-1]) + + class ImageBinary(ImageEncoder): allow_config = {CONF_OPAQUE, CONF_INVERT_ALPHA, CONF_CHROMA_KEY} @@ -111,7 +123,9 @@ class ImageBinary(ImageEncoder): super().__init__(self.width8, height, transparency, dither, invert_alpha) self.bitno = 0 - def convert(self, image): + def convert(self, image, path): + if is_alpha_only(image): + image = image.split()[-1] return image.convert("1", dither=self.dither) def encode(self, pixel): @@ -136,7 +150,16 @@ class ImageBinary(ImageEncoder): class ImageGrayscale(ImageEncoder): allow_config = {CONF_ALPHA_CHANNEL, CONF_CHROMA_KEY, CONF_INVERT_ALPHA, CONF_OPAQUE} - def convert(self, image): + def convert(self, image, path): + if is_alpha_only(image): + if self.transparency != CONF_ALPHA_CHANNEL: + _LOGGER.warning( + "Grayscale image %s is alpha only, but transparency is set to %s", + path, + self.transparency, + ) + self.transparency = CONF_ALPHA_CHANNEL + image = image.split()[-1] return image.convert("LA") def encode(self, pixel): @@ -166,7 +189,7 @@ class ImageRGB565(ImageEncoder): invert_alpha, ) - def convert(self, image): + def convert(self, image, path): return image.convert("RGBA") def encode(self, pixel): @@ -204,7 +227,7 @@ class ImageRGB(ImageEncoder): invert_alpha, ) - def convert(self, image): + def convert(self, image, path): return image.convert("RGBA") def encode(self, pixel): @@ -308,7 +331,7 @@ def is_svg_file(file): if not file: return False with open(file, "rb") as f: - return "get_grayscale_pixel_(img_x, img_y); - if (color.w >= 0x80) { - display->draw_pixel_at(x + img_x, y + img_y, color); + const uint32_t pos = (img_x + img_y * this->width_); + const uint8_t gray = progmem_read_byte(this->data_start_ + pos); + Color color = Color(gray, gray, gray, 0xFF); + switch (this->transparency_) { + case TRANSPARENCY_CHROMA_KEY: + if (gray == 1) { + continue; // skip drawing + } + break; + case TRANSPARENCY_ALPHA_CHANNEL: { + auto on = (float) gray / 255.0f; + auto off = 1.0f - on; + // blend color_on and color_off + color = Color(color_on.r * on + color_off.r * off, color_on.g * on + color_off.g * off, + color_on.b * on + color_off.b * off, 0xFF); + break; + } + default: + break; } + display->draw_pixel_at(x + img_x, y + img_y, color); } } break; @@ -179,8 +196,16 @@ Color Image::get_rgb565_pixel_(int x, int y) const { Color Image::get_grayscale_pixel_(int x, int y) const { const uint32_t pos = (x + y * this->width_); const uint8_t gray = progmem_read_byte(this->data_start_ + pos); - uint8_t alpha = (gray == 1 && this->transparency_ == TRANSPARENCY_CHROMA_KEY) ? 0 : 0xFF; - return Color(gray, gray, gray, alpha); + switch (this->transparency_) { + case TRANSPARENCY_CHROMA_KEY: + if (gray == 1) + return Color(0, 0, 0, 0); + return Color(gray, gray, gray, 0xFF); + case TRANSPARENCY_ALPHA_CHANNEL: + return Color(0, 0, 0, gray); + default: + return Color(gray, gray, gray, 0xFF); + } } int Image::get_width() const { return this->width_; } int Image::get_height() const { return this->height_; } From e8d2ad4ce856ab2e2ae9b156c6f0cbe10f069ddf Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 15 Jan 2025 09:53:44 +1100 Subject: [PATCH 46/86] [ili9xxx] psram and 8 bit changes (#8084) --- esphome/components/ili9xxx/display.py | 40 ++++++++++++++----- .../components/ili9xxx/ili9xxx_display.cpp | 7 +--- esphome/components/ili9xxx/ili9xxx_display.h | 3 +- tests/components/ili9xxx/test.esp32-ard.yaml | 1 + 4 files changed, 34 insertions(+), 17 deletions(-) diff --git a/esphome/components/ili9xxx/display.py b/esphome/components/ili9xxx/display.py index 3c9dd2dab9..e3abb7e98c 100644 --- a/esphome/components/ili9xxx/display.py +++ b/esphome/components/ili9xxx/display.py @@ -1,9 +1,12 @@ +import logging + from esphome import core, pins import esphome.codegen as cg from esphome.components import display, spi from esphome.components.display import validate_rotation import esphome.config_validation as cv from esphome.const import ( + CONF_AUTO_CLEAR_ENABLED, CONF_COLOR_ORDER, CONF_COLOR_PALETTE, CONF_DC_PIN, @@ -27,17 +30,12 @@ from esphome.const import ( CONF_WIDTH, ) from esphome.core import CORE, HexInt +from esphome.final_validate import full_config DEPENDENCIES = ["spi"] - -def AUTO_LOAD(): - if CORE.is_esp32: - return ["psram"] - return [] - - CODEOWNERS = ["@nielsnl68", "@clydebarrow"] +LOGGER = logging.getLogger(__name__) ili9xxx_ns = cg.esphome_ns.namespace("ili9xxx") ILI9XXXDisplay = ili9xxx_ns.class_( @@ -84,7 +82,7 @@ COLOR_ORDERS = { "BGR": ColorOrder.COLOR_ORDER_BGR, } -COLOR_PALETTE = cv.one_of("NONE", "GRAYSCALE", "IMAGE_ADAPTIVE") +COLOR_PALETTE = cv.one_of("NONE", "GRAYSCALE", "IMAGE_ADAPTIVE", "8BIT", upper=True) CONF_LED_PIN = "led_pin" CONF_COLOR_PALETTE_IMAGES = "color_palette_images" @@ -195,9 +193,27 @@ CONFIG_SCHEMA = cv.All( _validate, ) -FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema( - "ili9xxx", require_miso=False, require_mosi=True -) + +def final_validate(config): + global_config = full_config.get() + # Ideally would calculate buffer size here, but that info is not available on the Python side + needs_buffer = ( + CONF_LAMBDA in config or CONF_PAGES in config or config[CONF_AUTO_CLEAR_ENABLED] + ) + if ( + CORE.is_esp32 + and config[CONF_COLOR_PALETTE] == "NONE" + and "psram" not in global_config + and needs_buffer + ): + LOGGER.info("Consider enabling PSRAM if available for the display buffer") + + return spi.final_validate_device_schema( + "ili9xxx", require_miso=False, require_mosi=True + ) + + +FINAL_VALIDATE_SCHEMA = final_validate async def to_code(config): @@ -283,6 +299,8 @@ async def to_code(config): palette = converted.getpalette() assert len(palette) == 256 * 3 rhs = palette + elif config[CONF_COLOR_PALETTE] == "8BIT": + cg.add(var.set_buffer_color_mode(ILI9XXXColorMode.BITS_8)) else: cg.add(var.set_buffer_color_mode(ILI9XXXColorMode.BITS_16)) diff --git a/esphome/components/ili9xxx/ili9xxx_display.cpp b/esphome/components/ili9xxx/ili9xxx_display.cpp index b9664067a9..f056f0a128 100644 --- a/esphome/components/ili9xxx/ili9xxx_display.cpp +++ b/esphome/components/ili9xxx/ili9xxx_display.cpp @@ -66,12 +66,9 @@ void ILI9XXXDisplay::setup() { void ILI9XXXDisplay::alloc_buffer_() { if (this->buffer_color_mode_ == BITS_16) { this->init_internal_(this->get_buffer_length_() * 2); - if (this->buffer_ != nullptr) { - return; - } - this->buffer_color_mode_ = BITS_8; + } else { + this->init_internal_(this->get_buffer_length_()); } - this->init_internal_(this->get_buffer_length_()); if (this->buffer_ == nullptr) { this->mark_failed(); } diff --git a/esphome/components/ili9xxx/ili9xxx_display.h b/esphome/components/ili9xxx/ili9xxx_display.h index c141739d2a..87d7c86e5c 100644 --- a/esphome/components/ili9xxx/ili9xxx_display.h +++ b/esphome/components/ili9xxx/ili9xxx_display.h @@ -98,7 +98,8 @@ class ILI9XXXDisplay : public display::DisplayBuffer, protected: inline bool check_buffer_() { if (this->buffer_ == nullptr) { - this->alloc_buffer_(); + if (!this->is_failed()) + this->alloc_buffer_(); return !this->is_failed(); } return true; diff --git a/tests/components/ili9xxx/test.esp32-ard.yaml b/tests/components/ili9xxx/test.esp32-ard.yaml index 850273230a..c00c38ce3e 100644 --- a/tests/components/ili9xxx/test.esp32-ard.yaml +++ b/tests/components/ili9xxx/test.esp32-ard.yaml @@ -20,6 +20,7 @@ display: it.rectangle(0, 0, it.get_width(), it.get_height()); - platform: ili9xxx invert_colors: false + color_palette: 8bit dimensions: width: 320 height: 240 From dac9768f6ac897d46b093ecee81ee64101b29b59 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 15 Jan 2025 11:56:52 +1100 Subject: [PATCH 47/86] [spi] Restore ``SPIDelegateDummy`` (#8019) --- esphome/components/spi/spi.cpp | 6 ++++++ esphome/components/spi/spi.h | 21 +++++++++++++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/esphome/components/spi/spi.cpp b/esphome/components/spi/spi.cpp index f9435b0424..b13826c443 100644 --- a/esphome/components/spi/spi.cpp +++ b/esphome/components/spi/spi.cpp @@ -7,6 +7,10 @@ namespace spi { const char *const TAG = "spi"; +SPIDelegate *const SPIDelegate::NULL_DELEGATE = // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + new SPIDelegateDummy(); +// https://bugs.llvm.org/show_bug.cgi?id=48040 + bool SPIDelegate::is_ready() { return true; } GPIOPin *const NullPin::NULL_PIN = new NullPin(); // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) @@ -75,6 +79,8 @@ void SPIComponent::dump_config() { } } +void SPIDelegateDummy::begin_transaction() { ESP_LOGE(TAG, "SPIDevice not initialised - did you call spi_setup()?"); } + uint8_t SPIDelegateBitBash::transfer(uint8_t data) { return this->transfer_(data, 8); } void SPIDelegateBitBash::write(uint16_t data, size_t num_bits) { this->transfer_(data, num_bits); } diff --git a/esphome/components/spi/spi.h b/esphome/components/spi/spi.h index 4cd8d3383c..f581dc3f56 100644 --- a/esphome/components/spi/spi.h +++ b/esphome/components/spi/spi.h @@ -163,6 +163,8 @@ class Utility { } }; +class SPIDelegateDummy; + // represents a device attached to an SPI bus, with a defined clock rate, mode and bit order. On Arduino this is // a thin wrapper over SPIClass. class SPIDelegate { @@ -248,6 +250,21 @@ class SPIDelegate { uint32_t data_rate_{1000000}; SPIMode mode_{MODE0}; GPIOPin *cs_pin_{NullPin::NULL_PIN}; + static SPIDelegate *const NULL_DELEGATE; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +}; + +/** + * A dummy SPIDelegate that complains if it's used. + */ + +class SPIDelegateDummy : public SPIDelegate { + public: + SPIDelegateDummy() = default; + + uint8_t transfer(uint8_t data) override { return 0; } + void end_transaction() override{}; + + void begin_transaction() override; }; /** @@ -365,7 +382,7 @@ class SPIClient { virtual void spi_teardown() { this->parent_->unregister_device(this); - this->delegate_ = nullptr; + this->delegate_ = SPIDelegate::NULL_DELEGATE; } bool spi_is_ready() { return this->delegate_->is_ready(); } @@ -376,7 +393,7 @@ class SPIClient { uint32_t data_rate_{1000000}; SPIComponent *parent_{nullptr}; GPIOPin *cs_{nullptr}; - SPIDelegate *delegate_{nullptr}; + SPIDelegate *delegate_{SPIDelegate::NULL_DELEGATE}; }; /** From 17b88f2e3e5d873b1961b40e4c1fb6acccfc1305 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 15 Jan 2025 12:29:51 +1100 Subject: [PATCH 48/86] [lvgl] fix lvgl.widget.update and friends (#8087) --- esphome/components/lvgl/automation.py | 15 +++++++++++- esphome/components/lvgl/schemas.py | 27 ++++++++++++++------- esphome/components/lvgl/widgets/dropdown.py | 2 +- esphome/components/lvgl/widgets/keyboard.py | 9 ++++++- esphome/components/lvgl/widgets/msgbox.py | 2 +- esphome/components/lvgl/widgets/obj.py | 13 +--------- esphome/components/lvgl/widgets/tabview.py | 2 +- tests/components/lvgl/lvgl-package.yaml | 15 ++++++++++++ 8 files changed, 59 insertions(+), 26 deletions(-) diff --git a/esphome/components/lvgl/automation.py b/esphome/components/lvgl/automation.py index 7db6e1f045..168fc03cb7 100644 --- a/esphome/components/lvgl/automation.py +++ b/esphome/components/lvgl/automation.py @@ -15,6 +15,7 @@ from .defines import ( CONF_FREEZE, CONF_LVGL_ID, CONF_SHOW_SNOW, + PARTS, literal, ) from .lv_validation import lv_bool, lv_color, lv_image, opacity @@ -33,7 +34,7 @@ from .lvcode import ( lvgl_comp, static_cast, ) -from .schemas import DISP_BG_SCHEMA, LIST_ACTION_SCHEMA, LVGL_SCHEMA +from .schemas import DISP_BG_SCHEMA, LIST_ACTION_SCHEMA, LVGL_SCHEMA, base_update_schema from .types import ( LV_STATE, LvglAction, @@ -41,6 +42,7 @@ from .types import ( ObjUpdateAction, lv_disp_t, lv_group_t, + lv_obj_base_t, lv_obj_t, lv_pseudo_button_t, ) @@ -336,3 +338,14 @@ async def widget_focus(config, action_id, template_arg, args): lv.group_focus_freeze(group, True) var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda()) return var + + +@automation.register_action( + "lvgl.widget.update", ObjUpdateAction, base_update_schema(lv_obj_base_t, PARTS) +) +async def obj_update_to_code(config, action_id, template_arg, args): + async def do_update(widget: Widget): + await set_obj_properties(widget, config) + + widgets = await get_widgets(config[CONF_ID]) + return await action_to_code(widgets, do_update, action_id, template_arg, args) diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py index 271dbea19f..f0318dd17a 100644 --- a/esphome/components/lvgl/schemas.py +++ b/esphome/components/lvgl/schemas.py @@ -199,13 +199,12 @@ FLAG_SCHEMA = cv.Schema({cv.Optional(flag): lvalid.lv_bool for flag in df.OBJ_FL FLAG_LIST = cv.ensure_list(df.LvConstant("LV_OBJ_FLAG_", *df.OBJ_FLAGS).one_of) -def part_schema(widget_type: WidgetType): +def part_schema(parts): """ Generate a schema for the various parts (e.g. main:, indicator:) of a widget type - :param widget_type: The type of widget to generate for - :return: + :param parts: The parts to include in the schema + :return: The schema """ - parts = widget_type.parts return cv.Schema({cv.Optional(part): STATE_SCHEMA for part in parts}).extend( STATE_SCHEMA ) @@ -228,9 +227,15 @@ def automation_schema(typ: LvType): } -def create_modify_schema(widget_type): +def base_update_schema(widget_type, parts): + """ + Create a schema for updating a widgets style properties, states and flags + :param widget_type: The type of the ID + :param parts: The allowable parts to specify + :return: + """ return ( - part_schema(widget_type) + part_schema(parts) .extend( { cv.Required(CONF_ID): cv.ensure_list( @@ -245,7 +250,12 @@ def create_modify_schema(widget_type): } ) .extend(FLAG_SCHEMA) - .extend(widget_type.modify_schema) + ) + + +def create_modify_schema(widget_type): + return base_update_schema(widget_type.w_type, widget_type.parts).extend( + widget_type.modify_schema ) @@ -256,7 +266,7 @@ def obj_schema(widget_type: WidgetType): :return: """ return ( - part_schema(widget_type) + part_schema(widget_type.parts) .extend(FLAG_SCHEMA) .extend(LAYOUT_SCHEMA) .extend(ALIGN_TO_SCHEMA) @@ -341,7 +351,6 @@ FLEX_OBJ_SCHEMA = { cv.Optional(df.CONF_FLEX_GROW): cv.int_, } - DISP_BG_SCHEMA = cv.Schema( { cv.Optional(df.CONF_DISP_BG_IMAGE): cv.Any( diff --git a/esphome/components/lvgl/widgets/dropdown.py b/esphome/components/lvgl/widgets/dropdown.py index a6bfc6bb88..b32b5a2b2e 100644 --- a/esphome/components/lvgl/widgets/dropdown.py +++ b/esphome/components/lvgl/widgets/dropdown.py @@ -37,7 +37,7 @@ DROPDOWN_BASE_SCHEMA = cv.Schema( cv.Exclusive(CONF_SELECTED_INDEX, CONF_SELECTED_TEXT): lv_int, cv.Exclusive(CONF_SELECTED_TEXT, CONF_SELECTED_TEXT): lv_text, cv.Optional(CONF_DIR, default="BOTTOM"): DIRECTIONS.one_of, - cv.Optional(CONF_DROPDOWN_LIST): part_schema(dropdown_list_spec), + cv.Optional(CONF_DROPDOWN_LIST): part_schema(dropdown_list_spec.parts), } ) diff --git a/esphome/components/lvgl/widgets/keyboard.py b/esphome/components/lvgl/widgets/keyboard.py index ba7edb302e..d4a71078d0 100644 --- a/esphome/components/lvgl/widgets/keyboard.py +++ b/esphome/components/lvgl/widgets/keyboard.py @@ -16,6 +16,11 @@ KEYBOARD_SCHEMA = { cv.Optional(CONF_TEXTAREA): cv.use_id(lv_textarea_t), } +KEYBOARD_MODIFY_SCHEMA = { + cv.Optional(CONF_MODE): KEYBOARD_MODES.one_of, + cv.Optional(CONF_TEXTAREA): cv.use_id(lv_textarea_t), +} + lv_keyboard_t = LvType( "LvKeyboardType", parents=(KeyProvider, LvCompound), @@ -32,6 +37,7 @@ class KeyboardType(WidgetType): lv_keyboard_t, (CONF_MAIN, CONF_ITEMS), KEYBOARD_SCHEMA, + modify_schema=KEYBOARD_MODIFY_SCHEMA, ) def get_uses(self): @@ -41,7 +47,8 @@ class KeyboardType(WidgetType): lvgl_components_required.add("KEY_LISTENER") lvgl_components_required.add(CONF_KEYBOARD) add_lv_use("btnmatrix") - await w.set_property(CONF_MODE, await KEYBOARD_MODES.process(config[CONF_MODE])) + if mode := config.get(CONF_MODE): + await w.set_property(CONF_MODE, await KEYBOARD_MODES.process(mode)) if ta := await get_widgets(config, CONF_TEXTAREA): await w.set_property(CONF_TEXTAREA, ta[0].obj) diff --git a/esphome/components/lvgl/widgets/msgbox.py b/esphome/components/lvgl/widgets/msgbox.py index c3393940b6..82b2442378 100644 --- a/esphome/components/lvgl/widgets/msgbox.py +++ b/esphome/components/lvgl/widgets/msgbox.py @@ -51,7 +51,7 @@ MSGBOX_SCHEMA = container_schema( cv.Required(CONF_TITLE): STYLED_TEXT_SCHEMA, cv.Optional(CONF_BODY, default=""): STYLED_TEXT_SCHEMA, cv.Optional(CONF_BUTTONS): cv.ensure_list(BUTTONMATRIX_BUTTON_SCHEMA), - cv.Optional(CONF_BUTTON_STYLE): part_schema(buttonmatrix_spec), + cv.Optional(CONF_BUTTON_STYLE): part_schema(buttonmatrix_spec.parts), cv.Optional(CONF_CLOSE_BUTTON, default=True): lv_bool, cv.GenerateID(CONF_BUTTON_TEXT_LIST_ID): cv.declare_id(char_ptr), } diff --git a/esphome/components/lvgl/widgets/obj.py b/esphome/components/lvgl/widgets/obj.py index afb4c97f33..ab22a5ce86 100644 --- a/esphome/components/lvgl/widgets/obj.py +++ b/esphome/components/lvgl/widgets/obj.py @@ -1,9 +1,5 @@ -from esphome import automation - -from ..automation import update_to_code from ..defines import CONF_MAIN, CONF_OBJ, CONF_SCROLLBAR -from ..schemas import create_modify_schema -from ..types import ObjUpdateAction, WidgetType, lv_obj_t +from ..types import WidgetType, lv_obj_t class ObjType(WidgetType): @@ -21,10 +17,3 @@ class ObjType(WidgetType): obj_spec = ObjType() - - -@automation.register_action( - "lvgl.widget.update", ObjUpdateAction, create_modify_schema(obj_spec) -) -async def obj_update_to_code(config, action_id, template_arg, args): - return await update_to_code(config, action_id, template_arg, args) diff --git a/esphome/components/lvgl/widgets/tabview.py b/esphome/components/lvgl/widgets/tabview.py index 226fc3f286..1d18ddd259 100644 --- a/esphome/components/lvgl/widgets/tabview.py +++ b/esphome/components/lvgl/widgets/tabview.py @@ -38,7 +38,7 @@ TABVIEW_SCHEMA = cv.Schema( }, ) ), - cv.Optional(CONF_TAB_STYLE): part_schema(buttonmatrix_spec), + cv.Optional(CONF_TAB_STYLE): part_schema(buttonmatrix_spec.parts), cv.Optional(CONF_POSITION, default="top"): DIRECTIONS.one_of, cv.Optional(CONF_SIZE, default="10%"): size, } diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index 7c59cfa171..b3227bb96e 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -805,9 +805,24 @@ lvgl: - logger.log: format: "keyboard value %s" args: [text.c_str()] + - lvgl.keyboard.update: + id: lv_keyboard + hidden: true + on_ready: + - lvgl.widget.update: + id: lv_keyboard + - lvgl.keyboard.update: + id: lv_keyboard + hidden: true + - keyboard: id: lv_keyboard1 mode: special + on_ready: + lvgl.keyboard.update: + id: lv_keyboard1 + hidden: true + mode: text_lower font: - file: "gfonts://Roboto" From c43d8460bdbc8d2e3c77986e55dbdc7deee99184 Mon Sep 17 00:00:00 2001 From: Saninn Salas Diaz <5490201+distante@users.noreply.github.com> Date: Wed, 15 Jan 2025 03:14:58 +0100 Subject: [PATCH 49/86] fix(web_server/fan): send speed update values even when fan is off (#8086) --- esphome/components/web_server/web_server.cpp | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index ed0cb3db2c..8c09d607a7 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -455,8 +455,9 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc } else if (match.method == "toggle") { this->schedule_([obj]() { obj->toggle().perform(); }); request->send(200); - } else if (match.method == "turn_on") { - auto call = obj->turn_on(); + } else if (match.method == "turn_on" || match.method == "turn_off") { + auto call = match.method == "turn_on" ? obj->turn_on() : obj->turn_off(); + if (request->hasParam("speed_level")) { auto speed_level = request->getParam("speed_level")->value(); auto val = parse_number(speed_level.c_str()); @@ -486,9 +487,6 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc } this->schedule_([call]() mutable { call.perform(); }); request->send(200); - } else if (match.method == "turn_off") { - this->schedule_([obj]() { obj->turn_off().perform(); }); - request->send(200); } else { request->send(404); } From 98817a5bbfacced643d1046135eea26099a0ac36 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Tue, 14 Jan 2025 21:47:22 -0600 Subject: [PATCH 50/86] [es7210] add support for es7210 ADC (#8007) --- CODEOWNERS | 1 + esphome/components/es7210/__init__.py | 67 ++++++ esphome/components/es7210/es7210.cpp | 201 ++++++++++++++++++ esphome/components/es7210/es7210.h | 69 ++++++ esphome/components/es7210/es7210_const.h | 126 +++++++++++ esphome/components/es8311/audio_dac.py | 3 +- esphome/const.py | 1 + tests/components/es7210/common.yaml | 6 + tests/components/es7210/test.esp32-ard.yaml | 5 + .../components/es7210/test.esp32-c3-ard.yaml | 5 + .../components/es7210/test.esp32-c3-idf.yaml | 5 + tests/components/es7210/test.esp32-idf.yaml | 5 + 12 files changed, 492 insertions(+), 2 deletions(-) create mode 100644 esphome/components/es7210/__init__.py create mode 100644 esphome/components/es7210/es7210.cpp create mode 100644 esphome/components/es7210/es7210.h create mode 100644 esphome/components/es7210/es7210_const.h create mode 100644 tests/components/es7210/common.yaml create mode 100644 tests/components/es7210/test.esp32-ard.yaml create mode 100644 tests/components/es7210/test.esp32-c3-ard.yaml create mode 100644 tests/components/es7210/test.esp32-c3-idf.yaml create mode 100644 tests/components/es7210/test.esp32-idf.yaml diff --git a/CODEOWNERS b/CODEOWNERS index 088e350f5d..ba7106e6a3 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/es7210/* @kahrendt esphome/components/es8311/* @kahrendt @kroimon esphome/components/esp32/* @esphome/core esphome/components/esp32_ble/* @Rapsssito @jesserockz diff --git a/esphome/components/es7210/__init__.py b/esphome/components/es7210/__init__.py new file mode 100644 index 0000000000..8e63d7f04f --- /dev/null +++ b/esphome/components/es7210/__init__.py @@ -0,0 +1,67 @@ +import esphome.codegen as cg +from esphome.components import i2c +import esphome.config_validation as cv +from esphome.const import CONF_BITS_PER_SAMPLE, CONF_ID, CONF_MIC_GAIN, CONF_SAMPLE_RATE + +CODEOWNERS = ["@kahrendt"] +DEPENDENCIES = ["i2c"] + +es7210_ns = cg.esphome_ns.namespace("es7210") +ES7210 = es7210_ns.class_("ES7210", cg.Component, i2c.I2CDevice) + + +es7210_bits_per_sample = es7210_ns.enum("ES7210BitsPerSample") +ES7210_BITS_PER_SAMPLE_ENUM = { + 16: es7210_bits_per_sample.ES7210_BITS_PER_SAMPLE_16, + 24: es7210_bits_per_sample.ES7210_BITS_PER_SAMPLE_24, + 32: es7210_bits_per_sample.ES7210_BITS_PER_SAMPLE_32, +} + + +es7210_mic_gain = es7210_ns.enum("ES7210MicGain") +ES7210_MIC_GAIN_ENUM = { + "0DB": es7210_mic_gain.ES7210_MIC_GAIN_0DB, + "3DB": es7210_mic_gain.ES7210_MIC_GAIN_3DB, + "6DB": es7210_mic_gain.ES7210_MIC_GAIN_6DB, + "9DB": es7210_mic_gain.ES7210_MIC_GAIN_9DB, + "12DB": es7210_mic_gain.ES7210_MIC_GAIN_12DB, + "15DB": es7210_mic_gain.ES7210_MIC_GAIN_15DB, + "18DB": es7210_mic_gain.ES7210_MIC_GAIN_18DB, + "21DB": es7210_mic_gain.ES7210_MIC_GAIN_21DB, + "24DB": es7210_mic_gain.ES7210_MIC_GAIN_24DB, + "27DB": es7210_mic_gain.ES7210_MIC_GAIN_27DB, + "30DB": es7210_mic_gain.ES7210_MIC_GAIN_30DB, + "33DB": es7210_mic_gain.ES7210_MIC_GAIN_33DB, + "34.5DB": es7210_mic_gain.ES7210_MIC_GAIN_34_5DB, + "36DB": es7210_mic_gain.ES7210_MIC_GAIN_36DB, + "37.5DB": es7210_mic_gain.ES7210_MIC_GAIN_37_5DB, +} + +_validate_bits = cv.float_with_unit("bits", "bit") + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(ES7210), + cv.Optional(CONF_BITS_PER_SAMPLE, default="16bit"): cv.All( + _validate_bits, cv.enum(ES7210_BITS_PER_SAMPLE_ENUM) + ), + cv.Optional(CONF_MIC_GAIN, default="24DB"): cv.enum( + ES7210_MIC_GAIN_ENUM, upper=True + ), + cv.Optional(CONF_SAMPLE_RATE, default=16000): cv.int_range(min=1), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(0x40)) +) + + +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_rate(config[CONF_SAMPLE_RATE])) diff --git a/esphome/components/es7210/es7210.cpp b/esphome/components/es7210/es7210.cpp new file mode 100644 index 0000000000..d2f2c3c1ff --- /dev/null +++ b/esphome/components/es7210/es7210.cpp @@ -0,0 +1,201 @@ +#include "es7210.h" +#include "es7210_const.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" +#include + +namespace esphome { +namespace es7210 { + +static const char *const TAG = "es7210"; + +static const size_t MCLK_DIV_FRE = 256; + +// Mark the component as failed; use only in setup +#define ES7210_ERROR_FAILED(func) \ + if (!(func)) { \ + this->mark_failed(); \ + return; \ + } + +// Return false; use outside of setup +#define ES7210_ERROR_CHECK(func) \ + if (!(func)) { \ + return false; \ + } + +void ES7210::dump_config() { + ESP_LOGCONFIG(TAG, "ES7210 ADC:"); + ESP_LOGCONFIG(TAG, " Bits Per Sample: %" PRIu8, this->bits_per_sample_); + ESP_LOGCONFIG(TAG, " Sample Rate: %" PRIu32, this->sample_rate_); + + if (this->is_failed()) { + ESP_LOGCONFIG(TAG, " Failed to initialize!"); + return; + } +} + +void ES7210::setup() { + ESP_LOGCONFIG(TAG, "Setting up ES7210..."); + + // Software reset + ES7210_ERROR_FAILED(this->write_byte(ES7210_RESET_REG00, 0xff)); + ES7210_ERROR_FAILED(this->write_byte(ES7210_RESET_REG00, 0x32)); + ES7210_ERROR_FAILED(this->write_byte(ES7210_CLOCK_OFF_REG01, 0x3f)); + + // Set initialization time when device powers up + ES7210_ERROR_FAILED(this->write_byte(ES7210_TIME_CONTROL0_REG09, 0x30)); + ES7210_ERROR_FAILED(this->write_byte(ES7210_TIME_CONTROL1_REG0A, 0x30)); + + // Configure HFP for all ADC channels + ES7210_ERROR_FAILED(this->write_byte(ES7210_ADC12_HPF2_REG23, 0x2a)); + ES7210_ERROR_FAILED(this->write_byte(ES7210_ADC12_HPF1_REG22, 0x0a)); + ES7210_ERROR_FAILED(this->write_byte(ES7210_ADC34_HPF2_REG20, 0x0a)); + ES7210_ERROR_FAILED(this->write_byte(ES7210_ADC34_HPF1_REG21, 0x2a)); + + // Secondary I2S mode settings + ES7210_ERROR_FAILED(this->es7210_update_reg_bit_(ES7210_MODE_CONFIG_REG08, 0x01, 0x00)); + + // Configure analog power + ES7210_ERROR_FAILED(this->write_byte(ES7210_ANALOG_REG40, 0xC3)); + + // Set mic bias + ES7210_ERROR_FAILED(this->write_byte(ES7210_MIC12_BIAS_REG41, 0x70)); + ES7210_ERROR_FAILED(this->write_byte(ES7210_MIC34_BIAS_REG42, 0x70)); + + // Configure I2S settings, sample rate, and microphone gains + ES7210_ERROR_FAILED(this->configure_i2s_format_()); + ES7210_ERROR_FAILED(this->configure_sample_rate_()); + ES7210_ERROR_FAILED(this->configure_mic_gain_()); + + // Power on mics 1 through 4 + ES7210_ERROR_FAILED(this->write_byte(ES7210_MIC1_POWER_REG47, 0x08)); + ES7210_ERROR_FAILED(this->write_byte(ES7210_MIC2_POWER_REG48, 0x08)); + ES7210_ERROR_FAILED(this->write_byte(ES7210_MIC3_POWER_REG49, 0x08)); + ES7210_ERROR_FAILED(this->write_byte(ES7210_MIC4_POWER_REG4A, 0x08)); + + // Power down DLL + ES7210_ERROR_FAILED(this->write_byte(ES7210_POWER_DOWN_REG06, 0x04)); + + // Power on MIC1-4 bias & ADC1-4 & PGA1-4 Power + ES7210_ERROR_FAILED(this->write_byte(ES7210_MIC12_POWER_REG4B, 0x0F)); + ES7210_ERROR_FAILED(this->write_byte(ES7210_MIC34_POWER_REG4C, 0x0F)); + + // Enable device + ES7210_ERROR_FAILED(this->write_byte(ES7210_RESET_REG00, 0x71)); + ES7210_ERROR_FAILED(this->write_byte(ES7210_RESET_REG00, 0x41)); +} + +bool ES7210::configure_sample_rate_() { + int mclk_fre = this->sample_rate_ * MCLK_DIV_FRE; + int coeff = -1; + + for (int i = 0; i < (sizeof(ES7210_COEFFICIENTS) / sizeof(ES7210_COEFFICIENTS[0])); ++i) { + if (ES7210_COEFFICIENTS[i].lrclk == this->sample_rate_ && ES7210_COEFFICIENTS[i].mclk == mclk_fre) + coeff = i; + } + + if (coeff >= 0) { + // Set adc_div & doubler & dll + uint8_t regv; + ES7210_ERROR_CHECK(this->read_byte(ES7210_MAINCLK_REG02, ®v)); + regv = regv & 0x00; + regv |= ES7210_COEFFICIENTS[coeff].adc_div; + regv |= ES7210_COEFFICIENTS[coeff].doubler << 6; + regv |= ES7210_COEFFICIENTS[coeff].dll << 7; + + ES7210_ERROR_CHECK(this->write_byte(ES7210_MAINCLK_REG02, regv)); + + // Set osr + regv = ES7210_COEFFICIENTS[coeff].osr; + ES7210_ERROR_CHECK(this->write_byte(ES7210_OSR_REG07, regv)); + // Set lrck + regv = ES7210_COEFFICIENTS[coeff].lrck_h; + ES7210_ERROR_CHECK(this->write_byte(ES7210_LRCK_DIVH_REG04, regv)); + regv = ES7210_COEFFICIENTS[coeff].lrck_l; + ES7210_ERROR_CHECK(this->write_byte(ES7210_LRCK_DIVL_REG05, regv)); + } else { + // Invalid sample frequency + ESP_LOGE(TAG, "Invalid sample rate"); + return false; + } + + return true; +} +bool ES7210::configure_mic_gain_() { + for (int i = 0; i < 4; ++i) { + this->es7210_update_reg_bit_(ES7210_MIC1_GAIN_REG43 + i, 0x10, 0x00); + } + ES7210_ERROR_CHECK(this->write_byte(ES7210_MIC12_POWER_REG4B, 0xff)); + ES7210_ERROR_CHECK(this->write_byte(ES7210_MIC34_POWER_REG4C, 0xff)); + + // Configure mic 1 + ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_CLOCK_OFF_REG01, 0x0b, 0x00)); + ES7210_ERROR_CHECK(this->write_byte(ES7210_MIC12_POWER_REG4B, 0x00)); + ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC1_GAIN_REG43, 0x10, 0x10)); + ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC1_GAIN_REG43, 0x0f, this->mic_gain_)); + + // Configure mic 2 + ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_CLOCK_OFF_REG01, 0x0b, 0x00)); + ES7210_ERROR_CHECK(this->write_byte(ES7210_MIC12_POWER_REG4B, 0x00)); + ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC2_GAIN_REG44, 0x10, 0x10)); + ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC2_GAIN_REG44, 0x0f, this->mic_gain_)); + + // Configure mic 3 + ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_CLOCK_OFF_REG01, 0x0b, 0x00)); + ES7210_ERROR_CHECK(this->write_byte(ES7210_MIC34_POWER_REG4C, 0x00)); + ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC3_GAIN_REG45, 0x10, 0x10)); + ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC3_GAIN_REG45, 0x0f, this->mic_gain_)); + + // Configure mic 4 + ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_CLOCK_OFF_REG01, 0x0b, 0x00)); + ES7210_ERROR_CHECK(this->write_byte(ES7210_MIC34_POWER_REG4C, 0x00)); + ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC4_GAIN_REG46, 0x10, 0x10)); + ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC4_GAIN_REG46, 0x0f, this->mic_gain_)); + + return true; +} + +bool ES7210::configure_i2s_format_() { + // Configure bits per sample + uint8_t reg_val = 0; + switch (this->bits_per_sample_) { + case ES7210_BITS_PER_SAMPLE_16: + reg_val = 0x60; + break; + case ES7210_BITS_PER_SAMPLE_18: + reg_val = 0x40; + break; + case ES7210_BITS_PER_SAMPLE_20: + reg_val = 0x20; + break; + case ES7210_BITS_PER_SAMPLE_24: + reg_val = 0x00; + break; + case ES7210_BITS_PER_SAMPLE_32: + reg_val = 0x80; + break; + default: + return false; + } + ES7210_ERROR_CHECK(this->write_byte(ES7210_SDP_INTERFACE1_REG11, reg_val)); + + if (this->enable_tdm_) { + ES7210_ERROR_CHECK(this->write_byte(ES7210_SDP_INTERFACE2_REG12, 0x02)); + } else { + // Microphones 1 and 2 output on SDOUT1, microphones 3 and 4 output on SDOUT2 + ES7210_ERROR_CHECK(this->write_byte(ES7210_SDP_INTERFACE2_REG12, 0x00)); + } + + return true; +} + +bool ES7210::es7210_update_reg_bit_(uint8_t reg_addr, uint8_t update_bits, uint8_t data) { + uint8_t regv; + ES7210_ERROR_CHECK(this->read_byte(reg_addr, ®v)); + regv = (regv & (~update_bits)) | (update_bits & data); + return this->write_byte(reg_addr, regv); +} + +} // namespace es7210 +} // namespace esphome diff --git a/esphome/components/es7210/es7210.h b/esphome/components/es7210/es7210.h new file mode 100644 index 0000000000..a40dde5aa5 --- /dev/null +++ b/esphome/components/es7210/es7210.h @@ -0,0 +1,69 @@ +#pragma once + +#include "esphome/components/i2c/i2c.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace es7210 { + +enum ES7210BitsPerSample : uint8_t { + ES7210_BITS_PER_SAMPLE_16 = 16, + ES7210_BITS_PER_SAMPLE_18 = 18, + ES7210_BITS_PER_SAMPLE_20 = 20, + ES7210_BITS_PER_SAMPLE_24 = 24, + ES7210_BITS_PER_SAMPLE_32 = 32, +}; + +enum ES7210MicGain : uint8_t { + ES7210_MIC_GAIN_0DB = 0, + ES7210_MIC_GAIN_3DB, + ES7210_MIC_GAIN_6DB, + ES7210_MIC_GAIN_9DB, + ES7210_MIC_GAIN_12DB, + ES7210_MIC_GAIN_15DB, + ES7210_MIC_GAIN_18DB, + ES7210_MIC_GAIN_21DB, + ES7210_MIC_GAIN_24DB, + ES7210_MIC_GAIN_27DB, + ES7210_MIC_GAIN_30DB, + ES7210_MIC_GAIN_33DB, + ES7210_MIC_GAIN_34_5DB, + ES7210_MIC_GAIN_36DB, + ES7210_MIC_GAIN_37_5DB, +}; + +class ES7210 : public Component, public i2c::I2CDevice { + /* Class for configuring an ES7210 ADC for microphone input. + * Based on code from: + * - https://github.com/espressif/esp-bsp/ (accessed 20241219) + * - https://github.com/espressif/esp-adf/ (accessed 20241219) + */ + public: + void setup() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void dump_config() override; + + void set_bits_per_sample(ES7210BitsPerSample bits_per_sample) { this->bits_per_sample_ = bits_per_sample; } + void set_mic_gain(ES7210MicGain mic_gain) { this->mic_gain_ = mic_gain; } + void set_sample_rate(uint32_t sample_rate) { this->sample_rate_ = sample_rate; } + + protected: + /// @brief Updates an I2C registry address by modifying the current state + /// @param reg_addr I2C register address + /// @param update_bits Mask of allowed bits to be modified + /// @param data Bit values to be written + /// @return True if successful, false otherwise + bool es7210_update_reg_bit_(uint8_t reg_addr, uint8_t update_bits, uint8_t data); + + bool configure_i2s_format_(); + bool configure_mic_gain_(); + bool configure_sample_rate_(); + + bool enable_tdm_{false}; // TDM is unsupported in ESPHome as of version 2024.12 + ES7210MicGain mic_gain_; + ES7210BitsPerSample bits_per_sample_; + uint32_t sample_rate_; +}; + +} // namespace es7210 +} // namespace esphome diff --git a/esphome/components/es7210/es7210_const.h b/esphome/components/es7210/es7210_const.h new file mode 100644 index 0000000000..87fd6d86d2 --- /dev/null +++ b/esphome/components/es7210/es7210_const.h @@ -0,0 +1,126 @@ +#pragma once + +#include "es7210.h" + +namespace esphome { +namespace es7210 { + +// ES7210 register addresses +static const uint8_t ES7210_RESET_REG00 = 0x00; /* Reset control */ +static const uint8_t ES7210_CLOCK_OFF_REG01 = 0x01; /* Used to turn off the ADC clock */ +static const uint8_t ES7210_MAINCLK_REG02 = 0x02; /* Set ADC clock frequency division */ + +static const uint8_t ES7210_MASTER_CLK_REG03 = 0x03; /* MCLK source $ SCLK division */ +static const uint8_t ES7210_LRCK_DIVH_REG04 = 0x04; /* lrck_divh */ +static const uint8_t ES7210_LRCK_DIVL_REG05 = 0x05; /* lrck_divl */ +static const uint8_t ES7210_POWER_DOWN_REG06 = 0x06; /* power down */ +static const uint8_t ES7210_OSR_REG07 = 0x07; +static const uint8_t ES7210_MODE_CONFIG_REG08 = 0x08; /* Set primary/secondary & channels */ +static const uint8_t ES7210_TIME_CONTROL0_REG09 = 0x09; /* Set Chip intial state period*/ +static const uint8_t ES7210_TIME_CONTROL1_REG0A = 0x0A; /* Set Power up state period */ +static const uint8_t ES7210_SDP_INTERFACE1_REG11 = 0x11; /* Set sample & fmt */ +static const uint8_t ES7210_SDP_INTERFACE2_REG12 = 0x12; /* Pins state */ +static const uint8_t ES7210_ADC_AUTOMUTE_REG13 = 0x13; /* Set mute */ +static const uint8_t ES7210_ADC34_MUTERANGE_REG14 = 0x14; /* Set mute range */ +static const uint8_t ES7210_ADC12_MUTERANGE_REG15 = 0x15; /* Set mute range */ +static const uint8_t ES7210_ADC34_HPF2_REG20 = 0x20; /* HPF */ +static const uint8_t ES7210_ADC34_HPF1_REG21 = 0x21; /* HPF */ +static const uint8_t ES7210_ADC12_HPF1_REG22 = 0x22; /* HPF */ +static const uint8_t ES7210_ADC12_HPF2_REG23 = 0x23; /* HPF */ +static const uint8_t ES7210_ANALOG_REG40 = 0x40; /* ANALOG Power */ +static const uint8_t ES7210_MIC12_BIAS_REG41 = 0x41; +static const uint8_t ES7210_MIC34_BIAS_REG42 = 0x42; +static const uint8_t ES7210_MIC1_GAIN_REG43 = 0x43; +static const uint8_t ES7210_MIC2_GAIN_REG44 = 0x44; +static const uint8_t ES7210_MIC3_GAIN_REG45 = 0x45; +static const uint8_t ES7210_MIC4_GAIN_REG46 = 0x46; +static const uint8_t ES7210_MIC1_POWER_REG47 = 0x47; +static const uint8_t ES7210_MIC2_POWER_REG48 = 0x48; +static const uint8_t ES7210_MIC3_POWER_REG49 = 0x49; +static const uint8_t ES7210_MIC4_POWER_REG4A = 0x4A; +static const uint8_t ES7210_MIC12_POWER_REG4B = 0x4B; /* MICBias & ADC & PGA Power */ +static const uint8_t ES7210_MIC34_POWER_REG4C = 0x4C; + +/* + * Clock coefficient structer + */ +struct ES7210Coefficient { + uint32_t mclk; // mclk frequency + uint32_t lrclk; + uint8_t ss_ds; + uint8_t adc_div; + uint8_t dll; // dll_bypass + uint8_t doubler; // doubler_enable + uint8_t osr; // adc osr + uint8_t mclk_src; // sselect mclk source + uint8_t lrck_h; // High 4 bits of lrck + uint8_t lrck_l; // Low 8 bits of lrck +}; + +/* Codec hifi mclk clock divider coefficients + * MEMBER REG + * mclk: 0x03 + * lrck: standard + * ss_ds: -- + * adc_div: 0x02 + * dll: 0x06 + * doubler: 0x02 + * osr: 0x07 + * mclk_src: 0x03 + * lrckh: 0x04 + * lrckl: 0x05 + */ +static const ES7210Coefficient ES7210_COEFFICIENTS[] = { + // mclk lrck ss_ds adc_div dll doubler osr mclk_src lrckh lrckl + /* 8k */ + {12288000, 8000, 0x00, 0x03, 0x01, 0x00, 0x20, 0x00, 0x06, 0x00}, + {16384000, 8000, 0x00, 0x04, 0x01, 0x00, 0x20, 0x00, 0x08, 0x00}, + {19200000, 8000, 0x00, 0x1e, 0x00, 0x01, 0x28, 0x00, 0x09, 0x60}, + {4096000, 8000, 0x00, 0x01, 0x01, 0x00, 0x20, 0x00, 0x02, 0x00}, + + /* 11.025k */ + {11289600, 11025, 0x00, 0x02, 0x01, 0x00, 0x20, 0x00, 0x01, 0x00}, + + /* 12k */ + {12288000, 12000, 0x00, 0x02, 0x01, 0x00, 0x20, 0x00, 0x04, 0x00}, + {19200000, 12000, 0x00, 0x14, 0x00, 0x01, 0x28, 0x00, 0x06, 0x40}, + + /* 16k */ + {4096000, 16000, 0x00, 0x01, 0x01, 0x01, 0x20, 0x00, 0x01, 0x00}, + {19200000, 16000, 0x00, 0x0a, 0x00, 0x00, 0x1e, 0x00, 0x04, 0x80}, + {16384000, 16000, 0x00, 0x02, 0x01, 0x00, 0x20, 0x00, 0x04, 0x00}, + {12288000, 16000, 0x00, 0x03, 0x01, 0x01, 0x20, 0x00, 0x03, 0x00}, + + /* 22.05k */ + {11289600, 22050, 0x00, 0x01, 0x01, 0x00, 0x20, 0x00, 0x02, 0x00}, + + /* 24k */ + {12288000, 24000, 0x00, 0x01, 0x01, 0x00, 0x20, 0x00, 0x02, 0x00}, + {19200000, 24000, 0x00, 0x0a, 0x00, 0x01, 0x28, 0x00, 0x03, 0x20}, + + /* 32k */ + {12288000, 32000, 0x00, 0x03, 0x00, 0x00, 0x20, 0x00, 0x01, 0x80}, + {16384000, 32000, 0x00, 0x01, 0x01, 0x00, 0x20, 0x00, 0x02, 0x00}, + {19200000, 32000, 0x00, 0x05, 0x00, 0x00, 0x1e, 0x00, 0x02, 0x58}, + + /* 44.1k */ + {11289600, 44100, 0x00, 0x01, 0x01, 0x01, 0x20, 0x00, 0x01, 0x00}, + + /* 48k */ + {12288000, 48000, 0x00, 0x01, 0x01, 0x01, 0x20, 0x00, 0x01, 0x00}, + {19200000, 48000, 0x00, 0x05, 0x00, 0x01, 0x28, 0x00, 0x01, 0x90}, + + /* 64k */ + {16384000, 64000, 0x01, 0x01, 0x01, 0x00, 0x20, 0x00, 0x01, 0x00}, + {19200000, 64000, 0x00, 0x05, 0x00, 0x01, 0x1e, 0x00, 0x01, 0x2c}, + + /* 88.2k */ + {11289600, 88200, 0x01, 0x01, 0x01, 0x01, 0x20, 0x00, 0x00, 0x80}, + + /* 96k */ + {12288000, 96000, 0x01, 0x01, 0x01, 0x01, 0x20, 0x00, 0x00, 0x80}, + {19200000, 96000, 0x01, 0x05, 0x00, 0x01, 0x28, 0x00, 0x00, 0xc8}, +}; + +} // namespace es7210 +} // namespace esphome diff --git a/esphome/components/es8311/audio_dac.py b/esphome/components/es8311/audio_dac.py index 1b450c3c11..7d80cfd5fb 100644 --- a/esphome/components/es8311/audio_dac.py +++ b/esphome/components/es8311/audio_dac.py @@ -2,7 +2,7 @@ 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 +from esphome.const import CONF_BITS_PER_SAMPLE, CONF_ID, CONF_MIC_GAIN, CONF_SAMPLE_RATE CODEOWNERS = ["@kroimon", "@kahrendt"] DEPENDENCIES = ["i2c"] @@ -10,7 +10,6 @@ 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" diff --git a/esphome/const.py b/esphome/const.py index 0f41dc1aec..4c0203c6a9 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -490,6 +490,7 @@ CONF_MEMORY_BLOCKS = "memory_blocks" CONF_MESSAGE = "message" CONF_METHANE = "methane" CONF_METHOD = "method" +CONF_MIC_GAIN = "mic_gain" CONF_MICROPHONE = "microphone" CONF_MIN_BRIGHTNESS = "min_brightness" CONF_MIN_COOLING_OFF_TIME = "min_cooling_off_time" diff --git a/tests/components/es7210/common.yaml b/tests/components/es7210/common.yaml new file mode 100644 index 0000000000..5c30f7e883 --- /dev/null +++ b/tests/components/es7210/common.yaml @@ -0,0 +1,6 @@ +i2c: + - id: i2c_aic3204 + scl: ${scl_pin} + sda: ${sda_pin} + +es7210: diff --git a/tests/components/es7210/test.esp32-ard.yaml b/tests/components/es7210/test.esp32-ard.yaml new file mode 100644 index 0000000000..63c3bd6afd --- /dev/null +++ b/tests/components/es7210/test.esp32-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO16 + sda_pin: GPIO17 + +<<: !include common.yaml diff --git a/tests/components/es7210/test.esp32-c3-ard.yaml b/tests/components/es7210/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/es7210/test.esp32-c3-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/es7210/test.esp32-c3-idf.yaml b/tests/components/es7210/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/es7210/test.esp32-c3-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/es7210/test.esp32-idf.yaml b/tests/components/es7210/test.esp32-idf.yaml new file mode 100644 index 0000000000..63c3bd6afd --- /dev/null +++ b/tests/components/es7210/test.esp32-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO16 + sda_pin: GPIO17 + +<<: !include common.yaml From c458fd18df52923b6b3fb35e2ea6efd4d6603f72 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 15 Jan 2025 16:49:58 +1300 Subject: [PATCH 51/86] Bump version to 2025.2.0-dev --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 4c0203c6a9..284f8d5f78 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2025.1.0-dev" +__version__ = "2025.2.0-dev" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From e779a8bcb2e73eddb7eb3474814dd6c64bb98486 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 15 Jan 2025 16:54:45 +1300 Subject: [PATCH 52/86] [event] Store ``last_event_type`` in class (#8088) --- esphome/components/event/event.cpp | 6 ++++-- esphome/components/event/event.h | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/esphome/components/event/event.cpp b/esphome/components/event/event.cpp index 061afcb026..d27b3b378e 100644 --- a/esphome/components/event/event.cpp +++ b/esphome/components/event/event.cpp @@ -8,11 +8,13 @@ namespace event { static const char *const TAG = "event"; void Event::trigger(const std::string &event_type) { - if (types_.find(event_type) == types_.end()) { + auto found = types_.find(event_type); + if (found == types_.end()) { ESP_LOGE(TAG, "'%s': invalid event type for trigger(): %s", this->get_name().c_str(), event_type.c_str()); return; } - ESP_LOGD(TAG, "'%s' Triggered event '%s'", this->get_name().c_str(), event_type.c_str()); + last_event_type = &(*found); + ESP_LOGD(TAG, "'%s' Triggered event '%s'", this->get_name().c_str(), last_event_type->c_str()); this->event_callback_.call(event_type); } diff --git a/esphome/components/event/event.h b/esphome/components/event/event.h index 067a867360..03c3c8d95a 100644 --- a/esphome/components/event/event.h +++ b/esphome/components/event/event.h @@ -23,6 +23,8 @@ namespace event { class Event : public EntityBase, public EntityBase_DeviceClass { public: + const std::string *last_event_type; + void trigger(const std::string &event_type); void set_event_types(const std::set &event_types) { this->types_ = event_types; } std::set get_event_types() const { return this->types_; } From df26ace0f172d1bad7f019fc4d095f3a7be188f9 Mon Sep 17 00:00:00 2001 From: Jordan Zucker Date: Tue, 14 Jan 2025 19:56:22 -0800 Subject: [PATCH 53/86] [prometheus] Select, media_player, and number prometheus metrics (#7895) --- .../prometheus/prometheus_handler.cpp | 168 ++++++++++++++++++ .../prometheus/prometheus_handler.h | 24 +++ tests/components/prometheus/common.yaml | 20 +++ .../components/prometheus/test.esp32-ard.yaml | 33 ++++ 4 files changed, 245 insertions(+) diff --git a/esphome/components/prometheus/prometheus_handler.cpp b/esphome/components/prometheus/prometheus_handler.cpp index 5d1861202a..2d39d8ef3f 100644 --- a/esphome/components/prometheus/prometheus_handler.cpp +++ b/esphome/components/prometheus/prometheus_handler.cpp @@ -59,6 +59,24 @@ void PrometheusHandler::handleRequest(AsyncWebServerRequest *req) { this->text_sensor_row_(stream, obj, area, node, friendly_name); #endif +#ifdef USE_NUMBER + this->number_type_(stream); + for (auto *obj : App.get_numbers()) + this->number_row_(stream, obj, area, node, friendly_name); +#endif + +#ifdef USE_SELECT + this->select_type_(stream); + for (auto *obj : App.get_selects()) + this->select_row_(stream, obj, area, node, friendly_name); +#endif + +#ifdef USE_MEDIA_PLAYER + this->media_player_type_(stream); + for (auto *obj : App.get_media_players()) + this->media_player_row_(stream, obj, area, node, friendly_name); +#endif + req->send(stream); } @@ -511,6 +529,156 @@ void PrometheusHandler::text_sensor_row_(AsyncResponseStream *stream, text_senso } #endif +// Type-specific implementation +#ifdef USE_NUMBER +void PrometheusHandler::number_type_(AsyncResponseStream *stream) { + stream->print(F("#TYPE esphome_number_value gauge\n")); + stream->print(F("#TYPE esphome_number_failed gauge\n")); +} +void PrometheusHandler::number_row_(AsyncResponseStream *stream, number::Number *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_number_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_number_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("\"} ")); + stream->print(obj->state); + stream->print(F("\n")); + } else { + // Invalid state + stream->print(F("esphome_number_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")); + } +} +#endif + +#ifdef USE_SELECT +void PrometheusHandler::select_type_(AsyncResponseStream *stream) { + stream->print(F("#TYPE esphome_select_value gauge\n")); + stream->print(F("#TYPE esphome_select_failed gauge\n")); +} +void PrometheusHandler::select_row_(AsyncResponseStream *stream, select::Select *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_select_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_select_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=\"")); + stream->print(obj->state.c_str()); + stream->print(F("\"} ")); + stream->print(F("1.0")); + stream->print(F("\n")); + } else { + // Invalid state + stream->print(F("esphome_select_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")); + } +} +#endif + +#ifdef USE_MEDIA_PLAYER +void PrometheusHandler::media_player_type_(AsyncResponseStream *stream) { + stream->print(F("#TYPE esphome_media_player_state_value gauge\n")); + stream->print(F("#TYPE esphome_media_player_volume gauge\n")); + stream->print(F("#TYPE esphome_media_player_is_muted gauge\n")); + stream->print(F("#TYPE esphome_media_player_failed gauge\n")); +} +void PrometheusHandler::media_player_row_(AsyncResponseStream *stream, media_player::MediaPlayer *obj, + std::string &area, std::string &node, std::string &friendly_name) { + if (obj->is_internal() && !this->include_internal_) + return; + stream->print(F("esphome_media_player_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_media_player_state_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=\"")); + stream->print(media_player::media_player_state_to_string(obj->state)); + stream->print(F("\"} ")); + stream->print(F("1.0")); + stream->print(F("\n")); + stream->print(F("esphome_media_player_volume{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("\"} ")); + stream->print(obj->volume); + stream->print(F("\n")); + stream->print(F("esphome_media_player_is_muted{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("\"} ")); + if (obj->is_muted()) { + stream->print(F("1.0")); + } else { + stream->print(F("0.0")); + } + stream->print(F("\n")); +} +#endif + } // namespace prometheus } // namespace esphome #endif diff --git a/esphome/components/prometheus/prometheus_handler.h b/esphome/components/prometheus/prometheus_handler.h index 5d08aca63a..41a06537ed 100644 --- a/esphome/components/prometheus/prometheus_handler.h +++ b/esphome/components/prometheus/prometheus_handler.h @@ -128,6 +128,30 @@ class PrometheusHandler : public AsyncWebHandler, public Component { std::string &friendly_name); #endif +#ifdef USE_NUMBER + /// Return the type for prometheus + void number_type_(AsyncResponseStream *stream); + /// Return the sensor state as prometheus data point + void number_row_(AsyncResponseStream *stream, number::Number *obj, std::string &area, std::string &node, + std::string &friendly_name); +#endif + +#ifdef USE_SELECT + /// Return the type for prometheus + void select_type_(AsyncResponseStream *stream); + /// Return the select state as prometheus data point + void select_row_(AsyncResponseStream *stream, select::Select *obj, std::string &area, std::string &node, + std::string &friendly_name); +#endif + +#ifdef USE_MEDIA_PLAYER + /// Return the type for prometheus + void media_player_type_(AsyncResponseStream *stream); + /// Return the select state as prometheus data point + void media_player_row_(AsyncResponseStream *stream, media_player::MediaPlayer *obj, std::string &area, + std::string &node, std::string &friendly_name); +#endif + web_server_base::WebServerBase *base_; bool include_internal_{false}; std::map relabel_map_id_; diff --git a/tests/components/prometheus/common.yaml b/tests/components/prometheus/common.yaml index 68ef2a2f58..1b87c1d6c1 100644 --- a/tests/components/prometheus/common.yaml +++ b/tests/components/prometheus/common.yaml @@ -78,6 +78,26 @@ lock: } optimistic: true +select: + - platform: template + id: template_select1 + name: "Template select" + optimistic: true + options: + - one + - two + - three + initial_option: two + +number: + - platform: template + id: template_number1 + name: "Template number" + optimistic: true + min_value: 0 + max_value: 100 + step: 1 + prometheus: include_internal: true relabel: diff --git a/tests/components/prometheus/test.esp32-ard.yaml b/tests/components/prometheus/test.esp32-ard.yaml index dade44d145..3045a6db13 100644 --- a/tests/components/prometheus/test.esp32-ard.yaml +++ b/tests/components/prometheus/test.esp32-ard.yaml @@ -1 +1,34 @@ <<: !include common.yaml + +i2s_audio: + i2s_lrclk_pin: 1 + i2s_bclk_pin: 2 + i2s_mclk_pin: 3 + +media_player: + - platform: i2s_audio + name: "Media Player" + dac_type: external + i2s_dout_pin: 18 + mute_pin: 19 + on_state: + - media_player.play: + - media_player.play_media: http://localhost/media.mp3 + - media_player.play_media: !lambda 'return "http://localhost/media.mp3";' + on_idle: + - media_player.pause: + on_play: + - media_player.stop: + on_pause: + - media_player.toggle: + - wait_until: + media_player.is_idle: + - wait_until: + media_player.is_playing: + - wait_until: + media_player.is_announcing: + - wait_until: + media_player.is_paused: + - media_player.volume_up: + - media_player.volume_down: + - media_player.volume_set: 50% From b4a804cc779bcb8a3b1480a44741bf9f29caa33c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 Jan 2025 14:53:23 +1300 Subject: [PATCH 54/86] Bump docker/build-push-action from 6.11.0 to 6.12.0 in /.github/actions/build-image (#8090) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/actions/build-image/action.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/actions/build-image/action.yaml b/.github/actions/build-image/action.yaml index e6b6da4177..b2a3394563 100644 --- a/.github/actions/build-image/action.yaml +++ b/.github/actions/build-image/action.yaml @@ -46,7 +46,7 @@ runs: - name: Build and push to ghcr by digest id: build-ghcr - uses: docker/build-push-action@v6.11.0 + uses: docker/build-push-action@v6.12.0 env: DOCKER_BUILD_SUMMARY: false DOCKER_BUILD_RECORD_UPLOAD: false @@ -72,7 +72,7 @@ runs: - name: Build and push to dockerhub by digest id: build-dockerhub - uses: docker/build-push-action@v6.11.0 + uses: docker/build-push-action@v6.12.0 env: DOCKER_BUILD_SUMMARY: false DOCKER_BUILD_RECORD_UPLOAD: false From 49c01c26f1847debfa53dc7e31b6434debcfd40e Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Thu, 16 Jan 2025 16:12:30 +1100 Subject: [PATCH 55/86] Revert "Add resistance_sampler interface for config validation" (#8093) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 - esphome/components/ntc/sensor.py | 6 ++---- esphome/components/resistance/resistance_sensor.h | 5 ++--- esphome/components/resistance/sensor.py | 11 ++--------- esphome/components/resistance_sampler/__init__.py | 6 ------ .../resistance_sampler/resistance_sampler.h | 10 ---------- 6 files changed, 6 insertions(+), 33 deletions(-) delete mode 100644 esphome/components/resistance_sampler/__init__.py delete mode 100644 esphome/components/resistance_sampler/resistance_sampler.h diff --git a/CODEOWNERS b/CODEOWNERS index ba7106e6a3..f0075549fd 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -339,7 +339,6 @@ esphome/components/radon_eye_rd200/* @jeffeb3 esphome/components/rc522/* @glmnet esphome/components/rc522_i2c/* @glmnet esphome/components/rc522_spi/* @glmnet -esphome/components/resistance_sampler/* @jesserockz esphome/components/restart/* @esphome/core esphome/components/rf_bridge/* @jesserockz esphome/components/rgbct/* @jesserockz diff --git a/esphome/components/ntc/sensor.py b/esphome/components/ntc/sensor.py index 961511fe00..bd5f4a1841 100644 --- a/esphome/components/ntc/sensor.py +++ b/esphome/components/ntc/sensor.py @@ -2,7 +2,7 @@ from math import log import esphome.config_validation as cv import esphome.codegen as cg -from esphome.components import sensor, resistance_sampler +from esphome.components import sensor from esphome.const import ( CONF_CALIBRATION, CONF_REFERENCE_RESISTANCE, @@ -15,8 +15,6 @@ from esphome.const import ( UNIT_CELSIUS, ) -AUTO_LOAD = ["resistance_sampler"] - ntc_ns = cg.esphome_ns.namespace("ntc") NTC = ntc_ns.class_("NTC", cg.Component, sensor.Sensor) @@ -126,7 +124,7 @@ CONFIG_SCHEMA = ( ) .extend( { - cv.Required(CONF_SENSOR): cv.use_id(resistance_sampler.ResistanceSampler), + cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor), cv.Required(CONF_CALIBRATION): process_calibration, } ) diff --git a/esphome/components/resistance/resistance_sensor.h b/esphome/components/resistance/resistance_sensor.h index 8fa1f8b570..b57f90b59c 100644 --- a/esphome/components/resistance/resistance_sensor.h +++ b/esphome/components/resistance/resistance_sensor.h @@ -1,8 +1,7 @@ #pragma once -#include "esphome/components/resistance_sampler/resistance_sampler.h" -#include "esphome/components/sensor/sensor.h" #include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" namespace esphome { namespace resistance { @@ -12,7 +11,7 @@ enum ResistanceConfiguration { DOWNSTREAM, }; -class ResistanceSensor : public Component, public sensor::Sensor, resistance_sampler::ResistanceSampler { +class ResistanceSensor : public Component, public sensor::Sensor { public: void set_sensor(Sensor *sensor) { sensor_ = sensor; } void set_configuration(ResistanceConfiguration configuration) { configuration_ = configuration; } diff --git a/esphome/components/resistance/sensor.py b/esphome/components/resistance/sensor.py index ce4459fc6d..3622799a07 100644 --- a/esphome/components/resistance/sensor.py +++ b/esphome/components/resistance/sensor.py @@ -1,6 +1,6 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import sensor, resistance_sampler +from esphome.components import sensor from esphome.const import ( CONF_REFERENCE_VOLTAGE, CONF_SENSOR, @@ -9,15 +9,8 @@ from esphome.const import ( ICON_FLASH, ) -AUTO_LOAD = ["resistance_sampler"] - resistance_ns = cg.esphome_ns.namespace("resistance") -ResistanceSensor = resistance_ns.class_( - "ResistanceSensor", - cg.Component, - sensor.Sensor, - resistance_sampler.ResistanceSampler, -) +ResistanceSensor = resistance_ns.class_("ResistanceSensor", cg.Component, sensor.Sensor) CONF_CONFIGURATION = "configuration" CONF_RESISTOR = "resistor" diff --git a/esphome/components/resistance_sampler/__init__.py b/esphome/components/resistance_sampler/__init__.py deleted file mode 100644 index d2032848aa..0000000000 --- a/esphome/components/resistance_sampler/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -import esphome.codegen as cg - -resistance_sampler_ns = cg.esphome_ns.namespace("resistance_sampler") -ResistanceSampler = resistance_sampler_ns.class_("ResistanceSampler") - -CODEOWNERS = ["@jesserockz"] diff --git a/esphome/components/resistance_sampler/resistance_sampler.h b/esphome/components/resistance_sampler/resistance_sampler.h deleted file mode 100644 index 9e300bebcc..0000000000 --- a/esphome/components/resistance_sampler/resistance_sampler.h +++ /dev/null @@ -1,10 +0,0 @@ -#pragma once - -namespace esphome { -namespace resistance_sampler { - -/// Abstract interface to mark components that provide resistance values. -class ResistanceSampler {}; - -} // namespace resistance_sampler -} // namespace esphome From 16bf56b0f95b1c5a75cc2f4ae167e37485c8803b Mon Sep 17 00:00:00 2001 From: Katherine Whitlock Date: Thu, 16 Jan 2025 15:10:20 -0500 Subject: [PATCH 56/86] Fix running pre-commit on Windows (#8095) --- .pre-commit-config.yaml | 4 ++-- script/run-in-env | 53 +++++++++++++++++++++++++++++++++++++++++ script/run-in-env.sh | 13 ---------- 3 files changed, 55 insertions(+), 15 deletions(-) create mode 100644 script/run-in-env delete mode 100755 script/run-in-env.sh diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index aeb434167a..5308a2c047 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -53,6 +53,6 @@ repos: hooks: - id: pylint name: pylint - entry: script/run-in-env.sh pylint - language: script + entry: python script/run-in-env pylint + language: system types: [python] diff --git a/script/run-in-env b/script/run-in-env new file mode 100644 index 0000000000..57bfddccf7 --- /dev/null +++ b/script/run-in-env @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 + +import os +from pathlib import Path +import subprocess +import sys + + +def find_and_activate_virtualenv(): + try: + # Get the top-level directory of the git repository + my_path = subprocess.check_output( + ["git", "rev-parse", "--show-toplevel"], text=True + ).strip() + except subprocess.CalledProcessError: + print( + "Error: Not a git repository or unable to determine the top-level directory.", + file=sys.stderr, + ) + sys.exit(1) + + # Check for virtual environments + for venv in ["venv", ".venv", "."]: + activate_path = ( + Path(my_path) + / venv + / ("Scripts" if os.name == "nt" else "bin") + / "activate" + ) + if activate_path.exists(): + # Activate the virtual environment by updating PATH + env = os.environ.copy() + venv_bin_dir = activate_path.parent + env["PATH"] = f"{venv_bin_dir}{os.pathsep}{env['PATH']}" + env["VIRTUAL_ENV"] = str(venv_bin_dir.parent) + print(f"Activated virtual environment: {venv_bin_dir.parent}") + + # Execute the remaining arguments in the new environment + if len(sys.argv) > 1: + subprocess.run(sys.argv[1:], env=env, check=False) + else: + print( + "No command provided to run in the virtual environment.", + file=sys.stderr, + ) + return + + print("No virtual environment found.", file=sys.stderr) + sys.exit(1) + + +if __name__ == "__main__": + find_and_activate_virtualenv() diff --git a/script/run-in-env.sh b/script/run-in-env.sh deleted file mode 100755 index 2e05fe1d17..0000000000 --- a/script/run-in-env.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env sh -set -eu - -my_path=$(git rev-parse --show-toplevel) - -for venv in venv .venv .; do - if [ -f "${my_path}/${venv}/bin/activate" ]; then - . "${my_path}/${venv}/bin/activate" - break - fi -done - -exec "$@" From 8c6c45e6c197eb45400f6a2dd17568afe3977749 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Thu, 16 Jan 2025 15:43:41 -0600 Subject: [PATCH 57/86] [http_request] Bugfix: run update function in a task (#8018) --- .../http_request/http_request_idf.cpp | 24 ++- .../http_request/http_request_idf.h | 3 + .../update/http_request_update.cpp | 147 ++++++++++-------- .../http_request/update/http_request_update.h | 9 ++ 4 files changed, 115 insertions(+), 68 deletions(-) diff --git a/esphome/components/http_request/http_request_idf.cpp b/esphome/components/http_request/http_request_idf.cpp index b449f046ee..66f064c2ce 100644 --- a/esphome/components/http_request/http_request_idf.cpp +++ b/esphome/components/http_request/http_request_idf.cpp @@ -12,6 +12,8 @@ #include "esp_crt_bundle.h" #endif +#include "esp_task_wdt.h" + namespace esphome { namespace http_request { @@ -117,11 +119,11 @@ std::shared_ptr HttpRequestIDF::start(std::string url, std::strin return nullptr; } - App.feed_wdt(); + container->feed_wdt(); container->content_length = esp_http_client_fetch_headers(client); - App.feed_wdt(); + container->feed_wdt(); container->status_code = esp_http_client_get_status_code(client); - App.feed_wdt(); + container->feed_wdt(); if (is_success(container->status_code)) { container->duration_ms = millis() - start; return container; @@ -151,11 +153,11 @@ std::shared_ptr HttpRequestIDF::start(std::string url, std::strin return nullptr; } - App.feed_wdt(); + container->feed_wdt(); container->content_length = esp_http_client_fetch_headers(client); - App.feed_wdt(); + container->feed_wdt(); container->status_code = esp_http_client_get_status_code(client); - App.feed_wdt(); + container->feed_wdt(); if (is_success(container->status_code)) { container->duration_ms = millis() - start; return container; @@ -185,8 +187,9 @@ int HttpContainerIDF::read(uint8_t *buf, size_t max_len) { return 0; } - App.feed_wdt(); + this->feed_wdt(); int read_len = esp_http_client_read(this->client_, (char *) buf, bufsize); + this->feed_wdt(); this->bytes_read_ += read_len; this->duration_ms += (millis() - start); @@ -201,6 +204,13 @@ void HttpContainerIDF::end() { esp_http_client_cleanup(this->client_); } +void HttpContainerIDF::feed_wdt() { + // Tests to see if the executing task has a watchdog timer attached + if (esp_task_wdt_status(nullptr) == ESP_OK) { + App.feed_wdt(); + } +} + } // namespace http_request } // namespace esphome diff --git a/esphome/components/http_request/http_request_idf.h b/esphome/components/http_request/http_request_idf.h index 431794924b..2ed50698b9 100644 --- a/esphome/components/http_request/http_request_idf.h +++ b/esphome/components/http_request/http_request_idf.h @@ -18,6 +18,9 @@ class HttpContainerIDF : public HttpContainer { int read(uint8_t *buf, size_t max_len) override; void end() override; + /// @brief Feeds the watchdog timer if the executing task has one attached + void feed_wdt(); + protected: esp_http_client_handle_t client_; }; diff --git a/esphome/components/http_request/update/http_request_update.cpp b/esphome/components/http_request/update/http_request_update.cpp index 0e0966c22b..d683495ac6 100644 --- a/esphome/components/http_request/update/http_request_update.cpp +++ b/esphome/components/http_request/update/http_request_update.cpp @@ -9,6 +9,13 @@ namespace esphome { namespace http_request { +// The update function runs in a task only on ESP32s. +#ifdef USE_ESP32 +#define UPDATE_RETURN vTaskDelete(nullptr) // Delete the current update task +#else +#define UPDATE_RETURN return +#endif + static const char *const TAG = "http_request.update"; static const size_t MAX_READ_SIZE = 256; @@ -29,113 +36,131 @@ void HttpRequestUpdate::setup() { } void HttpRequestUpdate::update() { - auto container = this->request_parent_->get(this->source_url_); +#ifdef USE_ESP32 + xTaskCreate(HttpRequestUpdate::update_task, "update_task", 8192, (void *) this, 1, &this->update_task_handle_); +#else + this->update_task(this); +#endif +} + +void HttpRequestUpdate::update_task(void *params) { + HttpRequestUpdate *this_update = (HttpRequestUpdate *) params; + + auto container = this_update->request_parent_->get(this_update->source_url_); if (container == nullptr || container->status_code != HTTP_STATUS_OK) { - std::string msg = str_sprintf("Failed to fetch manifest from %s", this->source_url_.c_str()); - this->status_set_error(msg.c_str()); - return; + std::string msg = str_sprintf("Failed to fetch manifest from %s", this_update->source_url_.c_str()); + this_update->status_set_error(msg.c_str()); + UPDATE_RETURN; } ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); uint8_t *data = allocator.allocate(container->content_length); if (data == nullptr) { std::string msg = str_sprintf("Failed to allocate %d bytes for manifest", container->content_length); - this->status_set_error(msg.c_str()); + this_update->status_set_error(msg.c_str()); container->end(); - return; + UPDATE_RETURN; } size_t read_index = 0; while (container->get_bytes_read() < container->content_length) { int read_bytes = container->read(data + read_index, MAX_READ_SIZE); - App.feed_wdt(); yield(); read_index += read_bytes; } - std::string response((char *) data, read_index); - allocator.deallocate(data, container->content_length); + bool valid = false; + { // Ensures the response string falls out of scope and deallocates before the task ends + std::string response((char *) data, read_index); + allocator.deallocate(data, container->content_length); - container->end(); + container->end(); + container.reset(); // Release ownership of the container's shared_ptr - bool valid = json::parse_json(response, [this](JsonObject root) -> bool { - if (!root.containsKey("name") || !root.containsKey("version") || !root.containsKey("builds")) { - ESP_LOGE(TAG, "Manifest does not contain required fields"); - return false; - } - this->update_info_.title = root["name"].as(); - this->update_info_.latest_version = root["version"].as(); - - for (auto build : root["builds"].as()) { - if (!build.containsKey("chipFamily")) { + valid = json::parse_json(response, [this_update](JsonObject root) -> bool { + if (!root.containsKey("name") || !root.containsKey("version") || !root.containsKey("builds")) { ESP_LOGE(TAG, "Manifest does not contain required fields"); return false; } - if (build["chipFamily"] == ESPHOME_VARIANT) { - if (!build.containsKey("ota")) { + this_update->update_info_.title = root["name"].as(); + this_update->update_info_.latest_version = root["version"].as(); + + for (auto build : root["builds"].as()) { + if (!build.containsKey("chipFamily")) { ESP_LOGE(TAG, "Manifest does not contain required fields"); return false; } - auto ota = build["ota"]; - if (!ota.containsKey("path") || !ota.containsKey("md5")) { - ESP_LOGE(TAG, "Manifest does not contain required fields"); - return false; + if (build["chipFamily"] == ESPHOME_VARIANT) { + if (!build.containsKey("ota")) { + ESP_LOGE(TAG, "Manifest does not contain required fields"); + return false; + } + auto ota = build["ota"]; + if (!ota.containsKey("path") || !ota.containsKey("md5")) { + ESP_LOGE(TAG, "Manifest does not contain required fields"); + return false; + } + this_update->update_info_.firmware_url = ota["path"].as(); + this_update->update_info_.md5 = ota["md5"].as(); + + if (ota.containsKey("summary")) + this_update->update_info_.summary = ota["summary"].as(); + if (ota.containsKey("release_url")) + this_update->update_info_.release_url = ota["release_url"].as(); + + return true; } - this->update_info_.firmware_url = ota["path"].as(); - this->update_info_.md5 = ota["md5"].as(); - - if (ota.containsKey("summary")) - this->update_info_.summary = ota["summary"].as(); - if (ota.containsKey("release_url")) - this->update_info_.release_url = ota["release_url"].as(); - - return true; } - } - return false; - }); + return false; + }); + } if (!valid) { - std::string msg = str_sprintf("Failed to parse JSON from %s", this->source_url_.c_str()); - this->status_set_error(msg.c_str()); - return; + std::string msg = str_sprintf("Failed to parse JSON from %s", this_update->source_url_.c_str()); + this_update->status_set_error(msg.c_str()); + UPDATE_RETURN; } - // Merge source_url_ and this->update_info_.firmware_url - if (this->update_info_.firmware_url.find("http") == std::string::npos) { - std::string path = this->update_info_.firmware_url; + // Merge source_url_ and this_update->update_info_.firmware_url + if (this_update->update_info_.firmware_url.find("http") == std::string::npos) { + std::string path = this_update->update_info_.firmware_url; if (path[0] == '/') { - std::string domain = this->source_url_.substr(0, this->source_url_.find('/', 8)); - this->update_info_.firmware_url = domain + path; + std::string domain = this_update->source_url_.substr(0, this_update->source_url_.find('/', 8)); + this_update->update_info_.firmware_url = domain + path; } else { - std::string domain = this->source_url_.substr(0, this->source_url_.rfind('/') + 1); - this->update_info_.firmware_url = domain + path; + std::string domain = this_update->source_url_.substr(0, this_update->source_url_.rfind('/') + 1); + this_update->update_info_.firmware_url = domain + path; } } - std::string current_version; + { // Ensures the current version string falls out of scope and deallocates before the task ends + std::string current_version; #ifdef ESPHOME_PROJECT_VERSION - current_version = ESPHOME_PROJECT_VERSION; + current_version = ESPHOME_PROJECT_VERSION; #else - current_version = ESPHOME_VERSION; + current_version = ESPHOME_VERSION; #endif - this->update_info_.current_version = current_version; - - if (this->update_info_.latest_version.empty() || this->update_info_.latest_version == update_info_.current_version) { - this->state_ = update::UPDATE_STATE_NO_UPDATE; - } else { - this->state_ = update::UPDATE_STATE_AVAILABLE; + this_update->update_info_.current_version = current_version; } - this->update_info_.has_progress = false; - this->update_info_.progress = 0.0f; + if (this_update->update_info_.latest_version.empty() || + this_update->update_info_.latest_version == this_update->update_info_.current_version) { + this_update->state_ = update::UPDATE_STATE_NO_UPDATE; + } else { + this_update->state_ = update::UPDATE_STATE_AVAILABLE; + } - this->status_clear_error(); - this->publish_state(); + this_update->update_info_.has_progress = false; + this_update->update_info_.progress = 0.0f; + + this_update->status_clear_error(); + this_update->publish_state(); + + UPDATE_RETURN; } void HttpRequestUpdate::perform(bool force) { diff --git a/esphome/components/http_request/update/http_request_update.h b/esphome/components/http_request/update/http_request_update.h index 45c7e6a447..e05fdb0cc2 100644 --- a/esphome/components/http_request/update/http_request_update.h +++ b/esphome/components/http_request/update/http_request_update.h @@ -7,6 +7,10 @@ #include "esphome/components/http_request/ota/ota_http_request.h" #include "esphome/components/update/update_entity.h" +#ifdef USE_ESP32 +#include +#endif + namespace esphome { namespace http_request { @@ -29,6 +33,11 @@ class HttpRequestUpdate : public update::UpdateEntity, public PollingComponent { HttpRequestComponent *request_parent_; OtaHttpRequestComponent *ota_parent_; std::string source_url_; + + static void update_task(void *params); +#ifdef USE_ESP32 + TaskHandle_t update_task_handle_{nullptr}; +#endif }; } // namespace http_request From 820e3488d03f3644a5661156d78995c2d964a092 Mon Sep 17 00:00:00 2001 From: Katherine Whitlock Date: Thu, 16 Jan 2025 16:44:26 -0500 Subject: [PATCH 58/86] Remove black-formatter from pre-commit hooks (#8097) --- .pre-commit-config.yaml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5308a2c047..adf0ac6fc2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,14 +11,6 @@ repos: args: [--fix] # Run the formatter. - id: ruff-format - - repo: https://github.com/psf/black-pre-commit-mirror - rev: 24.4.2 - hooks: - - id: black - args: - - --safe - - --quiet - files: ^((esphome|script|tests)/.+)?[^/]+\.py$ - repo: https://github.com/PyCQA/flake8 rev: 6.1.0 hooks: From 07be7ad7e2f874eaedcfc20ea68a9f8c33b72494 Mon Sep 17 00:00:00 2001 From: j-sepul <117353037+j-sepul@users.noreply.github.com> Date: Thu, 16 Jan 2025 23:08:04 +0100 Subject: [PATCH 59/86] Increase Daly-BMS coltage cells from 16 to 18 cells (#8057) --- esphome/components/daly_bms/daly_bms.cpp | 6 ++++++ esphome/components/daly_bms/daly_bms.h | 2 ++ esphome/components/daly_bms/sensor.py | 6 ++++++ 3 files changed, 14 insertions(+) diff --git a/esphome/components/daly_bms/daly_bms.cpp b/esphome/components/daly_bms/daly_bms.cpp index 8f6fc0fb57..929f31e008 100644 --- a/esphome/components/daly_bms/daly_bms.cpp +++ b/esphome/components/daly_bms/daly_bms.cpp @@ -298,6 +298,12 @@ void DalyBmsComponent::decode_data_(std::vector data) { if (this->cell_16_voltage_sensor_) { this->cell_16_voltage_sensor_->publish_state((float) encode_uint16(it[5], it[6]) / 1000); } + if (this->cell_17_voltage_sensor_) { + this->cell_17_voltage_sensor_->publish_state((float) encode_uint16(it[7], it[8]) / 1000); + } + if (this->cell_18_voltage_sensor_) { + this->cell_18_voltage_sensor_->publish_state((float) encode_uint16(it[9], it[10]) / 1000); + } break; } break; diff --git a/esphome/components/daly_bms/daly_bms.h b/esphome/components/daly_bms/daly_bms.h index 52ea30ecde..e6d476bcdd 100644 --- a/esphome/components/daly_bms/daly_bms.h +++ b/esphome/components/daly_bms/daly_bms.h @@ -54,6 +54,8 @@ class DalyBmsComponent : public PollingComponent, public uart::UARTDevice { SUB_SENSOR(cell_14_voltage) SUB_SENSOR(cell_15_voltage) SUB_SENSOR(cell_16_voltage) + SUB_SENSOR(cell_17_voltage) + SUB_SENSOR(cell_18_voltage) #endif #ifdef USE_TEXT_SENSOR diff --git a/esphome/components/daly_bms/sensor.py b/esphome/components/daly_bms/sensor.py index c447fbd8a2..6d78946a02 100644 --- a/esphome/components/daly_bms/sensor.py +++ b/esphome/components/daly_bms/sensor.py @@ -52,6 +52,8 @@ CONF_CELL_13_VOLTAGE = "cell_13_voltage" CONF_CELL_14_VOLTAGE = "cell_14_voltage" CONF_CELL_15_VOLTAGE = "cell_15_voltage" CONF_CELL_16_VOLTAGE = "cell_16_voltage" +CONF_CELL_17_VOLTAGE = "cell_17_voltage" +CONF_CELL_18_VOLTAGE = "cell_18_voltage" ICON_CURRENT_DC = "mdi:current-dc" ICON_BATTERY_OUTLINE = "mdi:battery-outline" ICON_THERMOMETER_CHEVRON_UP = "mdi:thermometer-chevron-up" @@ -92,6 +94,8 @@ TYPES = [ CONF_CELL_14_VOLTAGE, CONF_CELL_15_VOLTAGE, CONF_CELL_16_VOLTAGE, + CONF_CELL_17_VOLTAGE, + CONF_CELL_18_VOLTAGE, ] CELL_VOLTAGE_SCHEMA = sensor.sensor_schema( @@ -212,6 +216,8 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_CELL_14_VOLTAGE): CELL_VOLTAGE_SCHEMA, cv.Optional(CONF_CELL_15_VOLTAGE): CELL_VOLTAGE_SCHEMA, cv.Optional(CONF_CELL_16_VOLTAGE): CELL_VOLTAGE_SCHEMA, + cv.Optional(CONF_CELL_17_VOLTAGE): CELL_VOLTAGE_SCHEMA, + cv.Optional(CONF_CELL_18_VOLTAGE): CELL_VOLTAGE_SCHEMA, } ).extend(cv.COMPONENT_SCHEMA) ) From abdd6b232f922b640ef93c335f71e9fc93f4cc2b Mon Sep 17 00:00:00 2001 From: Piotr Szulc Date: Mon, 13 Jan 2025 05:31:01 +0100 Subject: [PATCH 60/86] Fixed libretiny preference wrongly detecting change in the data to store (#7990) --- esphome/components/libretiny/preferences.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/libretiny/preferences.cpp b/esphome/components/libretiny/preferences.cpp index ceeb30baf5..a090f42aa7 100644 --- a/esphome/components/libretiny/preferences.cpp +++ b/esphome/components/libretiny/preferences.cpp @@ -147,7 +147,7 @@ class LibreTinyPreferences : public ESPPreferences { ESP_LOGV(TAG, "fdb_kv_get_obj('%s'): nullptr - the key might not be set yet", to_save.key.c_str()); return true; } - stored_data.data.reserve(kv.value_len); + stored_data.data.resize(kv.value_len); fdb_blob_make(&blob, stored_data.data.data(), kv.value_len); size_t actual_len = fdb_kv_get_blob(db, to_save.key.c_str(), &blob); if (actual_len != kv.value_len) { From 03c36920ffce80b4cf2505d2196ac8dca4c441eb Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Thu, 16 Jan 2025 15:43:41 -0600 Subject: [PATCH 61/86] [http_request] Bugfix: run update function in a task (#8018) --- .../http_request/http_request_idf.cpp | 24 ++- .../http_request/http_request_idf.h | 3 + .../update/http_request_update.cpp | 147 ++++++++++-------- .../http_request/update/http_request_update.h | 9 ++ 4 files changed, 115 insertions(+), 68 deletions(-) diff --git a/esphome/components/http_request/http_request_idf.cpp b/esphome/components/http_request/http_request_idf.cpp index b449f046ee..66f064c2ce 100644 --- a/esphome/components/http_request/http_request_idf.cpp +++ b/esphome/components/http_request/http_request_idf.cpp @@ -12,6 +12,8 @@ #include "esp_crt_bundle.h" #endif +#include "esp_task_wdt.h" + namespace esphome { namespace http_request { @@ -117,11 +119,11 @@ std::shared_ptr HttpRequestIDF::start(std::string url, std::strin return nullptr; } - App.feed_wdt(); + container->feed_wdt(); container->content_length = esp_http_client_fetch_headers(client); - App.feed_wdt(); + container->feed_wdt(); container->status_code = esp_http_client_get_status_code(client); - App.feed_wdt(); + container->feed_wdt(); if (is_success(container->status_code)) { container->duration_ms = millis() - start; return container; @@ -151,11 +153,11 @@ std::shared_ptr HttpRequestIDF::start(std::string url, std::strin return nullptr; } - App.feed_wdt(); + container->feed_wdt(); container->content_length = esp_http_client_fetch_headers(client); - App.feed_wdt(); + container->feed_wdt(); container->status_code = esp_http_client_get_status_code(client); - App.feed_wdt(); + container->feed_wdt(); if (is_success(container->status_code)) { container->duration_ms = millis() - start; return container; @@ -185,8 +187,9 @@ int HttpContainerIDF::read(uint8_t *buf, size_t max_len) { return 0; } - App.feed_wdt(); + this->feed_wdt(); int read_len = esp_http_client_read(this->client_, (char *) buf, bufsize); + this->feed_wdt(); this->bytes_read_ += read_len; this->duration_ms += (millis() - start); @@ -201,6 +204,13 @@ void HttpContainerIDF::end() { esp_http_client_cleanup(this->client_); } +void HttpContainerIDF::feed_wdt() { + // Tests to see if the executing task has a watchdog timer attached + if (esp_task_wdt_status(nullptr) == ESP_OK) { + App.feed_wdt(); + } +} + } // namespace http_request } // namespace esphome diff --git a/esphome/components/http_request/http_request_idf.h b/esphome/components/http_request/http_request_idf.h index 431794924b..2ed50698b9 100644 --- a/esphome/components/http_request/http_request_idf.h +++ b/esphome/components/http_request/http_request_idf.h @@ -18,6 +18,9 @@ class HttpContainerIDF : public HttpContainer { int read(uint8_t *buf, size_t max_len) override; void end() override; + /// @brief Feeds the watchdog timer if the executing task has one attached + void feed_wdt(); + protected: esp_http_client_handle_t client_; }; diff --git a/esphome/components/http_request/update/http_request_update.cpp b/esphome/components/http_request/update/http_request_update.cpp index 0e0966c22b..d683495ac6 100644 --- a/esphome/components/http_request/update/http_request_update.cpp +++ b/esphome/components/http_request/update/http_request_update.cpp @@ -9,6 +9,13 @@ namespace esphome { namespace http_request { +// The update function runs in a task only on ESP32s. +#ifdef USE_ESP32 +#define UPDATE_RETURN vTaskDelete(nullptr) // Delete the current update task +#else +#define UPDATE_RETURN return +#endif + static const char *const TAG = "http_request.update"; static const size_t MAX_READ_SIZE = 256; @@ -29,113 +36,131 @@ void HttpRequestUpdate::setup() { } void HttpRequestUpdate::update() { - auto container = this->request_parent_->get(this->source_url_); +#ifdef USE_ESP32 + xTaskCreate(HttpRequestUpdate::update_task, "update_task", 8192, (void *) this, 1, &this->update_task_handle_); +#else + this->update_task(this); +#endif +} + +void HttpRequestUpdate::update_task(void *params) { + HttpRequestUpdate *this_update = (HttpRequestUpdate *) params; + + auto container = this_update->request_parent_->get(this_update->source_url_); if (container == nullptr || container->status_code != HTTP_STATUS_OK) { - std::string msg = str_sprintf("Failed to fetch manifest from %s", this->source_url_.c_str()); - this->status_set_error(msg.c_str()); - return; + std::string msg = str_sprintf("Failed to fetch manifest from %s", this_update->source_url_.c_str()); + this_update->status_set_error(msg.c_str()); + UPDATE_RETURN; } ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); uint8_t *data = allocator.allocate(container->content_length); if (data == nullptr) { std::string msg = str_sprintf("Failed to allocate %d bytes for manifest", container->content_length); - this->status_set_error(msg.c_str()); + this_update->status_set_error(msg.c_str()); container->end(); - return; + UPDATE_RETURN; } size_t read_index = 0; while (container->get_bytes_read() < container->content_length) { int read_bytes = container->read(data + read_index, MAX_READ_SIZE); - App.feed_wdt(); yield(); read_index += read_bytes; } - std::string response((char *) data, read_index); - allocator.deallocate(data, container->content_length); + bool valid = false; + { // Ensures the response string falls out of scope and deallocates before the task ends + std::string response((char *) data, read_index); + allocator.deallocate(data, container->content_length); - container->end(); + container->end(); + container.reset(); // Release ownership of the container's shared_ptr - bool valid = json::parse_json(response, [this](JsonObject root) -> bool { - if (!root.containsKey("name") || !root.containsKey("version") || !root.containsKey("builds")) { - ESP_LOGE(TAG, "Manifest does not contain required fields"); - return false; - } - this->update_info_.title = root["name"].as(); - this->update_info_.latest_version = root["version"].as(); - - for (auto build : root["builds"].as()) { - if (!build.containsKey("chipFamily")) { + valid = json::parse_json(response, [this_update](JsonObject root) -> bool { + if (!root.containsKey("name") || !root.containsKey("version") || !root.containsKey("builds")) { ESP_LOGE(TAG, "Manifest does not contain required fields"); return false; } - if (build["chipFamily"] == ESPHOME_VARIANT) { - if (!build.containsKey("ota")) { + this_update->update_info_.title = root["name"].as(); + this_update->update_info_.latest_version = root["version"].as(); + + for (auto build : root["builds"].as()) { + if (!build.containsKey("chipFamily")) { ESP_LOGE(TAG, "Manifest does not contain required fields"); return false; } - auto ota = build["ota"]; - if (!ota.containsKey("path") || !ota.containsKey("md5")) { - ESP_LOGE(TAG, "Manifest does not contain required fields"); - return false; + if (build["chipFamily"] == ESPHOME_VARIANT) { + if (!build.containsKey("ota")) { + ESP_LOGE(TAG, "Manifest does not contain required fields"); + return false; + } + auto ota = build["ota"]; + if (!ota.containsKey("path") || !ota.containsKey("md5")) { + ESP_LOGE(TAG, "Manifest does not contain required fields"); + return false; + } + this_update->update_info_.firmware_url = ota["path"].as(); + this_update->update_info_.md5 = ota["md5"].as(); + + if (ota.containsKey("summary")) + this_update->update_info_.summary = ota["summary"].as(); + if (ota.containsKey("release_url")) + this_update->update_info_.release_url = ota["release_url"].as(); + + return true; } - this->update_info_.firmware_url = ota["path"].as(); - this->update_info_.md5 = ota["md5"].as(); - - if (ota.containsKey("summary")) - this->update_info_.summary = ota["summary"].as(); - if (ota.containsKey("release_url")) - this->update_info_.release_url = ota["release_url"].as(); - - return true; } - } - return false; - }); + return false; + }); + } if (!valid) { - std::string msg = str_sprintf("Failed to parse JSON from %s", this->source_url_.c_str()); - this->status_set_error(msg.c_str()); - return; + std::string msg = str_sprintf("Failed to parse JSON from %s", this_update->source_url_.c_str()); + this_update->status_set_error(msg.c_str()); + UPDATE_RETURN; } - // Merge source_url_ and this->update_info_.firmware_url - if (this->update_info_.firmware_url.find("http") == std::string::npos) { - std::string path = this->update_info_.firmware_url; + // Merge source_url_ and this_update->update_info_.firmware_url + if (this_update->update_info_.firmware_url.find("http") == std::string::npos) { + std::string path = this_update->update_info_.firmware_url; if (path[0] == '/') { - std::string domain = this->source_url_.substr(0, this->source_url_.find('/', 8)); - this->update_info_.firmware_url = domain + path; + std::string domain = this_update->source_url_.substr(0, this_update->source_url_.find('/', 8)); + this_update->update_info_.firmware_url = domain + path; } else { - std::string domain = this->source_url_.substr(0, this->source_url_.rfind('/') + 1); - this->update_info_.firmware_url = domain + path; + std::string domain = this_update->source_url_.substr(0, this_update->source_url_.rfind('/') + 1); + this_update->update_info_.firmware_url = domain + path; } } - std::string current_version; + { // Ensures the current version string falls out of scope and deallocates before the task ends + std::string current_version; #ifdef ESPHOME_PROJECT_VERSION - current_version = ESPHOME_PROJECT_VERSION; + current_version = ESPHOME_PROJECT_VERSION; #else - current_version = ESPHOME_VERSION; + current_version = ESPHOME_VERSION; #endif - this->update_info_.current_version = current_version; - - if (this->update_info_.latest_version.empty() || this->update_info_.latest_version == update_info_.current_version) { - this->state_ = update::UPDATE_STATE_NO_UPDATE; - } else { - this->state_ = update::UPDATE_STATE_AVAILABLE; + this_update->update_info_.current_version = current_version; } - this->update_info_.has_progress = false; - this->update_info_.progress = 0.0f; + if (this_update->update_info_.latest_version.empty() || + this_update->update_info_.latest_version == this_update->update_info_.current_version) { + this_update->state_ = update::UPDATE_STATE_NO_UPDATE; + } else { + this_update->state_ = update::UPDATE_STATE_AVAILABLE; + } - this->status_clear_error(); - this->publish_state(); + this_update->update_info_.has_progress = false; + this_update->update_info_.progress = 0.0f; + + this_update->status_clear_error(); + this_update->publish_state(); + + UPDATE_RETURN; } void HttpRequestUpdate::perform(bool force) { diff --git a/esphome/components/http_request/update/http_request_update.h b/esphome/components/http_request/update/http_request_update.h index 45c7e6a447..e05fdb0cc2 100644 --- a/esphome/components/http_request/update/http_request_update.h +++ b/esphome/components/http_request/update/http_request_update.h @@ -7,6 +7,10 @@ #include "esphome/components/http_request/ota/ota_http_request.h" #include "esphome/components/update/update_entity.h" +#ifdef USE_ESP32 +#include +#endif + namespace esphome { namespace http_request { @@ -29,6 +33,11 @@ class HttpRequestUpdate : public update::UpdateEntity, public PollingComponent { HttpRequestComponent *request_parent_; OtaHttpRequestComponent *ota_parent_; std::string source_url_; + + static void update_task(void *params); +#ifdef USE_ESP32 + TaskHandle_t update_task_handle_{nullptr}; +#endif }; } // namespace http_request From 7666581c547612984eaa7fb049ac0b5985c68066 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 17 Jan 2025 12:24:22 +1300 Subject: [PATCH 62/86] Bump version to 2024.12.3 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 10ad9454fd..a86e20c2e5 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2024.12.2" +__version__ = "2024.12.3" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From c2423b18cbd279d4545ca672bebb665944e4b378 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 13 Jan 2025 14:32:10 +1300 Subject: [PATCH 63/86] Bump python3-setuptools to 66.1.1-1+deb12u1 (#8074) --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 0bb558d35e..429f5c4a1f 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -29,7 +29,7 @@ RUN \ # Use pinned versions so that we get updates with build caching && apt-get install -y --no-install-recommends \ python3-pip=23.0.1+dfsg-1 \ - python3-setuptools=66.1.1-1 \ + python3-setuptools=66.1.1-1+deb12u1 \ python3-venv=3.11.2-1+b1 \ python3-wheel=0.38.4-2 \ iputils-ping=3:20221126-1+deb12u1 \ From 5a01670803ff6e78a5e605613f4855c6c16e33e1 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 17 Jan 2025 13:40:12 +1300 Subject: [PATCH 64/86] Bump version to 2024.12.4 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index a86e20c2e5..b3d64997c4 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2024.12.3" +__version__ = "2024.12.4" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 47a0ec467abd812e85f6a65946f3922f31ac5981 Mon Sep 17 00:00:00 2001 From: guillempages Date: Sun, 19 Jan 2025 22:34:38 +0100 Subject: [PATCH 65/86] [image]Rename option "use_transparency" (#8113) --- esphome/components/image/__init__.py | 14 ++++++------- esphome/components/online_image/__init__.py | 4 ++-- tests/components/animation/common.yaml | 4 ++-- tests/components/image/common.yaml | 22 ++++++++++----------- tests/components/image/test.host.yaml | 14 ++++++------- tests/components/online_image/common.yaml | 6 +++--- 6 files changed, 31 insertions(+), 33 deletions(-) diff --git a/esphome/components/image/__init__.py b/esphome/components/image/__init__.py index a503e8f471..7cdce757be 100644 --- a/esphome/components/image/__init__.py +++ b/esphome/components/image/__init__.py @@ -282,7 +282,7 @@ IMAGE_TYPE = { TransparencyType = image_ns.enum("TransparencyType") -CONF_USE_TRANSPARENCY = "use_transparency" +CONF_TRANSPARENCY = "transparency" # If the MDI file cannot be downloaded within this time, abort. IMAGE_DOWNLOAD_TIMEOUT = 30 # seconds @@ -417,7 +417,7 @@ def validate_type(image_types): def validate_settings(value): type = value[CONF_TYPE] - transparency = value[CONF_USE_TRANSPARENCY].lower() + transparency = value[CONF_TRANSPARENCY].lower() allow_config = IMAGE_TYPE[type].allow_config if transparency not in allow_config: raise cv.Invalid( @@ -458,9 +458,7 @@ BASE_SCHEMA = cv.Schema( IMAGE_SCHEMA = BASE_SCHEMA.extend( { cv.Required(CONF_TYPE): validate_type(IMAGE_TYPE), - cv.Optional( - CONF_USE_TRANSPARENCY, default=CONF_OPAQUE - ): validate_transparency(), + cv.Optional(CONF_TRANSPARENCY, default=CONF_OPAQUE): validate_transparency(), } ) @@ -476,7 +474,7 @@ def typed_image_schema(image_type): BASE_SCHEMA.extend( { cv.Optional( - CONF_USE_TRANSPARENCY, default=t + CONF_TRANSPARENCY, default=t ): validate_transparency((t,)), cv.Optional(CONF_TYPE, default=image_type): validate_type( (image_type,) @@ -494,7 +492,7 @@ def typed_image_schema(image_type): BASE_SCHEMA.extend( { cv.Optional( - CONF_USE_TRANSPARENCY, default=CONF_OPAQUE + CONF_TRANSPARENCY, default=CONF_OPAQUE ): validate_transparency(), cv.Optional(CONF_TYPE, default=image_type): validate_type( (image_type,) @@ -556,7 +554,7 @@ async def write_image(config, all_frames=False): else Image.Dither.FLOYDSTEINBERG ) type = config[CONF_TYPE] - transparency = config[CONF_USE_TRANSPARENCY] + transparency = config[CONF_TRANSPARENCY] invert_alpha = config[CONF_INVERT_ALPHA] frame_count = 1 if all_frames: diff --git a/esphome/components/online_image/__init__.py b/esphome/components/online_image/__init__.py index d1915c7364..ca4eefea6f 100644 --- a/esphome/components/online_image/__init__.py +++ b/esphome/components/online_image/__init__.py @@ -5,7 +5,7 @@ import esphome.codegen as cg from esphome.components.http_request import CONF_HTTP_REQUEST_ID, HttpRequestComponent from esphome.components.image import ( CONF_INVERT_ALPHA, - CONF_USE_TRANSPARENCY, + CONF_TRANSPARENCY, IMAGE_SCHEMA, Image_, get_image_type_enum, @@ -168,7 +168,7 @@ async def to_code(config): url = config[CONF_URL] width, height = config.get(CONF_RESIZE, (0, 0)) - transparent = get_transparency_enum(config[CONF_USE_TRANSPARENCY]) + transparent = get_transparency_enum(config[CONF_TRANSPARENCY]) var = cg.new_Pvariable( config[CONF_ID], diff --git a/tests/components/animation/common.yaml b/tests/components/animation/common.yaml index c0f04fe768..8bb2a2f4d8 100644 --- a/tests/components/animation/common.yaml +++ b/tests/components/animation/common.yaml @@ -2,12 +2,12 @@ animation: - id: rgb565_animation file: $component_dir/anim.gif type: RGB565 - use_transparency: opaque + transparency: opaque resize: 50x50 - id: rgb_animation file: $component_dir/anim.apng type: RGB - use_transparency: chroma_key + transparency: chroma_key resize: 50x50 - id: grayscale_animation file: $component_dir/anim.apng diff --git a/tests/components/image/common.yaml b/tests/components/image/common.yaml index fdb0493d2a..4c9b9ed670 100644 --- a/tests/components/image/common.yaml +++ b/tests/components/image/common.yaml @@ -6,54 +6,54 @@ image: - id: transparent_transparent_image file: ../../pnglogo.png type: BINARY - use_transparency: chroma_key + transparency: chroma_key - id: rgba_image file: ../../pnglogo.png type: RGB - use_transparency: alpha_channel + transparency: alpha_channel resize: 50x50 - id: rgb24_image file: ../../pnglogo.png type: RGB - use_transparency: chroma_key + transparency: chroma_key - id: rgb_image file: ../../pnglogo.png type: RGB - use_transparency: opaque + transparency: opaque - id: rgb565_image file: ../../pnglogo.png type: RGB565 - use_transparency: opaque + transparency: opaque - id: rgb565_ck_image file: ../../pnglogo.png type: RGB565 - use_transparency: chroma_key + transparency: chroma_key - id: rgb565_alpha_image file: ../../pnglogo.png type: RGB565 - use_transparency: alpha_channel + transparency: alpha_channel - id: grayscale_alpha_image file: ../../pnglogo.png type: grayscale - use_transparency: alpha_channel + transparency: alpha_channel resize: 50x50 - id: grayscale_ck_image file: ../../pnglogo.png type: grayscale - use_transparency: chroma_key + transparency: chroma_key - id: grayscale_image file: ../../pnglogo.png type: grayscale - use_transparency: opaque + transparency: opaque - id: web_svg_image file: https://raw.githubusercontent.com/esphome/esphome-docs/a62d7ab193c1a464ed791670170c7d518189109b/images/logo.svg resize: 256x48 type: BINARY - use_transparency: chroma_key + transparency: chroma_key - id: web_tiff_image file: https://upload.wikimedia.org/wikipedia/commons/b/b6/SIPI_Jelly_Beans_4.1.07.tiff type: RGB diff --git a/tests/components/image/test.host.yaml b/tests/components/image/test.host.yaml index 61ecd5e374..0411195e2a 100644 --- a/tests/components/image/test.host.yaml +++ b/tests/components/image/test.host.yaml @@ -12,7 +12,7 @@ image: dither: FloydSteinberg - id: transparent_transparent_image file: ../../pnglogo.png - use_transparency: chroma_key + transparency: chroma_key rgb: alpha_channel: - id: rgba_image @@ -28,21 +28,21 @@ image: rgb565: - id: rgb565_image file: ../../pnglogo.png - use_transparency: opaque + transparency: opaque - id: rgb565_ck_image file: ../../pnglogo.png - use_transparency: chroma_key + transparency: chroma_key - id: rgb565_alpha_image file: ../../pnglogo.png - use_transparency: alpha_channel + transparency: alpha_channel grayscale: - id: grayscale_alpha_image file: ../../pnglogo.png - use_transparency: alpha_channel + transparency: alpha_channel resize: 50x50 - id: grayscale_ck_image file: ../../pnglogo.png - use_transparency: chroma_key + transparency: chroma_key - id: grayscale_image file: ../../pnglogo.png - use_transparency: opaque + transparency: opaque diff --git a/tests/components/online_image/common.yaml b/tests/components/online_image/common.yaml index 81f43e9fdc..6c161e4f20 100644 --- a/tests/components/online_image/common.yaml +++ b/tests/components/online_image/common.yaml @@ -14,18 +14,18 @@ online_image: - id: online_binary_transparent_image url: http://www.libpng.org/pub/png/img_png/pnglogo-blk-tiny.png type: BINARY - use_transparency: chroma_key + transparency: chroma_key format: png - id: online_rgba_image url: http://www.libpng.org/pub/png/img_png/pnglogo-blk-tiny.png format: PNG type: RGB - use_transparency: alpha_channel + transparency: alpha_channel - id: online_rgb24_image url: http://www.libpng.org/pub/png/img_png/pnglogo-blk-tiny.png format: PNG type: RGB - use_transparency: chroma_key + transparency: chroma_key # Check the set_url action esphome: From 75026be9519a17f3f502dcaea3f21ec5f0c2f917 Mon Sep 17 00:00:00 2001 From: guillempages Date: Sun, 19 Jan 2025 23:16:37 +0100 Subject: [PATCH 66/86] [online_image] Use RAMAllocator (#8114) --- esphome/components/online_image/image_decoder.h | 2 +- esphome/components/online_image/online_image.h | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/esphome/components/online_image/image_decoder.h b/esphome/components/online_image/image_decoder.h index cde7f572e3..7c5175d72d 100644 --- a/esphome/components/online_image/image_decoder.h +++ b/esphome/components/online_image/image_decoder.h @@ -100,7 +100,7 @@ class DownloadBuffer { void reset() { this->unread_ = 0; } protected: - ExternalRAMAllocator allocator_; + RAMAllocator allocator_{}; uint8_t *buffer_; size_t size_; /** Total number of downloaded bytes not yet read. */ diff --git a/esphome/components/online_image/online_image.h b/esphome/components/online_image/online_image.h index e044b4f390..bafd8ba67e 100644 --- a/esphome/components/online_image/online_image.h +++ b/esphome/components/online_image/online_image.h @@ -83,8 +83,7 @@ class OnlineImage : public PollingComponent, protected: bool validate_url_(const std::string &url); - using Allocator = ExternalRAMAllocator; - Allocator allocator_{Allocator::Flags::ALLOW_FAILURE}; + RAMAllocator allocator_{}; uint32_t get_buffer_size_() const { return get_buffer_size_(this->buffer_width_, this->buffer_height_); } int get_buffer_size_(int width, int height) const { return (this->get_bpp() * width + 7u) / 8u * height; } From 98b872abc71b555640a9465fd30e3ba184ab49be Mon Sep 17 00:00:00 2001 From: Mikkel Jeppesen <2756925+Duckle29@users.noreply.github.com> Date: Sun, 19 Jan 2025 23:36:07 +0100 Subject: [PATCH 67/86] Fixed incorrect display dimension (#8110) --- esphome/components/waveshare_epaper/waveshare_213v3.cpp | 6 ++++-- esphome/components/waveshare_epaper/waveshare_epaper.h | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/esphome/components/waveshare_epaper/waveshare_213v3.cpp b/esphome/components/waveshare_epaper/waveshare_213v3.cpp index 196aeed3f7..85d7033d4b 100644 --- a/esphome/components/waveshare_epaper/waveshare_213v3.cpp +++ b/esphome/components/waveshare_epaper/waveshare_213v3.cpp @@ -72,7 +72,8 @@ void WaveshareEPaper2P13InV3::write_buffer_(uint8_t cmd, int top, int bottom) { this->set_window_(top, bottom); this->command(cmd); this->start_data_(); - auto width_bytes = this->get_width_internal() / 8; + + auto width_bytes = this->get_width_controller() / 8; this->write_array(this->buffer_ + top * width_bytes, (bottom - top) * width_bytes); this->end_data_(); } @@ -162,7 +163,8 @@ void WaveshareEPaper2P13InV3::display() { } } -int WaveshareEPaper2P13InV3::get_width_internal() { return 128; } +int WaveshareEPaper2P13InV3::get_width_controller() { return 128; } +int WaveshareEPaper2P13InV3::get_width_internal() { return 122; } int WaveshareEPaper2P13InV3::get_height_internal() { return 250; } diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.h b/esphome/components/waveshare_epaper/waveshare_epaper.h index 4544f7df59..0fc1051268 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.h +++ b/esphome/components/waveshare_epaper/waveshare_epaper.h @@ -811,6 +811,7 @@ class WaveshareEPaper2P13InV3 : public WaveshareEPaper { void initialize() override; protected: + int get_width_controller() override; int get_width_internal() override; int get_height_internal() override; uint32_t idle_timeout_() override; From c3d00b45f7703666fc06289d2ff84b72ce800ea5 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 21 Jan 2025 14:50:04 +1300 Subject: [PATCH 68/86] Update defines.h for esp-idf 5.1.5 (#8117) --- esphome/core/defines.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/core/defines.h b/esphome/core/defines.h index c38a26c6a8..96a05435ed 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -120,7 +120,7 @@ #endif #ifdef USE_ESP_IDF -#define USE_ESP_IDF_VERSION_CODE VERSION_CODE(4, 4, 2) +#define USE_ESP_IDF_VERSION_CODE VERSION_CODE(5, 1, 5) #endif #if defined(USE_ESP32_VARIANT_ESP32S2) From 576dbd6f0c33ccddb2be4dcffd79e39749857b21 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Mon, 20 Jan 2025 20:35:40 -0600 Subject: [PATCH 69/86] [audio_adc] Add new ``audio_adc`` component (#8094) --- CODEOWNERS | 1 + esphome/components/audio_adc/__init__.py | 41 ++++++++++++++ esphome/components/audio_adc/audio_adc.h | 17 ++++++ esphome/components/audio_adc/automation.h | 23 ++++++++ esphome/components/es7210/__init__.py | 67 ----------------------- esphome/components/es7210/audio_adc.py | 51 +++++++++++++++++ esphome/components/es7210/es7210.cpp | 43 ++++++++++++--- esphome/components/es7210/es7210.h | 39 ++++++------- esphome/components/es7210/es7210_const.h | 7 ++- tests/components/es7210/common.yaml | 12 +++- 10 files changed, 200 insertions(+), 101 deletions(-) create mode 100644 esphome/components/audio_adc/__init__.py create mode 100644 esphome/components/audio_adc/audio_adc.h create mode 100644 esphome/components/audio_adc/automation.h create mode 100644 esphome/components/es7210/audio_adc.py diff --git a/CODEOWNERS b/CODEOWNERS index f0075549fd..56e20133ef 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -49,6 +49,7 @@ esphome/components/atc_mithermometer/* @ahpohl esphome/components/atm90e26/* @danieltwagner esphome/components/atm90e32/* @circuitsetup @descipher esphome/components/audio/* @kahrendt +esphome/components/audio_adc/* @kbx81 esphome/components/audio_dac/* @kbx81 esphome/components/axs15231/* @clydebarrow esphome/components/b_parasite/* @rbaron diff --git a/esphome/components/audio_adc/__init__.py b/esphome/components/audio_adc/__init__.py new file mode 100644 index 0000000000..dd3c958821 --- /dev/null +++ b/esphome/components/audio_adc/__init__.py @@ -0,0 +1,41 @@ +from esphome import automation +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_MIC_GAIN +from esphome.core import coroutine_with_priority + +CODEOWNERS = ["@kbx81"] +IS_PLATFORM_COMPONENT = True + +audio_adc_ns = cg.esphome_ns.namespace("audio_adc") +AudioAdc = audio_adc_ns.class_("AudioAdc") + +SetMicGainAction = audio_adc_ns.class_("SetMicGainAction", automation.Action) + + +SET_MIC_GAIN_ACTION_SCHEMA = cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(AudioAdc), + cv.Required(CONF_MIC_GAIN): cv.templatable(cv.decibel), + }, + key=CONF_MIC_GAIN, +) + + +@automation.register_action( + "audio_adc.set_mic_gain", SetMicGainAction, SET_MIC_GAIN_ACTION_SCHEMA +) +async def audio_adc_set_mic_gain_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + + template_ = await cg.templatable(config.get(CONF_MIC_GAIN), args, float) + cg.add(var.set_mic_gain(template_)) + + return var + + +@coroutine_with_priority(100.0) +async def to_code(config): + cg.add_define("USE_AUDIO_ADC") + cg.add_global(audio_adc_ns.using) diff --git a/esphome/components/audio_adc/audio_adc.h b/esphome/components/audio_adc/audio_adc.h new file mode 100644 index 0000000000..94bfb57db5 --- /dev/null +++ b/esphome/components/audio_adc/audio_adc.h @@ -0,0 +1,17 @@ +#pragma once + +#include "esphome/core/defines.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace audio_adc { + +class AudioAdc { + public: + virtual bool set_mic_gain(float mic_gain) = 0; + + virtual float mic_gain() = 0; +}; + +} // namespace audio_adc +} // namespace esphome diff --git a/esphome/components/audio_adc/automation.h b/esphome/components/audio_adc/automation.h new file mode 100644 index 0000000000..1b0bc2a6ad --- /dev/null +++ b/esphome/components/audio_adc/automation.h @@ -0,0 +1,23 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "audio_adc.h" + +namespace esphome { +namespace audio_adc { + +template class SetMicGainAction : public Action { + public: + explicit SetMicGainAction(AudioAdc *audio_adc) : audio_adc_(audio_adc) {} + + TEMPLATABLE_VALUE(float, mic_gain) + + void play(Ts... x) override { this->audio_adc_->set_mic_gain(this->mic_gain_.value(x...)); } + + protected: + AudioAdc *audio_adc_; +}; + +} // namespace audio_adc +} // namespace esphome diff --git a/esphome/components/es7210/__init__.py b/esphome/components/es7210/__init__.py index 8e63d7f04f..e69de29bb2 100644 --- a/esphome/components/es7210/__init__.py +++ b/esphome/components/es7210/__init__.py @@ -1,67 +0,0 @@ -import esphome.codegen as cg -from esphome.components import i2c -import esphome.config_validation as cv -from esphome.const import CONF_BITS_PER_SAMPLE, CONF_ID, CONF_MIC_GAIN, CONF_SAMPLE_RATE - -CODEOWNERS = ["@kahrendt"] -DEPENDENCIES = ["i2c"] - -es7210_ns = cg.esphome_ns.namespace("es7210") -ES7210 = es7210_ns.class_("ES7210", cg.Component, i2c.I2CDevice) - - -es7210_bits_per_sample = es7210_ns.enum("ES7210BitsPerSample") -ES7210_BITS_PER_SAMPLE_ENUM = { - 16: es7210_bits_per_sample.ES7210_BITS_PER_SAMPLE_16, - 24: es7210_bits_per_sample.ES7210_BITS_PER_SAMPLE_24, - 32: es7210_bits_per_sample.ES7210_BITS_PER_SAMPLE_32, -} - - -es7210_mic_gain = es7210_ns.enum("ES7210MicGain") -ES7210_MIC_GAIN_ENUM = { - "0DB": es7210_mic_gain.ES7210_MIC_GAIN_0DB, - "3DB": es7210_mic_gain.ES7210_MIC_GAIN_3DB, - "6DB": es7210_mic_gain.ES7210_MIC_GAIN_6DB, - "9DB": es7210_mic_gain.ES7210_MIC_GAIN_9DB, - "12DB": es7210_mic_gain.ES7210_MIC_GAIN_12DB, - "15DB": es7210_mic_gain.ES7210_MIC_GAIN_15DB, - "18DB": es7210_mic_gain.ES7210_MIC_GAIN_18DB, - "21DB": es7210_mic_gain.ES7210_MIC_GAIN_21DB, - "24DB": es7210_mic_gain.ES7210_MIC_GAIN_24DB, - "27DB": es7210_mic_gain.ES7210_MIC_GAIN_27DB, - "30DB": es7210_mic_gain.ES7210_MIC_GAIN_30DB, - "33DB": es7210_mic_gain.ES7210_MIC_GAIN_33DB, - "34.5DB": es7210_mic_gain.ES7210_MIC_GAIN_34_5DB, - "36DB": es7210_mic_gain.ES7210_MIC_GAIN_36DB, - "37.5DB": es7210_mic_gain.ES7210_MIC_GAIN_37_5DB, -} - -_validate_bits = cv.float_with_unit("bits", "bit") - -CONFIG_SCHEMA = ( - cv.Schema( - { - cv.GenerateID(): cv.declare_id(ES7210), - cv.Optional(CONF_BITS_PER_SAMPLE, default="16bit"): cv.All( - _validate_bits, cv.enum(ES7210_BITS_PER_SAMPLE_ENUM) - ), - cv.Optional(CONF_MIC_GAIN, default="24DB"): cv.enum( - ES7210_MIC_GAIN_ENUM, upper=True - ), - cv.Optional(CONF_SAMPLE_RATE, default=16000): cv.int_range(min=1), - } - ) - .extend(cv.COMPONENT_SCHEMA) - .extend(i2c.i2c_device_schema(0x40)) -) - - -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_rate(config[CONF_SAMPLE_RATE])) diff --git a/esphome/components/es7210/audio_adc.py b/esphome/components/es7210/audio_adc.py new file mode 100644 index 0000000000..f0bd8bc25a --- /dev/null +++ b/esphome/components/es7210/audio_adc.py @@ -0,0 +1,51 @@ +import esphome.codegen as cg +from esphome.components import i2c +from esphome.components.audio_adc import AudioAdc +import esphome.config_validation as cv +from esphome.const import CONF_BITS_PER_SAMPLE, CONF_ID, CONF_MIC_GAIN, CONF_SAMPLE_RATE + +CODEOWNERS = ["@kahrendt"] +DEPENDENCIES = ["i2c"] + +es7210_ns = cg.esphome_ns.namespace("es7210") +ES7210 = es7210_ns.class_("ES7210", AudioAdc, cg.Component, i2c.I2CDevice) + + +es7210_bits_per_sample = es7210_ns.enum("ES7210BitsPerSample") +ES7210_BITS_PER_SAMPLE_ENUM = { + 16: es7210_bits_per_sample.ES7210_BITS_PER_SAMPLE_16, + 24: es7210_bits_per_sample.ES7210_BITS_PER_SAMPLE_24, + 32: es7210_bits_per_sample.ES7210_BITS_PER_SAMPLE_32, +} + + +ES7210_MIC_GAINS = [0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 34.5, 36, 37.5] + +_validate_bits = cv.float_with_unit("bits", "bit") + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(ES7210), + cv.Optional(CONF_BITS_PER_SAMPLE, default="16bit"): cv.All( + _validate_bits, cv.enum(ES7210_BITS_PER_SAMPLE_ENUM) + ), + cv.Optional(CONF_MIC_GAIN, default="24db"): cv.All( + cv.decibel, cv.one_of(*ES7210_MIC_GAINS) + ), + cv.Optional(CONF_SAMPLE_RATE, default=16000): cv.int_range(min=1), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(0x40)) +) + + +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_rate(config[CONF_SAMPLE_RATE])) diff --git a/esphome/components/es7210/es7210.cpp b/esphome/components/es7210/es7210.cpp index d2f2c3c1ff..9a99e60995 100644 --- a/esphome/components/es7210/es7210.cpp +++ b/esphome/components/es7210/es7210.cpp @@ -25,12 +25,12 @@ static const size_t MCLK_DIV_FRE = 256; } void ES7210::dump_config() { - ESP_LOGCONFIG(TAG, "ES7210 ADC:"); + ESP_LOGCONFIG(TAG, "ES7210 audio ADC:"); ESP_LOGCONFIG(TAG, " Bits Per Sample: %" PRIu8, this->bits_per_sample_); ESP_LOGCONFIG(TAG, " Sample Rate: %" PRIu32, this->sample_rate_); if (this->is_failed()) { - ESP_LOGCONFIG(TAG, " Failed to initialize!"); + ESP_LOGE(TAG, " Failed to initialize"); return; } } @@ -84,6 +84,16 @@ void ES7210::setup() { // Enable device ES7210_ERROR_FAILED(this->write_byte(ES7210_RESET_REG00, 0x71)); ES7210_ERROR_FAILED(this->write_byte(ES7210_RESET_REG00, 0x41)); + + this->setup_complete_ = true; +} + +bool ES7210::set_mic_gain(float mic_gain) { + this->mic_gain_ = clamp(mic_gain, ES7210_MIC_GAIN_MIN, ES7210_MIC_GAIN_MAX); + if (this->setup_complete_) { + return this->configure_mic_gain_(); + } + return true; } bool ES7210::configure_sample_rate_() { @@ -122,9 +132,11 @@ bool ES7210::configure_sample_rate_() { return true; } + bool ES7210::configure_mic_gain_() { - for (int i = 0; i < 4; ++i) { - this->es7210_update_reg_bit_(ES7210_MIC1_GAIN_REG43 + i, 0x10, 0x00); + auto regv = this->es7210_gain_reg_value_(this->mic_gain_); + for (uint8_t i = 0; i < 4; ++i) { + ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC1_GAIN_REG43 + i, 0x10, 0x00)); } ES7210_ERROR_CHECK(this->write_byte(ES7210_MIC12_POWER_REG4B, 0xff)); ES7210_ERROR_CHECK(this->write_byte(ES7210_MIC34_POWER_REG4C, 0xff)); @@ -133,29 +145,44 @@ bool ES7210::configure_mic_gain_() { ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_CLOCK_OFF_REG01, 0x0b, 0x00)); ES7210_ERROR_CHECK(this->write_byte(ES7210_MIC12_POWER_REG4B, 0x00)); ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC1_GAIN_REG43, 0x10, 0x10)); - ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC1_GAIN_REG43, 0x0f, this->mic_gain_)); + ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC1_GAIN_REG43, 0x0f, regv)); // Configure mic 2 ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_CLOCK_OFF_REG01, 0x0b, 0x00)); ES7210_ERROR_CHECK(this->write_byte(ES7210_MIC12_POWER_REG4B, 0x00)); ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC2_GAIN_REG44, 0x10, 0x10)); - ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC2_GAIN_REG44, 0x0f, this->mic_gain_)); + ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC2_GAIN_REG44, 0x0f, regv)); // Configure mic 3 ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_CLOCK_OFF_REG01, 0x0b, 0x00)); ES7210_ERROR_CHECK(this->write_byte(ES7210_MIC34_POWER_REG4C, 0x00)); ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC3_GAIN_REG45, 0x10, 0x10)); - ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC3_GAIN_REG45, 0x0f, this->mic_gain_)); + ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC3_GAIN_REG45, 0x0f, regv)); // Configure mic 4 ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_CLOCK_OFF_REG01, 0x0b, 0x00)); ES7210_ERROR_CHECK(this->write_byte(ES7210_MIC34_POWER_REG4C, 0x00)); ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC4_GAIN_REG46, 0x10, 0x10)); - ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC4_GAIN_REG46, 0x0f, this->mic_gain_)); + ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC4_GAIN_REG46, 0x0f, regv)); return true; } +uint8_t ES7210::es7210_gain_reg_value_(float mic_gain) { + // reg: 12 - 34.5dB, 13 - 36dB, 14 - 37.5dB + mic_gain += 0.5; + if (mic_gain <= 33.0) { + return (uint8_t) mic_gain / 3; + } + if (mic_gain < 36.0) { + return 12; + } + if (mic_gain < 37.0) { + return 13; + } + return 14; +} + bool ES7210::configure_i2s_format_() { // Configure bits per sample uint8_t reg_val = 0; diff --git a/esphome/components/es7210/es7210.h b/esphome/components/es7210/es7210.h index a40dde5aa5..8f6d9d8136 100644 --- a/esphome/components/es7210/es7210.h +++ b/esphome/components/es7210/es7210.h @@ -1,8 +1,11 @@ #pragma once +#include "esphome/components/audio_adc/audio_adc.h" #include "esphome/components/i2c/i2c.h" #include "esphome/core/component.h" +#include "es7210_const.h" + namespace esphome { namespace es7210 { @@ -14,25 +17,7 @@ enum ES7210BitsPerSample : uint8_t { ES7210_BITS_PER_SAMPLE_32 = 32, }; -enum ES7210MicGain : uint8_t { - ES7210_MIC_GAIN_0DB = 0, - ES7210_MIC_GAIN_3DB, - ES7210_MIC_GAIN_6DB, - ES7210_MIC_GAIN_9DB, - ES7210_MIC_GAIN_12DB, - ES7210_MIC_GAIN_15DB, - ES7210_MIC_GAIN_18DB, - ES7210_MIC_GAIN_21DB, - ES7210_MIC_GAIN_24DB, - ES7210_MIC_GAIN_27DB, - ES7210_MIC_GAIN_30DB, - ES7210_MIC_GAIN_33DB, - ES7210_MIC_GAIN_34_5DB, - ES7210_MIC_GAIN_36DB, - ES7210_MIC_GAIN_37_5DB, -}; - -class ES7210 : public Component, public i2c::I2CDevice { +class ES7210 : public audio_adc::AudioAdc, public Component, public i2c::I2CDevice { /* Class for configuring an ES7210 ADC for microphone input. * Based on code from: * - https://github.com/espressif/esp-bsp/ (accessed 20241219) @@ -44,9 +29,11 @@ class ES7210 : public Component, public i2c::I2CDevice { void dump_config() override; void set_bits_per_sample(ES7210BitsPerSample bits_per_sample) { this->bits_per_sample_ = bits_per_sample; } - void set_mic_gain(ES7210MicGain mic_gain) { this->mic_gain_ = mic_gain; } + bool set_mic_gain(float mic_gain) override; void set_sample_rate(uint32_t sample_rate) { this->sample_rate_ = sample_rate; } + float mic_gain() override { return this->mic_gain_; }; + protected: /// @brief Updates an I2C registry address by modifying the current state /// @param reg_addr I2C register address @@ -55,14 +42,20 @@ class ES7210 : public Component, public i2c::I2CDevice { /// @return True if successful, false otherwise bool es7210_update_reg_bit_(uint8_t reg_addr, uint8_t update_bits, uint8_t data); + /// @brief Convert floating point mic gain value to register value + /// @param mic_gain Gain value to convert + /// @return Corresponding register value for specified gain + uint8_t es7210_gain_reg_value_(float mic_gain); + bool configure_i2s_format_(); bool configure_mic_gain_(); bool configure_sample_rate_(); + bool setup_complete_{false}; bool enable_tdm_{false}; // TDM is unsupported in ESPHome as of version 2024.12 - ES7210MicGain mic_gain_; - ES7210BitsPerSample bits_per_sample_; - uint32_t sample_rate_; + float mic_gain_{0}; + ES7210BitsPerSample bits_per_sample_{ES7210_BITS_PER_SAMPLE_16}; + uint32_t sample_rate_{0}; }; } // namespace es7210 diff --git a/esphome/components/es7210/es7210_const.h b/esphome/components/es7210/es7210_const.h index 87fd6d86d2..e5ffea5743 100644 --- a/esphome/components/es7210/es7210_const.h +++ b/esphome/components/es7210/es7210_const.h @@ -1,6 +1,6 @@ #pragma once -#include "es7210.h" +#include namespace esphome { namespace es7210 { @@ -42,7 +42,7 @@ static const uint8_t ES7210_MIC12_POWER_REG4B = 0x4B; /* MICBias & ADC & PGA Pow static const uint8_t ES7210_MIC34_POWER_REG4C = 0x4C; /* - * Clock coefficient structer + * Clock coefficient structure */ struct ES7210Coefficient { uint32_t mclk; // mclk frequency @@ -122,5 +122,8 @@ static const ES7210Coefficient ES7210_COEFFICIENTS[] = { {19200000, 96000, 0x01, 0x05, 0x00, 0x01, 0x28, 0x00, 0x00, 0xc8}, }; +static const float ES7210_MIC_GAIN_MIN = 0.0; +static const float ES7210_MIC_GAIN_MAX = 37.5; + } // namespace es7210 } // namespace esphome diff --git a/tests/components/es7210/common.yaml b/tests/components/es7210/common.yaml index 5c30f7e883..3fab177cb3 100644 --- a/tests/components/es7210/common.yaml +++ b/tests/components/es7210/common.yaml @@ -1,6 +1,16 @@ +esphome: + on_boot: + then: + - audio_adc.set_mic_gain: 0db + - audio_adc.set_mic_gain: !lambda 'return 4;' + i2c: - id: i2c_aic3204 scl: ${scl_pin} sda: ${sda_pin} -es7210: +audio_adc: + - platform: es7210 + id: es7210_adc + bits_per_sample: 16bit + sample_rate: 16000 From 0f4e274e522bb403b2e73fb535e0743c4129e2c4 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 21 Jan 2025 13:43:50 +1100 Subject: [PATCH 70/86] [uptime] Cosmetic improvements for uptime text_sensor (#8101) --- .../components/uptime/text_sensor/__init__.py | 2 +- .../uptime/text_sensor/uptime_text_sensor.cpp | 59 ++++++++++++------- .../uptime/text_sensor/uptime_text_sensor.h | 4 +- 3 files changed, 41 insertions(+), 24 deletions(-) diff --git a/esphome/components/uptime/text_sensor/__init__.py b/esphome/components/uptime/text_sensor/__init__.py index 996d983e71..e4a7ac6517 100644 --- a/esphome/components/uptime/text_sensor/__init__.py +++ b/esphome/components/uptime/text_sensor/__init__.py @@ -11,7 +11,7 @@ CONFIG_SCHEMA = text_sensor.text_sensor_schema( UptimeTextSensor, icon=ICON_TIMER, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, -).extend(cv.polling_component_schema("60s")) +).extend(cv.polling_component_schema("30s")) async def to_code(config): diff --git a/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp b/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp index 0fa5e199f3..409af6e4ff 100644 --- a/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp +++ b/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp @@ -9,34 +9,51 @@ namespace uptime { static const char *const TAG = "uptime.sensor"; -void UptimeTextSensor::setup() { this->last_ms_ = millis(); } +void UptimeTextSensor::setup() { + this->last_ms_ = millis(); + if (this->last_ms_ < 60 * 1000) + this->last_ms_ = 0; + this->update(); +} void UptimeTextSensor::update() { - const auto now = millis(); + auto now = millis(); // get whole seconds since last update. Note that even if the millis count has overflowed between updates, // the difference will still be correct due to the way twos-complement arithmetic works. - const uint32_t delta = (now - this->last_ms_) / 1000; - if (delta == 0) - return; - // set last_ms_ to the last second boundary - this->last_ms_ = now - (now % 1000); + uint32_t delta = now - this->last_ms_; + this->last_ms_ = now - delta % 1000; // save remainder for next update + delta /= 1000; this->uptime_ += delta; auto uptime = this->uptime_; - unsigned days = uptime / (24 * 3600); - unsigned seconds = uptime % (24 * 3600); - unsigned hours = seconds / 3600; - seconds %= 3600; - unsigned minutes = seconds / 60; - seconds %= 60; - if (days != 0) { - this->publish_state(str_sprintf("%dd%dh%dm%ds", days, hours, minutes, seconds)); - } else if (hours != 0) { - this->publish_state(str_sprintf("%dh%dm%ds", hours, minutes, seconds)); - } else if (minutes != 0) { - this->publish_state(str_sprintf("%dm%ds", minutes, seconds)); - } else { - this->publish_state(str_sprintf("%ds", seconds)); + unsigned interval = this->get_update_interval() / 1000; + std::string buffer{}; + // display from the largest unit that corresponds to the update interval, drop larger units that are zero. + while (true) { // enable use of break for early exit + unsigned remainder = uptime % 60; + uptime /= 60; + if (interval < 30) { + buffer.insert(0, str_sprintf("%us", remainder)); + if (uptime == 0) + break; + } + remainder = uptime % 60; + uptime /= 60; + if (interval < 1800) { + buffer.insert(0, str_sprintf("%um", remainder)); + if (uptime == 0) + break; + } + remainder = uptime % 24; + uptime /= 24; + if (interval < 12 * 3600) { + buffer.insert(0, str_sprintf("%uh", remainder)); + if (uptime == 0) + break; + } + buffer.insert(0, str_sprintf("%ud", (unsigned) uptime)); + break; } + this->publish_state(buffer); } float UptimeTextSensor::get_setup_priority() const { return setup_priority::HARDWARE; } diff --git a/esphome/components/uptime/text_sensor/uptime_text_sensor.h b/esphome/components/uptime/text_sensor/uptime_text_sensor.h index 4baf1039b6..5719ef38a2 100644 --- a/esphome/components/uptime/text_sensor/uptime_text_sensor.h +++ b/esphome/components/uptime/text_sensor/uptime_text_sensor.h @@ -17,8 +17,8 @@ class UptimeTextSensor : public text_sensor::TextSensor, public PollingComponent float get_setup_priority() const override; protected: - uint64_t uptime_{0}; - uint64_t last_ms_{0}; + uint32_t uptime_{0}; // uptime in seconds, will overflow after 136 years + uint32_t last_ms_{0}; }; } // namespace uptime From 716a8b87e1b033b454e3a1f50e3e37bb37177403 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Mon, 20 Jan 2025 21:15:18 -0600 Subject: [PATCH 71/86] [es8156] Add support for ES8156 audio DAC (#8085) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/es8156/__init__.py | 0 esphome/components/es8156/audio_dac.py | 27 ++++++ esphome/components/es8156/es8156.cpp | 87 +++++++++++++++++++ esphome/components/es8156/es8156.h | 51 +++++++++++ esphome/components/es8156/es8156_const.h | 68 +++++++++++++++ tests/components/es8156/common.yaml | 15 ++++ tests/components/es8156/test.esp32-ard.yaml | 5 ++ .../components/es8156/test.esp32-c3-ard.yaml | 5 ++ .../components/es8156/test.esp32-c3-idf.yaml | 5 ++ tests/components/es8156/test.esp32-idf.yaml | 5 ++ tests/components/es8156/test.esp8266-ard.yaml | 5 ++ 12 files changed, 274 insertions(+) create mode 100644 esphome/components/es8156/__init__.py create mode 100644 esphome/components/es8156/audio_dac.py create mode 100644 esphome/components/es8156/es8156.cpp create mode 100644 esphome/components/es8156/es8156.h create mode 100644 esphome/components/es8156/es8156_const.h create mode 100644 tests/components/es8156/common.yaml create mode 100644 tests/components/es8156/test.esp32-ard.yaml create mode 100644 tests/components/es8156/test.esp32-c3-ard.yaml create mode 100644 tests/components/es8156/test.esp32-c3-idf.yaml create mode 100644 tests/components/es8156/test.esp32-idf.yaml create mode 100644 tests/components/es8156/test.esp8266-ard.yaml diff --git a/CODEOWNERS b/CODEOWNERS index 56e20133ef..e2c674cfd3 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -133,6 +133,7 @@ esphome/components/ens160_i2c/* @latonita esphome/components/ens160_spi/* @latonita esphome/components/ens210/* @itn3rd77 esphome/components/es7210/* @kahrendt +esphome/components/es8156/* @kbx81 esphome/components/es8311/* @kahrendt @kroimon esphome/components/esp32/* @esphome/core esphome/components/esp32_ble/* @Rapsssito @jesserockz diff --git a/esphome/components/es8156/__init__.py b/esphome/components/es8156/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/es8156/audio_dac.py b/esphome/components/es8156/audio_dac.py new file mode 100644 index 0000000000..b9d8eae6b0 --- /dev/null +++ b/esphome/components/es8156/audio_dac.py @@ -0,0 +1,27 @@ +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_ID + +CODEOWNERS = ["@kbx81"] +DEPENDENCIES = ["i2c"] + +es8156_ns = cg.esphome_ns.namespace("es8156") +ES8156 = es8156_ns.class_("ES8156", AudioDac, cg.Component, i2c.I2CDevice) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(ES8156), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(0x08)) +) + + +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) diff --git a/esphome/components/es8156/es8156.cpp b/esphome/components/es8156/es8156.cpp new file mode 100644 index 0000000000..62d35aa2d2 --- /dev/null +++ b/esphome/components/es8156/es8156.cpp @@ -0,0 +1,87 @@ +#include "es8156.h" +#include "es8156_const.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" +#include + +namespace esphome { +namespace es8156 { + +static const char *const TAG = "es8156"; + +// Mark the component as failed; use only in setup +#define ES8156_ERROR_FAILED(func) \ + if (!(func)) { \ + this->mark_failed(); \ + return; \ + } + +void ES8156::setup() { + ESP_LOGCONFIG(TAG, "Setting up ES8156..."); + + ES8156_ERROR_FAILED(this->write_byte(ES8156_REG02_SCLK_MODE, 0x04)); + ES8156_ERROR_FAILED(this->write_byte(ES8156_REG20_ANALOG_SYS1, 0x2A)); + ES8156_ERROR_FAILED(this->write_byte(ES8156_REG21_ANALOG_SYS2, 0x3C)); + ES8156_ERROR_FAILED(this->write_byte(ES8156_REG22_ANALOG_SYS3, 0x00)); + ES8156_ERROR_FAILED(this->write_byte(ES8156_REG24_ANALOG_LP, 0x07)); + ES8156_ERROR_FAILED(this->write_byte(ES8156_REG23_ANALOG_SYS4, 0x00)); + + ES8156_ERROR_FAILED(this->write_byte(ES8156_REG0A_TIME_CONTROL1, 0x01)); + ES8156_ERROR_FAILED(this->write_byte(ES8156_REG0B_TIME_CONTROL2, 0x01)); + ES8156_ERROR_FAILED(this->write_byte(ES8156_REG11_DAC_SDP, 0x00)); + ES8156_ERROR_FAILED(this->write_byte(ES8156_REG19_EQ_CONTROL1, 0x20)); + + ES8156_ERROR_FAILED(this->write_byte(ES8156_REG0D_P2S_CONTROL, 0x14)); + ES8156_ERROR_FAILED(this->write_byte(ES8156_REG09_MISC_CONTROL2, 0x00)); + ES8156_ERROR_FAILED(this->write_byte(ES8156_REG18_MISC_CONTROL3, 0x00)); + ES8156_ERROR_FAILED(this->write_byte(ES8156_REG08_CLOCK_ON_OFF, 0x3F)); + ES8156_ERROR_FAILED(this->write_byte(ES8156_REG00_RESET, 0x02)); + ES8156_ERROR_FAILED(this->write_byte(ES8156_REG00_RESET, 0x03)); + ES8156_ERROR_FAILED(this->write_byte(ES8156_REG25_ANALOG_SYS5, 0x20)); +} + +void ES8156::dump_config() { + ESP_LOGCONFIG(TAG, "ES8156 Audio Codec:"); + + if (this->is_failed()) { + ESP_LOGCONFIG(TAG, " Failed to initialize"); + return; + } +} + +bool ES8156::set_volume(float volume) { + volume = clamp(volume, 0.0f, 1.0f); + uint8_t reg = remap(volume, 0.0f, 1.0f, 0, 255); + ESP_LOGV(TAG, "Setting ES8156_REG14_VOLUME_CONTROL to %u (volume: %f)", reg, volume); + return this->write_byte(ES8156_REG14_VOLUME_CONTROL, reg); +} + +float ES8156::volume() { + uint8_t reg; + this->read_byte(ES8156_REG14_VOLUME_CONTROL, ®); + return remap(reg, 0, 255, 0.0f, 1.0f); +} + +bool ES8156::set_mute_state_(bool mute_state) { + uint8_t reg13; + + this->is_muted_ = mute_state; + + if (!this->read_byte(ES8156_REG13_DAC_MUTE, ®13)) { + return false; + } + + ESP_LOGV(TAG, "Read ES8156_REG13_DAC_MUTE: %u", reg13); + + if (mute_state) { + reg13 |= BIT(1) | BIT(2); + } else { + reg13 &= ~(BIT(1) | BIT(2)); + } + + ESP_LOGV(TAG, "Setting ES8156_REG13_DAC_MUTE to %u (muted: %s)", reg13, YESNO(mute_state)); + return this->write_byte(ES8156_REG13_DAC_MUTE, reg13); +} + +} // namespace es8156 +} // namespace esphome diff --git a/esphome/components/es8156/es8156.h b/esphome/components/es8156/es8156.h new file mode 100644 index 0000000000..e973599a7a --- /dev/null +++ b/esphome/components/es8156/es8156.h @@ -0,0 +1,51 @@ +#pragma once + +#include "esphome/components/audio_dac/audio_dac.h" +#include "esphome/components/i2c/i2c.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace es8156 { + +class ES8156 : 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_; } + + protected: + /// @brief Mutes or unmutes the DAC audio out + /// @param mute_state True to mute, false to unmute + /// @return True if successful and false otherwise + bool set_mute_state_(bool mute_state); +}; + +} // namespace es8156 +} // namespace esphome diff --git a/esphome/components/es8156/es8156_const.h b/esphome/components/es8156/es8156_const.h new file mode 100644 index 0000000000..0bc8f89dd4 --- /dev/null +++ b/esphome/components/es8156/es8156_const.h @@ -0,0 +1,68 @@ +#pragma once + +#include "es8156.h" + +namespace esphome { +namespace es8156 { + +/* ES8156 register addresses */ +/* + * RESET Control + */ +static const uint8_t ES8156_REG00_RESET = 0x00; +/* + * Clock Managerment + */ +static const uint8_t ES8156_REG01_MAINCLOCK_CTL = 0x01; +static const uint8_t ES8156_REG02_SCLK_MODE = 0x02; +static const uint8_t ES8156_REG03_LRCLK_DIV_H = 0x03; +static const uint8_t ES8156_REG04_LRCLK_DIV_L = 0x04; +static const uint8_t ES8156_REG05_SCLK_DIV = 0x05; +static const uint8_t ES8156_REG06_NFS_CONFIG = 0x06; +static const uint8_t ES8156_REG07_MISC_CONTROL1 = 0x07; +static const uint8_t ES8156_REG08_CLOCK_ON_OFF = 0x08; +static const uint8_t ES8156_REG09_MISC_CONTROL2 = 0x09; +static const uint8_t ES8156_REG0A_TIME_CONTROL1 = 0x0a; +static const uint8_t ES8156_REG0B_TIME_CONTROL2 = 0x0b; +/* + * System Control + */ +static const uint8_t ES8156_REG0C_CHIP_STATUS = 0x0c; +static const uint8_t ES8156_REG0D_P2S_CONTROL = 0x0d; +static const uint8_t ES8156_REG10_DAC_OSR_COUNTER = 0x10; +/* + * SDP Control + */ +static const uint8_t ES8156_REG11_DAC_SDP = 0x11; +static const uint8_t ES8156_REG12_AUTOMUTE_SET = 0x12; +static const uint8_t ES8156_REG13_DAC_MUTE = 0x13; +static const uint8_t ES8156_REG14_VOLUME_CONTROL = 0x14; + +/* + * ALC Control + */ +static const uint8_t ES8156_REG15_ALC_CONFIG1 = 0x15; +static const uint8_t ES8156_REG16_ALC_CONFIG2 = 0x16; +static const uint8_t ES8156_REG17_ALC_CONFIG3 = 0x17; +static const uint8_t ES8156_REG18_MISC_CONTROL3 = 0x18; +static const uint8_t ES8156_REG19_EQ_CONTROL1 = 0x19; +static const uint8_t ES8156_REG1A_EQ_CONTROL2 = 0x1a; +/* + * Analog System Control + */ +static const uint8_t ES8156_REG20_ANALOG_SYS1 = 0x20; +static const uint8_t ES8156_REG21_ANALOG_SYS2 = 0x21; +static const uint8_t ES8156_REG22_ANALOG_SYS3 = 0x22; +static const uint8_t ES8156_REG23_ANALOG_SYS4 = 0x23; +static const uint8_t ES8156_REG24_ANALOG_LP = 0x24; +static const uint8_t ES8156_REG25_ANALOG_SYS5 = 0x25; +/* + * Chip Information + */ +static const uint8_t ES8156_REGFC_I2C_PAGESEL = 0xFC; +static const uint8_t ES8156_REGFD_CHIPID1 = 0xFD; +static const uint8_t ES8156_REGFE_CHIPID0 = 0xFE; +static const uint8_t ES8156_REGFF_CHIP_VERSION = 0xFF; + +} // namespace es8156 +} // namespace esphome diff --git a/tests/components/es8156/common.yaml b/tests/components/es8156/common.yaml new file mode 100644 index 0000000000..addaa0b70a --- /dev/null +++ b/tests/components/es8156/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_es8156 + scl: ${scl_pin} + sda: ${sda_pin} + +audio_dac: + - platform: es8156 diff --git a/tests/components/es8156/test.esp32-ard.yaml b/tests/components/es8156/test.esp32-ard.yaml new file mode 100644 index 0000000000..63c3bd6afd --- /dev/null +++ b/tests/components/es8156/test.esp32-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO16 + sda_pin: GPIO17 + +<<: !include common.yaml diff --git a/tests/components/es8156/test.esp32-c3-ard.yaml b/tests/components/es8156/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/es8156/test.esp32-c3-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/es8156/test.esp32-c3-idf.yaml b/tests/components/es8156/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/es8156/test.esp32-c3-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/es8156/test.esp32-idf.yaml b/tests/components/es8156/test.esp32-idf.yaml new file mode 100644 index 0000000000..63c3bd6afd --- /dev/null +++ b/tests/components/es8156/test.esp32-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO16 + sda_pin: GPIO17 + +<<: !include common.yaml diff --git a/tests/components/es8156/test.esp8266-ard.yaml b/tests/components/es8156/test.esp8266-ard.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/es8156/test.esp8266-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml From db644542eda3482a50f0d30c4ab36ea3c635ff6c Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 20 Jan 2025 22:17:32 -0500 Subject: [PATCH 72/86] [esp32_touch] Fix deprecated warning (#8092) Co-authored-by: Jonathan Swoboda --- esphome/components/esp32_touch/esp32_touch.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/esphome/components/esp32_touch/esp32_touch.cpp b/esphome/components/esp32_touch/esp32_touch.cpp index e43c3b844c..0ae414fa12 100644 --- a/esphome/components/esp32_touch/esp32_touch.cpp +++ b/esphome/components/esp32_touch/esp32_touch.cpp @@ -52,7 +52,12 @@ void ESP32TouchComponent::setup() { } #endif +#if ESP_IDF_VERSION_MAJOR >= 5 + touch_pad_set_measurement_clock_cycles(this->meas_cycle_); + touch_pad_set_measurement_interval(this->sleep_cycle_); +#else touch_pad_set_meas_time(this->sleep_cycle_, this->meas_cycle_); +#endif touch_pad_set_voltage(this->high_voltage_reference_, this->low_voltage_reference_, this->voltage_attenuation_); for (auto *child : this->children_) { From b454f63b3604d766abb038fe3c0f79dc20ab5cad Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 21 Jan 2025 19:32:47 +1300 Subject: [PATCH 73/86] [core] Remove old style platform configuration (#8118) --- esphome/core/config.py | 75 ++----------------- .../binary_sensor/test_binary_sensor.yaml | 3 +- tests/component_tests/button/test_button.yaml | 3 +- .../deep_sleep/test_deep_sleep1.yaml | 3 +- .../deep_sleep/test_deep_sleep2.yaml | 3 +- tests/component_tests/sensor/test_sensor.yaml | 3 +- .../text_sensor/test_text_sensor.yaml | 3 +- .../yaml_util/broken_includetest.yaml | 6 +- .../fixtures/yaml_util/includetest.yaml | 6 +- tests/unit_tests/test_yaml_util.py | 2 +- 10 files changed, 27 insertions(+), 80 deletions(-) diff --git a/esphome/core/config.py b/esphome/core/config.py index eee8b73934..c6a3b1b294 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -1,21 +1,16 @@ import logging import multiprocessing import os -import re from esphome import automation import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import ( - CONF_ARDUINO_VERSION, CONF_AREA, - CONF_BOARD, - CONF_BOARD_FLASH_MODE, CONF_BUILD_PATH, CONF_COMMENT, CONF_COMPILE_PROCESS_LIMIT, CONF_ESPHOME, - CONF_FRAMEWORK, CONF_FRIENDLY_NAME, CONF_INCLUDES, CONF_LIBRARIES, @@ -30,12 +25,9 @@ from esphome.const import ( CONF_PLATFORMIO_OPTIONS, CONF_PRIORITY, CONF_PROJECT, - CONF_SOURCE, CONF_TRIGGER_ID, - CONF_TYPE, CONF_VERSION, KEY_CORE, - PLATFORM_ESP8266, TARGET_PLATFORMS, __version__ as ESPHOME_VERSION, ) @@ -44,7 +36,6 @@ from esphome.helpers import copy_file_if_changed, get_str_env, walk_files _LOGGER = logging.getLogger(__name__) -BUILD_FLASH_MODES = ["qio", "qout", "dio", "dout"] StartupTrigger = cg.esphome_ns.class_( "StartupTrigger", cg.Component, automation.Trigger.template() ) @@ -58,8 +49,6 @@ ProjectUpdateTrigger = cg.esphome_ns.class_( "ProjectUpdateTrigger", cg.Component, automation.Trigger.template(cg.std_string) ) -VERSION_REGEX = re.compile(r"^[0-9]+\.[0-9]+\.[0-9]+(?:[ab]\d+)?$") - VALID_INCLUDE_EXTS = {".h", ".hpp", ".tcc", ".ino", ".cpp", ".c"} @@ -111,7 +100,6 @@ else: _compile_process_limit_default = cv.UNDEFINED -CONF_ESP8266_RESTORE_FROM_FLASH = "esp8266_restore_from_flash" CONFIG_SCHEMA = cv.All( cv.Schema( { @@ -175,14 +163,9 @@ PRELOAD_CONFIG_SCHEMA = cv.Schema( { cv.Required(CONF_NAME): cv.valid_name, cv.Optional(CONF_BUILD_PATH): cv.string, - # Compat options, these were moved to target-platform specific sections - # but we'll keep these around for a long time because every config would - # be impacted - cv.Optional(CONF_PLATFORM): cv.one_of(*TARGET_PLATFORMS, lower=True), - cv.Optional(CONF_BOARD): cv.string_strict, - cv.Optional(CONF_ESP8266_RESTORE_FROM_FLASH): cv.valid, - cv.Optional(CONF_BOARD_FLASH_MODE): cv.valid, - cv.Optional(CONF_ARDUINO_VERSION): cv.valid, + cv.Optional(CONF_PLATFORM): cv.invalid( + "Please remove the `platform` key from the [esphome] block and use the correct platform component. This style of configuration has now been removed." + ), cv.Optional(CONF_MIN_VERSION, default=ESPHOME_VERSION): cv.All( cv.version_number, cv.validate_esphome_version ), @@ -204,62 +187,20 @@ def preload_core_config(config, result): conf[CONF_BUILD_PATH] = os.path.join(build_path, CORE.name) CORE.build_path = CORE.relative_internal_path(conf[CONF_BUILD_PATH]) - has_oldstyle = CONF_PLATFORM in conf - newstyle_found = [key for key in TARGET_PLATFORMS if key in config] - oldstyle_opts = [ - CONF_ESP8266_RESTORE_FROM_FLASH, - CONF_BOARD_FLASH_MODE, - CONF_ARDUINO_VERSION, - CONF_BOARD, - ] + target_platforms = [key for key in TARGET_PLATFORMS if key in config] - if not has_oldstyle and not newstyle_found: + if not target_platforms: raise cv.Invalid( "Platform missing. You must include one of the available platform keys: " + ", ".join(TARGET_PLATFORMS), [CONF_ESPHOME], ) - if has_oldstyle and newstyle_found: + if len(target_platforms) > 1: raise cv.Invalid( - f"Please remove the `platform` key from the [esphome] block. You're already using the new style with the [{conf[CONF_PLATFORM]}] block", - [CONF_ESPHOME, CONF_PLATFORM], + f"Found multiple target platform blocks: {', '.join(target_platforms)}. Only one is allowed.", + [target_platforms[0]], ) - if len(newstyle_found) > 1: - raise cv.Invalid( - f"Found multiple target platform blocks: {', '.join(newstyle_found)}. Only one is allowed.", - [newstyle_found[0]], - ) - if newstyle_found: - # Convert to newstyle - for key in oldstyle_opts: - if key in conf: - raise cv.Invalid( - f"Please move {key} to the [{newstyle_found[0]}] block.", - [CONF_ESPHOME, key], - ) - if has_oldstyle: - plat = conf.pop(CONF_PLATFORM) - plat_conf = {} - if CONF_ESP8266_RESTORE_FROM_FLASH in conf: - plat_conf["restore_from_flash"] = conf.pop(CONF_ESP8266_RESTORE_FROM_FLASH) - if CONF_BOARD_FLASH_MODE in conf: - plat_conf[CONF_BOARD_FLASH_MODE] = conf.pop(CONF_BOARD_FLASH_MODE) - if CONF_ARDUINO_VERSION in conf: - plat_conf[CONF_FRAMEWORK] = {} - if plat != PLATFORM_ESP8266: - plat_conf[CONF_FRAMEWORK][CONF_TYPE] = "arduino" - - try: - if conf[CONF_ARDUINO_VERSION] not in ("recommended", "latest", "dev"): - cv.Version.parse(conf[CONF_ARDUINO_VERSION]) - plat_conf[CONF_FRAMEWORK][CONF_VERSION] = conf.pop(CONF_ARDUINO_VERSION) - except ValueError: - plat_conf[CONF_FRAMEWORK][CONF_SOURCE] = conf.pop(CONF_ARDUINO_VERSION) - if CONF_BOARD in conf: - plat_conf[CONF_BOARD] = conf.pop(CONF_BOARD) - # Insert generated target platform config to main config - config[plat] = plat_conf config[CONF_ESPHOME] = conf diff --git a/tests/component_tests/binary_sensor/test_binary_sensor.yaml b/tests/component_tests/binary_sensor/test_binary_sensor.yaml index f98ce693f7..8842dda837 100644 --- a/tests/component_tests/binary_sensor/test_binary_sensor.yaml +++ b/tests/component_tests/binary_sensor/test_binary_sensor.yaml @@ -1,7 +1,8 @@ --- esphome: name: test - platform: ESP8266 + +esp8266: board: d1_mini_lite binary_sensor: diff --git a/tests/component_tests/button/test_button.yaml b/tests/component_tests/button/test_button.yaml index 48e13f0353..916c85cb8b 100644 --- a/tests/component_tests/button/test_button.yaml +++ b/tests/component_tests/button/test_button.yaml @@ -1,7 +1,8 @@ --- esphome: name: test - platform: ESP8266 + +esp8266: board: d1_mini_lite wifi: diff --git a/tests/component_tests/deep_sleep/test_deep_sleep1.yaml b/tests/component_tests/deep_sleep/test_deep_sleep1.yaml index 96514a677f..03145290a9 100644 --- a/tests/component_tests/deep_sleep/test_deep_sleep1.yaml +++ b/tests/component_tests/deep_sleep/test_deep_sleep1.yaml @@ -1,7 +1,8 @@ --- esphome: name: test - platform: ESP32 + +esp32: board: nodemcu-32s deep_sleep: diff --git a/tests/component_tests/deep_sleep/test_deep_sleep2.yaml b/tests/component_tests/deep_sleep/test_deep_sleep2.yaml index 0e8e598402..88ebb31728 100644 --- a/tests/component_tests/deep_sleep/test_deep_sleep2.yaml +++ b/tests/component_tests/deep_sleep/test_deep_sleep2.yaml @@ -1,7 +1,8 @@ --- esphome: name: test - platform: ESP32 + +esp32: board: nodemcu-32s deep_sleep: diff --git a/tests/component_tests/sensor/test_sensor.yaml b/tests/component_tests/sensor/test_sensor.yaml index 8c0fd85b17..612b8e5e56 100644 --- a/tests/component_tests/sensor/test_sensor.yaml +++ b/tests/component_tests/sensor/test_sensor.yaml @@ -1,7 +1,8 @@ --- esphome: name: test - platform: ESP8266 + +esp8266: board: d1_mini_lite sensor: diff --git a/tests/component_tests/text_sensor/test_text_sensor.yaml b/tests/component_tests/text_sensor/test_text_sensor.yaml index b426cb102c..9cc75082ac 100644 --- a/tests/component_tests/text_sensor/test_text_sensor.yaml +++ b/tests/component_tests/text_sensor/test_text_sensor.yaml @@ -1,7 +1,8 @@ --- esphome: name: test - platform: ESP8266 + +esp8266: board: d1_mini_lite text_sensor: diff --git a/tests/unit_tests/fixtures/yaml_util/broken_includetest.yaml b/tests/unit_tests/fixtures/yaml_util/broken_includetest.yaml index aaca55b807..a49ae706a4 100644 --- a/tests/unit_tests/fixtures/yaml_util/broken_includetest.yaml +++ b/tests/unit_tests/fixtures/yaml_util/broken_includetest.yaml @@ -12,7 +12,7 @@ esphome: # not overwritten by vars in the !include above name: ${name} name_add_mac_suffix: true - platform: esp8266 - board: !include {file: includes/scalar.yaml, vars: {var1: nodemcu}} - libraries: !include {file: includes/list.yaml, vars: {var1: Wire}} + +esp8266: + board: !include {file: includes/scalar.yaml, vars: {var1: nodemcu}} diff --git a/tests/unit_tests/fixtures/yaml_util/includetest.yaml b/tests/unit_tests/fixtures/yaml_util/includetest.yaml index af0a4e2030..7b01da6a17 100644 --- a/tests/unit_tests/fixtures/yaml_util/includetest.yaml +++ b/tests/unit_tests/fixtures/yaml_util/includetest.yaml @@ -12,7 +12,7 @@ esphome: # not overwritten by vars in the !include above name: ${name} name_add_mac_suffix: true - platform: esp8266 - board: !include {file: includes/scalar.yaml, vars: {var1: nodemcu}} - libraries: !include {file: includes/list.yaml, vars: {var1: Wire}} + +esp8266: + board: !include {file: includes/scalar.yaml, vars: {var1: nodemcu}} diff --git a/tests/unit_tests/test_yaml_util.py b/tests/unit_tests/test_yaml_util.py index 9178726247..828b2bf14b 100644 --- a/tests/unit_tests/test_yaml_util.py +++ b/tests/unit_tests/test_yaml_util.py @@ -10,7 +10,7 @@ def test_include_with_vars(fixture_path): substitutions.do_substitution_pass(actual, None) assert actual["esphome"]["name"] == "original" assert actual["esphome"]["libraries"][0] == "Wire" - assert actual["esphome"]["board"] == "nodemcu" + assert actual["esp8266"]["board"] == "nodemcu" assert actual["wifi"]["ssid"] == "my_custom_ssid" From 78ce8f014a8d8cf7b5f7e88101c6c9baa8a2da7d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Jan 2025 08:15:56 +1300 Subject: [PATCH 74/86] Bump actions/stale from 9.0.0 to 9.1.0 (#8120) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/stale.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 95f275e5a4..b79939fc8e 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -17,7 +17,7 @@ jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@v9.0.0 + - uses: actions/stale@v9.1.0 with: days-before-pr-stale: 90 days-before-pr-close: 7 @@ -37,7 +37,7 @@ jobs: close-issues: runs-on: ubuntu-latest steps: - - uses: actions/stale@v9.0.0 + - uses: actions/stale@v9.1.0 with: days-before-pr-stale: -1 days-before-pr-close: -1 From 4843bbd38a313911e7cb7ae3d87fc5d8a00079b3 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Tue, 21 Jan 2025 17:56:51 -0600 Subject: [PATCH 75/86] [custom] Remove platforms (#8119) --- esphome/components/custom/__init__.py | 3 - .../custom/binary_sensor/__init__.py | 30 +--------- .../binary_sensor/custom_binary_sensor.cpp | 16 ----- .../binary_sensor/custom_binary_sensor.h | 26 -------- esphome/components/custom/climate/__init__.py | 29 +-------- .../custom/climate/custom_climate.h | 22 ------- esphome/components/custom/cover/__init__.py | 29 +-------- .../components/custom/cover/custom_cover.h | 21 ------- esphome/components/custom/light/__init__.py | 29 +-------- .../custom/light/custom_light_output.h | 24 -------- esphome/components/custom/output/__init__.py | 60 +------------------ .../components/custom/output/custom_output.h | 37 ------------ esphome/components/custom/sensor/__init__.py | 26 +------- .../custom/sensor/custom_sensor.cpp | 16 ----- .../components/custom/sensor/custom_sensor.h | 24 -------- esphome/components/custom/switch/__init__.py | 26 +------- .../custom/switch/custom_switch.cpp | 16 ----- .../components/custom/switch/custom_switch.h | 24 -------- .../components/custom/text_sensor/__init__.py | 31 +--------- .../custom/text_sensor/custom_text_sensor.cpp | 16 ----- .../custom/text_sensor/custom_text_sensor.h | 26 -------- .../components/custom_component/__init__.py | 30 +--------- .../custom_component/custom_component.h | 28 --------- 23 files changed, 19 insertions(+), 570 deletions(-) delete mode 100644 esphome/components/custom/binary_sensor/custom_binary_sensor.cpp delete mode 100644 esphome/components/custom/binary_sensor/custom_binary_sensor.h delete mode 100644 esphome/components/custom/climate/custom_climate.h delete mode 100644 esphome/components/custom/cover/custom_cover.h delete mode 100644 esphome/components/custom/light/custom_light_output.h delete mode 100644 esphome/components/custom/output/custom_output.h delete mode 100644 esphome/components/custom/sensor/custom_sensor.cpp delete mode 100644 esphome/components/custom/sensor/custom_sensor.h delete mode 100644 esphome/components/custom/switch/custom_switch.cpp delete mode 100644 esphome/components/custom/switch/custom_switch.h delete mode 100644 esphome/components/custom/text_sensor/custom_text_sensor.cpp delete mode 100644 esphome/components/custom/text_sensor/custom_text_sensor.h delete mode 100644 esphome/components/custom_component/custom_component.h diff --git a/esphome/components/custom/__init__.py b/esphome/components/custom/__init__.py index 74450300f3..e69de29bb2 100644 --- a/esphome/components/custom/__init__.py +++ b/esphome/components/custom/__init__.py @@ -1,3 +0,0 @@ -import esphome.codegen as cg - -custom_ns = cg.esphome_ns.namespace("custom") diff --git a/esphome/components/custom/binary_sensor/__init__.py b/esphome/components/custom/binary_sensor/__init__.py index 8d6d621b3a..ca9747ea3e 100644 --- a/esphome/components/custom/binary_sensor/__init__.py +++ b/esphome/components/custom/binary_sensor/__init__.py @@ -1,31 +1,5 @@ -import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import binary_sensor -from esphome.const import CONF_BINARY_SENSORS, CONF_ID, CONF_LAMBDA -from .. import custom_ns -CustomBinarySensorConstructor = custom_ns.class_("CustomBinarySensorConstructor") - -CONFIG_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.declare_id(CustomBinarySensorConstructor), - cv.Required(CONF_LAMBDA): cv.returning_lambda, - cv.Required(CONF_BINARY_SENSORS): cv.ensure_list( - binary_sensor.binary_sensor_schema() - ), - } +CONFIG_SCHEMA = cv.invalid( + 'The "custom" component has been removed. Consider conversion to an external component.\nhttps://esphome.io/guides/contributing#a-note-about-custom-components' ) - - -async def to_code(config): - template_ = await cg.process_lambda( - config[CONF_LAMBDA], - [], - return_type=cg.std_vector.template(binary_sensor.BinarySensorPtr), - ) - - rhs = CustomBinarySensorConstructor(template_) - custom = cg.variable(config[CONF_ID], rhs) - for i, conf in enumerate(config[CONF_BINARY_SENSORS]): - rhs = custom.Pget_binary_sensor(i) - await binary_sensor.register_binary_sensor(rhs, conf) diff --git a/esphome/components/custom/binary_sensor/custom_binary_sensor.cpp b/esphome/components/custom/binary_sensor/custom_binary_sensor.cpp deleted file mode 100644 index ea83198568..0000000000 --- a/esphome/components/custom/binary_sensor/custom_binary_sensor.cpp +++ /dev/null @@ -1,16 +0,0 @@ -#include "custom_binary_sensor.h" -#include "esphome/core/log.h" - -namespace esphome { -namespace custom { - -static const char *const TAG = "custom.binary_sensor"; - -void CustomBinarySensorConstructor::dump_config() { - for (auto *child : this->binary_sensors_) { - LOG_BINARY_SENSOR("", "Custom Binary Sensor", child); - } -} - -} // namespace custom -} // namespace esphome diff --git a/esphome/components/custom/binary_sensor/custom_binary_sensor.h b/esphome/components/custom/binary_sensor/custom_binary_sensor.h deleted file mode 100644 index b7d5458d9e..0000000000 --- a/esphome/components/custom/binary_sensor/custom_binary_sensor.h +++ /dev/null @@ -1,26 +0,0 @@ -#pragma once - -#include "esphome/core/component.h" -#include "esphome/components/binary_sensor/binary_sensor.h" - -#include - -namespace esphome { -namespace custom { - -class CustomBinarySensorConstructor : public Component { - public: - CustomBinarySensorConstructor(const std::function()> &init) { - this->binary_sensors_ = init(); - } - - binary_sensor::BinarySensor *get_binary_sensor(int i) { return this->binary_sensors_[i]; } - - void dump_config() override; - - protected: - std::vector binary_sensors_; -}; - -} // namespace custom -} // namespace esphome diff --git a/esphome/components/custom/climate/__init__.py b/esphome/components/custom/climate/__init__.py index a95456133a..ca9747ea3e 100644 --- a/esphome/components/custom/climate/__init__.py +++ b/esphome/components/custom/climate/__init__.py @@ -1,30 +1,5 @@ -import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import climate -from esphome.const import CONF_ID, CONF_LAMBDA -from .. import custom_ns -CustomClimateConstructor = custom_ns.class_("CustomClimateConstructor") -CONF_CLIMATES = "climates" - -CONFIG_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.declare_id(CustomClimateConstructor), - cv.Required(CONF_LAMBDA): cv.returning_lambda, - cv.Required(CONF_CLIMATES): cv.ensure_list(climate.CLIMATE_SCHEMA), - } +CONFIG_SCHEMA = cv.invalid( + 'The "custom" component has been removed. Consider conversion to an external component.\nhttps://esphome.io/guides/contributing#a-note-about-custom-components' ) - - -async def to_code(config): - template_ = await cg.process_lambda( - config[CONF_LAMBDA], - [], - return_type=cg.std_vector.template(climate.Climate.operator("ptr")), - ) - - rhs = CustomClimateConstructor(template_) - custom = cg.variable(config[CONF_ID], rhs) - for i, conf in enumerate(config[CONF_CLIMATES]): - rhs = custom.Pget_climate(i) - await climate.register_climate(rhs, conf) diff --git a/esphome/components/custom/climate/custom_climate.h b/esphome/components/custom/climate/custom_climate.h deleted file mode 100644 index 37876f7115..0000000000 --- a/esphome/components/custom/climate/custom_climate.h +++ /dev/null @@ -1,22 +0,0 @@ -#pragma once - -#include "esphome/core/component.h" -#include "esphome/components/climate/climate.h" - -#include - -namespace esphome { -namespace custom { - -class CustomClimateConstructor { - public: - CustomClimateConstructor(const std::function()> &init) { this->climates_ = init(); } - - climate::Climate *get_climate(int i) { return this->climates_[i]; } - - protected: - std::vector climates_; -}; - -} // namespace custom -} // namespace esphome diff --git a/esphome/components/custom/cover/__init__.py b/esphome/components/custom/cover/__init__.py index 37fd4cdbbc..ca9747ea3e 100644 --- a/esphome/components/custom/cover/__init__.py +++ b/esphome/components/custom/cover/__init__.py @@ -1,30 +1,5 @@ -import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import cover -from esphome.const import CONF_ID, CONF_LAMBDA -from .. import custom_ns -CustomCoverConstructor = custom_ns.class_("CustomCoverConstructor") -CONF_COVERS = "covers" - -CONFIG_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.declare_id(CustomCoverConstructor), - cv.Required(CONF_LAMBDA): cv.returning_lambda, - cv.Required(CONF_COVERS): cv.ensure_list(cover.COVER_SCHEMA), - } +CONFIG_SCHEMA = cv.invalid( + 'The "custom" component has been removed. Consider conversion to an external component.\nhttps://esphome.io/guides/contributing#a-note-about-custom-components' ) - - -async def to_code(config): - template_ = await cg.process_lambda( - config[CONF_LAMBDA], - [], - return_type=cg.std_vector.template(cover.Cover.operator("ptr")), - ) - - rhs = CustomCoverConstructor(template_) - custom = cg.variable(config[CONF_ID], rhs) - for i, conf in enumerate(config[CONF_COVERS]): - rhs = custom.Pget_cover(i) - await cover.register_cover(rhs, conf) diff --git a/esphome/components/custom/cover/custom_cover.h b/esphome/components/custom/cover/custom_cover.h deleted file mode 100644 index 58330b9d54..0000000000 --- a/esphome/components/custom/cover/custom_cover.h +++ /dev/null @@ -1,21 +0,0 @@ -#pragma once - -#include "esphome/components/cover/cover.h" - -#include - -namespace esphome { -namespace custom { - -class CustomCoverConstructor { - public: - CustomCoverConstructor(const std::function()> &init) { this->covers_ = init(); } - - cover::Cover *get_cover(int i) { return this->covers_[i]; } - - protected: - std::vector covers_; -}; - -} // namespace custom -} // namespace esphome diff --git a/esphome/components/custom/light/__init__.py b/esphome/components/custom/light/__init__.py index b6ebe13ab2..ca9747ea3e 100644 --- a/esphome/components/custom/light/__init__.py +++ b/esphome/components/custom/light/__init__.py @@ -1,30 +1,5 @@ -import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import light -from esphome.const import CONF_ID, CONF_LAMBDA -from .. import custom_ns -CustomLightOutputConstructor = custom_ns.class_("CustomLightOutputConstructor") -CONF_LIGHTS = "lights" - -CONFIG_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.declare_id(CustomLightOutputConstructor), - cv.Required(CONF_LAMBDA): cv.returning_lambda, - cv.Required(CONF_LIGHTS): cv.ensure_list(light.ADDRESSABLE_LIGHT_SCHEMA), - } +CONFIG_SCHEMA = cv.invalid( + 'The "custom" component has been removed. Consider conversion to an external component.\nhttps://esphome.io/guides/contributing#a-note-about-custom-components' ) - - -async def to_code(config): - template_ = await cg.process_lambda( - config[CONF_LAMBDA], - [], - return_type=cg.std_vector.template(light.LightOutput.operator("ptr")), - ) - - rhs = CustomLightOutputConstructor(template_) - custom = cg.variable(config[CONF_ID], rhs) - for i, conf in enumerate(config[CONF_LIGHTS]): - rhs = custom.Pget_light(i) - await light.register_light(rhs, conf) diff --git a/esphome/components/custom/light/custom_light_output.h b/esphome/components/custom/light/custom_light_output.h deleted file mode 100644 index c2c83ebe97..0000000000 --- a/esphome/components/custom/light/custom_light_output.h +++ /dev/null @@ -1,24 +0,0 @@ -#pragma once - -#include "esphome/core/component.h" -#include "esphome/components/light/light_output.h" - -#include - -namespace esphome { -namespace custom { - -class CustomLightOutputConstructor { - public: - CustomLightOutputConstructor(const std::function()> &init) { - this->outputs_ = init(); - } - - light::LightOutput *get_light(int i) { return this->outputs_[i]; } - - protected: - std::vector outputs_; -}; - -} // namespace custom -} // namespace esphome diff --git a/esphome/components/custom/output/__init__.py b/esphome/components/custom/output/__init__.py index 97ef070fc3..ca9747ea3e 100644 --- a/esphome/components/custom/output/__init__.py +++ b/esphome/components/custom/output/__init__.py @@ -1,61 +1,5 @@ -import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import output -from esphome.const import CONF_ID, CONF_LAMBDA, CONF_OUTPUTS, CONF_TYPE, CONF_BINARY -from .. import custom_ns -CustomBinaryOutputConstructor = custom_ns.class_("CustomBinaryOutputConstructor") -CustomFloatOutputConstructor = custom_ns.class_("CustomFloatOutputConstructor") - -CONF_FLOAT = "float" - -CONFIG_SCHEMA = cv.typed_schema( - { - CONF_BINARY: cv.Schema( - { - cv.GenerateID(): cv.declare_id(CustomBinaryOutputConstructor), - cv.Required(CONF_LAMBDA): cv.returning_lambda, - cv.Required(CONF_OUTPUTS): cv.ensure_list( - output.BINARY_OUTPUT_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(output.BinaryOutput), - } - ) - ), - } - ), - CONF_FLOAT: cv.Schema( - { - cv.GenerateID(): cv.declare_id(CustomFloatOutputConstructor), - cv.Required(CONF_LAMBDA): cv.returning_lambda, - cv.Required(CONF_OUTPUTS): cv.ensure_list( - output.FLOAT_OUTPUT_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(output.FloatOutput), - } - ) - ), - } - ), - }, - lower=True, +CONFIG_SCHEMA = cv.invalid( + 'The "custom" component has been removed. Consider conversion to an external component.\nhttps://esphome.io/guides/contributing#a-note-about-custom-components' ) - - -async def to_code(config): - type = config[CONF_TYPE] - if type == "binary": - ret_type = output.BinaryOutputPtr - klass = CustomBinaryOutputConstructor - else: - ret_type = output.FloatOutputPtr - klass = CustomFloatOutputConstructor - template_ = await cg.process_lambda( - config[CONF_LAMBDA], [], return_type=cg.std_vector.template(ret_type) - ) - - rhs = klass(template_) - custom = cg.variable(config[CONF_ID], rhs) - for i, conf in enumerate(config[CONF_OUTPUTS]): - out = cg.Pvariable(conf[CONF_ID], custom.get_output(i)) - await output.register_output(out, conf) diff --git a/esphome/components/custom/output/custom_output.h b/esphome/components/custom/output/custom_output.h deleted file mode 100644 index 4624642420..0000000000 --- a/esphome/components/custom/output/custom_output.h +++ /dev/null @@ -1,37 +0,0 @@ -#pragma once - -#include "esphome/core/component.h" -#include "esphome/components/output/binary_output.h" -#include "esphome/components/output/float_output.h" - -#include - -namespace esphome { -namespace custom { - -class CustomBinaryOutputConstructor { - public: - CustomBinaryOutputConstructor(const std::function()> &init) { - this->outputs_ = init(); - } - - output::BinaryOutput *get_output(int i) { return this->outputs_[i]; } - - protected: - std::vector outputs_; -}; - -class CustomFloatOutputConstructor { - public: - CustomFloatOutputConstructor(const std::function()> &init) { - this->outputs_ = init(); - } - - output::FloatOutput *get_output(int i) { return this->outputs_[i]; } - - protected: - std::vector outputs_; -}; - -} // namespace custom -} // namespace esphome diff --git a/esphome/components/custom/sensor/__init__.py b/esphome/components/custom/sensor/__init__.py index be17d9a334..ca9747ea3e 100644 --- a/esphome/components/custom/sensor/__init__.py +++ b/esphome/components/custom/sensor/__init__.py @@ -1,27 +1,5 @@ -import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import sensor -from esphome.const import CONF_ID, CONF_LAMBDA, CONF_SENSORS -from .. import custom_ns -CustomSensorConstructor = custom_ns.class_("CustomSensorConstructor") - -CONFIG_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.declare_id(CustomSensorConstructor), - cv.Required(CONF_LAMBDA): cv.returning_lambda, - cv.Required(CONF_SENSORS): cv.ensure_list(sensor.sensor_schema()), - } +CONFIG_SCHEMA = cv.invalid( + 'The "custom" component has been removed. Consider conversion to an external component.\nhttps://esphome.io/guides/contributing#a-note-about-custom-components' ) - - -async def to_code(config): - template_ = await cg.process_lambda( - config[CONF_LAMBDA], [], return_type=cg.std_vector.template(sensor.SensorPtr) - ) - - rhs = CustomSensorConstructor(template_) - var = cg.variable(config[CONF_ID], rhs) - for i, conf in enumerate(config[CONF_SENSORS]): - sens = cg.Pvariable(conf[CONF_ID], var.get_sensor(i)) - await sensor.register_sensor(sens, conf) diff --git a/esphome/components/custom/sensor/custom_sensor.cpp b/esphome/components/custom/sensor/custom_sensor.cpp deleted file mode 100644 index e670f09530..0000000000 --- a/esphome/components/custom/sensor/custom_sensor.cpp +++ /dev/null @@ -1,16 +0,0 @@ -#include "custom_sensor.h" -#include "esphome/core/log.h" - -namespace esphome { -namespace custom { - -static const char *const TAG = "custom.sensor"; - -void CustomSensorConstructor::dump_config() { - for (auto *child : this->sensors_) { - LOG_SENSOR("", "Custom Sensor", child); - } -} - -} // namespace custom -} // namespace esphome diff --git a/esphome/components/custom/sensor/custom_sensor.h b/esphome/components/custom/sensor/custom_sensor.h deleted file mode 100644 index d8f4fbc109..0000000000 --- a/esphome/components/custom/sensor/custom_sensor.h +++ /dev/null @@ -1,24 +0,0 @@ -#pragma once - -#include "esphome/core/component.h" -#include "esphome/components/sensor/sensor.h" - -#include - -namespace esphome { -namespace custom { - -class CustomSensorConstructor : public Component { - public: - CustomSensorConstructor(const std::function()> &init) { this->sensors_ = init(); } - - sensor::Sensor *get_sensor(int i) { return this->sensors_[i]; } - - void dump_config() override; - - protected: - std::vector sensors_; -}; - -} // namespace custom -} // namespace esphome diff --git a/esphome/components/custom/switch/__init__.py b/esphome/components/custom/switch/__init__.py index 5538ae6aa0..ca9747ea3e 100644 --- a/esphome/components/custom/switch/__init__.py +++ b/esphome/components/custom/switch/__init__.py @@ -1,27 +1,5 @@ -import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import switch -from esphome.const import CONF_ID, CONF_LAMBDA, CONF_SWITCHES -from .. import custom_ns -CustomSwitchConstructor = custom_ns.class_("CustomSwitchConstructor") - -CONFIG_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.declare_id(CustomSwitchConstructor), - cv.Required(CONF_LAMBDA): cv.returning_lambda, - cv.Required(CONF_SWITCHES): cv.ensure_list(switch.switch_schema(switch.Switch)), - } +CONFIG_SCHEMA = cv.invalid( + 'The "custom" component has been removed. Consider conversion to an external component.\nhttps://esphome.io/guides/contributing#a-note-about-custom-components' ) - - -async def to_code(config): - template_ = await cg.process_lambda( - config[CONF_LAMBDA], [], return_type=cg.std_vector.template(switch.SwitchPtr) - ) - - rhs = CustomSwitchConstructor(template_) - var = cg.variable(config[CONF_ID], rhs) - for i, conf in enumerate(config[CONF_SWITCHES]): - switch_ = cg.Pvariable(conf[CONF_ID], var.get_switch(i)) - await switch.register_switch(switch_, conf) diff --git a/esphome/components/custom/switch/custom_switch.cpp b/esphome/components/custom/switch/custom_switch.cpp deleted file mode 100644 index 6d0a8fa621..0000000000 --- a/esphome/components/custom/switch/custom_switch.cpp +++ /dev/null @@ -1,16 +0,0 @@ -#include "custom_switch.h" -#include "esphome/core/log.h" - -namespace esphome { -namespace custom { - -static const char *const TAG = "custom.switch"; - -void CustomSwitchConstructor::dump_config() { - for (auto *child : this->switches_) { - LOG_SWITCH("", "Custom Switch", child); - } -} - -} // namespace custom -} // namespace esphome diff --git a/esphome/components/custom/switch/custom_switch.h b/esphome/components/custom/switch/custom_switch.h deleted file mode 100644 index 9657e4b44d..0000000000 --- a/esphome/components/custom/switch/custom_switch.h +++ /dev/null @@ -1,24 +0,0 @@ -#pragma once - -#include "esphome/core/component.h" -#include "esphome/components/switch/switch.h" - -#include - -namespace esphome { -namespace custom { - -class CustomSwitchConstructor : public Component { - public: - CustomSwitchConstructor(const std::function()> &init) { this->switches_ = init(); } - - switch_::Switch *get_switch(int i) { return this->switches_[i]; } - - void dump_config() override; - - protected: - std::vector switches_; -}; - -} // namespace custom -} // namespace esphome diff --git a/esphome/components/custom/text_sensor/__init__.py b/esphome/components/custom/text_sensor/__init__.py index 70728af604..ca9747ea3e 100644 --- a/esphome/components/custom/text_sensor/__init__.py +++ b/esphome/components/custom/text_sensor/__init__.py @@ -1,32 +1,5 @@ -import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import text_sensor -from esphome.const import CONF_ID, CONF_LAMBDA, CONF_TEXT_SENSORS -from .. import custom_ns -CustomTextSensorConstructor = custom_ns.class_("CustomTextSensorConstructor") - -CONFIG_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.declare_id(CustomTextSensorConstructor), - cv.Required(CONF_LAMBDA): cv.returning_lambda, - cv.Required(CONF_TEXT_SENSORS): cv.ensure_list( - text_sensor.text_sensor_schema() - ), - } +CONFIG_SCHEMA = cv.invalid( + 'The "custom" component has been removed. Consider conversion to an external component.\nhttps://esphome.io/guides/contributing#a-note-about-custom-components' ) - - -async def to_code(config): - template_ = await cg.process_lambda( - config[CONF_LAMBDA], - [], - return_type=cg.std_vector.template(text_sensor.TextSensorPtr), - ) - - rhs = CustomTextSensorConstructor(template_) - var = cg.variable(config[CONF_ID], rhs) - - for i, conf in enumerate(config[CONF_TEXT_SENSORS]): - text = cg.Pvariable(conf[CONF_ID], var.get_text_sensor(i)) - await text_sensor.register_text_sensor(text, conf) diff --git a/esphome/components/custom/text_sensor/custom_text_sensor.cpp b/esphome/components/custom/text_sensor/custom_text_sensor.cpp deleted file mode 100644 index 618ba832a5..0000000000 --- a/esphome/components/custom/text_sensor/custom_text_sensor.cpp +++ /dev/null @@ -1,16 +0,0 @@ -#include "custom_text_sensor.h" -#include "esphome/core/log.h" - -namespace esphome { -namespace custom { - -static const char *const TAG = "custom.text_sensor"; - -void CustomTextSensorConstructor::dump_config() { - for (auto *child : this->text_sensors_) { - LOG_TEXT_SENSOR("", "Custom Text Sensor", child); - } -} - -} // namespace custom -} // namespace esphome diff --git a/esphome/components/custom/text_sensor/custom_text_sensor.h b/esphome/components/custom/text_sensor/custom_text_sensor.h deleted file mode 100644 index 13732c00b6..0000000000 --- a/esphome/components/custom/text_sensor/custom_text_sensor.h +++ /dev/null @@ -1,26 +0,0 @@ -#pragma once - -#include "esphome/core/component.h" -#include "esphome/components/text_sensor/text_sensor.h" - -#include - -namespace esphome { -namespace custom { - -class CustomTextSensorConstructor : public Component { - public: - CustomTextSensorConstructor(const std::function()> &init) { - this->text_sensors_ = init(); - } - - text_sensor::TextSensor *get_text_sensor(int i) { return this->text_sensors_[i]; } - - void dump_config() override; - - protected: - std::vector text_sensors_; -}; - -} // namespace custom -} // namespace esphome diff --git a/esphome/components/custom_component/__init__.py b/esphome/components/custom_component/__init__.py index d41dd7ea59..982153414d 100644 --- a/esphome/components/custom_component/__init__.py +++ b/esphome/components/custom_component/__init__.py @@ -1,31 +1,7 @@ -import esphome.codegen as cg import esphome.config_validation as cv -from esphome.const import CONF_COMPONENTS, CONF_ID, CONF_LAMBDA - -custom_component_ns = cg.esphome_ns.namespace("custom_component") -CustomComponentConstructor = custom_component_ns.class_("CustomComponentConstructor") MULTI_CONF = True -CONFIG_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.declare_id(CustomComponentConstructor), - cv.Required(CONF_LAMBDA): cv.returning_lambda, - cv.Optional(CONF_COMPONENTS): cv.ensure_list( - cv.Schema({cv.GenerateID(): cv.declare_id(cg.Component)}).extend( - cv.COMPONENT_SCHEMA - ) - ), - } + +CONFIG_SCHEMA = cv.invalid( + 'The "custom" component has been removed. Consider conversion to an external component.\nhttps://esphome.io/guides/contributing#a-note-about-custom-components' ) - - -async def to_code(config): - template_ = await cg.process_lambda( - config[CONF_LAMBDA], [], return_type=cg.std_vector.template(cg.ComponentPtr) - ) - - rhs = CustomComponentConstructor(template_) - var = cg.variable(config[CONF_ID], rhs) - for i, conf in enumerate(config.get(CONF_COMPONENTS, [])): - comp = cg.Pvariable(conf[CONF_ID], var.get_component(i)) - await cg.register_component(comp, conf) diff --git a/esphome/components/custom_component/custom_component.h b/esphome/components/custom_component/custom_component.h deleted file mode 100644 index 3b34019a7a..0000000000 --- a/esphome/components/custom_component/custom_component.h +++ /dev/null @@ -1,28 +0,0 @@ -#pragma once - -#include "esphome/core/component.h" -#include "esphome/core/application.h" - -#include - -namespace esphome { -namespace custom_component { - -class CustomComponentConstructor { - public: - CustomComponentConstructor(const std::function()> &init) { - this->components_ = init(); - - for (auto *comp : this->components_) { - App.register_component(comp); - } - } - - Component *get_component(int i) const { return this->components_[i]; } - - protected: - std::vector components_; -}; - -} // namespace custom_component -} // namespace esphome From c2e52f4b1177224a80e5585bbdc3151a61ec5a6d Mon Sep 17 00:00:00 2001 From: Citric Li <37475446+limengdu@users.noreply.github.com> Date: Wed, 22 Jan 2025 08:01:15 +0800 Subject: [PATCH 76/86] Add: Human Presence and Target Count to the Seeed Studio MR60BHA2 (#8010) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Co-authored-by: Spencer Yan --- .../seeed_mr60bha2/binary_sensor.py | 25 ++++++++++++++++ .../seeed_mr60bha2/seeed_mr60bha2.cpp | 28 ++++++++++++++++-- .../seeed_mr60bha2/seeed_mr60bha2.h | 29 ++++++------------- esphome/components/seeed_mr60bha2/sensor.py | 10 ++++++- tests/components/seeed_mr60bha2/common.yaml | 7 +++++ 5 files changed, 76 insertions(+), 23 deletions(-) create mode 100644 esphome/components/seeed_mr60bha2/binary_sensor.py diff --git a/esphome/components/seeed_mr60bha2/binary_sensor.py b/esphome/components/seeed_mr60bha2/binary_sensor.py new file mode 100644 index 0000000000..ae9e1c23e6 --- /dev/null +++ b/esphome/components/seeed_mr60bha2/binary_sensor.py @@ -0,0 +1,25 @@ +import esphome.codegen as cg +from esphome.components import binary_sensor +import esphome.config_validation as cv +from esphome.const import ( + DEVICE_CLASS_OCCUPANCY, + CONF_HAS_TARGET, +) +from . import CONF_MR60BHA2_ID, MR60BHA2Component + +DEPENDENCIES = ["seeed_mr60bha2"] + +CONFIG_SCHEMA = { + cv.GenerateID(CONF_MR60BHA2_ID): cv.use_id(MR60BHA2Component), + cv.Optional(CONF_HAS_TARGET): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_OCCUPANCY, icon="mdi:motion-sensor" + ), +} + + +async def to_code(config): + mr60bha2_component = await cg.get_variable(config[CONF_MR60BHA2_ID]) + + if has_target_config := config.get(CONF_HAS_TARGET): + sens = await binary_sensor.new_binary_sensor(has_target_config) + cg.add(mr60bha2_component.set_has_target_binary_sensor(sens)) diff --git a/esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp b/esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp index 50d709c3b0..75f3f092a6 100644 --- a/esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp +++ b/esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp @@ -1,6 +1,7 @@ #include "seeed_mr60bha2.h" #include "esphome/core/log.h" +#include #include namespace esphome { @@ -12,10 +13,14 @@ static const char *const TAG = "seeed_mr60bha2"; // items in an easy-to-read format, including the configuration key-value pairs. void MR60BHA2Component::dump_config() { ESP_LOGCONFIG(TAG, "MR60BHA2:"); +#ifdef USE_BINARY_SENSOR + LOG_BINARY_SENSOR(" ", "People Exist Binary Sensor", this->has_target_binary_sensor_); +#endif #ifdef USE_SENSOR LOG_SENSOR(" ", "Breath Rate Sensor", this->breath_rate_sensor_); LOG_SENSOR(" ", "Heart Rate Sensor", this->heart_rate_sensor_); LOG_SENSOR(" ", "Distance Sensor", this->distance_sensor_); + LOG_SENSOR(" ", "Target Number Sensor", this->num_targets_sensor_); #endif } @@ -94,7 +99,8 @@ bool MR60BHA2Component::validate_message_() { uint16_t frame_type = encode_uint16(data[5], data[6]); if (frame_type != BREATH_RATE_TYPE_BUFFER && frame_type != HEART_RATE_TYPE_BUFFER && - frame_type != DISTANCE_TYPE_BUFFER) { + frame_type != DISTANCE_TYPE_BUFFER && frame_type != PEOPLE_EXIST_TYPE_BUFFER && + frame_type != PRINT_CLOUD_BUFFER) { return false; } @@ -144,6 +150,18 @@ void MR60BHA2Component::process_frame_(uint16_t frame_id, uint16_t frame_type, c } } break; + case PEOPLE_EXIST_TYPE_BUFFER: + if (this->has_target_binary_sensor_ != nullptr && length >= 2) { + uint16_t has_target_int = encode_uint16(data[1], data[0]); + this->has_target_binary_sensor_->publish_state(has_target_int); + if (has_target_int == 0) { + this->breath_rate_sensor_->publish_state(0.0); + this->heart_rate_sensor_->publish_state(0.0); + this->distance_sensor_->publish_state(0.0); + this->num_targets_sensor_->publish_state(0); + } + } + break; case HEART_RATE_TYPE_BUFFER: if (this->heart_rate_sensor_ != nullptr && length >= 4) { uint32_t current_heart_rate_int = encode_uint32(data[3], data[2], data[1], data[0]); @@ -155,7 +173,7 @@ void MR60BHA2Component::process_frame_(uint16_t frame_id, uint16_t frame_type, c } break; case DISTANCE_TYPE_BUFFER: - if (!data[0]) { + if (data[0] != 0) { if (this->distance_sensor_ != nullptr && length >= 8) { uint32_t current_distance_int = encode_uint32(data[7], data[6], data[5], data[4]); float distance_float; @@ -164,6 +182,12 @@ void MR60BHA2Component::process_frame_(uint16_t frame_id, uint16_t frame_type, c } } break; + case PRINT_CLOUD_BUFFER: + if (this->num_targets_sensor_ != nullptr && length >= 4) { + uint32_t current_num_targets_int = encode_uint32(data[3], data[2], data[1], data[0]); + this->num_targets_sensor_->publish_state(current_num_targets_int); + } + break; default: break; } diff --git a/esphome/components/seeed_mr60bha2/seeed_mr60bha2.h b/esphome/components/seeed_mr60bha2/seeed_mr60bha2.h index 0a4f21f1ad..d20c8e50cc 100644 --- a/esphome/components/seeed_mr60bha2/seeed_mr60bha2.h +++ b/esphome/components/seeed_mr60bha2/seeed_mr60bha2.h @@ -1,6 +1,9 @@ #pragma once #include "esphome/core/component.h" #include "esphome/core/defines.h" +#ifdef USE_BINARY_SENSOR +#include "esphome/components/binary_sensor/binary_sensor.h" +#endif #ifdef USE_SENSOR #include "esphome/components/sensor/sensor.h" #endif @@ -12,37 +15,23 @@ namespace esphome { namespace seeed_mr60bha2 { - -static const uint8_t DATA_BUF_MAX_SIZE = 12; -static const uint8_t FRAME_BUF_MAX_SIZE = 21; -static const uint8_t LEN_TO_HEAD_CKSUM = 8; -static const uint8_t LEN_TO_DATA_FRAME = 9; - static const uint8_t FRAME_HEADER_BUFFER = 0x01; static const uint16_t BREATH_RATE_TYPE_BUFFER = 0x0A14; +static const uint16_t PEOPLE_EXIST_TYPE_BUFFER = 0x0F09; static const uint16_t HEART_RATE_TYPE_BUFFER = 0x0A15; static const uint16_t DISTANCE_TYPE_BUFFER = 0x0A16; - -enum FrameLocation { - LOCATE_FRAME_HEADER, - LOCATE_ID_FRAME1, - LOCATE_ID_FRAME2, - LOCATE_LENGTH_FRAME_H, - LOCATE_LENGTH_FRAME_L, - LOCATE_TYPE_FRAME1, - LOCATE_TYPE_FRAME2, - LOCATE_HEAD_CKSUM_FRAME, // Header checksum: [from the first byte to the previous byte of the HEAD_CKSUM bit] - LOCATE_DATA_FRAME, - LOCATE_DATA_CKSUM_FRAME, // Data checksum: [from the first to the previous byte of the DATA_CKSUM bit] - LOCATE_PROCESS_FRAME, -}; +static const uint16_t PRINT_CLOUD_BUFFER = 0x0A04; class MR60BHA2Component : public Component, public uart::UARTDevice { // The class name must be the name defined by text_sensor.py +#ifdef USE_BINARY_SENSOR + SUB_BINARY_SENSOR(has_target); +#endif #ifdef USE_SENSOR SUB_SENSOR(breath_rate); SUB_SENSOR(heart_rate); SUB_SENSOR(distance); + SUB_SENSOR(num_targets); #endif public: diff --git a/esphome/components/seeed_mr60bha2/sensor.py b/esphome/components/seeed_mr60bha2/sensor.py index 5f30b363bf..916d4b4ba2 100644 --- a/esphome/components/seeed_mr60bha2/sensor.py +++ b/esphome/components/seeed_mr60bha2/sensor.py @@ -7,6 +7,7 @@ from esphome.const import ( ICON_HEART_PULSE, ICON_PULSE, ICON_SIGNAL, + ICON_COUNTER, STATE_CLASS_MEASUREMENT, UNIT_BEATS_PER_MINUTE, UNIT_CENTIMETER, @@ -18,12 +19,13 @@ DEPENDENCIES = ["seeed_mr60bha2"] CONF_BREATH_RATE = "breath_rate" CONF_HEART_RATE = "heart_rate" +CONF_NUM_TARGETS = "num_targets" CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(CONF_MR60BHA2_ID): cv.use_id(MR60BHA2Component), cv.Optional(CONF_BREATH_RATE): sensor.sensor_schema( - accuracy_decimals=2, + accuracy_decimals=0, state_class=STATE_CLASS_MEASUREMENT, icon=ICON_PULSE, ), @@ -40,6 +42,9 @@ CONFIG_SCHEMA = cv.Schema( accuracy_decimals=2, icon=ICON_SIGNAL, ), + cv.Optional(CONF_NUM_TARGETS): sensor.sensor_schema( + icon=ICON_COUNTER, + ), } ) @@ -55,3 +60,6 @@ async def to_code(config): if distance_config := config.get(CONF_DISTANCE): sens = await sensor.new_sensor(distance_config) cg.add(mr60bha2_component.set_distance_sensor(sens)) + if num_targets_config := config.get(CONF_NUM_TARGETS): + sens = await sensor.new_sensor(num_targets_config) + cg.add(mr60bha2_component.set_num_targets_sensor(sens)) diff --git a/tests/components/seeed_mr60bha2/common.yaml b/tests/components/seeed_mr60bha2/common.yaml index e9d0c735af..9eb0c8d527 100644 --- a/tests/components/seeed_mr60bha2/common.yaml +++ b/tests/components/seeed_mr60bha2/common.yaml @@ -9,6 +9,11 @@ uart: seeed_mr60bha2: id: my_seeed_mr60bha2 +binary_sensor: + - platform: seeed_mr60bha2 + has_target: + name: "Person Information" + sensor: - platform: seeed_mr60bha2 breath_rate: @@ -17,3 +22,5 @@ sensor: name: "Real-time heart rate" distance: name: "Distance to detection object" + num_targets: + name: "Target Number" From f2170c633a3ffe54960f84803e838f31944428b7 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Wed, 22 Jan 2025 14:23:22 -0600 Subject: [PATCH 77/86] [es7243e] Add support for ES7243E audio ADC (#8098) --- CODEOWNERS | 1 + esphome/components/es7243e/__init__.py | 0 esphome/components/es7243e/audio_adc.py | 34 +++++ esphome/components/es7243e/es7243e.cpp | 125 ++++++++++++++++++ esphome/components/es7243e/es7243e.h | 37 ++++++ esphome/components/es7243e/es7243e_const.h | 54 ++++++++ tests/components/es7243e/common.yaml | 14 ++ tests/components/es7243e/test.esp32-ard.yaml | 5 + .../components/es7243e/test.esp32-c3-ard.yaml | 5 + .../components/es7243e/test.esp32-c3-idf.yaml | 5 + tests/components/es7243e/test.esp32-idf.yaml | 5 + 11 files changed, 285 insertions(+) create mode 100644 esphome/components/es7243e/__init__.py create mode 100644 esphome/components/es7243e/audio_adc.py create mode 100644 esphome/components/es7243e/es7243e.cpp create mode 100644 esphome/components/es7243e/es7243e.h create mode 100644 esphome/components/es7243e/es7243e_const.h create mode 100644 tests/components/es7243e/common.yaml create mode 100644 tests/components/es7243e/test.esp32-ard.yaml create mode 100644 tests/components/es7243e/test.esp32-c3-ard.yaml create mode 100644 tests/components/es7243e/test.esp32-c3-idf.yaml create mode 100644 tests/components/es7243e/test.esp32-idf.yaml diff --git a/CODEOWNERS b/CODEOWNERS index e2c674cfd3..aa24b6cb82 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -133,6 +133,7 @@ esphome/components/ens160_i2c/* @latonita esphome/components/ens160_spi/* @latonita esphome/components/ens210/* @itn3rd77 esphome/components/es7210/* @kahrendt +esphome/components/es7243e/* @kbx81 esphome/components/es8156/* @kbx81 esphome/components/es8311/* @kahrendt @kroimon esphome/components/esp32/* @esphome/core diff --git a/esphome/components/es7243e/__init__.py b/esphome/components/es7243e/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/es7243e/audio_adc.py b/esphome/components/es7243e/audio_adc.py new file mode 100644 index 0000000000..c305d60172 --- /dev/null +++ b/esphome/components/es7243e/audio_adc.py @@ -0,0 +1,34 @@ +import esphome.codegen as cg +from esphome.components import i2c +from esphome.components.audio_adc import AudioAdc +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_MIC_GAIN + +CODEOWNERS = ["@kbx81"] +DEPENDENCIES = ["i2c"] + +es7243e_ns = cg.esphome_ns.namespace("es7243e") +ES7243E = es7243e_ns.class_("ES7243E", AudioAdc, cg.Component, i2c.I2CDevice) + +ES7243E_MIC_GAINS = [0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 34.5, 36, 37.5] + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(ES7243E), + cv.Optional(CONF_MIC_GAIN, default="24db"): cv.All( + cv.decibel, cv.one_of(*ES7243E_MIC_GAINS) + ), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(0x10)) +) + + +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_mic_gain(config[CONF_MIC_GAIN])) diff --git a/esphome/components/es7243e/es7243e.cpp b/esphome/components/es7243e/es7243e.cpp new file mode 100644 index 0000000000..ce65ce973e --- /dev/null +++ b/esphome/components/es7243e/es7243e.cpp @@ -0,0 +1,125 @@ +#include "es7243e.h" +#include "es7243e_const.h" + +#include "esphome/core/hal.h" +#include "esphome/core/log.h" + +#include + +namespace esphome { +namespace es7243e { + +static const char *const TAG = "es7243e"; + +// Mark the component as failed; use only in setup +#define ES7243E_ERROR_FAILED(func) \ + if (!(func)) { \ + this->mark_failed(); \ + return; \ + } + +// Return false; use outside of setup +#define ES7243E_ERROR_CHECK(func) \ + if (!(func)) { \ + return false; \ + } + +void ES7243E::dump_config() { + ESP_LOGCONFIG(TAG, "ES7243E audio ADC:"); + + if (this->is_failed()) { + ESP_LOGE(TAG, " Failed to initialize"); + return; + } +} + +void ES7243E::setup() { + ESP_LOGCONFIG(TAG, "Setting up ES7243E..."); + + ES7243E_ERROR_FAILED(this->write_byte(ES7243E_CLOCK_MGR_REG01, 0x3A)); + ES7243E_ERROR_FAILED(this->write_byte(ES7243E_RESET_REG00, 0x80)); + ES7243E_ERROR_FAILED(this->write_byte(ES7243E_TEST_MODE_REGF9, 0x00)); + ES7243E_ERROR_FAILED(this->write_byte(ES7243E_CLOCK_MGR_REG04, 0x02)); + ES7243E_ERROR_FAILED(this->write_byte(ES7243E_CLOCK_MGR_REG04, 0x01)); + ES7243E_ERROR_FAILED(this->write_byte(ES7243E_TEST_MODE_REGF9, 0x01)); + ES7243E_ERROR_FAILED(this->write_byte(ES7243E_RESET_REG00, 0x1E)); + ES7243E_ERROR_FAILED(this->write_byte(ES7243E_CLOCK_MGR_REG01, 0x00)); + + ES7243E_ERROR_FAILED(this->write_byte(ES7243E_CLOCK_MGR_REG02, 0x00)); + ES7243E_ERROR_FAILED(this->write_byte(ES7243E_CLOCK_MGR_REG03, 0x20)); + ES7243E_ERROR_FAILED(this->write_byte(ES7243E_CLOCK_MGR_REG04, 0x01)); + ES7243E_ERROR_FAILED(this->write_byte(ES7243E_ADC_CTRL_REG0D, 0x00)); + ES7243E_ERROR_FAILED(this->write_byte(ES7243E_CLOCK_MGR_REG05, 0x00)); + ES7243E_ERROR_FAILED(this->write_byte(ES7243E_CLOCK_MGR_REG06, 0x03)); // SCLK=MCLK/4 + ES7243E_ERROR_FAILED(this->write_byte(ES7243E_CLOCK_MGR_REG07, 0x00)); // LRCK=MCLK/256 + ES7243E_ERROR_FAILED(this->write_byte(ES7243E_CLOCK_MGR_REG08, 0xFF)); // LRCK=MCLK/256 + + ES7243E_ERROR_FAILED(this->write_byte(ES7243E_CLOCK_MGR_REG09, 0xCA)); + ES7243E_ERROR_FAILED(this->write_byte(ES7243E_SDP_REG0A, 0x85)); + ES7243E_ERROR_FAILED(this->write_byte(ES7243E_SDP_REG0B, 0x00)); + ES7243E_ERROR_FAILED(this->write_byte(ES7243E_ADC_CTRL_REG0E, 0xBF)); + ES7243E_ERROR_FAILED(this->write_byte(ES7243E_ADC_CTRL_REG0F, 0x80)); + ES7243E_ERROR_FAILED(this->write_byte(ES7243E_ADC_CTRL_REG14, 0x0C)); + ES7243E_ERROR_FAILED(this->write_byte(ES7243E_ADC_CTRL_REG15, 0x0C)); + ES7243E_ERROR_FAILED(this->write_byte(ES7243E_ANALOG_REG17, 0x02)); + ES7243E_ERROR_FAILED(this->write_byte(ES7243E_ANALOG_REG18, 0x26)); + ES7243E_ERROR_FAILED(this->write_byte(ES7243E_ANALOG_REG19, 0x77)); + ES7243E_ERROR_FAILED(this->write_byte(ES7243E_ANALOG_REG1A, 0xF4)); + ES7243E_ERROR_FAILED(this->write_byte(ES7243E_ANALOG_REG1B, 0x66)); + ES7243E_ERROR_FAILED(this->write_byte(ES7243E_ANALOG_REG1C, 0x44)); + ES7243E_ERROR_FAILED(this->write_byte(ES7243E_ANALOG_REG1E, 0x00)); + ES7243E_ERROR_FAILED(this->write_byte(ES7243E_ANALOG_REG1F, 0x0C)); + ES7243E_ERROR_FAILED(this->write_byte(ES7243E_ANALOG_REG20, 0x1A)); // PGA gain +30dB + ES7243E_ERROR_FAILED(this->write_byte(ES7243E_ANALOG_REG21, 0x1A)); + + ES7243E_ERROR_FAILED(this->write_byte(ES7243E_RESET_REG00, 0x80)); + ES7243E_ERROR_FAILED(this->write_byte(ES7243E_CLOCK_MGR_REG01, 0x3A)); + ES7243E_ERROR_FAILED(this->write_byte(ES7243E_ANALOG_REG16, 0x3F)); + ES7243E_ERROR_FAILED(this->write_byte(ES7243E_ANALOG_REG16, 0x00)); + + ES7243E_ERROR_FAILED(this->write_byte(ES7243E_TEST_MODE_REGF9, 0x00)); + ES7243E_ERROR_FAILED(this->write_byte(ES7243E_CLOCK_MGR_REG04, 0x01)); + ES7243E_ERROR_FAILED(this->write_byte(ES7243E_ANALOG_REG17, 0x01)); + ES7243E_ERROR_FAILED(this->configure_mic_gain_()); + ES7243E_ERROR_FAILED(this->write_byte(ES7243E_RESET_REG00, 0x80)); + ES7243E_ERROR_FAILED(this->write_byte(ES7243E_CLOCK_MGR_REG01, 0x3A)); + ES7243E_ERROR_FAILED(this->write_byte(ES7243E_ANALOG_REG16, 0x3F)); + ES7243E_ERROR_FAILED(this->write_byte(ES7243E_ANALOG_REG16, 0x00)); + + this->setup_complete_ = true; +} + +bool ES7243E::set_mic_gain(float mic_gain) { + this->mic_gain_ = clamp(mic_gain, 0, 37.5); + if (this->setup_complete_) { + return this->configure_mic_gain_(); + } + return true; +} + +bool ES7243E::configure_mic_gain_() { + auto regv = this->es7243e_gain_reg_value_(this->mic_gain_); + + ES7243E_ERROR_CHECK(this->write_byte(ES7243E_ANALOG_REG20, 0x10 | regv)); + ES7243E_ERROR_CHECK(this->write_byte(ES7243E_ANALOG_REG21, 0x10 | regv)); + + return true; +} + +uint8_t ES7243E::es7243e_gain_reg_value_(float mic_gain) { + // reg: 12 - 34.5dB, 13 - 36dB, 14 - 37.5dB + mic_gain += 0.5; + if (mic_gain <= 33.0) { + return (uint8_t) mic_gain / 3; + } + if (mic_gain < 36.0) { + return 12; + } + if (mic_gain < 37.0) { + return 13; + } + return 14; +} + +} // namespace es7243e +} // namespace esphome diff --git a/esphome/components/es7243e/es7243e.h b/esphome/components/es7243e/es7243e.h new file mode 100644 index 0000000000..41a8acac8d --- /dev/null +++ b/esphome/components/es7243e/es7243e.h @@ -0,0 +1,37 @@ +#pragma once + +#include "esphome/components/audio_adc/audio_adc.h" +#include "esphome/components/i2c/i2c.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace es7243e { + +class ES7243E : public audio_adc::AudioAdc, public Component, public i2c::I2CDevice { + /* Class for configuring an ES7243E ADC for microphone input. + * Based on code from: + * - https://github.com/espressif/esp-adf/ (accessed 20250116) + */ + public: + void setup() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void dump_config() override; + + bool set_mic_gain(float mic_gain) override; + + float mic_gain() override { return this->mic_gain_; }; + + protected: + /// @brief Convert floating point mic gain value to register value + /// @param mic_gain Gain value to convert + /// @return Corresponding register value for specified gain + uint8_t es7243e_gain_reg_value_(float mic_gain); + + bool configure_mic_gain_(); + + bool setup_complete_{false}; + float mic_gain_{0}; +}; + +} // namespace es7243e +} // namespace esphome diff --git a/esphome/components/es7243e/es7243e_const.h b/esphome/components/es7243e/es7243e_const.h new file mode 100644 index 0000000000..daae53a108 --- /dev/null +++ b/esphome/components/es7243e/es7243e_const.h @@ -0,0 +1,54 @@ +#pragma once + +#include + +namespace esphome { +namespace es7243e { + +// ES7243E register addresses +static const uint8_t ES7243E_RESET_REG00 = 0x00; // Reset control +static const uint8_t ES7243E_CLOCK_MGR_REG01 = 0x01; // MCLK/BCLK/ADCCLK/Analog clocks on/off +static const uint8_t ES7243E_CLOCK_MGR_REG02 = 0x02; // MCLK & BCLK configuration, source selection + +static const uint8_t ES7243E_CLOCK_MGR_REG03 = 0x03; // ADC Over-sample rate control +static const uint8_t ES7243E_CLOCK_MGR_REG04 = 0x04; // Pre-divide/Pre-multiplication +static const uint8_t ES7243E_CLOCK_MGR_REG05 = 0x05; // CF/DSP clock divider +static const uint8_t ES7243E_CLOCK_MGR_REG06 = 0x06; // BCLK divider at master mode +static const uint8_t ES7243E_CLOCK_MGR_REG07 = 0x07; // BCLK/LRCK/SDOUT tri-state control/LRCK divider bit 11->8 +static const uint8_t ES7243E_CLOCK_MGR_REG08 = 0x08; // Master LRCK divider bit 7 to bit 0 +static const uint8_t ES7243E_CLOCK_MGR_REG09 = 0x09; // SEL S1/Timer for S1 +static const uint8_t ES7243E_SDP_REG0A = 0x0A; // SEL S3/Timer for S3 +static const uint8_t ES7243E_SDP_REG0B = 0x0B; // SDP out mute control/I2S/left-justify case/word length/format +static const uint8_t ES7243E_SDP_REG0C = 0x0C; // NFS flag at slot0/LSB/TDM mode selection +static const uint8_t ES7243E_ADC_CTRL_REG0D = 0x0D; // data mux/pol. inv./ram clear on lrck/mclk active/gain scale up +static const uint8_t ES7243E_ADC_CTRL_REG0E = 0x0E; // volume control +static const uint8_t ES7243E_ADC_CTRL_REG0F = 0x0F; // offset freeze/auto level control/automute control/VC ramp rate +static const uint8_t ES7243E_ADC_CTRL_REG10 = 0x10; // automute noise gate/detection +static const uint8_t ES7243E_ADC_CTRL_REG11 = 0x11; // automute SDP control/out gain select +static const uint8_t ES7243E_ADC_CTRL_REG12 = 0x12; // controls for automute PDN_PGA/MOD/reset/digital circuit +static const uint8_t ES7243E_ADC_CTRL_REG13 = 0x13; // ALC rate selection/ALC target level +static const uint8_t ES7243E_ADC_CTRL_REG14 = 0x14; // ADCHPF stage1 coeff +static const uint8_t ES7243E_ADC_CTRL_REG15 = 0x15; // ADCHPF stage2 coeff +static const uint8_t ES7243E_ANALOG_REG16 = 0x16; // power-down/reset +static const uint8_t ES7243E_ANALOG_REG17 = 0x17; // VMIDSEL +static const uint8_t ES7243E_ANALOG_REG18 = 0x18; // ADC/ADCFL bias +static const uint8_t ES7243E_ANALOG_REG19 = 0x19; // PGA1/PGA2 bias +static const uint8_t ES7243E_ANALOG_REG1A = 0x1A; // ADCI1/ADCI23 bias +static const uint8_t ES7243E_ANALOG_REG1B = 0x1B; // ADCSM/ADCCM bias +static const uint8_t ES7243E_ANALOG_REG1C = 0x1C; // ADCVRP/ADCCPP bias +static const uint8_t ES7243E_ANALOG_REG1D = 0x1D; // low power bits +static const uint8_t ES7243E_ANALOG_REG1E = 0x1E; // low power bits +static const uint8_t ES7243E_ANALOG_REG1F = 0x1F; // ADC_DMIC_ON/REFSEL/VX2OFF/VX1SEL/VMIDLVL +static const uint8_t ES7243E_ANALOG_REG20 = 0x20; // select MIC1 as PGA1 input/PGA1 gain +static const uint8_t ES7243E_ANALOG_REG21 = 0x21; // select MIC2 as PGA1 input/PGA2 gain +static const uint8_t ES7243E_TEST_MODE_REGF7 = 0xF7; +static const uint8_t ES7243E_TEST_MODE_REGF8 = 0xF8; +static const uint8_t ES7243E_TEST_MODE_REGF9 = 0xF9; +static const uint8_t ES7243E_I2C_CONF_REGFA = 0xFA; // I2C signals retime/reset registers to default +static const uint8_t ES7243E_FLAG_REGFC = 0xFC; // CSM flag/ADC automute flag (RO) +static const uint8_t ES7243E_CHIP_ID1_REGFD = 0xFD; // chip ID 1, reads 0x7A (RO) +static const uint8_t ES7243E_CHIP_ID2_REGFE = 0xFE; // chip ID 2, reads 0x43 (RO) +static const uint8_t ES7243E_CHIP_VERSION_REGFF = 0xFF; // chip version, reads 0x00 (RO) + +} // namespace es7243e +} // namespace esphome diff --git a/tests/components/es7243e/common.yaml b/tests/components/es7243e/common.yaml new file mode 100644 index 0000000000..3de76909e9 --- /dev/null +++ b/tests/components/es7243e/common.yaml @@ -0,0 +1,14 @@ +esphome: + on_boot: + then: + - audio_adc.set_mic_gain: 0db + - audio_adc.set_mic_gain: !lambda 'return 4;' + +i2c: + - id: i2c_es7243e + scl: ${scl_pin} + sda: ${sda_pin} + +audio_adc: + - platform: es7243e + id: es7243e_adc diff --git a/tests/components/es7243e/test.esp32-ard.yaml b/tests/components/es7243e/test.esp32-ard.yaml new file mode 100644 index 0000000000..63c3bd6afd --- /dev/null +++ b/tests/components/es7243e/test.esp32-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO16 + sda_pin: GPIO17 + +<<: !include common.yaml diff --git a/tests/components/es7243e/test.esp32-c3-ard.yaml b/tests/components/es7243e/test.esp32-c3-ard.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/es7243e/test.esp32-c3-ard.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/es7243e/test.esp32-c3-idf.yaml b/tests/components/es7243e/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..ee2c29ca4e --- /dev/null +++ b/tests/components/es7243e/test.esp32-c3-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO5 + sda_pin: GPIO4 + +<<: !include common.yaml diff --git a/tests/components/es7243e/test.esp32-idf.yaml b/tests/components/es7243e/test.esp32-idf.yaml new file mode 100644 index 0000000000..63c3bd6afd --- /dev/null +++ b/tests/components/es7243e/test.esp32-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + scl_pin: GPIO16 + sda_pin: GPIO17 + +<<: !include common.yaml From 01ab6d3ddca81633d39c3279d5feb89eae3bec22 Mon Sep 17 00:00:00 2001 From: Frederik <5511687+fightforlife@users.noreply.github.com> Date: Wed, 22 Jan 2025 23:37:32 +0100 Subject: [PATCH 78/86] [debug] fix debug_esp32 printf for partition size and address (#8122) Co-authored-by: Djordje Mandic <6750655+DjordjeMandic@users.noreply.github.com> --- esphome/components/debug/debug_esp32.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/debug/debug_esp32.cpp b/esphome/components/debug/debug_esp32.cpp index 69ae7e3678..caa9f8d743 100644 --- a/esphome/components/debug/debug_esp32.cpp +++ b/esphome/components/debug/debug_esp32.cpp @@ -35,8 +35,8 @@ void DebugComponent::log_partition_info_() { esp_partition_iterator_t it = esp_partition_find(ESP_PARTITION_TYPE_ANY, ESP_PARTITION_SUBTYPE_ANY, NULL); while (it != NULL) { const esp_partition_t *partition = esp_partition_get(it); - ESP_LOGCONFIG(TAG, " %-12s %-4d %-8d 0x%08X 0x%08X", partition->label, partition->type, partition->subtype, - partition->address, partition->size); + ESP_LOGCONFIG(TAG, " %-12s %-4d %-8d 0x%08" PRIX32 " 0x%08" PRIX32, partition->label, partition->type, + partition->subtype, partition->address, partition->size); it = esp_partition_next(it); } esp_partition_iterator_release(it); From 5a103543c43385d240c5f80cae749e47d55f4269 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Wed, 22 Jan 2025 17:00:40 -0600 Subject: [PATCH 79/86] [esp32] Set logger default interface for C6 (#8126) --- esphome/components/logger/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index f30bc23e38..6e92777058 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -186,6 +186,8 @@ CONFIG_SCHEMA = cv.All( esp32_s3_idf=USB_SERIAL_JTAG, esp32_c3_arduino=USB_CDC, esp32_c3_idf=USB_SERIAL_JTAG, + esp32_c6_arduino=USB_CDC, + esp32_c6_idf=USB_SERIAL_JTAG, rp2040=USB_CDC, bk72xx=DEFAULT, rtl87xx=DEFAULT, From 0c032bc431fb76ab5bc239e64733390c4bdfeef4 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Thu, 23 Jan 2025 00:06:07 +0100 Subject: [PATCH 80/86] [core] add support for custom platform (#7616) Co-authored-by: Tomasz Duda --- esphome/components/bk72xx/__init__.py | 1 + esphome/components/esp32/__init__.py | 1 + esphome/components/esp8266/__init__.py | 1 + esphome/components/host/__init__.py | 1 + esphome/components/libretiny/__init__.py | 1 + esphome/components/rp2040/__init__.py | 1 + esphome/components/rtl87xx/__init__.py | 2 ++ esphome/config.py | 9 +++--- esphome/const.py | 9 ------ esphome/core/config.py | 37 +++++++++++++++++++++--- esphome/loader.py | 16 +++++++--- 11 files changed, 57 insertions(+), 22 deletions(-) diff --git a/esphome/components/bk72xx/__init__.py b/esphome/components/bk72xx/__init__.py index b5122de956..5b14d0529d 100644 --- a/esphome/components/bk72xx/__init__.py +++ b/esphome/components/bk72xx/__init__.py @@ -19,6 +19,7 @@ from .boards import BK72XX_BOARD_PINS, BK72XX_BOARDS CODEOWNERS = ["@kuba2k2"] AUTO_LOAD = ["libretiny"] +IS_TARGET_PLATFORM = True COMPONENT_DATA = LibreTinyComponent( name=COMPONENT_BK72XX, diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 98db45831a..23b84f3d13 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -64,6 +64,7 @@ from .gpio import esp32_pin_to_code # noqa _LOGGER = logging.getLogger(__name__) CODEOWNERS = ["@esphome/core"] AUTO_LOAD = ["preferences"] +IS_TARGET_PLATFORM = True CONF_RELEASE = "release" diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index c73027fe1b..c949e53aa6 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -34,6 +34,7 @@ from .gpio import PinInitialState, add_pin_initial_states_array CODEOWNERS = ["@esphome/core"] _LOGGER = logging.getLogger(__name__) AUTO_LOAD = ["preferences"] +IS_TARGET_PLATFORM = True def set_core_data(config): diff --git a/esphome/components/host/__init__.py b/esphome/components/host/__init__.py index eb8cfbd984..e275adafa9 100644 --- a/esphome/components/host/__init__.py +++ b/esphome/components/host/__init__.py @@ -17,6 +17,7 @@ from .gpio import host_pin_to_code # noqa CODEOWNERS = ["@esphome/core", "@clydebarrow"] AUTO_LOAD = ["network", "preferences"] +IS_TARGET_PLATFORM = True def set_core_data(config): diff --git a/esphome/components/libretiny/__init__.py b/esphome/components/libretiny/__init__.py index b29d2e309c..5bdfb15e19 100644 --- a/esphome/components/libretiny/__init__.py +++ b/esphome/components/libretiny/__init__.py @@ -47,6 +47,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) CODEOWNERS = ["@kuba2k2"] AUTO_LOAD = ["preferences"] +IS_TARGET_PLATFORM = True def _detect_variant(value): diff --git a/esphome/components/rp2040/__init__.py b/esphome/components/rp2040/__init__.py index b04e539182..3d73cad195 100644 --- a/esphome/components/rp2040/__init__.py +++ b/esphome/components/rp2040/__init__.py @@ -27,6 +27,7 @@ from .gpio import rp2040_pin_to_code # noqa _LOGGER = logging.getLogger(__name__) CODEOWNERS = ["@jesserockz"] AUTO_LOAD = ["preferences"] +IS_TARGET_PLATFORM = True def set_core_data(config): diff --git a/esphome/components/rtl87xx/__init__.py b/esphome/components/rtl87xx/__init__.py index 4c1956f0f4..109c986f75 100644 --- a/esphome/components/rtl87xx/__init__.py +++ b/esphome/components/rtl87xx/__init__.py @@ -19,6 +19,8 @@ from .boards import RTL87XX_BOARD_PINS, RTL87XX_BOARDS CODEOWNERS = ["@kuba2k2"] AUTO_LOAD = ["libretiny"] +IS_TARGET_PLATFORM = True + COMPONENT_DATA = LibreTinyComponent( name=COMPONENT_RTL87XX, diff --git a/esphome/config.py b/esphome/config.py index 65e9ac29bc..09ee2a8f9b 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -22,7 +22,6 @@ from esphome.const import ( CONF_PACKAGES, CONF_PLATFORM, CONF_SUBSTITUTIONS, - TARGET_PLATFORMS, ) from esphome.core import CORE, DocumentRange, EsphomeError import esphome.core.config as core_config @@ -833,7 +832,7 @@ def validate_config( result[CONF_ESPHOME] = config[CONF_ESPHOME] result.add_output_path([CONF_ESPHOME], CONF_ESPHOME) try: - core_config.preload_core_config(config, result) + target_platform = core_config.preload_core_config(config, result) except vol.Invalid as err: result.add_error(err) return result @@ -845,9 +844,9 @@ def validate_config( cv.All(cv.version_number, cv.validate_esphome_version)(min_version) # First run platform validation steps - for key in TARGET_PLATFORMS: - if key in config: - result.add_validation_step(LoadValidationStep(key, config[key])) + result.add_validation_step( + LoadValidationStep(target_platform, config[target_platform]) + ) result.run_validation_steps() if result.errors: diff --git a/esphome/const.py b/esphome/const.py index 284f8d5f78..95bf6afc02 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -15,15 +15,6 @@ PLATFORM_LIBRETINY_OLDSTYLE = "libretiny" PLATFORM_RP2040 = "rp2040" PLATFORM_RTL87XX = "rtl87xx" -TARGET_PLATFORMS = [ - PLATFORM_BK72XX, - PLATFORM_ESP32, - PLATFORM_ESP8266, - PLATFORM_HOST, - PLATFORM_LIBRETINY_OLDSTYLE, - PLATFORM_RP2040, - PLATFORM_RTL87XX, -] SOURCE_FILE_EXTENSIONS = {".cpp", ".hpp", ".h", ".c", ".tcc", ".ino"} HEADER_FILE_EXTENSIONS = {".h", ".hpp", ".tcc"} diff --git a/esphome/core/config.py b/esphome/core/config.py index c6a3b1b294..06ae1d7747 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -1,6 +1,7 @@ import logging import multiprocessing import os +from pathlib import Path from esphome import automation import esphome.codegen as cg @@ -28,7 +29,6 @@ from esphome.const import ( CONF_TRIGGER_ID, CONF_VERSION, KEY_CORE, - TARGET_PLATFORMS, __version__ as ESPHOME_VERSION, ) from esphome.core import CORE, coroutine_with_priority @@ -174,7 +174,31 @@ PRELOAD_CONFIG_SCHEMA = cv.Schema( ) -def preload_core_config(config, result): +def _is_target_platform(name): + from esphome.loader import get_component + + try: + if get_component(name, True).is_target_platform: + return True + except KeyError: + pass + return False + + +def _list_target_platforms(): + target_platforms = [] + root = Path(__file__).parents[1] + for path in (root / "components").iterdir(): + if not path.is_dir(): + continue + if not (path / "__init__.py").is_file(): + continue + if _is_target_platform(path.name): + target_platforms += [path.name] + return target_platforms + + +def preload_core_config(config, result) -> str: with cv.prepend_path(CONF_ESPHOME): conf = PRELOAD_CONFIG_SCHEMA(config[CONF_ESPHOME]) @@ -187,12 +211,16 @@ def preload_core_config(config, result): conf[CONF_BUILD_PATH] = os.path.join(build_path, CORE.name) CORE.build_path = CORE.relative_internal_path(conf[CONF_BUILD_PATH]) - target_platforms = [key for key in TARGET_PLATFORMS if key in config] + target_platforms = [] + + for domain, _ in config.items(): + if _is_target_platform(domain): + target_platforms += [domain] if not target_platforms: raise cv.Invalid( "Platform missing. You must include one of the available platform keys: " - + ", ".join(TARGET_PLATFORMS), + + ", ".join(_list_target_platforms()), [CONF_ESPHOME], ) if len(target_platforms) > 1: @@ -202,6 +230,7 @@ def preload_core_config(config, result): ) config[CONF_ESPHOME] = conf + return target_platforms[0] def include_file(path, basename): diff --git a/esphome/loader.py b/esphome/loader.py index d808805119..0fb4187b04 100644 --- a/esphome/loader.py +++ b/esphome/loader.py @@ -52,6 +52,10 @@ class ComponentManifest: def is_platform_component(self) -> bool: return getattr(self.module, "IS_PLATFORM_COMPONENT", False) + @property + def is_target_platform(self) -> bool: + return getattr(self.module, "IS_TARGET_PLATFORM", False) + @property def config_schema(self) -> Optional[Any]: return getattr(self.module, "CONFIG_SCHEMA", None) @@ -169,13 +173,15 @@ def install_custom_components_meta_finder(): install_meta_finder(custom_components_dir) -def _lookup_module(domain): +def _lookup_module(domain, exception): if domain in _COMPONENT_CACHE: return _COMPONENT_CACHE[domain] try: module = importlib.import_module(f"esphome.components.{domain}") except ImportError as e: + if exception: + raise if "No module named" in str(e): _LOGGER.info( "Unable to import component %s: %s", domain, str(e), exc_info=False @@ -184,6 +190,8 @@ def _lookup_module(domain): _LOGGER.error("Unable to import component %s:", domain, exc_info=True) return None except Exception: # pylint: disable=broad-except + if exception: + raise _LOGGER.error("Unable to load component %s:", domain, exc_info=True) return None @@ -192,14 +200,14 @@ def _lookup_module(domain): return manif -def get_component(domain): +def get_component(domain, exception=False): assert "." not in domain - return _lookup_module(domain) + return _lookup_module(domain, exception) def get_platform(domain, platform): full = f"{platform}.{domain}" - return _lookup_module(full) + return _lookup_module(full, False) _COMPONENT_CACHE = {} From d4857a17271122d1304b298b8f71dd27b0760f1b Mon Sep 17 00:00:00 2001 From: Djordje Mandic <6750655+DjordjeMandic@users.noreply.github.com> Date: Thu, 23 Jan 2025 00:07:26 +0100 Subject: [PATCH 81/86] Add verbose logging for pulse width calculation in pulse_meter (#8124) --- esphome/components/pulse_meter/pulse_meter_sensor.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/pulse_meter/pulse_meter_sensor.cpp b/esphome/components/pulse_meter/pulse_meter_sensor.cpp index 530425563c..836a84b391 100644 --- a/esphome/components/pulse_meter/pulse_meter_sensor.cpp +++ b/esphome/components/pulse_meter/pulse_meter_sensor.cpp @@ -75,6 +75,8 @@ void PulseMeterSensor::loop() { case MeterState::RUNNING: { uint32_t delta_us = this->get_->last_detected_edge_us_ - this->last_processed_edge_us_; float pulse_width_us = delta_us / float(this->get_->count_); + ESP_LOGV(TAG, "New pulse, delta: %" PRIu32 " µs, count: %" PRIu32 ", width: %.5f µs", delta_us, + this->get_->count_, pulse_width_us); this->publish_state((60.0f * 1000000.0f) / pulse_width_us); } break; } From 8aeb08f8689a5d746c6ef0d36f4d3f958edd2ac3 Mon Sep 17 00:00:00 2001 From: brambo123 <52667932+brambo123@users.noreply.github.com> Date: Thu, 23 Jan 2025 00:31:07 +0100 Subject: [PATCH 82/86] [ads1115] Add sample rate control (#8102) --- esphome/components/ads1115/ads1115.cpp | 63 ++++++++++++++++--- esphome/components/ads1115/ads1115.h | 14 ++++- esphome/components/ads1115/sensor/__init__.py | 16 +++++ .../ads1115/sensor/ads1115_sensor.cpp | 3 +- .../ads1115/sensor/ads1115_sensor.h | 2 + tests/components/ads1115/test.esp32-ard.yaml | 1 + .../components/ads1115/test.esp32-c3-ard.yaml | 1 + .../components/ads1115/test.esp32-c3-idf.yaml | 1 + tests/components/ads1115/test.esp32-idf.yaml | 1 + .../components/ads1115/test.esp8266-ard.yaml | 1 + tests/components/ads1115/test.rp2040-ard.yaml | 1 + 11 files changed, 95 insertions(+), 9 deletions(-) diff --git a/esphome/components/ads1115/ads1115.cpp b/esphome/components/ads1115/ads1115.cpp index 218edc4c81..c05064383c 100644 --- a/esphome/components/ads1115/ads1115.cpp +++ b/esphome/components/ads1115/ads1115.cpp @@ -9,8 +9,6 @@ static const char *const TAG = "ads1115"; static const uint8_t ADS1115_REGISTER_CONVERSION = 0x00; static const uint8_t ADS1115_REGISTER_CONFIG = 0x01; -static const uint8_t ADS1115_DATA_RATE_860_SPS = 0b111; // 3300_SPS for ADS1015 - void ADS1115Component::setup() { ESP_LOGCONFIG(TAG, "Setting up ADS1115..."); uint16_t value; @@ -43,9 +41,9 @@ void ADS1115Component::setup() { config |= 0b0000000100000000; } - // Set data rate - 860 samples per second (we're in singleshot mode) + // Set data rate - 860 samples per second // 0bxxxxxxxx100xxxxx - config |= ADS1115_DATA_RATE_860_SPS << 5; + config |= ADS1115_860SPS << 5; // Set comparator mode - hysteresis // 0bxxxxxxxxxxx0xxxx @@ -77,7 +75,7 @@ void ADS1115Component::dump_config() { } } float ADS1115Component::request_measurement(ADS1115Multiplexer multiplexer, ADS1115Gain gain, - ADS1115Resolution resolution) { + ADS1115Resolution resolution, ADS1115Samplerate samplerate) { uint16_t config = this->prev_config_; // Multiplexer // 0bxBBBxxxxxxxxxxxx @@ -89,6 +87,11 @@ float ADS1115Component::request_measurement(ADS1115Multiplexer multiplexer, ADS1 config &= 0b1111000111111111; config |= (gain & 0b111) << 9; + // Sample rate + // 0bxxxxxxxxBBBxxxxx + config &= 0b1111111100011111; + config |= (samplerate & 0b111) << 5; + if (!this->continuous_mode_) { // Start conversion config |= 0b1000000000000000; @@ -101,8 +104,54 @@ float ADS1115Component::request_measurement(ADS1115Multiplexer multiplexer, ADS1 } this->prev_config_ = config; - // about 1.2 ms with 860 samples per second - delay(2); + // Delay calculated as: ceil((1000/SPS)+.5) + if (resolution == ADS1015_12_BITS) { + switch (samplerate) { + case ADS1115_8SPS: + delay(9); + break; + case ADS1115_16SPS: + delay(5); + break; + case ADS1115_32SPS: + delay(3); + break; + case ADS1115_64SPS: + case ADS1115_128SPS: + delay(2); + break; + default: + delay(1); + break; + } + } else { + switch (samplerate) { + case ADS1115_8SPS: + delay(126); // NOLINT + break; + case ADS1115_16SPS: + delay(63); // NOLINT + break; + case ADS1115_32SPS: + delay(32); + break; + case ADS1115_64SPS: + delay(17); + break; + case ADS1115_128SPS: + delay(9); + break; + case ADS1115_250SPS: + delay(5); + break; + case ADS1115_475SPS: + delay(3); + break; + case ADS1115_860SPS: + delay(2); + break; + } + } // in continuous mode, conversion will always be running, rely on the delay // to ensure conversion is taking place with the correct settings diff --git a/esphome/components/ads1115/ads1115.h b/esphome/components/ads1115/ads1115.h index 509333d2c8..e65835a386 100644 --- a/esphome/components/ads1115/ads1115.h +++ b/esphome/components/ads1115/ads1115.h @@ -33,6 +33,17 @@ enum ADS1115Resolution { ADS1015_12_BITS = 12, }; +enum ADS1115Samplerate { + ADS1115_8SPS = 0b000, + ADS1115_16SPS = 0b001, + ADS1115_32SPS = 0b010, + ADS1115_64SPS = 0b011, + ADS1115_128SPS = 0b100, + ADS1115_250SPS = 0b101, + ADS1115_475SPS = 0b110, + ADS1115_860SPS = 0b111 +}; + class ADS1115Component : public Component, public i2c::I2CDevice { public: void setup() override; @@ -42,7 +53,8 @@ class ADS1115Component : public Component, public i2c::I2CDevice { void set_continuous_mode(bool continuous_mode) { continuous_mode_ = continuous_mode; } /// Helper method to request a measurement from a sensor. - float request_measurement(ADS1115Multiplexer multiplexer, ADS1115Gain gain, ADS1115Resolution resolution); + float request_measurement(ADS1115Multiplexer multiplexer, ADS1115Gain gain, ADS1115Resolution resolution, + ADS1115Samplerate samplerate); protected: uint16_t prev_config_{0}; diff --git a/esphome/components/ads1115/sensor/__init__.py b/esphome/components/ads1115/sensor/__init__.py index baec31d35c..f346a71198 100644 --- a/esphome/components/ads1115/sensor/__init__.py +++ b/esphome/components/ads1115/sensor/__init__.py @@ -5,6 +5,7 @@ from esphome.const import ( CONF_GAIN, CONF_MULTIPLEXER, CONF_RESOLUTION, + CONF_SAMPLE_RATE, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT, UNIT_VOLT, @@ -43,6 +44,17 @@ RESOLUTION = { "12_BITS": ADS1115Resolution.ADS1015_12_BITS, } +ADS1115Samplerate = ads1115_ns.enum("ADS1115Samplerate") +SAMPLERATE = { + "8": ADS1115Samplerate.ADS1115_8SPS, + "16": ADS1115Samplerate.ADS1115_16SPS, + "32": ADS1115Samplerate.ADS1115_32SPS, + "64": ADS1115Samplerate.ADS1115_64SPS, + "128": ADS1115Samplerate.ADS1115_128SPS, + "250": ADS1115Samplerate.ADS1115_250SPS, + "475": ADS1115Samplerate.ADS1115_475SPS, + "860": ADS1115Samplerate.ADS1115_860SPS, +} ADS1115Sensor = ads1115_ns.class_( "ADS1115Sensor", sensor.Sensor, cg.PollingComponent, voltage_sampler.VoltageSampler @@ -64,6 +76,9 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_RESOLUTION, default="16_BITS"): cv.enum( RESOLUTION, upper=True, space="_" ), + cv.Optional(CONF_SAMPLE_RATE, default="860"): cv.enum( + SAMPLERATE, string=True + ), } ) .extend(cv.polling_component_schema("60s")) @@ -79,3 +94,4 @@ async def to_code(config): cg.add(var.set_multiplexer(config[CONF_MULTIPLEXER])) cg.add(var.set_gain(config[CONF_GAIN])) cg.add(var.set_resolution(config[CONF_RESOLUTION])) + cg.add(var.set_samplerate(config[CONF_SAMPLE_RATE])) diff --git a/esphome/components/ads1115/sensor/ads1115_sensor.cpp b/esphome/components/ads1115/sensor/ads1115_sensor.cpp index 335fca4845..6de95f1d12 100644 --- a/esphome/components/ads1115/sensor/ads1115_sensor.cpp +++ b/esphome/components/ads1115/sensor/ads1115_sensor.cpp @@ -8,7 +8,7 @@ namespace ads1115 { static const char *const TAG = "ads1115.sensor"; float ADS1115Sensor::sample() { - return this->parent_->request_measurement(this->multiplexer_, this->gain_, this->resolution_); + return this->parent_->request_measurement(this->multiplexer_, this->gain_, this->resolution_, this->samplerate_); } void ADS1115Sensor::update() { @@ -24,6 +24,7 @@ void ADS1115Sensor::dump_config() { ESP_LOGCONFIG(TAG, " Multiplexer: %u", this->multiplexer_); ESP_LOGCONFIG(TAG, " Gain: %u", this->gain_); ESP_LOGCONFIG(TAG, " Resolution: %u", this->resolution_); + ESP_LOGCONFIG(TAG, " Sample rate: %u", this->samplerate_); } } // namespace ads1115 diff --git a/esphome/components/ads1115/sensor/ads1115_sensor.h b/esphome/components/ads1115/sensor/ads1115_sensor.h index 191afc3de6..5ca25c13ad 100644 --- a/esphome/components/ads1115/sensor/ads1115_sensor.h +++ b/esphome/components/ads1115/sensor/ads1115_sensor.h @@ -21,6 +21,7 @@ class ADS1115Sensor : public sensor::Sensor, void set_multiplexer(ADS1115Multiplexer multiplexer) { this->multiplexer_ = multiplexer; } void set_gain(ADS1115Gain gain) { this->gain_ = gain; } void set_resolution(ADS1115Resolution resolution) { this->resolution_ = resolution; } + void set_samplerate(ADS1115Samplerate samplerate) { this->samplerate_ = samplerate; } float sample() override; void dump_config() override; @@ -29,6 +30,7 @@ class ADS1115Sensor : public sensor::Sensor, ADS1115Multiplexer multiplexer_; ADS1115Gain gain_; ADS1115Resolution resolution_; + ADS1115Samplerate samplerate_; }; } // namespace ads1115 diff --git a/tests/components/ads1115/test.esp32-ard.yaml b/tests/components/ads1115/test.esp32-ard.yaml index a869f2379b..0fdaeff275 100644 --- a/tests/components/ads1115/test.esp32-ard.yaml +++ b/tests/components/ads1115/test.esp32-ard.yaml @@ -10,4 +10,5 @@ sensor: - platform: ads1115 multiplexer: A0_A1 gain: 1.024 + sample_rate: 128 id: ads1115_sensor diff --git a/tests/components/ads1115/test.esp32-c3-ard.yaml b/tests/components/ads1115/test.esp32-c3-ard.yaml index 7ac5a09f3f..265d2cad2c 100644 --- a/tests/components/ads1115/test.esp32-c3-ard.yaml +++ b/tests/components/ads1115/test.esp32-c3-ard.yaml @@ -10,4 +10,5 @@ sensor: - platform: ads1115 multiplexer: A0_A1 gain: 1.024 + sample_rate: 128 id: ads1115_sensor diff --git a/tests/components/ads1115/test.esp32-c3-idf.yaml b/tests/components/ads1115/test.esp32-c3-idf.yaml index 7ac5a09f3f..265d2cad2c 100644 --- a/tests/components/ads1115/test.esp32-c3-idf.yaml +++ b/tests/components/ads1115/test.esp32-c3-idf.yaml @@ -10,4 +10,5 @@ sensor: - platform: ads1115 multiplexer: A0_A1 gain: 1.024 + sample_rate: 128 id: ads1115_sensor diff --git a/tests/components/ads1115/test.esp32-idf.yaml b/tests/components/ads1115/test.esp32-idf.yaml index a869f2379b..0fdaeff275 100644 --- a/tests/components/ads1115/test.esp32-idf.yaml +++ b/tests/components/ads1115/test.esp32-idf.yaml @@ -10,4 +10,5 @@ sensor: - platform: ads1115 multiplexer: A0_A1 gain: 1.024 + sample_rate: 128 id: ads1115_sensor diff --git a/tests/components/ads1115/test.esp8266-ard.yaml b/tests/components/ads1115/test.esp8266-ard.yaml index 7ac5a09f3f..265d2cad2c 100644 --- a/tests/components/ads1115/test.esp8266-ard.yaml +++ b/tests/components/ads1115/test.esp8266-ard.yaml @@ -10,4 +10,5 @@ sensor: - platform: ads1115 multiplexer: A0_A1 gain: 1.024 + sample_rate: 128 id: ads1115_sensor diff --git a/tests/components/ads1115/test.rp2040-ard.yaml b/tests/components/ads1115/test.rp2040-ard.yaml index 7ac5a09f3f..265d2cad2c 100644 --- a/tests/components/ads1115/test.rp2040-ard.yaml +++ b/tests/components/ads1115/test.rp2040-ard.yaml @@ -10,4 +10,5 @@ sensor: - platform: ads1115 multiplexer: A0_A1 gain: 1.024 + sample_rate: 128 id: ads1115_sensor From 65b2d48a6fea3a0ee249d6c2dcf4288ed1a63704 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskari=20Lemmel=C3=A4?= <41640457+olemmela@users.noreply.github.com> Date: Thu, 23 Jan 2025 01:32:45 +0200 Subject: [PATCH 83/86] Fix mqtt climate step rounding (#8121) --- esphome/components/mqtt/mqtt_climate.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/mqtt/mqtt_climate.cpp b/esphome/components/mqtt/mqtt_climate.cpp index f06574fa26..a8768114a4 100644 --- a/esphome/components/mqtt/mqtt_climate.cpp +++ b/esphome/components/mqtt/mqtt_climate.cpp @@ -72,9 +72,9 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo // max_temp root[MQTT_MAX_TEMP] = traits.get_visual_max_temperature(); // target_temp_step - root[MQTT_TARGET_TEMPERATURE_STEP] = traits.get_visual_target_temperature_step(); + root[MQTT_TARGET_TEMPERATURE_STEP] = roundf(traits.get_visual_target_temperature_step() * 10) * 0.1; // current_temp_step - root[MQTT_CURRENT_TEMPERATURE_STEP] = traits.get_visual_current_temperature_step(); + root[MQTT_CURRENT_TEMPERATURE_STEP] = roundf(traits.get_visual_current_temperature_step() * 10) * 0.1; // temperature units are always coerced to Celsius internally root[MQTT_TEMPERATURE_UNIT] = "C"; From dee1d849793ae789a6f4bab730f02268b3975e60 Mon Sep 17 00:00:00 2001 From: Olliver Schinagl Date: Thu, 23 Jan 2025 00:41:55 +0100 Subject: [PATCH 84/86] [spi] Fix data type in bitbash transfer_() (#8125) --- esphome/components/spi/spi.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/spi/spi.cpp b/esphome/components/spi/spi.cpp index b13826c443..18f7852757 100644 --- a/esphome/components/spi/spi.cpp +++ b/esphome/components/spi/spi.cpp @@ -88,7 +88,7 @@ void SPIDelegateBitBash::write(uint16_t data, size_t num_bits) { this->transfer_ uint16_t SPIDelegateBitBash::transfer_(uint16_t data, size_t num_bits) { // Clock starts out at idle level this->clk_pin_->digital_write(clock_polarity_); - uint8_t out_data = 0; + uint16_t out_data = 0; for (uint8_t i = 0; i != num_bits; i++) { uint8_t shift; From 7fccc9ff86193a5f7d094c9253dcd4a5b924e6ad Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 23 Jan 2025 15:10:19 +1300 Subject: [PATCH 85/86] [online_image] Add binary bmp support (#8116) Co-authored-by: guillempages --- esphome/components/online_image/__init__.py | 16 ++- esphome/components/online_image/bmp_image.cpp | 101 ++++++++++++++++++ esphome/components/online_image/bmp_image.h | 40 +++++++ .../components/online_image/image_decoder.cpp | 5 +- .../components/online_image/image_decoder.h | 9 +- .../components/online_image/online_image.cpp | 11 +- .../components/online_image/online_image.h | 8 +- esphome/core/defines.h | 1 + tests/components/online_image/common.yaml | 4 + 9 files changed, 187 insertions(+), 8 deletions(-) create mode 100644 esphome/components/online_image/bmp_image.cpp create mode 100644 esphome/components/online_image/bmp_image.h diff --git a/esphome/components/online_image/__init__.py b/esphome/components/online_image/__init__.py index ca4eefea6f..fdb49fc493 100644 --- a/esphome/components/online_image/__init__.py +++ b/esphome/components/online_image/__init__.py @@ -61,8 +61,22 @@ class PNGFormat(Format): cg.add_library("pngle", "1.0.2") +class BMPFormat(Format): + def __init__(self): + super().__init__("BMP") + + def actions(self): + cg.add_define("USE_ONLINE_IMAGE_BMP_SUPPORT") + + # New formats can be added here. -IMAGE_FORMATS = {x.image_type: x for x in (PNGFormat(),)} +IMAGE_FORMATS = { + x.image_type: x + for x in ( + PNGFormat(), + BMPFormat(), + ) +} OnlineImage = online_image_ns.class_("OnlineImage", cg.PollingComponent, Image_) diff --git a/esphome/components/online_image/bmp_image.cpp b/esphome/components/online_image/bmp_image.cpp new file mode 100644 index 0000000000..af9019a4d2 --- /dev/null +++ b/esphome/components/online_image/bmp_image.cpp @@ -0,0 +1,101 @@ +#include "bmp_image.h" + +#ifdef USE_ONLINE_IMAGE_BMP_SUPPORT + +#include "esphome/components/display/display.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace online_image { + +static const char *const TAG = "online_image.bmp"; + +int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) { + size_t index = 0; + if (this->current_index_ == 0 && index == 0 && size > 14) { + /** + * BMP file format: + * 0-1: Signature (BM) + * 2-5: File size + * 6-9: Reserved + * 10-13: Pixel data offset + * + * Integer values are stored in little-endian format. + */ + + // Check if the file is a BMP image + if (buffer[0] != 'B' || buffer[1] != 'M') { + ESP_LOGE(TAG, "Not a BMP file"); + return DECODE_ERROR_INVALID_TYPE; + } + + this->download_size_ = encode_uint32(buffer[5], buffer[4], buffer[3], buffer[2]); + this->data_offset_ = encode_uint32(buffer[13], buffer[12], buffer[11], buffer[10]); + + this->current_index_ = 14; + index = 14; + } + if (this->current_index_ == 14 && index == 14 && size > this->data_offset_) { + /** + * BMP DIB header: + * 14-17: DIB header size + * 18-21: Image width + * 22-25: Image height + * 26-27: Number of color planes + * 28-29: Bits per pixel + * 30-33: Compression method + * 34-37: Image data size + * 38-41: Horizontal resolution + * 42-45: Vertical resolution + * 46-49: Number of colors in the color table + */ + + this->width_ = encode_uint32(buffer[21], buffer[20], buffer[19], buffer[18]); + this->height_ = encode_uint32(buffer[25], buffer[24], buffer[23], buffer[22]); + this->bits_per_pixel_ = encode_uint16(buffer[29], buffer[28]); + this->compression_method_ = encode_uint32(buffer[33], buffer[32], buffer[31], buffer[30]); + this->image_data_size_ = encode_uint32(buffer[37], buffer[36], buffer[35], buffer[34]); + this->color_table_entries_ = encode_uint32(buffer[49], buffer[48], buffer[47], buffer[46]); + + switch (this->bits_per_pixel_) { + case 1: + this->width_bytes_ = (this->width_ % 8 == 0) ? (this->width_ / 8) : (this->width_ / 8 + 1); + break; + default: + ESP_LOGE(TAG, "Unsupported bits per pixel: %d", this->bits_per_pixel_); + return DECODE_ERROR_UNSUPPORTED_FORMAT; + } + + if (this->compression_method_ != 0) { + ESP_LOGE(TAG, "Unsupported compression method: %d", this->compression_method_); + return DECODE_ERROR_UNSUPPORTED_FORMAT; + } + + if (!this->set_size(this->width_, this->height_)) { + return DECODE_ERROR_OUT_OF_MEMORY; + } + this->current_index_ = this->data_offset_; + index = this->data_offset_; + } + while (index < size) { + size_t paint_index = this->current_index_ - this->data_offset_; + + uint8_t current_byte = buffer[index]; + for (uint8_t i = 0; i < 8; i++) { + size_t x = (paint_index * 8) % this->width_ + i; + size_t y = (this->height_ - 1) - (paint_index / this->width_bytes_); + Color c = (current_byte & (1 << (7 - i))) ? display::COLOR_ON : display::COLOR_OFF; + this->draw(x, y, 1, 1, c); + } + this->current_index_++; + index++; + } + this->decoded_bytes_ += size; + return size; +}; + +} // namespace online_image +} // namespace esphome + +#endif // USE_ONLINE_IMAGE_BMP_SUPPORT diff --git a/esphome/components/online_image/bmp_image.h b/esphome/components/online_image/bmp_image.h new file mode 100644 index 0000000000..61192f6a46 --- /dev/null +++ b/esphome/components/online_image/bmp_image.h @@ -0,0 +1,40 @@ +#pragma once + +#include "esphome/core/defines.h" +#ifdef USE_ONLINE_IMAGE_BMP_SUPPORT + +#include "image_decoder.h" + +namespace esphome { +namespace online_image { + +/** + * @brief Image decoder specialization for PNG images. + */ +class BmpDecoder : public ImageDecoder { + public: + /** + * @brief Construct a new BMP Decoder object. + * + * @param display The image to decode the stream into. + */ + BmpDecoder(OnlineImage *image) : ImageDecoder(image) {} + + int HOT decode(uint8_t *buffer, size_t size) override; + + protected: + size_t current_index_{0}; + ssize_t width_{0}; + ssize_t height_{0}; + uint16_t bits_per_pixel_{0}; + uint32_t compression_method_{0}; + uint32_t image_data_size_{0}; + uint32_t color_table_entries_{0}; + size_t width_bytes_{0}; + size_t data_offset_{0}; +}; + +} // namespace online_image +} // namespace esphome + +#endif // USE_ONLINE_IMAGE_BMP_SUPPORT diff --git a/esphome/components/online_image/image_decoder.cpp b/esphome/components/online_image/image_decoder.cpp index 50ec39dfcc..d0d0495ba6 100644 --- a/esphome/components/online_image/image_decoder.cpp +++ b/esphome/components/online_image/image_decoder.cpp @@ -8,10 +8,11 @@ namespace online_image { static const char *const TAG = "online_image.decoder"; -void ImageDecoder::set_size(int width, int height) { - this->image_->resize_(width, height); +bool ImageDecoder::set_size(int width, int height) { + bool resized = this->image_->resize_(width, height); this->x_scale_ = static_cast(this->image_->buffer_width_) / width; this->y_scale_ = static_cast(this->image_->buffer_height_) / height; + return resized; } void ImageDecoder::draw(int x, int y, int w, int h, const Color &color) { diff --git a/esphome/components/online_image/image_decoder.h b/esphome/components/online_image/image_decoder.h index 7c5175d72d..3b04824bb9 100644 --- a/esphome/components/online_image/image_decoder.h +++ b/esphome/components/online_image/image_decoder.h @@ -4,6 +4,12 @@ namespace esphome { namespace online_image { +enum DecodeError : int { + DECODE_ERROR_INVALID_TYPE = -1, + DECODE_ERROR_UNSUPPORTED_FORMAT = -2, + DECODE_ERROR_OUT_OF_MEMORY = -3, +}; + class OnlineImage; /** @@ -45,8 +51,9 @@ class ImageDecoder { * * @param width The image's width. * @param height The image's height. + * @return true if the image was resized, false otherwise. */ - void set_size(int width, int height); + bool set_size(int width, int height); /** * @brief Fill a rectangle on the display_buffer using the defined color. diff --git a/esphome/components/online_image/online_image.cpp b/esphome/components/online_image/online_image.cpp index 93d070c6a9..23fe61b534 100644 --- a/esphome/components/online_image/online_image.cpp +++ b/esphome/components/online_image/online_image.cpp @@ -10,6 +10,10 @@ static const char *const TAG = "online_image"; #include "png_image.h" #endif +#ifdef USE_ONLINE_IMAGE_BMP_SUPPORT +#include "bmp_image.h" +#endif + namespace esphome { namespace online_image { @@ -120,9 +124,14 @@ void OnlineImage::update() { #ifdef USE_ONLINE_IMAGE_PNG_SUPPORT if (this->format_ == ImageFormat::PNG) { - this->decoder_ = esphome::make_unique(this); + this->decoder_ = make_unique(this); } #endif // ONLINE_IMAGE_PNG_SUPPORT +#ifdef USE_ONLINE_IMAGE_BMP_SUPPORT + if (this->format_ == ImageFormat::BMP) { + this->decoder_ = make_unique(this); + } +#endif // ONLINE_IMAGE_BMP_SUPPORT if (!this->decoder_) { ESP_LOGE(TAG, "Could not instantiate decoder. Image format unsupported."); diff --git a/esphome/components/online_image/online_image.h b/esphome/components/online_image/online_image.h index bafd8ba67e..849f860ad5 100644 --- a/esphome/components/online_image/online_image.h +++ b/esphome/components/online_image/online_image.h @@ -1,10 +1,10 @@ #pragma once +#include "esphome/components/http_request/http_request.h" +#include "esphome/components/image/image.h" #include "esphome/core/component.h" #include "esphome/core/defines.h" #include "esphome/core/helpers.h" -#include "esphome/components/http_request/http_request.h" -#include "esphome/components/image/image.h" #include "image_decoder.h" @@ -27,6 +27,8 @@ enum ImageFormat { JPEG, /** PNG format. */ PNG, + /** BMP format. */ + BMP, }; /** @@ -146,7 +148,7 @@ class OnlineImage : public PollingComponent, */ int buffer_height_; - friend void ImageDecoder::set_size(int width, int height); + friend bool ImageDecoder::set_size(int width, int height); friend void ImageDecoder::draw(int x, int y, int w, int h, const Color &color); }; diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 96a05435ed..074b19809f 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -60,6 +60,7 @@ #define USE_NETWORK #define USE_NEXTION_TFT_UPLOAD #define USE_NUMBER +#define USE_ONLINE_IMAGE_BMP_SUPPORT #define USE_ONLINE_IMAGE_PNG_SUPPORT #define USE_OTA #define USE_OTA_PASSWORD diff --git a/tests/components/online_image/common.yaml b/tests/components/online_image/common.yaml index 6c161e4f20..d75900adf9 100644 --- a/tests/components/online_image/common.yaml +++ b/tests/components/online_image/common.yaml @@ -26,6 +26,10 @@ online_image: format: PNG type: RGB transparency: chroma_key + - id: online_binary_bmp + url: https://samples-files.com/samples/images/bmp/480-360-sample.bmp + format: BMP + type: BINARY # Check the set_url action esphome: From fc847c1de8927871e783db1a35e967efabda9f04 Mon Sep 17 00:00:00 2001 From: guillempages Date: Thu, 23 Jan 2025 21:32:03 +0100 Subject: [PATCH 86/86] [online_image] Code Improvements (#8130) --- esphome/components/online_image/__init__.py | 18 +++++++++--------- .../components/online_image/image_decoder.h | 6 +++--- .../components/online_image/online_image.cpp | 17 ++++++++--------- esphome/components/online_image/png_image.cpp | 4 ++-- esphome/components/online_image/png_image.h | 2 +- 5 files changed, 23 insertions(+), 24 deletions(-) diff --git a/esphome/components/online_image/__init__.py b/esphome/components/online_image/__init__.py index fdb49fc493..eb6debf8eb 100644 --- a/esphome/components/online_image/__init__.py +++ b/esphome/components/online_image/__init__.py @@ -52,6 +52,14 @@ class Format: pass +class BMPFormat(Format): + def __init__(self): + super().__init__("BMP") + + def actions(self): + cg.add_define("USE_ONLINE_IMAGE_BMP_SUPPORT") + + class PNGFormat(Format): def __init__(self): super().__init__("PNG") @@ -61,20 +69,12 @@ class PNGFormat(Format): cg.add_library("pngle", "1.0.2") -class BMPFormat(Format): - def __init__(self): - super().__init__("BMP") - - def actions(self): - cg.add_define("USE_ONLINE_IMAGE_BMP_SUPPORT") - - # New formats can be added here. IMAGE_FORMATS = { x.image_type: x for x in ( - PNGFormat(), BMPFormat(), + PNGFormat(), ) } diff --git a/esphome/components/online_image/image_decoder.h b/esphome/components/online_image/image_decoder.h index 3b04824bb9..4e5dd7b229 100644 --- a/esphome/components/online_image/image_decoder.h +++ b/esphome/components/online_image/image_decoder.h @@ -30,7 +30,7 @@ class ImageDecoder { * * @param download_size The total number of bytes that need to be downloaded for the image. */ - virtual void prepare(uint32_t download_size) { this->download_size_ = download_size; } + virtual void prepare(size_t download_size) { this->download_size_ = download_size; } /** * @brief Decode a part of the image. It will try reading from the buffer. @@ -75,8 +75,8 @@ class ImageDecoder { OnlineImage *image_; // Initializing to 1, to ensure it is distinguishable from initial "decoded_bytes_". // Will be overwritten anyway once the download size is known. - uint32_t download_size_ = 1; - uint32_t decoded_bytes_ = 0; + size_t download_size_ = 1; + size_t decoded_bytes_ = 0; double x_scale_ = 1.0; double y_scale_ = 1.0; }; diff --git a/esphome/components/online_image/online_image.cpp b/esphome/components/online_image/online_image.cpp index 23fe61b534..c6499c24e4 100644 --- a/esphome/components/online_image/online_image.cpp +++ b/esphome/components/online_image/online_image.cpp @@ -6,13 +6,12 @@ static const char *const TAG = "online_image"; #include "image_decoder.h" -#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT -#include "png_image.h" -#endif - #ifdef USE_ONLINE_IMAGE_BMP_SUPPORT #include "bmp_image.h" #endif +#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT +#include "png_image.h" +#endif namespace esphome { namespace online_image { @@ -122,16 +121,16 @@ void OnlineImage::update() { ESP_LOGD(TAG, "Starting download"); size_t total_size = this->downloader_->content_length; -#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT - if (this->format_ == ImageFormat::PNG) { - this->decoder_ = make_unique(this); - } -#endif // ONLINE_IMAGE_PNG_SUPPORT #ifdef USE_ONLINE_IMAGE_BMP_SUPPORT if (this->format_ == ImageFormat::BMP) { this->decoder_ = make_unique(this); } #endif // ONLINE_IMAGE_BMP_SUPPORT +#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT + if (this->format_ == ImageFormat::PNG) { + this->decoder_ = make_unique(this); + } +#endif // ONLINE_IMAGE_PNG_SUPPORT if (!this->decoder_) { ESP_LOGE(TAG, "Could not instantiate decoder. Image format unsupported."); diff --git a/esphome/components/online_image/png_image.cpp b/esphome/components/online_image/png_image.cpp index 4c4c22f9b7..59c1ce6c7d 100644 --- a/esphome/components/online_image/png_image.cpp +++ b/esphome/components/online_image/png_image.cpp @@ -41,7 +41,7 @@ static void draw_callback(pngle_t *pngle, uint32_t x, uint32_t y, uint32_t w, ui decoder->draw(x, y, w, h, color); } -void PngDecoder::prepare(uint32_t download_size) { +void PngDecoder::prepare(size_t download_size) { ImageDecoder::prepare(download_size); pngle_set_user_data(this->pngle_, this); pngle_set_init_callback(this->pngle_, init_callback); @@ -51,7 +51,7 @@ void PngDecoder::prepare(uint32_t download_size) { int HOT PngDecoder::decode(uint8_t *buffer, size_t size) { if (!this->pngle_) { ESP_LOGE(TAG, "PNG decoder engine not initialized!"); - return -1; + return DECODE_ERROR_OUT_OF_MEMORY; } if (size < 256 && size < this->download_size_ - this->decoded_bytes_) { ESP_LOGD(TAG, "Waiting for data"); diff --git a/esphome/components/online_image/png_image.h b/esphome/components/online_image/png_image.h index d82ff93149..c137227907 100644 --- a/esphome/components/online_image/png_image.h +++ b/esphome/components/online_image/png_image.h @@ -21,7 +21,7 @@ class PngDecoder : public ImageDecoder { PngDecoder(OnlineImage *image) : ImageDecoder(image), pngle_(pngle_new()) {} ~PngDecoder() override { pngle_destroy(this->pngle_); } - void prepare(uint32_t download_size) override; + void prepare(size_t download_size) override; int HOT decode(uint8_t *buffer, size_t size) override; protected: