From a779592414092163f5e29299567102f9072f6475 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 13 Oct 2021 16:40:46 +1300 Subject: [PATCH 0001/1729] Bump version to 2021.10.0b1 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index a9eec3e249..ce7d363e92 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2021.10.0-dev" +__version__ = "2021.10.0b1" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From f57980b069759037c7217336ba1aca590b9699f6 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 13 Oct 2021 22:30:29 +1300 Subject: [PATCH 0002/1729] Bump version to 2021.10.0b2 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index ce7d363e92..456b6d9209 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2021.10.0b1" +__version__ = "2021.10.0b2" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From b3b9ccd314d7117eee216037ca1ec6c075a3573b Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Wed, 13 Oct 2021 21:53:00 +0200 Subject: [PATCH 0003/1729] Fix light state remaining on after turn off with transition (#2509) --- esphome/components/light/transformers.h | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/esphome/components/light/transformers.h b/esphome/components/light/transformers.h index 90646f4e61..c22846ceb1 100644 --- a/esphome/components/light/transformers.h +++ b/esphome/components/light/transformers.h @@ -18,10 +18,13 @@ class LightTransitionTransformer : public LightTransformer { this->start_values_.set_brightness(0.0f); } - // When turning light off from on state, use source state and only decrease brightness to zero. + // When turning light off from on state, use source state and only decrease brightness to zero. Use a second + // variable for transition end state, as overwriting target_values breaks LightState logic. if (this->start_values_.is_on() && !this->target_values_.is_on()) { - this->target_values_ = LightColorValues(this->start_values_); - this->target_values_.set_brightness(0.0f); + this->end_values_ = LightColorValues(this->start_values_); + this->end_values_.set_brightness(0.0f); + } else { + this->end_values_ = LightColorValues(this->target_values_); } // When changing color mode, go through off state, as color modes are orthogonal and there can't be two active. @@ -43,7 +46,7 @@ class LightTransitionTransformer : public LightTransformer { } LightColorValues &start = this->changing_color_mode_ && p > 0.5f ? this->intermediate_values_ : this->start_values_; - LightColorValues &end = this->changing_color_mode_ && p < 0.5f ? this->intermediate_values_ : this->target_values_; + LightColorValues &end = this->changing_color_mode_ && p < 0.5f ? this->intermediate_values_ : this->end_values_; if (this->changing_color_mode_) p = p < 0.5f ? p * 2 : (p - 0.5) * 2; @@ -57,6 +60,7 @@ class LightTransitionTransformer : public LightTransformer { static float smoothed_progress(float x) { return x * x * x * (x * (x * 6.0f - 15.0f) + 10.0f); } bool changing_color_mode_{false}; + LightColorValues end_values_{}; LightColorValues intermediate_values_{}; }; From 48ff2ffc68479963dc76ea678fb2297a9e6bdc92 Mon Sep 17 00:00:00 2001 From: Paul Monigatti Date: Thu, 14 Oct 2021 08:59:52 +1300 Subject: [PATCH 0004/1729] Fix: Light flash not restoring previous LightState (#2383) * Update light state when transformer has finished * Revert writing direct to output * Correct handling of zero-length light transformers * Allow transformers to handle zero-length transitions, and check more boundary conditions when transitioning back to start state * Removed log.h * Fixed race condition between LightFlashTransformer.apply() and is_finished() * clang-format * Step progress from 0.0f to 1.0f at t=start_time for zero-length transforms to avoid divide-by-zero --- .../components/light/addressable_light.cpp | 2 +- esphome/components/light/light_transformer.h | 10 ++++- esphome/components/light/transformers.h | 37 ++++++++++--------- 3 files changed, 30 insertions(+), 19 deletions(-) diff --git a/esphome/components/light/addressable_light.cpp b/esphome/components/light/addressable_light.cpp index f3e6c0ef1d..a8e0c7b762 100644 --- a/esphome/components/light/addressable_light.cpp +++ b/esphome/components/light/addressable_light.cpp @@ -79,7 +79,7 @@ optional AddressableLightTransformer::apply() { // dynamically-calculated alpha values to match the look. float denom = (1.0f - smoothed_progress); - float alpha = denom == 0.0f ? 0.0f : (smoothed_progress - this->last_transition_progress_) / denom; + float alpha = denom == 0.0f ? 1.0f : (smoothed_progress - this->last_transition_progress_) / denom; // We need to use a low-resolution alpha here which makes the transition set in only after ~half of the length // We solve this by accumulating the fractional part of the alpha over time. diff --git a/esphome/components/light/light_transformer.h b/esphome/components/light/light_transformer.h index dd904d0eed..35b045d5b4 100644 --- a/esphome/components/light/light_transformer.h +++ b/esphome/components/light/light_transformer.h @@ -39,7 +39,15 @@ class LightTransformer { protected: /// The progress of this transition, on a scale of 0 to 1. - float get_progress_() { return clamp((millis() - this->start_time_) / float(this->length_), 0.0f, 1.0f); } + float get_progress_() { + uint32_t now = esphome::millis(); + if (now < this->start_time_) + return 0.0f; + if (now >= this->start_time_ + this->length_) + return 1.0f; + + return clamp((now - this->start_time_) / float(this->length_), 0.0f, 1.0f); + } uint32_t start_time_; uint32_t length_; diff --git a/esphome/components/light/transformers.h b/esphome/components/light/transformers.h index c22846ceb1..a557bd39b1 100644 --- a/esphome/components/light/transformers.h +++ b/esphome/components/light/transformers.h @@ -73,9 +73,7 @@ class LightFlashTransformer : public LightTransformer { if (this->transition_length_ * 2 > this->length_) this->transition_length_ = this->length_ / 2; - // do not create transition if length is 0 - if (this->transition_length_ == 0) - return; + this->begun_lightstate_restore_ = false; // first transition to original target this->transformer_ = this->state_.get_output()->create_default_transition(); @@ -83,40 +81,45 @@ class LightFlashTransformer : public LightTransformer { } optional apply() override { - // transition transformer does not handle 0 length as progress returns nan - if (this->transition_length_ == 0) - return this->target_values_; + optional result = {}; + + if (this->transformer_ == nullptr && millis() > this->start_time_ + this->length_ - this->transition_length_) { + // second transition back to start value + this->transformer_ = this->state_.get_output()->create_default_transition(); + this->transformer_->setup(this->state_.current_values, this->get_start_values(), this->transition_length_); + this->begun_lightstate_restore_ = true; + } if (this->transformer_ != nullptr) { - if (!this->transformer_->is_finished()) { - return this->transformer_->apply(); - } else { + result = this->transformer_->apply(); + + if (this->transformer_->is_finished()) { this->transformer_->stop(); this->transformer_ = nullptr; } } - if (millis() > this->start_time_ + this->length_ - this->transition_length_) { - // second transition back to start value - this->transformer_ = this->state_.get_output()->create_default_transition(); - this->transformer_->setup(this->state_.current_values, this->get_start_values(), this->transition_length_); - } - - // once transition is complete, don't change states until next transition - return optional(); + return result; } // Restore the original values after the flash. void stop() override { + if (this->transformer_ != nullptr) { + this->transformer_->stop(); + this->transformer_ = nullptr; + } this->state_.current_values = this->get_start_values(); this->state_.remote_values = this->get_start_values(); this->state_.publish_state(); } + bool is_finished() override { return this->begun_lightstate_restore_ && LightTransformer::is_finished(); } + protected: LightState &state_; uint32_t transition_length_; std::unique_ptr transformer_{nullptr}; + bool begun_lightstate_restore_; }; } // namespace light From 5f7cef0b06b0f12db3d7131db2fbcf5b71848225 Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Wed, 13 Oct 2021 22:21:43 +0200 Subject: [PATCH 0005/1729] Disallow using UART2 for logger on ESP-32 variants that lack it (#2510) --- esphome/components/esp32/__init__.py | 6 +++++- esphome/components/logger/__init__.py | 7 +++++++ esphome/components/logger/logger.cpp | 8 +++----- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 704f9bb3e8..09eabe1fa7 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -21,12 +21,16 @@ from esphome.core import CORE, HexInt import esphome.config_validation as cv import esphome.codegen as cg -from .const import ( +from .const import ( # noqa KEY_BOARD, KEY_ESP32, KEY_SDKCONFIG_OPTIONS, KEY_VARIANT, + VARIANT_ESP32, + VARIANT_ESP32S2, + VARIANT_ESP32S3, VARIANT_ESP32C3, + VARIANT_ESP32H2, VARIANTS, ) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index bc1bc6bb41..fe2a3ec8f8 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -19,6 +19,7 @@ from esphome.const import ( CONF_TX_BUFFER_SIZE, ) from esphome.core import CORE, EsphomeError, Lambda, coroutine_with_priority +from esphome.components.esp32 import get_esp32_variant, VARIANT_ESP32S2, VARIANT_ESP32C3 CODEOWNERS = ["@esphome/core"] logger_ns = cg.esphome_ns.namespace("logger") @@ -52,6 +53,10 @@ LOG_LEVEL_SEVERITY = [ "VERY_VERBOSE", ] +ESP32_REDUCED_VARIANTS = [VARIANT_ESP32C3, VARIANT_ESP32S2] + +UART_SELECTION_ESP32_REDUCED = ["UART0", "UART1"] + UART_SELECTION_ESP32 = ["UART0", "UART1", "UART2"] UART_SELECTION_ESP8266 = ["UART0", "UART0_SWAP", "UART1"] @@ -75,6 +80,8 @@ is_log_level = cv.one_of(*LOG_LEVELS, upper=True) def uart_selection(value): if CORE.is_esp32: + if get_esp32_variant() in ESP32_REDUCED_VARIANTS: + return cv.one_of(*UART_SELECTION_ESP32_REDUCED, upper=True)(value) return cv.one_of(*UART_SELECTION_ESP32, upper=True)(value) if CORE.is_esp8266: return cv.one_of(*UART_SELECTION_ESP8266, upper=True)(value) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 2d85969bf3..b38c7f1a69 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -153,13 +153,9 @@ void Logger::pre_setup() { case UART_SELECTION_UART1: this->hw_serial_ = &Serial1; break; -#ifdef USE_ESP32 +#if defined(USE_ESP32) && !CONFIG_IDF_TARGET_ESP32C3 && !CONFIG_IDF_TARGET_ESP32S2 case UART_SELECTION_UART2: -#if !CONFIG_IDF_TARGET_ESP32S2 && !CONFIG_IDF_TARGET_ESP32C3 - // FIXME: Validate in config that UART2 can't be set for ESP32-S2 (only has - // UART0-UART1) this->hw_serial_ = &Serial2; -#endif break; #endif } @@ -173,9 +169,11 @@ void Logger::pre_setup() { case UART_SELECTION_UART1: uart_num_ = UART_NUM_1; break; +#if !CONFIG_IDF_TARGET_ESP32C3 && !CONFIG_IDF_TARGET_ESP32S2 case UART_SELECTION_UART2: uart_num_ = UART_NUM_2; break; +#endif } uart_config_t uart_config{}; uart_config.baud_rate = (int) baud_rate_; From d8a6dfe5ce9ca7b5ea4d25a3a3e99ce800ca6ee2 Mon Sep 17 00:00:00 2001 From: Paul Monigatti Date: Thu, 14 Oct 2021 20:58:35 +1300 Subject: [PATCH 0006/1729] Fix BME680_BSEC compilation issue with ESP32 (#2516) --- esphome/components/bme680_bsec/__init__.py | 8 +++++++- tests/test1.yaml | 16 ++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/esphome/components/bme680_bsec/__init__.py b/esphome/components/bme680_bsec/__init__.py index d258819aa4..38da18d702 100644 --- a/esphome/components/bme680_bsec/__init__.py +++ b/esphome/components/bme680_bsec/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c from esphome.const import CONF_ID +from esphome.core import CORE CODEOWNERS = ["@trvrnrth"] DEPENDENCIES = ["i2c"] @@ -44,7 +45,8 @@ CONFIG_SCHEMA = cv.Schema( cv.Optional( CONF_STATE_SAVE_INTERVAL, default="6hours" ): cv.positive_time_period_minutes, - } + }, + cv.only_with_arduino, ).extend(i2c.i2c_device_schema(0x76)) @@ -60,5 +62,9 @@ async def to_code(config): var.set_state_save_interval(config[CONF_STATE_SAVE_INTERVAL].total_milliseconds) ) + if CORE.is_esp32: + # Although this component does not use SPI, the BSEC library requires the SPI library + cg.add_library("SPI", None) + cg.add_define("USE_BSEC") cg.add_library("BSEC Software Library", "1.6.1480") diff --git a/tests/test1.yaml b/tests/test1.yaml index 157ccfc5d1..62fc781eca 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -265,6 +265,9 @@ wled: adalight: +bme680_bsec: + i2c_id: i2c_bus + esp32_ble_tracker: ble_client: @@ -478,6 +481,19 @@ sensor: duration: 150ms update_interval: 15s i2c_id: i2c_bus + - platform: bme680_bsec + temperature: + name: "BME680 Temperature" + pressure: + name: "BME680 Pressure" + humidity: + name: "BME680 Humidity" + iaq: + name: "BME680 IAQ" + co2_equivalent: + name: "BME680 CO2 Equivalent" + breath_voc_equivalent: + name: "BME680 Breath VOC Equivalent" - platform: bmp085 temperature: name: 'Outside Temperature' From c51d8c90214efa10ffb8c8a85da1ecb19c992b8a Mon Sep 17 00:00:00 2001 From: Dmitriy Lopatko Date: Thu, 14 Oct 2021 10:00:53 +0200 Subject: [PATCH 0007/1729] add missing include in sgp30 (#2517) --- esphome/components/sgp30/sgp30.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/sgp30/sgp30.cpp b/esphome/components/sgp30/sgp30.cpp index 1a64a12907..87cf0fa61a 100644 --- a/esphome/components/sgp30/sgp30.cpp +++ b/esphome/components/sgp30/sgp30.cpp @@ -1,4 +1,5 @@ #include "sgp30.h" +#include "esphome/core/hal.h" #include "esphome/core/log.h" #include "esphome/core/application.h" #include From 9b7fb829f9297e45b3ae4e641f310072bbfd9065 Mon Sep 17 00:00:00 2001 From: Paul Monigatti Date: Thu, 14 Oct 2021 21:04:50 +1300 Subject: [PATCH 0008/1729] Fix: Color modes not being correctly used in light partitions (#2513) --- .../light/addressable_light_wrapper.h | 89 +++++++++++++++++-- 1 file changed, 80 insertions(+), 9 deletions(-) diff --git a/esphome/components/light/addressable_light_wrapper.h b/esphome/components/light/addressable_light_wrapper.h index cd5bcabd47..d358502430 100644 --- a/esphome/components/light/addressable_light_wrapper.h +++ b/esphome/components/light/addressable_light_wrapper.h @@ -16,24 +16,94 @@ class AddressableLightWrapper : public light::AddressableLight { void clear_effect_data() override { this->wrapper_state_[4] = 0; } - light::LightTraits get_traits() override { return this->light_state_->get_traits(); } + light::LightTraits get_traits() override { + LightTraits traits; + + // Choose which color mode to use. + // This is ordered by how closely each color mode matches the underlying RGBW data structure used in LightPartition. + ColorMode color_mode_precedence[] = {ColorMode::RGB_WHITE, + ColorMode::RGB_COLD_WARM_WHITE, + ColorMode::RGB_COLOR_TEMPERATURE, + ColorMode::RGB, + ColorMode::WHITE, + ColorMode::COLD_WARM_WHITE, + ColorMode::COLOR_TEMPERATURE, + ColorMode::BRIGHTNESS, + ColorMode::ON_OFF, + ColorMode::UNKNOWN}; + + LightTraits parent_traits = this->light_state_->get_traits(); + for (auto cm : color_mode_precedence) { + if (parent_traits.supports_color_mode(cm)) { + this->color_mode_ = cm; + break; + } + } + + // Report a color mode that's compatible with both the partition and the underlying light + switch (this->color_mode_) { + case ColorMode::RGB_WHITE: + case ColorMode::RGB_COLD_WARM_WHITE: + case ColorMode::RGB_COLOR_TEMPERATURE: + traits.set_supported_color_modes({light::ColorMode::RGB_WHITE}); + break; + + case ColorMode::RGB: + traits.set_supported_color_modes({light::ColorMode::RGB}); + break; + + case ColorMode::WHITE: + case ColorMode::COLD_WARM_WHITE: + case ColorMode::COLOR_TEMPERATURE: + case ColorMode::BRIGHTNESS: + traits.set_supported_color_modes({light::ColorMode::BRIGHTNESS}); + break; + + case ColorMode::ON_OFF: + traits.set_supported_color_modes({light::ColorMode::ON_OFF}); + break; + + default: + traits.set_supported_color_modes({light::ColorMode::UNKNOWN}); + } + + return traits; + } void write_state(light::LightState *state) override { + // Don't overwrite state if the underlying light is turned on + if (this->light_state_->remote_values.is_on()) { + this->mark_shown_(); + return; + } + float gamma = this->light_state_->get_gamma_correct(); float r = gamma_uncorrect(this->wrapper_state_[0] / 255.0f, gamma); float g = gamma_uncorrect(this->wrapper_state_[1] / 255.0f, gamma); float b = gamma_uncorrect(this->wrapper_state_[2] / 255.0f, gamma); float w = gamma_uncorrect(this->wrapper_state_[3] / 255.0f, gamma); - float brightness = fmaxf(r, fmaxf(g, b)); auto call = this->light_state_->make_call(); - call.set_state(true); - call.set_brightness_if_supported(1.0f); - call.set_color_brightness_if_supported(brightness); - call.set_red_if_supported(r); - call.set_green_if_supported(g); - call.set_blue_if_supported(b); - call.set_white_if_supported(w); + + float color_brightness = fmaxf(r, fmaxf(g, b)); + float brightness = fmaxf(color_brightness, w); + if (brightness == 0.0f) { + call.set_state(false); + } else { + color_brightness /= brightness; + w /= brightness; + + call.set_state(true); + call.set_color_mode_if_supported(this->color_mode_); + call.set_brightness_if_supported(brightness); + call.set_color_brightness_if_supported(color_brightness); + call.set_red_if_supported(r); + call.set_green_if_supported(g); + call.set_blue_if_supported(b); + call.set_white_if_supported(w); + call.set_warm_white_if_supported(w); + call.set_cold_white_if_supported(w); + } call.set_transition_length_if_supported(0); call.set_publish(false); call.set_save(false); @@ -50,6 +120,7 @@ class AddressableLightWrapper : public light::AddressableLight { light::LightState *light_state_; uint8_t *wrapper_state_; + ColorMode color_mode_{ColorMode::UNKNOWN}; }; } // namespace light From 73940bc1bd421df14d66ac3ae67fac40e28b9775 Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Thu, 14 Oct 2021 11:25:10 +0200 Subject: [PATCH 0009/1729] Don't define UART_SELECTION_UART2 when UART2 is unavailable (#2512) --- esphome/components/logger/logger.cpp | 4 ++-- esphome/components/logger/logger.h | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index b38c7f1a69..97ad4c2cb9 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -153,7 +153,7 @@ void Logger::pre_setup() { case UART_SELECTION_UART1: this->hw_serial_ = &Serial1; break; -#if defined(USE_ESP32) && !CONFIG_IDF_TARGET_ESP32C3 && !CONFIG_IDF_TARGET_ESP32S2 +#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32S2) case UART_SELECTION_UART2: this->hw_serial_ = &Serial2; break; @@ -169,7 +169,7 @@ void Logger::pre_setup() { case UART_SELECTION_UART1: uart_num_ = UART_NUM_1; break; -#if !CONFIG_IDF_TARGET_ESP32C3 && !CONFIG_IDF_TARGET_ESP32S2 +#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32S2) case UART_SELECTION_UART2: uart_num_ = UART_NUM_2; break; diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index e6fa6e2058..8756bc2387 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -24,7 +24,7 @@ namespace logger { enum UARTSelection { UART_SELECTION_UART0 = 0, UART_SELECTION_UART1, -#ifdef USE_ESP32 +#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32S2) UART_SELECTION_UART2, #endif #ifdef USE_ESP8266 From 10c6601b0abe4138a4292679208d917f290df9b4 Mon Sep 17 00:00:00 2001 From: Paul Monigatti Date: Thu, 14 Oct 2021 23:31:52 +1300 Subject: [PATCH 0010/1729] Revert "Added test for bme680_bsec" (#2518) This reverts commit 7f6a50d291b14935b17802b4dce52135fad1e49e due to BSEC library license restrictions. --- tests/test1.yaml | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/tests/test1.yaml b/tests/test1.yaml index 62fc781eca..157ccfc5d1 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -265,9 +265,6 @@ wled: adalight: -bme680_bsec: - i2c_id: i2c_bus - esp32_ble_tracker: ble_client: @@ -481,19 +478,6 @@ sensor: duration: 150ms update_interval: 15s i2c_id: i2c_bus - - platform: bme680_bsec - temperature: - name: "BME680 Temperature" - pressure: - name: "BME680 Pressure" - humidity: - name: "BME680 Humidity" - iaq: - name: "BME680 IAQ" - co2_equivalent: - name: "BME680 CO2 Equivalent" - breath_voc_equivalent: - name: "BME680 Breath VOC Equivalent" - platform: bmp085 temperature: name: 'Outside Temperature' From d4e65eb82ad972bafdf8a0017980a7b85e4c7e12 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 15 Oct 2021 09:42:44 +1300 Subject: [PATCH 0011/1729] Bump version to 2021.10.0b3 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 456b6d9209..eacbb19a7f 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2021.10.0b2" +__version__ = "2021.10.0b3" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From 15b5ea43a759a15d3dd0059aa58ff5c153a1ff80 Mon Sep 17 00:00:00 2001 From: Martin <25747549+martgras@users.noreply.github.com> Date: Thu, 14 Oct 2021 11:24:57 +0200 Subject: [PATCH 0012/1729] Add pressure compensation during runtime (#2493) Co-authored-by: Oxan van Leeuwen --- esphome/components/scd4x/scd4x.cpp | 133 ++++++++++++++++++----------- esphome/components/scd4x/scd4x.h | 9 +- esphome/components/scd4x/sensor.py | 9 +- 3 files changed, 97 insertions(+), 54 deletions(-) diff --git a/esphome/components/scd4x/scd4x.cpp b/esphome/components/scd4x/scd4x.cpp index c91fd5e882..eacb39edf1 100644 --- a/esphome/components/scd4x/scd4x.cpp +++ b/esphome/components/scd4x/scd4x.cpp @@ -1,4 +1,5 @@ #include "scd4x.h" +#include "esphome/core/hal.h" #include "esphome/core/log.h" namespace esphome { @@ -38,6 +39,7 @@ void SCD4XComponent::setup() { return; } + uint32_t stop_measurement_delay = 0; // In order to query the device periodic measurement must be ceased if (raw_read_status[0]) { ESP_LOGD(TAG, "Sensor has data available, stopping periodic measurement"); @@ -46,68 +48,72 @@ void SCD4XComponent::setup() { this->mark_failed(); return; } + // According to the SCD4x datasheet the sensor will only respond to other commands after waiting 500 ms after + // issuing the stop_periodic_measurement command + stop_measurement_delay = 500; } + this->set_timeout(stop_measurement_delay, [this]() { + if (!this->write_command_(SCD4X_CMD_GET_SERIAL_NUMBER)) { + ESP_LOGE(TAG, "Failed to write get serial command"); + this->error_code_ = COMMUNICATION_FAILED; + this->mark_failed(); + return; + } - if (!this->write_command_(SCD4X_CMD_GET_SERIAL_NUMBER)) { - ESP_LOGE(TAG, "Failed to write get serial command"); - this->error_code_ = COMMUNICATION_FAILED; - this->mark_failed(); - return; - } + uint16_t raw_serial_number[3]; + if (!this->read_data_(raw_serial_number, 3)) { + ESP_LOGE(TAG, "Failed to read serial number"); + this->error_code_ = SERIAL_NUMBER_IDENTIFICATION_FAILED; + this->mark_failed(); + return; + } + ESP_LOGD(TAG, "Serial number %02d.%02d.%02d", (uint16_t(raw_serial_number[0]) >> 8), + uint16_t(raw_serial_number[0] & 0xFF), (uint16_t(raw_serial_number[1]) >> 8)); - uint16_t raw_serial_number[3]; - if (!this->read_data_(raw_serial_number, 3)) { - ESP_LOGE(TAG, "Failed to read serial number"); - this->error_code_ = SERIAL_NUMBER_IDENTIFICATION_FAILED; - this->mark_failed(); - return; - } - ESP_LOGD(TAG, "Serial number %02d.%02d.%02d", (uint16_t(raw_serial_number[0]) >> 8), - uint16_t(raw_serial_number[0] & 0xFF), (uint16_t(raw_serial_number[1]) >> 8)); - - if (!this->write_command_(SCD4X_CMD_TEMPERATURE_OFFSET, - (uint16_t)(temperature_offset_ * SCD4X_TEMPERATURE_OFFSET_MULTIPLIER))) { - ESP_LOGE(TAG, "Error setting temperature offset."); - this->error_code_ = MEASUREMENT_INIT_FAILED; - this->mark_failed(); - return; - } - - // If pressure compensation available use it - // else use altitude - if (ambient_pressure_compensation_) { - if (!this->write_command_(SCD4X_CMD_AMBIENT_PRESSURE_COMPENSATION, ambient_pressure_compensation_)) { - ESP_LOGE(TAG, "Error setting ambient pressure compensation."); + if (!this->write_command_(SCD4X_CMD_TEMPERATURE_OFFSET, + (uint16_t)(temperature_offset_ * SCD4X_TEMPERATURE_OFFSET_MULTIPLIER))) { + ESP_LOGE(TAG, "Error setting temperature offset."); this->error_code_ = MEASUREMENT_INIT_FAILED; this->mark_failed(); return; } - } else { - if (!this->write_command_(SCD4X_CMD_ALTITUDE_COMPENSATION, altitude_compensation_)) { - ESP_LOGE(TAG, "Error setting altitude compensation."); + + // If pressure compensation available use it + // else use altitude + if (ambient_pressure_compensation_) { + if (!this->update_ambient_pressure_compensation_(ambient_pressure_)) { + ESP_LOGE(TAG, "Error setting ambient pressure compensation."); + this->error_code_ = MEASUREMENT_INIT_FAILED; + this->mark_failed(); + return; + } + } else { + if (!this->write_command_(SCD4X_CMD_ALTITUDE_COMPENSATION, altitude_compensation_)) { + ESP_LOGE(TAG, "Error setting altitude compensation."); + this->error_code_ = MEASUREMENT_INIT_FAILED; + this->mark_failed(); + return; + } + } + + if (!this->write_command_(SCD4X_CMD_AUTOMATIC_SELF_CALIBRATION, enable_asc_ ? 1 : 0)) { + ESP_LOGE(TAG, "Error setting automatic self calibration."); this->error_code_ = MEASUREMENT_INIT_FAILED; this->mark_failed(); return; } - } - if (!this->write_command_(SCD4X_CMD_AUTOMATIC_SELF_CALIBRATION, enable_asc_ ? 1 : 0)) { - ESP_LOGE(TAG, "Error setting automatic self calibration."); - this->error_code_ = MEASUREMENT_INIT_FAILED; - this->mark_failed(); - return; - } + // Finally start sensor measurements + if (!this->write_command_(SCD4X_CMD_START_CONTINUOUS_MEASUREMENTS)) { + ESP_LOGE(TAG, "Error starting continuous measurements."); + this->error_code_ = MEASUREMENT_INIT_FAILED; + this->mark_failed(); + return; + } - // Finally start sensor measurements - if (!this->write_command_(SCD4X_CMD_START_CONTINUOUS_MEASUREMENTS)) { - ESP_LOGE(TAG, "Error starting continuous measurements."); - this->error_code_ = MEASUREMENT_INIT_FAILED; - this->mark_failed(); - return; - } - - initialized_ = true; - ESP_LOGD(TAG, "Sensor initialized"); + initialized_ = true; + ESP_LOGD(TAG, "Sensor initialized"); + }); }); } @@ -150,6 +156,13 @@ void SCD4XComponent::update() { return; } + if (this->ambient_pressure_source_ != nullptr) { + float pressure = this->ambient_pressure_source_->state / 1000.0f; + if (!std::isnan(pressure)) { + set_ambient_pressure_compensation(this->ambient_pressure_source_->state / 1000.0f); + } + } + // Check if data is ready if (!this->write_command_(SCD4X_CMD_GET_DATA_READY_STATUS)) { this->status_set_warning(); @@ -191,6 +204,28 @@ void SCD4XComponent::update() { this->status_clear_warning(); } +// Note pressure in bar here. Convert to hPa +void SCD4XComponent::set_ambient_pressure_compensation(float pressure_in_bar) { + ambient_pressure_compensation_ = true; + uint16_t new_ambient_pressure = (uint16_t)(pressure_in_bar * 1000); + // remove millibar from comparison to avoid frequent updates +/- 10 millibar doesn't matter + if (initialized_ && (new_ambient_pressure / 10 != ambient_pressure_ / 10)) { + update_ambient_pressure_compensation_(new_ambient_pressure); + ambient_pressure_ = new_ambient_pressure; + } else { + ESP_LOGD(TAG, "ambient pressure compensation skipped - no change required"); + } +} + +bool SCD4XComponent::update_ambient_pressure_compensation_(uint16_t pressure_in_hpa) { + if (this->write_command_(SCD4X_CMD_AMBIENT_PRESSURE_COMPENSATION, pressure_in_hpa)) { + ESP_LOGD(TAG, "setting ambient pressure compensation to %d hPa", pressure_in_hpa); + return true; + } else { + ESP_LOGE(TAG, "Error setting ambient pressure compensation."); + return false; + } +} uint8_t SCD4XComponent::sht_crc_(uint8_t data1, uint8_t data2) { uint8_t bit; diff --git a/esphome/components/scd4x/scd4x.h b/esphome/components/scd4x/scd4x.h index 3c428b8623..4fe2bf14cc 100644 --- a/esphome/components/scd4x/scd4x.h +++ b/esphome/components/scd4x/scd4x.h @@ -18,10 +18,8 @@ class SCD4XComponent : public PollingComponent, public i2c::I2CDevice { void set_automatic_self_calibration(bool asc) { enable_asc_ = asc; } void set_altitude_compensation(uint16_t altitude) { altitude_compensation_ = altitude; } - void set_ambient_pressure_compensation(float pressure) { - ambient_pressure_compensation_ = true; - ambient_pressure_ = (uint16_t)(pressure * 1000); - } + void set_ambient_pressure_compensation(float pressure_in_bar); + void set_ambient_pressure_source(sensor::Sensor *pressure) { ambient_pressure_source_ = pressure; } void set_temperature_offset(float offset) { temperature_offset_ = offset; }; void set_co2_sensor(sensor::Sensor *co2) { co2_sensor_ = co2; } @@ -33,6 +31,7 @@ class SCD4XComponent : public PollingComponent, public i2c::I2CDevice { bool read_data_(uint16_t *data, uint8_t len); bool write_command_(uint16_t command); bool write_command_(uint16_t command, uint16_t data); + bool update_ambient_pressure_compensation_(uint16_t pressure_in_hpa); ERRORCODE error_code_; @@ -47,6 +46,8 @@ class SCD4XComponent : public PollingComponent, public i2c::I2CDevice { sensor::Sensor *co2_sensor_{nullptr}; sensor::Sensor *temperature_sensor_{nullptr}; sensor::Sensor *humidity_sensor_{nullptr}; + // used for compensation + sensor::Sensor *ambient_pressure_source_{nullptr}; }; } // namespace scd4x diff --git a/esphome/components/scd4x/sensor.py b/esphome/components/scd4x/sensor.py index 0b1a960f6f..3e814ffe78 100644 --- a/esphome/components/scd4x/sensor.py +++ b/esphome/components/scd4x/sensor.py @@ -29,6 +29,7 @@ CONF_AUTOMATIC_SELF_CALIBRATION = "automatic_self_calibration" CONF_ALTITUDE_COMPENSATION = "altitude_compensation" CONF_AMBIENT_PRESSURE_COMPENSATION = "ambient_pressure_compensation" CONF_TEMPERATURE_OFFSET = "temperature_offset" +CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE = "ambient_pressure_compensation_source" CONFIG_SCHEMA = ( cv.Schema( @@ -62,6 +63,9 @@ CONFIG_SCHEMA = ( ), cv.Optional(CONF_AMBIENT_PRESSURE_COMPENSATION): cv.pressure, cv.Optional(CONF_TEMPERATURE_OFFSET, default="4°C"): cv.temperature, + cv.Optional(CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE): cv.use_id( + sensor.Sensor + ), } ) .extend(cv.polling_component_schema("60s")) @@ -92,7 +96,10 @@ async def to_code(config): cg.add(getattr(var, funcName)(config[key])) for key, funcName in SENSOR_MAP.items(): - if key in config: sens = await sensor.new_sensor(config[key]) cg.add(getattr(var, funcName)(sens)) + + if CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE in config: + sens = await cg.get_variable(config[CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE]) + cg.add(var.set_ambient_pressure_source(sens)) From c3a8a044b93b9d53b1a5a2fb451ac18a5bb62196 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Fri, 15 Oct 2021 02:26:26 -0500 Subject: [PATCH 0013/1729] Fix Nextion HTTPClient error for ESP32 (#2524) --- esphome/components/nextion/display.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/esphome/components/nextion/display.py b/esphome/components/nextion/display.py index f4b35fd56f..d95810bfbe 100644 --- a/esphome/components/nextion/display.py +++ b/esphome/components/nextion/display.py @@ -8,7 +8,7 @@ from esphome.const import ( CONF_BRIGHTNESS, CONF_TRIGGER_ID, ) - +from esphome.core import CORE from . import Nextion, nextion_ns, nextion_ref from .base_component import ( CONF_ON_SLEEP, @@ -76,6 +76,9 @@ async def to_code(config): if CONF_TFT_URL in config: cg.add_define("USE_NEXTION_TFT_UPLOAD") cg.add(var.set_tft_url(config[CONF_TFT_URL])) + if CORE.is_esp32: + cg.add_library("WiFiClientSecure", None) + cg.add_library("HTTPClient", None) if CONF_TOUCH_SLEEP_TIMEOUT in config: cg.add(var.set_touch_sleep_timeout_internal(config[CONF_TOUCH_SLEEP_TIMEOUT])) From 98755f36212d689bbc3fc7abc227c2587fc8e282 Mon Sep 17 00:00:00 2001 From: Martin <25747549+martgras@users.noreply.github.com> Date: Fri, 15 Oct 2021 09:27:56 +0200 Subject: [PATCH 0014/1729] Fix bug in register name definition (#2526) --- esphome/components/modbus_controller/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/modbus_controller/__init__.py b/esphome/components/modbus_controller/__init__.py index 7a69029dab..6b452ea25c 100644 --- a/esphome/components/modbus_controller/__init__.py +++ b/esphome/components/modbus_controller/__init__.py @@ -38,7 +38,7 @@ ModbusRegisterType_ns = modbus_controller_ns.namespace("ModbusRegisterType") ModbusRegisterType = ModbusRegisterType_ns.enum("ModbusRegisterType") MODBUS_REGISTER_TYPE = { "coil": ModbusRegisterType.COIL, - "discrete_input": ModbusRegisterType.DISCRETE, + "discrete_input": ModbusRegisterType.DISCRETE_INPUT, "holding": ModbusRegisterType.HOLDING, "read": ModbusRegisterType.READ, } From 4dd1bf920d824aa4bae6ca6836859264e05b567f Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Fri, 15 Oct 2021 22:07:05 +0200 Subject: [PATCH 0015/1729] Replace framework version_hint with source option (#2529) --- esphome/components/esp32/__init__.py | 134 +++++++++++-------------- esphome/components/esp8266/__init__.py | 75 ++++++-------- esphome/core/config.py | 11 +- 3 files changed, 97 insertions(+), 123 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 09eabe1fa7..8a13468c76 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -7,6 +7,7 @@ from esphome.helpers import write_file_if_changed from esphome.const import ( CONF_BOARD, CONF_FRAMEWORK, + CONF_SOURCE, CONF_TYPE, CONF_VARIANT, CONF_VERSION, @@ -53,7 +54,7 @@ def set_core_data(config): elif conf[CONF_TYPE] == FRAMEWORK_ARDUINO: CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = "arduino" CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version.parse( - config[CONF_FRAMEWORK][CONF_VERSION_HINT] + config[CONF_FRAMEWORK][CONF_VERSION] ) CORE.data[KEY_ESP32][KEY_BOARD] = config[CONF_BOARD] CORE.data[KEY_ESP32][KEY_VARIANT] = config[CONF_VARIANT] @@ -94,6 +95,13 @@ def _format_framework_arduino_version(ver: cv.Version) -> str: return f"~3.{ver.major}{ver.minor:02d}{ver.patch:02d}.0" +def _format_framework_espidf_version(ver: cv.Version) -> str: + # format the given arduino (https://github.com/espressif/esp-idf/releases) version to + # a PIO platformio/framework-espidf value + # List of package versions: https://api.registry.platformio.org/v3/packages/platformio/tool/framework-espidf + return f"~3.{ver.major}{ver.minor:02d}{ver.patch:02d}.0" + + # NOTE: Keep this in mind when updating the recommended version: # * New framework historically have had some regressions, especially for WiFi. # The new version needs to be thoroughly validated before changing the @@ -123,119 +131,97 @@ ESP_IDF_PLATFORM_VERSION = cv.Version(3, 3, 2) def _arduino_check_versions(value): value = value.copy() lookups = { - "dev": ("https://github.com/espressif/arduino-esp32.git", cv.Version(2, 0, 0)), - "latest": ("", cv.Version(1, 0, 3)), - "recommended": ( - _format_framework_arduino_version(RECOMMENDED_ARDUINO_FRAMEWORK_VERSION), - RECOMMENDED_ARDUINO_FRAMEWORK_VERSION, - ), + "dev": (cv.Version(2, 0, 0), "https://github.com/espressif/arduino-esp32.git"), + "latest": (cv.Version(1, 0, 6), None), + "recommended": (RECOMMENDED_ARDUINO_FRAMEWORK_VERSION, None), } - ver_value = value[CONF_VERSION] - default_ver_hint = None - if ver_value.lower() in lookups: - default_ver_hint = str(lookups[ver_value.lower()][1]) - ver_value = lookups[ver_value.lower()][0] + + if value[CONF_VERSION] in lookups: + if CONF_SOURCE in value: + raise cv.Invalid( + "Framework version needs to be explicitly specified when custom source is used." + ) + + version, source = lookups[value[CONF_VERSION]] else: - with cv.suppress_invalid(): - ver = cv.Version.parse(cv.version_number(value)) - if ver <= cv.Version(1, 0, 3): - ver_value = f"~2.{ver.major}{ver.minor:02d}{ver.patch:02d}.0" - else: - ver_value = f"~3.{ver.major}{ver.minor:02d}{ver.patch:02d}.0" - default_ver_hint = str(ver) - value[CONF_VERSION] = ver_value + version = cv.Version.parse(cv.version_number(value[CONF_VERSION])) + source = value.get(CONF_SOURCE, None) - if CONF_VERSION_HINT not in value and default_ver_hint is None: - raise cv.Invalid("Needs a version hint to understand the framework version") + value[CONF_VERSION] = str(version) + value[CONF_SOURCE] = source or _format_framework_arduino_version(version) - ver_hint_s = value.get(CONF_VERSION_HINT, default_ver_hint) - value[CONF_VERSION_HINT] = ver_hint_s - plat_ver = value.get(CONF_PLATFORM_VERSION, ARDUINO_PLATFORM_VERSION) - value[CONF_PLATFORM_VERSION] = str(plat_ver) + platform_version = value.get(CONF_PLATFORM_VERSION, ARDUINO_PLATFORM_VERSION) + value[CONF_PLATFORM_VERSION] = str(platform_version) - if cv.Version.parse(ver_hint_s) != RECOMMENDED_ARDUINO_FRAMEWORK_VERSION: + if version != RECOMMENDED_ARDUINO_FRAMEWORK_VERSION: _LOGGER.warning( - "The selected arduino framework version is not the recommended one" - ) - _LOGGER.warning( - "If there are connectivity or build issues please remove the manual version" + "The selected Arduino framework version is not the recommended one. " + "If there are connectivity or build issues please remove the manual version." ) return value -def _format_framework_espidf_version(ver: cv.Version) -> str: - # format the given arduino (https://github.com/espressif/esp-idf/releases) version to - # a PIO platformio/framework-espidf value - # List of package versions: https://api.registry.platformio.org/v3/packages/platformio/tool/framework-espidf - return f"~3.{ver.major}{ver.minor:02d}{ver.patch:02d}.0" - - def _esp_idf_check_versions(value): value = value.copy() lookups = { - "dev": ("https://github.com/espressif/esp-idf.git", cv.Version(4, 3, 1)), - "latest": ("", cv.Version(4, 3, 0)), - "recommended": ( - _format_framework_espidf_version(RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION), - RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION, - ), + "dev": (cv.Version(4, 3, 1), "https://github.com/espressif/esp-idf.git"), + "latest": (cv.Version(4, 3, 0), None), + "recommended": (RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION, None), } - ver_value = value[CONF_VERSION] - default_ver_hint = None - if ver_value.lower() in lookups: - default_ver_hint = str(lookups[ver_value.lower()][1]) - ver_value = lookups[ver_value.lower()][0] + + if value[CONF_VERSION] in lookups: + if CONF_SOURCE in value: + raise cv.Invalid( + "Framework version needs to be explicitly specified when custom source is used." + ) + + version, source = lookups[value[CONF_VERSION]] else: - with cv.suppress_invalid(): - ver = cv.Version.parse(cv.version_number(value)) - ver_value = f"~3.{ver.major}{ver.minor:02d}{ver.patch:02d}.0" - default_ver_hint = str(ver) - value[CONF_VERSION] = ver_value + version = cv.Version.parse(cv.version_number(value[CONF_VERSION])) + source = value.get(CONF_SOURCE, None) - if CONF_VERSION_HINT not in value and default_ver_hint is None: - raise cv.Invalid("Needs a version hint to understand the framework version") + if version < cv.Version(4, 0, 0): + raise cv.Invalid("Only ESP-IDF 4.0+ is supported.") - ver_hint_s = value.get(CONF_VERSION_HINT, default_ver_hint) - value[CONF_VERSION_HINT] = ver_hint_s - if cv.Version.parse(ver_hint_s) < cv.Version(4, 0, 0): - raise cv.Invalid("Only ESP-IDF 4.0+ is supported") - if cv.Version.parse(ver_hint_s) != RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION: + value[CONF_VERSION] = str(version) + value[CONF_SOURCE] = source or _format_framework_espidf_version(version) + + platform_version = value.get(CONF_PLATFORM_VERSION, ESP_IDF_PLATFORM_VERSION) + value[CONF_PLATFORM_VERSION] = str(platform_version) + + if version != RECOMMENDED_ARDUINO_FRAMEWORK_VERSION: _LOGGER.warning( - "The selected esp-idf framework version is not the recommended one" + "The selected ESP-IDF framework version is not the recommended one. " + "If there are connectivity or build issues please remove the manual version." ) - _LOGGER.warning( - "If there are connectivity or build issues please remove the manual version" - ) - - plat_ver = value.get(CONF_PLATFORM_VERSION, ESP_IDF_PLATFORM_VERSION) - value[CONF_PLATFORM_VERSION] = str(plat_ver) return value -CONF_VERSION_HINT = "version_hint" CONF_PLATFORM_VERSION = "platform_version" + ARDUINO_FRAMEWORK_SCHEMA = cv.All( cv.Schema( { cv.Optional(CONF_VERSION, default="recommended"): cv.string_strict, - cv.Optional(CONF_VERSION_HINT): cv.version_number, + cv.Optional(CONF_SOURCE): cv.string_strict, cv.Optional(CONF_PLATFORM_VERSION): cv.string_strict, } ), _arduino_check_versions, ) + CONF_SDKCONFIG_OPTIONS = "sdkconfig_options" ESP_IDF_FRAMEWORK_SCHEMA = cv.All( cv.Schema( { cv.Optional(CONF_VERSION, default="recommended"): cv.string_strict, - cv.Optional(CONF_VERSION_HINT): cv.version_number, + cv.Optional(CONF_SOURCE): cv.string_strict, + cv.Optional(CONF_PLATFORM_VERSION): cv.string_strict, cv.Optional(CONF_SDKCONFIG_OPTIONS, default={}): { cv.string_strict: cv.string_strict }, - cv.Optional(CONF_PLATFORM_VERSION): cv.string_strict, cv.Optional(CONF_ADVANCED, default={}): cv.Schema( { cv.Optional(CONF_IGNORE_EFUSE_MAC_CRC, default=False): cv.boolean, @@ -293,7 +279,7 @@ async def to_code(config): cg.add_build_flag("-Wno-nonnull-compare") cg.add_platformio_option( "platform_packages", - [f"platformio/framework-espidf @ {conf[CONF_VERSION]}"], + [f"platformio/framework-espidf @ {conf[CONF_SOURCE]}"], ) add_idf_sdkconfig_option("CONFIG_PARTITION_TABLE_SINGLE_APP", False) add_idf_sdkconfig_option("CONFIG_PARTITION_TABLE_CUSTOM", True) @@ -323,7 +309,7 @@ async def to_code(config): cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ARDUINO") cg.add_platformio_option( "platform_packages", - [f"platformio/framework-arduinoespressif32 @ {conf[CONF_VERSION]}"], + [f"platformio/framework-arduinoespressif32 @ {conf[CONF_SOURCE]}"], ) cg.add_platformio_option("board_build.partitions", "partitions.csv") diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index 93a461ba1f..a5323db8bf 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -4,6 +4,7 @@ from esphome.const import ( CONF_BOARD, CONF_BOARD_FLASH_MODE, CONF_FRAMEWORK, + CONF_SOURCE, CONF_VERSION, KEY_CORE, KEY_FRAMEWORK_VERSION, @@ -31,7 +32,7 @@ def set_core_data(config): CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = "esp8266" CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = "arduino" CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version.parse( - config[CONF_FRAMEWORK][CONF_VERSION_HINT] + config[CONF_FRAMEWORK][CONF_VERSION] ) CORE.data[KEY_ESP8266][KEY_BOARD] = config[CONF_BOARD] return config @@ -70,66 +71,50 @@ ARDUINO_3_PLATFORM_VERSION = cv.Version(3, 0, 2) def _arduino_check_versions(value): value = value.copy() lookups = { - "dev": ("https://github.com/esp8266/Arduino.git", cv.Version(3, 0, 2)), - "latest": ("", cv.Version(3, 0, 2)), - "recommended": ( - _format_framework_arduino_version(RECOMMENDED_ARDUINO_FRAMEWORK_VERSION), - RECOMMENDED_ARDUINO_FRAMEWORK_VERSION, - ), + "dev": (cv.Version(3, 0, 2), "https://github.com/esp8266/Arduino.git"), + "latest": (cv.Version(3, 0, 2), None), + "recommended": (RECOMMENDED_ARDUINO_FRAMEWORK_VERSION, None), } - ver_value = value[CONF_VERSION] - default_ver_hint = None - if ver_value.lower() in lookups: - default_ver_hint = str(lookups[ver_value.lower()][1]) - ver_value = lookups[ver_value.lower()][0] + + if value[CONF_VERSION] in lookups: + if CONF_SOURCE in value: + raise cv.Invalid( + "Framework version needs to be explicitly specified when custom source is used." + ) + + version, source = lookups[value[CONF_VERSION]] else: - with cv.suppress_invalid(): - ver = cv.Version.parse(cv.version_number(value)) - if ver <= cv.Version(2, 4, 1): - ver_value = f"~1.{ver.major}{ver.minor:02d}{ver.patch:02d}.0" - elif ver <= cv.Version(2, 6, 2): - ver_value = f"~2.{ver.major}{ver.minor:02d}{ver.patch:02d}.0" - else: - ver_value = f"~3.{ver.major}{ver.minor:02d}{ver.patch:02d}.0" - default_ver_hint = str(ver) + version = cv.Version.parse(cv.version_number(value[CONF_VERSION])) + source = value.get(CONF_SOURCE, None) - value[CONF_VERSION] = ver_value + value[CONF_VERSION] = str(version) + value[CONF_SOURCE] = source or _format_framework_arduino_version(version) - if CONF_VERSION_HINT not in value and default_ver_hint is None: - raise cv.Invalid("Needs a version hint to understand the framework version") - - ver_hint_s = value.get(CONF_VERSION_HINT, default_ver_hint) - value[CONF_VERSION_HINT] = ver_hint_s - plat_ver = value.get(CONF_PLATFORM_VERSION) - - if plat_ver is None: - ver_hint = cv.Version.parse(ver_hint_s) - if ver_hint >= cv.Version(3, 0, 0): - plat_ver = ARDUINO_3_PLATFORM_VERSION - elif ver_hint >= cv.Version(2, 5, 0): - plat_ver = ARDUINO_2_PLATFORM_VERSION + platform_version = value.get(CONF_PLATFORM_VERSION) + if platform_version is None: + if version >= cv.Version(3, 0, 0): + platform_version = ARDUINO_3_PLATFORM_VERSION + elif version >= cv.Version(2, 5, 0): + platform_version = ARDUINO_2_PLATFORM_VERSION else: - plat_ver = cv.Version(1, 8, 0) - value[CONF_PLATFORM_VERSION] = str(plat_ver) + platform_version = cv.Version(1, 8, 0) + value[CONF_PLATFORM_VERSION] = str(platform_version) - if cv.Version.parse(ver_hint_s) != RECOMMENDED_ARDUINO_FRAMEWORK_VERSION: + if version != RECOMMENDED_ARDUINO_FRAMEWORK_VERSION: _LOGGER.warning( - "The selected arduino framework version is not the recommended one" - ) - _LOGGER.warning( - "If there are connectivity or build issues please remove the manual version" + "The selected Arduino framework version is not the recommended one. " + "If there are connectivity or build issues please remove the manual version." ) return value -CONF_VERSION_HINT = "version_hint" CONF_PLATFORM_VERSION = "platform_version" ARDUINO_FRAMEWORK_SCHEMA = cv.All( cv.Schema( { cv.Optional(CONF_VERSION, default="recommended"): cv.string_strict, - cv.Optional(CONF_VERSION_HINT): cv.version_number, + cv.Optional(CONF_SOURCE): cv.string_strict, cv.Optional(CONF_PLATFORM_VERSION): cv.string_strict, } ), @@ -167,7 +152,7 @@ async def to_code(config): cg.add_build_flag("-DUSE_ESP8266_FRAMEWORK_ARDUINO") cg.add_platformio_option( "platform_packages", - [f"platformio/framework-arduinoespressif8266 @ {conf[CONF_VERSION]}"], + [f"platformio/framework-arduinoespressif8266 @ {conf[CONF_SOURCE]}"], ) cg.add_platformio_option( "platform", f"platformio/espressif8266 @ {conf[CONF_PLATFORM_VERSION]}" diff --git a/esphome/core/config.py b/esphome/core/config.py index bbdfcf124c..c495fefddd 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -23,6 +23,7 @@ from esphome.const import ( CONF_PLATFORMIO_OPTIONS, CONF_PRIORITY, CONF_PROJECT, + CONF_SOURCE, CONF_TRIGGER_ID, CONF_TYPE, CONF_VERSION, @@ -181,10 +182,12 @@ def preload_core_config(config, result): 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] = { - CONF_TYPE: "arduino", - CONF_VERSION: conf.pop(CONF_ARDUINO_VERSION), - } + plat_conf[CONF_FRAMEWORK] = {CONF_TYPE: "arduino"} + try: + 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 From f83950fd75e0c0156484c7d54ac556b762a7f636 Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Sun, 17 Oct 2021 08:53:49 +0200 Subject: [PATCH 0016/1729] Fix bitshift on read in ADE7953 (#2537) --- esphome/components/ade7953/ade7953.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/ade7953/ade7953.h b/esphome/components/ade7953/ade7953.h index c6fb383ed8..bb160cd8eb 100644 --- a/esphome/components/ade7953/ade7953.h +++ b/esphome/components/ade7953/ade7953.h @@ -76,9 +76,9 @@ class ADE7953 : public i2c::I2CDevice, public PollingComponent { return err; *value = 0; *value |= ((uint32_t) recv[0]) << 24; - *value |= ((uint32_t) recv[1]) << 24; - *value |= ((uint32_t) recv[2]) << 24; - *value |= ((uint32_t) recv[3]) << 24; + *value |= ((uint32_t) recv[1]) << 16; + *value |= ((uint32_t) recv[2]) << 8; + *value |= ((uint32_t) recv[3]); return i2c::ERROR_OK; } From db3fa1ade72a253d4ed90d95c05f803b0d890ff3 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Sun, 17 Oct 2021 19:54:09 +1300 Subject: [PATCH 0017/1729] Allow downloading all bin files from backend in dashboard (#2514) Co-authored-by: Otto Winter --- esphome/__main__.py | 29 +++++++++++- esphome/dashboard/dashboard.py | 82 +++++++++++++++++++++++++++++----- 2 files changed, 99 insertions(+), 12 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index feb95e93c7..1b9c601091 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -180,7 +180,11 @@ def compile_program(args, config): from esphome import platformio_api _LOGGER.info("Compiling app...") - return platformio_api.run_compile(config, CORE.verbose) + rc = platformio_api.run_compile(config, CORE.verbose) + if rc != 0: + return rc + idedata = platformio_api.get_idedata(config) + return 0 if idedata is not None else 1 def upload_using_esptool(config, port): @@ -458,6 +462,21 @@ def command_update_all(args): return failed +def command_idedata(args, config): + from esphome import platformio_api + import json + + logging.disable(logging.INFO) + logging.disable(logging.WARNING) + + idedata = platformio_api.get_idedata(config) + if idedata is None: + return 1 + + print(json.dumps(idedata.raw, indent=2) + "\n") + return 0 + + PRE_CONFIG_ACTIONS = { "wizard": command_wizard, "version": command_version, @@ -475,6 +494,7 @@ POST_CONFIG_ACTIONS = { "clean-mqtt": command_clean_mqtt, "mqtt-fingerprint": command_mqtt_fingerprint, "clean": command_clean, + "idedata": command_idedata, } @@ -650,6 +670,11 @@ def parse_args(argv): "configuration", help="Your YAML configuration file directories.", nargs="+" ) + parser_idedata = subparsers.add_parser("idedata") + parser_idedata.add_argument( + "configuration", help="Your YAML configuration file(s).", nargs=1 + ) + # Keep backward compatibility with the old command line format of # esphome . # @@ -762,7 +787,7 @@ def run_esphome(argv): config = read_config(dict(args.substitution) if args.substitution else {}) if config is None: - return 1 + return 2 CORE.config = config if args.command not in POST_CONFIG_ACTIONS: diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index eb698a7de1..501666b100 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -9,6 +9,7 @@ import json import logging import multiprocessing import os +from pathlib import Path import secrets import shutil import subprocess @@ -26,7 +27,7 @@ import tornado.process import tornado.web import tornado.websocket -from esphome import const, util +from esphome import const, platformio_api, util from esphome.helpers import mkdir_p, get_bool_env, run_system_command from esphome.storage_json import ( EsphomeStorageJSON, @@ -398,17 +399,45 @@ class DownloadBinaryRequestHandler(BaseHandler): @authenticated @bind_config def get(self, configuration=None): - # pylint: disable=no-value-for-parameter - storage_path = ext_storage_path(settings.config_dir, configuration) - storage_json = StorageJSON.load(storage_path) - if storage_json is None: - self.send_error() + type = self.get_argument("type", "firmware.bin") + + if type == "firmware.bin": + storage_path = ext_storage_path(settings.config_dir, configuration) + storage_json = StorageJSON.load(storage_path) + if storage_json is None: + self.send_error(404) + return + filename = f"{storage_json.name}.bin" + path = storage_json.firmware_bin_path + + else: + args = ["esphome", "idedata", settings.rel_path(configuration)] + rc, stdout, _ = run_system_command(*args) + + if rc != 0: + self.send_error(404 if rc == 2 else 500) + return + + idedata = platformio_api.IDEData(json.loads(stdout)) + + found = False + for image in idedata.extra_flash_images: + if image.path.endswith(type): + path = image.path + filename = type + found = True + break + + if not found: + self.send_error(404) + return + + self.set_header("Content-Type", "application/octet-stream") + self.set_header("Content-Disposition", f'attachment; filename="{filename}"') + if not Path(path).is_file(): + self.send_error(404) return - path = storage_json.firmware_bin_path - self.set_header("Content-Type", "application/octet-stream") - filename = f"{storage_json.name}.bin" - self.set_header("Content-Disposition", f'attachment; filename="{filename}"') with open(path, "rb") as f: while True: data = f.read(16384) @@ -418,6 +447,38 @@ class DownloadBinaryRequestHandler(BaseHandler): self.finish() +class ManifestRequestHandler(BaseHandler): + @authenticated + @bind_config + def get(self, configuration=None): + args = ["esphome", "idedata", settings.rel_path(configuration)] + rc, stdout, _ = run_system_command(*args) + + if rc != 0: + self.send_error(404 if rc == 2 else 500) + return + + idedata = platformio_api.IDEData(json.loads(stdout)) + + firmware_offset = "0x10000" if idedata.extra_flash_images else "0x0" + flash_images = [ + { + "path": f"./download.bin?configuration={configuration}&type=firmware.bin", + "offset": firmware_offset, + } + ] + [ + { + "path": f"./download.bin?configuration={configuration}&type={os.path.basename(image.path)}", + "offset": image.offset, + } + for image in idedata.extra_flash_images + ] + + self.set_header("Content-Type", "application/json") + self.write(json.dumps(flash_images)) + self.finish() + + def _list_dashboard_entries(): files = settings.list_yaml_files() return [DashboardEntry(file) for file in files] @@ -862,6 +923,7 @@ def make_app(debug=get_bool_env(ENV_DEV)): (f"{rel}info", InfoRequestHandler), (f"{rel}edit", EditRequestHandler), (f"{rel}download.bin", DownloadBinaryRequestHandler), + (f"{rel}manifest.json", ManifestRequestHandler), (f"{rel}serial-ports", SerialPortRequestHandler), (f"{rel}ping", PingRequestHandler), (f"{rel}delete", DeleteRequestHandler), From f045382d2095faa3149c5272129da521fa68ebd4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 17 Oct 2021 00:26:59 -0700 Subject: [PATCH 0018/1729] Bump dashboard to 20211015.0 (#2525) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 23a00d3755..8028007a0a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ pyserial==3.5 platformio==5.2.1 esptool==3.1 click==8.0.3 -esphome-dashboard==20211011.1 +esphome-dashboard==20211015.0 aioesphomeapi==9.1.5 # esp-idf requires this, but doesn't bundle it by default From 4b44280d5392d6b1f1e3722993a405bfe268d3b5 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Sun, 17 Oct 2021 20:34:07 +1300 Subject: [PATCH 0019/1729] Bump version to 2021.10.0b4 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index eacbb19a7f..b505087115 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2021.10.0b3" +__version__ = "2021.10.0b4" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From 70b62f272eb2eeb7639f641240a20f02a1ffaefa Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sun, 17 Oct 2021 21:01:51 +0200 Subject: [PATCH 0020/1729] Only show timestamp for dashboard access logs (#2540) --- esphome/__main__.py | 7 ++++++- esphome/log.py | 14 ++++++++++---- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index 1b9c601091..97059154fd 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -758,7 +758,12 @@ def run_esphome(argv): args = parse_args(argv) CORE.dashboard = args.dashboard - setup_log(args.verbose, args.quiet) + setup_log( + args.verbose, + args.quiet, + # Show timestamp for dashboard access logs + args.command == "dashboard", + ) if args.deprecated_argv_suggestion is not None and args.command != "vscode": _LOGGER.warning( "Calling ESPHome with the configuration before the command is deprecated " diff --git a/esphome/log.py b/esphome/log.py index abefcf6308..e7ba0fdd82 100644 --- a/esphome/log.py +++ b/esphome/log.py @@ -49,8 +49,10 @@ def color(col: str, msg: str, reset: bool = True) -> bool: class ESPHomeLogFormatter(logging.Formatter): - def __init__(self): - super().__init__(fmt="%(asctime)s %(levelname)s %(message)s", style="%") + def __init__(self, *, include_timestamp: bool): + fmt = "%(asctime)s " if include_timestamp else "" + fmt += "%(levelname)s %(message)s" + super().__init__(fmt=fmt, style="%") def format(self, record): formatted = super().format(record) @@ -64,7 +66,9 @@ class ESPHomeLogFormatter(logging.Formatter): return f"{prefix}{formatted}{Style.RESET_ALL}" -def setup_log(debug=False, quiet=False): +def setup_log( + debug: bool = False, quiet: bool = False, include_timestamp: bool = False +) -> None: import colorama if debug: @@ -79,4 +83,6 @@ def setup_log(debug=False, quiet=False): logging.getLogger("urllib3").setLevel(logging.WARNING) colorama.init() - logging.getLogger().handlers[0].setFormatter(ESPHomeLogFormatter()) + logging.getLogger().handlers[0].setFormatter( + ESPHomeLogFormatter(include_timestamp=include_timestamp) + ) From 0524f8c677c864c7ebfbbe3bff2fad30cc949e91 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 18 Oct 2021 09:55:35 +1300 Subject: [PATCH 0021/1729] Fix const used for IDF recommended version (#2542) --- esphome/components/esp32/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 8a13468c76..44e24b21d2 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -190,7 +190,7 @@ def _esp_idf_check_versions(value): platform_version = value.get(CONF_PLATFORM_VERSION, ESP_IDF_PLATFORM_VERSION) value[CONF_PLATFORM_VERSION] = str(platform_version) - if version != RECOMMENDED_ARDUINO_FRAMEWORK_VERSION: + if version != RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION: _LOGGER.warning( "The selected ESP-IDF framework version is not the recommended one. " "If there are connectivity or build issues please remove the manual version." From 63a9acaa19c6292610d92e1764ae6995883cd331 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 18 Oct 2021 09:56:31 +1300 Subject: [PATCH 0022/1729] Fix Bluetooth setup_priorities (#2458) Co-authored-by: Otto Winter --- esphome/components/ble_client/ble_client.cpp | 2 ++ esphome/components/ble_client/ble_client.h | 1 + esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp | 2 +- esphome/components/esp32_ble_server/ble_server.cpp | 2 +- esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp | 2 ++ esphome/components/esp32_ble_tracker/esp32_ble_tracker.h | 1 + 6 files changed, 8 insertions(+), 2 deletions(-) diff --git a/esphome/components/ble_client/ble_client.cpp b/esphome/components/ble_client/ble_client.cpp index 8ff516d735..e6cdb0c23d 100644 --- a/esphome/components/ble_client/ble_client.cpp +++ b/esphome/components/ble_client/ble_client.cpp @@ -11,6 +11,8 @@ namespace ble_client { static const char *const TAG = "ble_client"; +float BLEClient::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; } + void BLEClient::setup() { auto ret = esp_ble_gattc_app_register(this->app_id); if (ret) { diff --git a/esphome/components/ble_client/ble_client.h b/esphome/components/ble_client/ble_client.h index 4a17ccb79b..23123914e8 100644 --- a/esphome/components/ble_client/ble_client.h +++ b/esphome/components/ble_client/ble_client.h @@ -81,6 +81,7 @@ class BLEClient : public espbt::ESPBTClient, public Component { void setup() override; void dump_config() override; void loop() override; + float get_setup_priority() const override; void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override; diff --git a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp index f6bab8e6df..955bc8595f 100644 --- a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp +++ b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp @@ -57,7 +57,7 @@ void ESP32BLEBeacon::setup() { ); } -float ESP32BLEBeacon::get_setup_priority() const { return setup_priority::DATA; } +float ESP32BLEBeacon::get_setup_priority() const { return setup_priority::BLUETOOTH; } void ESP32BLEBeacon::ble_core_task(void *params) { ble_setup(); diff --git a/esphome/components/esp32_ble_server/ble_server.cpp b/esphome/components/esp32_ble_server/ble_server.cpp index 0b91c238c3..e0fb80f94b 100644 --- a/esphome/components/esp32_ble_server/ble_server.cpp +++ b/esphome/components/esp32_ble_server/ble_server.cpp @@ -154,7 +154,7 @@ void BLEServer::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t ga } } -float BLEServer::get_setup_priority() const { return setup_priority::BLUETOOTH - 10; } +float BLEServer::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; } void BLEServer::dump_config() { ESP_LOGCONFIG(TAG, "ESP32 BLE Server:"); } diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index 65749f5124..303cb34aa7 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -40,6 +40,8 @@ uint64_t ble_addr_to_uint64(const esp_bd_addr_t address) { return u; } +float ESP32BLETracker::get_setup_priority() const { return setup_priority::BLUETOOTH; } + void ESP32BLETracker::setup() { global_esp32_ble_tracker = this; this->scan_result_lock_ = xSemaphoreCreateMutex(); diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index 1308119df5..02e102f06c 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -171,6 +171,7 @@ class ESP32BLETracker : public Component { /// Setup the FreeRTOS task and the Bluetooth stack. void setup() override; void dump_config() override; + float get_setup_priority() const override; void loop() override; From 723fb7eaac75cbfcea472db58bc311f66ee6ee77 Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Mon, 18 Oct 2021 02:36:18 +0200 Subject: [PATCH 0023/1729] Autodetect ESP32 variant (#2530) Co-authored-by: Otto winter --- esphome/components/esp32/__init__.py | 25 ++++-- esphome/components/esp32/boards.py | 124 ++++++++++++++++++++++++++ esphome/components/logger/__init__.py | 3 +- 3 files changed, 144 insertions(+), 8 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 44e24b21d2..e1128ff227 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -27,13 +27,10 @@ from .const import ( # noqa KEY_ESP32, KEY_SDKCONFIG_OPTIONS, KEY_VARIANT, - VARIANT_ESP32, - VARIANT_ESP32S2, - VARIANT_ESP32S3, VARIANT_ESP32C3, - VARIANT_ESP32H2, VARIANTS, ) +from .boards import BOARD_TO_VARIANT # force import gpio to register pin schema from .gpio import esp32_pin_to_code # noqa @@ -199,6 +196,21 @@ def _esp_idf_check_versions(value): return value +def _detect_variant(value): + if CONF_VARIANT not in value: + board = value[CONF_BOARD] + if board not in BOARD_TO_VARIANT: + raise cv.Invalid( + "This board is unknown, please set the variant manually", + path=[CONF_BOARD], + ) + + value = value.copy() + value[CONF_VARIANT] = BOARD_TO_VARIANT[board] + + return value + + CONF_PLATFORM_VERSION = "platform_version" ARDUINO_FRAMEWORK_SCHEMA = cv.All( @@ -250,12 +262,11 @@ CONFIG_SCHEMA = cv.All( cv.Schema( { cv.Required(CONF_BOARD): cv.string_strict, - cv.Optional(CONF_VARIANT, default="ESP32"): cv.one_of( - *VARIANTS, upper=True - ), + cv.Optional(CONF_VARIANT): cv.one_of(*VARIANTS, upper=True), cv.Optional(CONF_FRAMEWORK, default={}): FRAMEWORK_SCHEMA, } ), + _detect_variant, set_core_data, ) diff --git a/esphome/components/esp32/boards.py b/esphome/components/esp32/boards.py index ddf4bf2026..7f7bb2259f 100644 --- a/esphome/components/esp32/boards.py +++ b/esphome/components/esp32/boards.py @@ -1,3 +1,5 @@ +from .const import VARIANT_ESP32, VARIANT_ESP32S2, VARIANT_ESP32C3 + ESP32_BASE_PINS = { "TX": 1, "RX": 3, @@ -925,3 +927,125 @@ ESP32_BOARD_PINS = { }, "xinabox_cw02": {"LED": 27}, } + +""" +BOARD_TO_VARIANT generated with: + +git clone https://github.com/platformio/platform-espressif32 +for x in platform-espressif32/boards/*.json; do + mcu=$(jq -r .build.mcu <"$x"); + fname=$(basename "$x") + board="${fname%.*}" + variant=$(echo "$mcu" | tr '[:lower:]' '[:upper:]') + echo " \"$board\": VARIANT_${variant}," +done | sort +""" + +BOARD_TO_VARIANT = { + "alksesp32": VARIANT_ESP32, + "az-delivery-devkit-v4": VARIANT_ESP32, + "bpi-bit": VARIANT_ESP32, + "briki_abc_esp32": VARIANT_ESP32, + "briki_mbc-wb_esp32": VARIANT_ESP32, + "d-duino-32": VARIANT_ESP32, + "esp320": VARIANT_ESP32, + "esp32-c3-devkitm-1": VARIANT_ESP32C3, + "esp32cam": VARIANT_ESP32, + "esp32-devkitlipo": VARIANT_ESP32, + "esp32dev": VARIANT_ESP32, + "esp32doit-devkit-v1": VARIANT_ESP32, + "esp32doit-espduino": VARIANT_ESP32, + "esp32-evb": VARIANT_ESP32, + "esp32-gateway": VARIANT_ESP32, + "esp32-poe-iso": VARIANT_ESP32, + "esp32-poe": VARIANT_ESP32, + "esp32-pro": VARIANT_ESP32, + "esp32-s2-kaluga-1": VARIANT_ESP32S2, + "esp32-s2-saola-1": VARIANT_ESP32S2, + "esp32thing_plus": VARIANT_ESP32, + "esp32thing": VARIANT_ESP32, + "esp32vn-iot-uno": VARIANT_ESP32, + "espea32": VARIANT_ESP32, + "espectro32": VARIANT_ESP32, + "espino32": VARIANT_ESP32, + "esp-wrover-kit": VARIANT_ESP32, + "etboard": VARIANT_ESP32, + "featheresp32-s2": VARIANT_ESP32S2, + "featheresp32": VARIANT_ESP32, + "firebeetle32": VARIANT_ESP32, + "fm-devkit": VARIANT_ESP32, + "frogboard": VARIANT_ESP32, + "healthypi4": VARIANT_ESP32, + "heltec_wifi_kit_32_v2": VARIANT_ESP32, + "heltec_wifi_kit_32": VARIANT_ESP32, + "heltec_wifi_lora_32_V2": VARIANT_ESP32, + "heltec_wifi_lora_32": VARIANT_ESP32, + "heltec_wireless_stick_lite": VARIANT_ESP32, + "heltec_wireless_stick": VARIANT_ESP32, + "honeylemon": VARIANT_ESP32, + "hornbill32dev": VARIANT_ESP32, + "hornbill32minima": VARIANT_ESP32, + "imbrios-logsens-v1p1": VARIANT_ESP32, + "inex_openkb": VARIANT_ESP32, + "intorobot": VARIANT_ESP32, + "iotaap_magnolia": VARIANT_ESP32, + "iotbusio": VARIANT_ESP32, + "iotbusproteus": VARIANT_ESP32, + "kits-edu": VARIANT_ESP32, + "labplus_mpython": VARIANT_ESP32, + "lolin32_lite": VARIANT_ESP32, + "lolin32": VARIANT_ESP32, + "lolin_d32_pro": VARIANT_ESP32, + "lolin_d32": VARIANT_ESP32, + "lopy4": VARIANT_ESP32, + "lopy": VARIANT_ESP32, + "m5stack-atom": VARIANT_ESP32, + "m5stack-core2": VARIANT_ESP32, + "m5stack-core-esp32": VARIANT_ESP32, + "m5stack-coreink": VARIANT_ESP32, + "m5stack-fire": VARIANT_ESP32, + "m5stack-grey": VARIANT_ESP32, + "m5stack-timer-cam": VARIANT_ESP32, + "m5stick-c": VARIANT_ESP32, + "magicbit": VARIANT_ESP32, + "mgbot-iotik32a": VARIANT_ESP32, + "mgbot-iotik32b": VARIANT_ESP32, + "mhetesp32devkit": VARIANT_ESP32, + "mhetesp32minikit": VARIANT_ESP32, + "microduino-core-esp32": VARIANT_ESP32, + "nano32": VARIANT_ESP32, + "nina_w10": VARIANT_ESP32, + "node32s": VARIANT_ESP32, + "nodemcu-32s": VARIANT_ESP32, + "nscreen-32": VARIANT_ESP32, + "odroid_esp32": VARIANT_ESP32, + "onehorse32dev": VARIANT_ESP32, + "oroca_edubot": VARIANT_ESP32, + "pico32": VARIANT_ESP32, + "piranha_esp32": VARIANT_ESP32, + "pocket_32": VARIANT_ESP32, + "pycom_gpy": VARIANT_ESP32, + "qchip": VARIANT_ESP32, + "quantum": VARIANT_ESP32, + "sensesiot_weizen": VARIANT_ESP32, + "sg-o_airMon": VARIANT_ESP32, + "s_odi_ultra": VARIANT_ESP32, + "sparkfun_lora_gateway_1-channel": VARIANT_ESP32, + "tinypico": VARIANT_ESP32, + "ttgo-lora32-v1": VARIANT_ESP32, + "ttgo-lora32-v21": VARIANT_ESP32, + "ttgo-lora32-v2": VARIANT_ESP32, + "ttgo-t1": VARIANT_ESP32, + "ttgo-t7-v13-mini32": VARIANT_ESP32, + "ttgo-t7-v14-mini32": VARIANT_ESP32, + "ttgo-t-beam": VARIANT_ESP32, + "ttgo-t-watch": VARIANT_ESP32, + "turta_iot_node": VARIANT_ESP32, + "vintlabs-devkit-v1": VARIANT_ESP32, + "wemosbat": VARIANT_ESP32, + "wemos_d1_mini32": VARIANT_ESP32, + "wesp32": VARIANT_ESP32, + "widora-air": VARIANT_ESP32, + "wifiduino32": VARIANT_ESP32, + "xinabox_cw02": VARIANT_ESP32, +} diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index fe2a3ec8f8..20a0b0f792 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -19,7 +19,8 @@ from esphome.const import ( CONF_TX_BUFFER_SIZE, ) from esphome.core import CORE, EsphomeError, Lambda, coroutine_with_priority -from esphome.components.esp32 import get_esp32_variant, VARIANT_ESP32S2, VARIANT_ESP32C3 +from esphome.components.esp32 import get_esp32_variant +from esphome.components.esp32.const import VARIANT_ESP32S2, VARIANT_ESP32C3 CODEOWNERS = ["@esphome/core"] logger_ns = cg.esphome_ns.namespace("logger") From b5734c2b208e7b5285baa75d94307162944f0493 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 18 Oct 2021 15:31:01 +1300 Subject: [PATCH 0024/1729] Bump version to 2021.10.0b5 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index b505087115..39616ca062 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2021.10.0b4" +__version__ = "2021.10.0b5" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From 4a1e50fed171e44eb8f80cbd6e3ab9db7b394e2a Mon Sep 17 00:00:00 2001 From: Maurice Makaay Date: Fri, 15 Oct 2021 22:06:32 +0200 Subject: [PATCH 0025/1729] OTA firmware MD5 check + password support for esp-idf (#2507) Co-authored-by: Maurice Makaay --- CODEOWNERS | 1 + esphome/components/md5/__init__.py | 1 + esphome/components/md5/md5.cpp | 51 ++++++++++++++++ esphome/components/md5/md5.h | 58 +++++++++++++++++++ esphome/components/ota/__init__.py | 12 +--- .../ota/ota_backend_arduino_esp32.h | 1 + .../components/ota/ota_backend_esp_idf.cpp | 12 +++- esphome/components/ota/ota_backend_esp_idf.h | 3 + esphome/components/ota/ota_component.cpp | 27 ++++----- esphome/core/defines.h | 1 + 10 files changed, 139 insertions(+), 28 deletions(-) create mode 100644 esphome/components/md5/__init__.py create mode 100644 esphome/components/md5/md5.cpp create mode 100644 esphome/components/md5/md5.h diff --git a/CODEOWNERS b/CODEOWNERS index 4c3084d463..a7cf3a1b68 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -89,6 +89,7 @@ esphome/components/mcp23x17_base/* @jesserockz esphome/components/mcp23xxx_base/* @jesserockz esphome/components/mcp2515/* @danielschramm @mvturnho esphome/components/mcp9808/* @k7hpn +esphome/components/md5/* @esphome/core esphome/components/mdns/* @esphome/core esphome/components/midea/* @dudanov esphome/components/mitsubishi/* @RubyBailey diff --git a/esphome/components/md5/__init__.py b/esphome/components/md5/__init__.py new file mode 100644 index 0000000000..f70ffa9520 --- /dev/null +++ b/esphome/components/md5/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@esphome/core"] diff --git a/esphome/components/md5/md5.cpp b/esphome/components/md5/md5.cpp new file mode 100644 index 0000000000..c6ff783439 --- /dev/null +++ b/esphome/components/md5/md5.cpp @@ -0,0 +1,51 @@ +#include +#include +#include "md5.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace md5 { + +void MD5Digest::init() { + memset(this->digest_, 0, 16); + MD5Init(&this->ctx_); +} + +void MD5Digest::add(const uint8_t *data, size_t len) { MD5Update(&this->ctx_, data, len); } + +void MD5Digest::calculate() { MD5Final(this->digest_, &this->ctx_); } + +void MD5Digest::get_bytes(uint8_t *output) { memcpy(output, this->digest_, 16); } + +void MD5Digest::get_hex(char *output) { + for (size_t i = 0; i < 16; i++) { + sprintf(output + i * 2, "%02x", this->digest_[i]); + } +} + +bool MD5Digest::equals_bytes(const char *expected) { + for (size_t i = 0; i < 16; i++) { + if (expected[i] != this->digest_[i]) { + return false; + } + } + return true; +} + +bool MD5Digest::equals_hex(const char *expected) { + for (size_t i = 0; i < 16; i++) { + auto high = parse_hex(expected[i * 2]); + auto low = parse_hex(expected[i * 2 + 1]); + if (!high.has_value() || !low.has_value()) { + return false; + } + auto value = (*high << 4) | *low; + if (value != this->digest_[i]) { + return false; + } + } + return true; +} + +} // namespace md5 +} // namespace esphome diff --git a/esphome/components/md5/md5.h b/esphome/components/md5/md5.h new file mode 100644 index 0000000000..e40f419347 --- /dev/null +++ b/esphome/components/md5/md5.h @@ -0,0 +1,58 @@ +#pragma once + +#include "esphome/core/defines.h" + +#ifdef USE_ESP_IDF +#include "esp32/rom/md5_hash.h" +#define MD5_CTX_TYPE MD5Context +#endif + +#if defined(USE_ARDUINO) && defined(USE_ESP32) +#include "rom/md5_hash.h" +#define MD5_CTX_TYPE MD5Context +#endif + +#if defined(USE_ARDUINO) && defined(USE_ESP8266) +#include +#define MD5_CTX_TYPE md5_context_t +#endif + +namespace esphome { +namespace md5 { + +class MD5Digest { + public: + MD5Digest() = default; + ~MD5Digest() = default; + + /// Initialize a new MD5 digest computation. + void init(); + + /// Add bytes of data for the digest. + void add(const uint8_t *data, size_t len); + void add(const char *data, size_t len) { this->add((const uint8_t *) data, len); } + + /// Compute the digest, based on the provided data. + void calculate(); + + /// Retrieve the MD5 digest as bytes. + /// The output must be able to hold 16 bytes or more. + void get_bytes(uint8_t *output); + + /// Retrieve the MD5 digest as hex characters. + /// The output must be able to hold 32 bytes or more. + void get_hex(char *output); + + /// Compare the digest against a provided byte-encoded digest (16 bytes). + bool equals_bytes(const char *expected); + + /// Compare the digest against a provided hex-encoded digest (32 bytes). + bool equals_hex(const char *expected); + + protected: + MD5_CTX_TYPE ctx_{}; + uint8_t digest_[16]; +}; + +} // namespace md5 +} // namespace esphome diff --git a/esphome/components/ota/__init__.py b/esphome/components/ota/__init__.py index bcfb28979d..53b282c43e 100644 --- a/esphome/components/ota/__init__.py +++ b/esphome/components/ota/__init__.py @@ -15,7 +15,7 @@ from esphome.core import CORE, coroutine_with_priority CODEOWNERS = ["@esphome/core"] DEPENDENCIES = ["network"] -AUTO_LOAD = ["socket"] +AUTO_LOAD = ["socket", "md5"] CONF_ON_STATE_CHANGE = "on_state_change" CONF_ON_BEGIN = "on_begin" @@ -35,20 +35,12 @@ OTAEndTrigger = ota_ns.class_("OTAEndTrigger", automation.Trigger.template()) OTAErrorTrigger = ota_ns.class_("OTAErrorTrigger", automation.Trigger.template()) -def validate_password_support(value): - if CORE.using_arduino: - return value - if CORE.using_esp_idf: - raise cv.Invalid("Password support is not implemented yet for ESP-IDF") - raise NotImplementedError - - CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(OTAComponent), cv.Optional(CONF_SAFE_MODE, default=True): cv.boolean, cv.SplitDefault(CONF_PORT, esp8266=8266, esp32=3232): cv.port, - cv.Optional(CONF_PASSWORD): cv.All(cv.string, validate_password_support), + cv.Optional(CONF_PASSWORD): cv.string, cv.Optional( CONF_REBOOT_TIMEOUT, default="5min" ): cv.positive_time_period_milliseconds, diff --git a/esphome/components/ota/ota_backend_arduino_esp32.h b/esphome/components/ota/ota_backend_arduino_esp32.h index 8343bdf94f..6b712502fb 100644 --- a/esphome/components/ota/ota_backend_arduino_esp32.h +++ b/esphome/components/ota/ota_backend_arduino_esp32.h @@ -9,6 +9,7 @@ namespace esphome { namespace ota { class ArduinoESP32OTABackend : public OTABackend { + public: OTAResponseTypes begin(size_t image_size) override; void set_update_md5(const char *md5) override; OTAResponseTypes write(uint8_t *data, size_t len) override; diff --git a/esphome/components/ota/ota_backend_esp_idf.cpp b/esphome/components/ota/ota_backend_esp_idf.cpp index 4eb17d82f1..336b3798d9 100644 --- a/esphome/components/ota/ota_backend_esp_idf.cpp +++ b/esphome/components/ota/ota_backend_esp_idf.cpp @@ -4,6 +4,7 @@ #include "ota_backend_esp_idf.h" #include "ota_component.h" #include +#include "esphome/components/md5/md5.h" namespace esphome { namespace ota { @@ -24,15 +25,15 @@ OTAResponseTypes IDFOTABackend::begin(size_t image_size) { } return OTA_RESPONSE_ERROR_UNKNOWN; } + this->md5_.init(); return OTA_RESPONSE_OK; } -void IDFOTABackend::set_update_md5(const char *md5) { - // pass -} +void IDFOTABackend::set_update_md5(const char *expected_md5) { memcpy(this->expected_bin_md5_, expected_md5, 32); } OTAResponseTypes IDFOTABackend::write(uint8_t *data, size_t len) { esp_err_t err = esp_ota_write(this->update_handle_, data, len); + this->md5_.add(data, len); if (err != ESP_OK) { if (err == ESP_ERR_OTA_VALIDATE_FAILED) { return OTA_RESPONSE_ERROR_MAGIC; @@ -45,6 +46,11 @@ OTAResponseTypes IDFOTABackend::write(uint8_t *data, size_t len) { } OTAResponseTypes IDFOTABackend::end() { + this->md5_.calculate(); + if (!this->md5_.equals_hex(this->expected_bin_md5_)) { + this->abort(); + return OTA_RESPONSE_ERROR_UPDATE_END; + } esp_err_t err = esp_ota_end(this->update_handle_); this->update_handle_ = 0; if (err == ESP_OK) { diff --git a/esphome/components/ota/ota_backend_esp_idf.h b/esphome/components/ota/ota_backend_esp_idf.h index d6e2e2742a..49c6e124fa 100644 --- a/esphome/components/ota/ota_backend_esp_idf.h +++ b/esphome/components/ota/ota_backend_esp_idf.h @@ -5,6 +5,7 @@ #include "ota_component.h" #include "ota_backend.h" #include +#include "esphome/components/md5/md5.h" namespace esphome { namespace ota { @@ -20,6 +21,8 @@ class IDFOTABackend : public OTABackend { private: esp_ota_handle_t update_handle_{0}; const esp_partition_t *partition_; + md5::MD5Digest md5_{}; + char expected_bin_md5_[32]; }; } // namespace ota diff --git a/esphome/components/ota/ota_component.cpp b/esphome/components/ota/ota_component.cpp index 9ad3814f5c..89bee17452 100644 --- a/esphome/components/ota/ota_component.cpp +++ b/esphome/components/ota/ota_component.cpp @@ -8,15 +8,12 @@ #include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/util.h" +#include "esphome/components/md5/md5.h" #include "esphome/components/network/util.h" #include #include -#ifdef USE_OTA_PASSWORD -#include -#endif - namespace esphome { namespace ota { @@ -173,12 +170,12 @@ void OTAComponent::handle_() { if (!this->password_.empty()) { buf[0] = OTA_RESPONSE_REQUEST_AUTH; this->writeall_(buf, 1); - MD5Builder md5_builder{}; - md5_builder.begin(); + md5::MD5Digest md5{}; + md5.init(); sprintf(sbuf, "%08X", random_uint32()); - md5_builder.add(sbuf); - md5_builder.calculate(); - md5_builder.getChars(sbuf); + md5.add(sbuf, 8); + md5.calculate(); + md5.get_hex(sbuf); ESP_LOGV(TAG, "Auth: Nonce is %s", sbuf); // Send nonce, 32 bytes hex MD5 @@ -188,10 +185,10 @@ void OTAComponent::handle_() { } // prepare challenge - md5_builder.begin(); - md5_builder.add(this->password_.c_str()); + md5.init(); + md5.add(this->password_.c_str(), this->password_.length()); // add nonce - md5_builder.add(sbuf); + md5.add(sbuf, 32); // Receive cnonce, 32 bytes hex MD5 if (!this->readall_(buf, 32)) { @@ -201,11 +198,11 @@ void OTAComponent::handle_() { sbuf[32] = '\0'; ESP_LOGV(TAG, "Auth: CNonce is %s", sbuf); // add cnonce - md5_builder.add(sbuf); + md5.add(sbuf, 32); // calculate result - md5_builder.calculate(); - md5_builder.getChars(sbuf); + md5.calculate(); + md5.get_hex(sbuf); ESP_LOGV(TAG, "Auth: Result is %s", sbuf); // Receive result, 32 bytes hex MD5 diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 7c2261920a..b44987a768 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -26,6 +26,7 @@ #define USE_LOGGER #define USE_MDNS #define USE_NUMBER +#define USE_OTA_PASSWORD #define USE_OTA_STATE_CALLBACK #define USE_POWER_SUPPLY #define USE_PROMETHEUS From ecd115851fd4ed9d82a1db6865130fcbeb73eec1 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 18 Oct 2021 21:26:36 +1300 Subject: [PATCH 0026/1729] Bump version to 2021.10.0b6 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 39616ca062..6bf9ef2a64 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2021.10.0b5" +__version__ = "2021.10.0b6" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From 430598b7a1d07f9c2f702a9f1314bae7f671f5af Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 19 Oct 2021 15:16:39 +1300 Subject: [PATCH 0027/1729] Bump dashboard to 20211019.0 (#2549) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 8028007a0a..665ba27ed6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ pyserial==3.5 platformio==5.2.1 esptool==3.1 click==8.0.3 -esphome-dashboard==20211015.0 +esphome-dashboard==20211019.0 aioesphomeapi==9.1.5 # esp-idf requires this, but doesn't bundle it by default From 3e9c7f2e9ffdd356f254b054d3792646de7b66e0 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 19 Oct 2021 15:47:31 +1300 Subject: [PATCH 0028/1729] Bump version to 2021.10.0b7 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 6bf9ef2a64..f83fc7a651 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2021.10.0b6" +__version__ = "2021.10.0b7" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From 3a760fbb4460a91cff240b65f62151eec17b41d2 Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Tue, 19 Oct 2021 12:56:49 +0200 Subject: [PATCH 0029/1729] Fix ADC pin validation on ESP32-C3 (#2551) --- esphome/components/adc/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/adc/sensor.py b/esphome/components/adc/sensor.py index 9a0407d0f4..26ef504c1a 100644 --- a/esphome/components/adc/sensor.py +++ b/esphome/components/adc/sensor.py @@ -35,7 +35,7 @@ def validate_adc_pin(value): if is_esp32c3(): if not (0 <= value <= 4): # ADC1 raise cv.Invalid("ESP32-C3: Only pins 0 though 4 support ADC.") - if not (32 <= value <= 39): # ADC1 + elif not (32 <= value <= 39): # ADC1 raise cv.Invalid("ESP32: Only pins 32 though 39 support ADC.") elif CORE.is_esp8266: from esphome.components.esp8266.gpio import CONF_ANALOG From 1b0e60374bd997a69ea0ba63bc02d4e3a5c6bcf6 Mon Sep 17 00:00:00 2001 From: Martin <25747549+martgras@users.noreply.github.com> Date: Tue, 19 Oct 2021 23:10:24 +0200 Subject: [PATCH 0030/1729] ignore exception when not waiting for a response (#2552) --- esphome/components/modbus/modbus.cpp | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/esphome/components/modbus/modbus.cpp b/esphome/components/modbus/modbus.cpp index 1f6d868baf..45c5bfb603 100644 --- a/esphome/components/modbus/modbus.cpp +++ b/esphome/components/modbus/modbus.cpp @@ -96,23 +96,27 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) { ESP_LOGW(TAG, "Modbus CRC Check failed! %02X!=%02X", computed_crc, remote_crc); return false; } - - waiting_for_response = 0; std::vector data(this->rx_buffer_.begin() + data_offset, this->rx_buffer_.begin() + data_offset + data_len); - bool found = false; for (auto *device : this->devices_) { if (device->address_ == address) { // Is it an error response? if ((function_code & 0x80) == 0x80) { - ESP_LOGW(TAG, "Modbus error function code: 0x%X exception: %d", function_code, raw[2]); - device->on_modbus_error(function_code & 0x7F, raw[2]); + ESP_LOGD(TAG, "Modbus error function code: 0x%X exception: %d", function_code, raw[2]); + if (waiting_for_response != 0) { + device->on_modbus_error(function_code & 0x7F, raw[2]); + } else { + // Ignore modbus exception not related to a pending command + ESP_LOGD(TAG, "Ignoring Modbus error - not expecting a response"); + } } else { device->on_modbus_data(data); } found = true; } } + waiting_for_response = 0; + if (!found) { ESP_LOGW(TAG, "Got Modbus frame from unknown address 0x%02X! ", address); } @@ -196,6 +200,7 @@ void Modbus::send_raw(const std::vector &payload) { if (this->flow_control_pin_ != nullptr) this->flow_control_pin_->digital_write(false); waiting_for_response = payload[0]; + ESP_LOGV(TAG, "Modbus write raw: %s", hexencode(payload).c_str()); last_send_ = millis(); } From e10ab1da78658ef75ec8ebc328fa9b3c21fd775b Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 20 Oct 2021 10:14:30 +1300 Subject: [PATCH 0031/1729] Bump version to 2021.10.0b8 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index f83fc7a651..ad95096b94 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2021.10.0b7" +__version__ = "2021.10.0b8" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From ea0977abb4451c35a39e076c5ba92e3f8666c8ed Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 20 Oct 2021 10:53:49 +1300 Subject: [PATCH 0032/1729] Bump dashboard to 20211020.0 (#2556) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 665ba27ed6..120c71ff69 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ pyserial==3.5 platformio==5.2.1 esptool==3.1 click==8.0.3 -esphome-dashboard==20211019.0 +esphome-dashboard==20211020.0 aioesphomeapi==9.1.5 # esp-idf requires this, but doesn't bundle it by default From 7feffa64f3788db14e48d724b0b8405ff3c5a92f Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 20 Oct 2021 11:00:02 +1300 Subject: [PATCH 0033/1729] Bump version to 2021.10.0b9 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index ad95096b94..4ade772cc1 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2021.10.0b8" +__version__ = "2021.10.0b9" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From 9ac365feef5dad4412af5c271aa6f6c02be6e5b5 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 20 Oct 2021 13:25:00 +1300 Subject: [PATCH 0034/1729] Fix HA addon so it does not have logout button (#2558) --- esphome/dashboard/dashboard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index 501666b100..63378a38b5 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -597,7 +597,7 @@ class MainRequestHandler(BaseHandler): get_template_path("index"), begin=begin, **template_args(), - login_enabled=settings.using_auth, + login_enabled=settings.using_password, ) From b9f66373c1b5ffc78dc15b87dbbde884d830bbeb Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 20 Oct 2021 16:17:00 +1300 Subject: [PATCH 0035/1729] Bump esphome-dashboard to 20211020.1 (#2559) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 120c71ff69..16f23f4b7d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ pyserial==3.5 platformio==5.2.1 esptool==3.1 click==8.0.3 -esphome-dashboard==20211020.0 +esphome-dashboard==20211020.1 aioesphomeapi==9.1.5 # esp-idf requires this, but doesn't bundle it by default From 8456a8cecb3125c318accc407aec3b71e135fb75 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 20 Oct 2021 16:39:24 +1300 Subject: [PATCH 0036/1729] Bump version to 2021.10.0b10 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 4ade772cc1..f0951cbdaa 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2021.10.0b9" +__version__ = "2021.10.0b10" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From dad244fb7a998d52e1900f5692ea97d5d65f252d Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 21 Oct 2021 06:45:10 +1300 Subject: [PATCH 0037/1729] A few esp32_ble_server/improv fixes (#2562) --- .../components/esp32_ble_server/ble_server.cpp | 17 +++++++++-------- .../components/esp32_ble_server/ble_server.h | 14 ++++++++------ .../esp32_improv/esp32_improv_component.h | 2 +- 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/esphome/components/esp32_ble_server/ble_server.cpp b/esphome/components/esp32_ble_server/ble_server.cpp index e0fb80f94b..15bea07021 100644 --- a/esphome/components/esp32_ble_server/ble_server.cpp +++ b/esphome/components/esp32_ble_server/ble_server.cpp @@ -98,19 +98,20 @@ bool BLEServer::create_device_characteristics_() { return true; } -BLEService *BLEServer::create_service(const uint8_t *uuid, bool advertise) { +std::shared_ptr BLEServer::create_service(const uint8_t *uuid, bool advertise) { return this->create_service(ESPBTUUID::from_raw(uuid), advertise); } -BLEService *BLEServer::create_service(uint16_t uuid, bool advertise) { +std::shared_ptr BLEServer::create_service(uint16_t uuid, bool advertise) { return this->create_service(ESPBTUUID::from_uint16(uuid), advertise); } -BLEService *BLEServer::create_service(const std::string &uuid, bool advertise) { +std::shared_ptr BLEServer::create_service(const std::string &uuid, bool advertise) { return this->create_service(ESPBTUUID::from_raw(uuid), advertise); } -BLEService *BLEServer::create_service(ESPBTUUID uuid, bool advertise, uint16_t num_handles, uint8_t inst_id) { +std::shared_ptr BLEServer::create_service(ESPBTUUID uuid, bool advertise, uint16_t num_handles, + uint8_t inst_id) { ESP_LOGV(TAG, "Creating service - %s", uuid.to_string().c_str()); - BLEService *service = new BLEService(uuid, num_handles, inst_id); // NOLINT(cppcoreguidelines-owning-memory) - this->services_.push_back(service); + std::shared_ptr service = std::make_shared(uuid, num_handles, inst_id); + this->services_.emplace_back(service); if (advertise) { esp32_ble::global_ble->get_advertising()->add_service_uuid(uuid); } @@ -149,12 +150,12 @@ void BLEServer::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t ga break; } - for (auto *service : this->services_) { + for (const auto &service : this->services_) { service->gatts_event_handler(event, gatts_if, param); } } -float BLEServer::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; } +float BLEServer::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH + 10; } void BLEServer::dump_config() { ESP_LOGCONFIG(TAG, "ESP32 BLE Server:"); } diff --git a/esphome/components/esp32_ble_server/ble_server.h b/esphome/components/esp32_ble_server/ble_server.h index 9f7e8b8fc0..d275eeab01 100644 --- a/esphome/components/esp32_ble_server/ble_server.h +++ b/esphome/components/esp32_ble_server/ble_server.h @@ -11,6 +11,7 @@ #include "esphome/core/preferences.h" #include +#include #ifdef USE_ESP32 @@ -43,10 +44,11 @@ class BLEServer : public Component { void set_manufacturer(const std::string &manufacturer) { this->manufacturer_ = manufacturer; } void set_model(const std::string &model) { this->model_ = model; } - BLEService *create_service(const uint8_t *uuid, bool advertise = false); - BLEService *create_service(uint16_t uuid, bool advertise = false); - BLEService *create_service(const std::string &uuid, bool advertise = false); - BLEService *create_service(ESPBTUUID uuid, bool advertise = false, uint16_t num_handles = 15, uint8_t inst_id = 0); + std::shared_ptr create_service(const uint8_t *uuid, bool advertise = false); + std::shared_ptr create_service(uint16_t uuid, bool advertise = false); + std::shared_ptr create_service(const std::string &uuid, bool advertise = false); + std::shared_ptr create_service(ESPBTUUID uuid, bool advertise = false, uint16_t num_handles = 15, + uint8_t inst_id = 0); esp_gatt_if_t get_gatts_if() { return this->gatts_if_; } uint32_t get_connected_client_count() { return this->connected_clients_; } @@ -74,8 +76,8 @@ class BLEServer : public Component { uint32_t connected_clients_{0}; std::map clients_; - std::vector services_; - BLEService *device_information_service_; + std::vector> services_; + std::shared_ptr device_information_service_; std::vector service_components_; diff --git a/esphome/components/esp32_improv/esp32_improv_component.h b/esphome/components/esp32_improv/esp32_improv_component.h index 53cda5f399..3a5d150fbe 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.h +++ b/esphome/components/esp32_improv/esp32_improv_component.h @@ -48,7 +48,7 @@ class ESP32ImprovComponent : public Component, public BLEServiceComponent { std::vector incoming_data_; wifi::WiFiAP connecting_sta_; - BLEService *service_; + std::shared_ptr service_; BLECharacteristic *status_; BLECharacteristic *error_; BLECharacteristic *rpc_; From 95593eeeabe091cc80ea485ef57d469b69765bd5 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 21 Oct 2021 07:08:34 +1300 Subject: [PATCH 0038/1729] Bump esphome-dashboard to 20211021.0 (#2564) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 16f23f4b7d..63804e89e8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ pyserial==3.5 platformio==5.2.1 esptool==3.1 click==8.0.3 -esphome-dashboard==20211020.1 +esphome-dashboard==20211021.0 aioesphomeapi==9.1.5 # esp-idf requires this, but doesn't bundle it by default From 996ec59d286d0888c48313439feb404b6c9818fb Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Wed, 20 Oct 2021 20:15:09 +0200 Subject: [PATCH 0039/1729] Move running process log line to debug level (#2565) --- esphome/util.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/util.py b/esphome/util.py index 527e370ad8..0f168cade3 100644 --- a/esphome/util.py +++ b/esphome/util.py @@ -178,7 +178,7 @@ def run_external_command( orig_argv = sys.argv orig_exit = sys.exit # mock sys.exit full_cmd = " ".join(shlex_quote(x) for x in cmd) - _LOGGER.info("Running: %s", full_cmd) + _LOGGER.debug("Running: %s", full_cmd) orig_stdout = sys.stdout sys.stdout = RedirectText(sys.stdout, filter_lines=filter_lines) @@ -214,7 +214,7 @@ def run_external_command( def run_external_process(*cmd, **kwargs): full_cmd = " ".join(shlex_quote(x) for x in cmd) - _LOGGER.info("Running: %s", full_cmd) + _LOGGER.debug("Running: %s", full_cmd) filter_lines = kwargs.get("filter_lines") capture_stdout = kwargs.get("capture_stdout", False) From 3af297aa7660b2dd6f5f10bd8185619d24d511d1 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Wed, 20 Oct 2021 20:31:13 +0200 Subject: [PATCH 0040/1729] Revert nextion clang-tidy changes (#2566) --- .../binary_sensor/nextion_binarysensor.cpp | 4 +- .../binary_sensor/nextion_binarysensor.h | 3 +- esphome/components/nextion/nextion.cpp | 73 ++++++++++++------- esphome/components/nextion/nextion.h | 11 ++- esphome/components/nextion/nextion_base.h | 9 +-- .../nextion/nextion_component_base.h | 3 +- esphome/components/nextion/nextion_upload.cpp | 14 ++-- .../nextion/sensor/nextion_sensor.cpp | 8 +- .../nextion/sensor/nextion_sensor.h | 5 +- .../nextion/switch/nextion_switch.cpp | 4 +- .../nextion/switch/nextion_switch.h | 5 +- .../text_sensor/nextion_textsensor.cpp | 4 +- .../nextion/text_sensor/nextion_textsensor.h | 5 +- 13 files changed, 78 insertions(+), 70 deletions(-) diff --git a/esphome/components/nextion/binary_sensor/nextion_binarysensor.cpp b/esphome/components/nextion/binary_sensor/nextion_binarysensor.cpp index c5bfa78efe..bf6e74cb38 100644 --- a/esphome/components/nextion/binary_sensor/nextion_binarysensor.cpp +++ b/esphome/components/nextion/binary_sensor/nextion_binarysensor.cpp @@ -33,7 +33,7 @@ void NextionBinarySensor::update() { if (this->variable_name_.empty()) // This is a touch component return; - this->nextion_->add_to_get_queue(shared_from_this()); + this->nextion_->add_to_get_queue(this); } void NextionBinarySensor::set_state(bool state, bool publish, bool send_to_nextion) { @@ -48,7 +48,7 @@ void NextionBinarySensor::set_state(bool state, bool publish, bool send_to_nexti this->needs_to_send_update_ = true; } else { this->needs_to_send_update_ = false; - this->nextion_->add_no_result_to_queue_with_set(shared_from_this(), (int) state); + this->nextion_->add_no_result_to_queue_with_set(this, (int) state); } } diff --git a/esphome/components/nextion/binary_sensor/nextion_binarysensor.h b/esphome/components/nextion/binary_sensor/nextion_binarysensor.h index b86ee74013..b6b23ada85 100644 --- a/esphome/components/nextion/binary_sensor/nextion_binarysensor.h +++ b/esphome/components/nextion/binary_sensor/nextion_binarysensor.h @@ -10,8 +10,7 @@ class NextionBinarySensor; class NextionBinarySensor : public NextionComponent, public binary_sensor::BinarySensorInitiallyOff, - public PollingComponent, - public std::enable_shared_from_this { + public PollingComponent { public: NextionBinarySensor(NextionBase *nextion) { this->nextion_ = nextion; } diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index d56c370412..f23f55c9bb 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -196,7 +196,7 @@ void Nextion::print_queue_members_() { ESP_LOGN(TAG, "print_queue_members_ (top 10) size %zu", this->nextion_queue_.size()); ESP_LOGN(TAG, "*******************************************"); int count = 0; - for (auto &i : this->nextion_queue_) { + for (auto *i : this->nextion_queue_) { if (count++ == 10) break; @@ -257,9 +257,8 @@ bool Nextion::remove_from_q_(bool report_empty) { return false; } - auto nb = std::move(this->nextion_queue_.front()); - this->nextion_queue_.pop_front(); - auto &component = nb->component; + NextionQueue *nb = this->nextion_queue_.front(); + NextionComponentBase *component = nb->component; ESP_LOGN(TAG, "Removing %s from the queue", component->get_variable_name().c_str()); @@ -267,8 +266,10 @@ bool Nextion::remove_from_q_(bool report_empty) { if (component->get_variable_name() == "sleep_wake") { this->is_sleeping_ = false; } + delete component; // NOLINT(cppcoreguidelines-owning-memory) } - + delete nb; // NOLINT(cppcoreguidelines-owning-memory) + this->nextion_queue_.pop_front(); return true; } @@ -357,7 +358,7 @@ void Nextion::process_nextion_commands_() { int index = 0; int found = -1; for (auto &nb : this->nextion_queue_) { - auto &component = nb->component; + NextionComponentBase *component = nb->component; if (component->get_queue_type() == NextionQueueType::WAVEFORM_SENSOR) { ESP_LOGW(TAG, "Nextion reported invalid Waveform ID %d or Channel # %d was used!", @@ -368,6 +369,9 @@ void Nextion::process_nextion_commands_() { found = index; + delete component; // NOLINT(cppcoreguidelines-owning-memory) + delete nb; // NOLINT(cppcoreguidelines-owning-memory) + break; } ++index; @@ -464,9 +468,8 @@ void Nextion::process_nextion_commands_() { break; } - auto nb = std::move(this->nextion_queue_.front()); - this->nextion_queue_.pop_front(); - auto &component = nb->component; + NextionQueue *nb = this->nextion_queue_.front(); + NextionComponentBase *component = nb->component; if (component->get_queue_type() != NextionQueueType::TEXT_SENSOR) { ESP_LOGE(TAG, "ERROR: Received string return but next in queue \"%s\" is not a text sensor", @@ -477,6 +480,9 @@ void Nextion::process_nextion_commands_() { component->set_state_from_string(to_process, true, false); } + delete nb; // NOLINT(cppcoreguidelines-owning-memory) + this->nextion_queue_.pop_front(); + break; } // 0x71 0x01 0x02 0x03 0x04 0xFF 0xFF 0xFF @@ -505,9 +511,8 @@ void Nextion::process_nextion_commands_() { ++dataindex; } - auto nb = std::move(this->nextion_queue_.front()); - this->nextion_queue_.pop_front(); - auto &component = nb->component; + NextionQueue *nb = this->nextion_queue_.front(); + NextionComponentBase *component = nb->component; if (component->get_queue_type() != NextionQueueType::SENSOR && component->get_queue_type() != NextionQueueType::BINARY_SENSOR && @@ -521,6 +526,9 @@ void Nextion::process_nextion_commands_() { component->set_state_from_int(value, true, false); } + delete nb; // NOLINT(cppcoreguidelines-owning-memory) + this->nextion_queue_.pop_front(); + break; } @@ -682,7 +690,7 @@ void Nextion::process_nextion_commands_() { int index = 0; int found = -1; for (auto &nb : this->nextion_queue_) { - auto &component = nb->component; + auto component = nb->component; if (component->get_queue_type() == NextionQueueType::WAVEFORM_SENSOR) { size_t buffer_to_send = component->get_wave_buffer().size() < 255 ? component->get_wave_buffer().size() : 255; // ADDT command can only send 255 @@ -699,6 +707,8 @@ void Nextion::process_nextion_commands_() { component->get_wave_buffer().begin() + buffer_to_send); } found = index; + delete component; // NOLINT(cppcoreguidelines-owning-memory) + delete nb; // NOLINT(cppcoreguidelines-owning-memory) break; } ++index; @@ -727,7 +737,7 @@ void Nextion::process_nextion_commands_() { if (!this->nextion_queue_.empty() && this->nextion_queue_.front()->queue_time + this->max_q_age_ms_ < ms) { for (int i = 0; i < this->nextion_queue_.size(); i++) { - auto &component = this->nextion_queue_[i]->component; + NextionComponentBase *component = this->nextion_queue_[i]->component; if (this->nextion_queue_[i]->queue_time + this->max_q_age_ms_ < ms) { if (this->nextion_queue_[i]->queue_time == 0) ESP_LOGD(TAG, "Removing old queue type \"%s\" name \"%s\" queue_time 0", @@ -744,8 +754,11 @@ void Nextion::process_nextion_commands_() { if (component->get_variable_name() == "sleep_wake") { this->is_sleeping_ = false; } + delete component; // NOLINT(cppcoreguidelines-owning-memory) } + delete this->nextion_queue_[i]; // NOLINT(cppcoreguidelines-owning-memory) + this->nextion_queue_.erase(this->nextion_queue_.begin() + i); i--; @@ -899,16 +912,18 @@ uint16_t Nextion::recv_ret_string_(std::string &response, uint32_t timeout, bool * @param variable_name Name for the queue */ void Nextion::add_no_result_to_queue_(const std::string &variable_name) { - auto nextion_queue = make_unique(); + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + nextion::NextionQueue *nextion_queue = new nextion::NextionQueue; - nextion_queue->component = make_unique(); + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + nextion_queue->component = new nextion::NextionComponentBase; nextion_queue->component->set_variable_name(variable_name); nextion_queue->queue_time = millis(); - ESP_LOGN(TAG, "Add to queue type: NORESULT component %s", nextion_queue->component->get_variable_name().c_str()); + this->nextion_queue_.push_back(nextion_queue); - this->nextion_queue_.push_back(std::move(nextion_queue)); + ESP_LOGN(TAG, "Add to queue type: NORESULT component %s", nextion_queue->component->get_variable_name().c_str()); } /** @@ -979,7 +994,7 @@ bool Nextion::add_no_result_to_queue_with_printf_(const std::string &variable_na * @param is_sleep_safe The command is safe to send when the Nextion is sleeping */ -void Nextion::add_no_result_to_queue_with_set(std::shared_ptr component, int state_value) { +void Nextion::add_no_result_to_queue_with_set(NextionComponentBase *component, int state_value) { this->add_no_result_to_queue_with_set(component->get_variable_name(), component->get_variable_name_to_send(), state_value); } @@ -1007,8 +1022,7 @@ void Nextion::add_no_result_to_queue_with_set_internal_(const std::string &varia * @param state_value String value to set * @param is_sleep_safe The command is safe to send when the Nextion is sleeping */ -void Nextion::add_no_result_to_queue_with_set(std::shared_ptr component, - const std::string &state_value) { +void Nextion::add_no_result_to_queue_with_set(NextionComponentBase *component, const std::string &state_value) { this->add_no_result_to_queue_with_set(component->get_variable_name(), component->get_variable_name_to_send(), state_value); } @@ -1028,11 +1042,12 @@ void Nextion::add_no_result_to_queue_with_set_internal_(const std::string &varia state_value.c_str()); } -void Nextion::add_to_get_queue(std::shared_ptr component) { +void Nextion::add_to_get_queue(NextionComponentBase *component) { if ((!this->is_setup() && !this->ignore_is_setup_)) return; - auto nextion_queue = make_unique(); + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + nextion::NextionQueue *nextion_queue = new nextion::NextionQueue; nextion_queue->component = component; nextion_queue->queue_time = millis(); @@ -1043,7 +1058,7 @@ void Nextion::add_to_get_queue(std::shared_ptr component) std::string command = "get " + component->get_variable_name_to_send(); if (this->send_command_(command)) { - this->nextion_queue_.push_back(std::move(nextion_queue)); + this->nextion_queue_.push_back(nextion_queue); } } @@ -1055,13 +1070,15 @@ void Nextion::add_to_get_queue(std::shared_ptr component) * @param buffer_to_send The buffer size * @param buffer_size The buffer data */ -void Nextion::add_addt_command_to_queue(std::shared_ptr component) { +void Nextion::add_addt_command_to_queue(NextionComponentBase *component) { if ((!this->is_setup() && !this->ignore_is_setup_) || this->is_sleeping()) return; - auto nextion_queue = make_unique(); + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + nextion::NextionQueue *nextion_queue = new nextion::NextionQueue; - nextion_queue->component = std::make_shared(); + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + nextion_queue->component = new nextion::NextionComponentBase; nextion_queue->queue_time = millis(); size_t buffer_to_send = component->get_wave_buffer_size() < 255 ? component->get_wave_buffer_size() @@ -1070,7 +1087,7 @@ void Nextion::add_addt_command_to_queue(std::shared_ptr co std::string command = "addt " + to_string(component->get_component_id()) + "," + to_string(component->get_wave_channel_id()) + "," + to_string(buffer_to_send); if (this->send_command_(command)) { - this->nextion_queue_.push_back(std::move(nextion_queue)); + this->nextion_queue_.push_back(nextion_queue); } } diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index 1bee41f6cf..285b3ac9a3 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -707,18 +707,17 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe void set_nextion_sensor_state(NextionQueueType queue_type, const std::string &name, float state); void set_nextion_text_state(const std::string &name, const std::string &state); - void add_no_result_to_queue_with_set(std::shared_ptr component, int state_value) override; + void add_no_result_to_queue_with_set(NextionComponentBase *component, int state_value) override; void add_no_result_to_queue_with_set(const std::string &variable_name, const std::string &variable_name_to_send, int state_value) override; - void add_no_result_to_queue_with_set(std::shared_ptr component, - const std::string &state_value) override; + void add_no_result_to_queue_with_set(NextionComponentBase *component, const std::string &state_value) override; void add_no_result_to_queue_with_set(const std::string &variable_name, const std::string &variable_name_to_send, const std::string &state_value) override; - void add_to_get_queue(std::shared_ptr component) override; + void add_to_get_queue(NextionComponentBase *component) override; - void add_addt_command_to_queue(std::shared_ptr component) override; + void add_addt_command_to_queue(NextionComponentBase *component) override; void update_components_by_prefix(const std::string &prefix); @@ -729,7 +728,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe void set_auto_wake_on_touch_internal(bool auto_wake_on_touch) { this->auto_wake_on_touch_ = auto_wake_on_touch; } protected: - std::deque> nextion_queue_; + std::deque nextion_queue_; uint16_t recv_ret_string_(std::string &response, uint32_t timeout, bool recv_flag); void all_components_send_state_(bool force_update = false); uint64_t comok_sent_ = 0; diff --git a/esphome/components/nextion/nextion_base.h b/esphome/components/nextion/nextion_base.h index d91c70c960..a24fd74060 100644 --- a/esphome/components/nextion/nextion_base.h +++ b/esphome/components/nextion/nextion_base.h @@ -24,19 +24,18 @@ class NextionBase; class NextionBase { public: - virtual void add_no_result_to_queue_with_set(std::shared_ptr component, int state_value) = 0; + virtual void add_no_result_to_queue_with_set(NextionComponentBase *component, int state_value) = 0; virtual void add_no_result_to_queue_with_set(const std::string &variable_name, const std::string &variable_name_to_send, int state_value) = 0; - virtual void add_no_result_to_queue_with_set(std::shared_ptr component, - const std::string &state_value) = 0; + virtual void add_no_result_to_queue_with_set(NextionComponentBase *component, const std::string &state_value) = 0; virtual void add_no_result_to_queue_with_set(const std::string &variable_name, const std::string &variable_name_to_send, const std::string &state_value) = 0; - virtual void add_addt_command_to_queue(std::shared_ptr component) = 0; + virtual void add_addt_command_to_queue(NextionComponentBase *component) = 0; - virtual void add_to_get_queue(std::shared_ptr component) = 0; + virtual void add_to_get_queue(NextionComponentBase *component) = 0; virtual void set_component_background_color(const char *component, Color color) = 0; virtual void set_component_pressed_background_color(const char *component, Color color) = 0; diff --git a/esphome/components/nextion/nextion_component_base.h b/esphome/components/nextion/nextion_component_base.h index 2725d5a30c..71ad803bc4 100644 --- a/esphome/components/nextion/nextion_component_base.h +++ b/esphome/components/nextion/nextion_component_base.h @@ -1,6 +1,5 @@ #pragma once #include -#include #include "esphome/core/defines.h" namespace esphome { @@ -23,7 +22,7 @@ class NextionComponentBase; class NextionQueue { public: virtual ~NextionQueue() = default; - std::shared_ptr component; + NextionComponentBase *component; uint32_t queue_time = 0; }; diff --git a/esphome/components/nextion/nextion_upload.cpp b/esphome/components/nextion/nextion_upload.cpp index cebdbec31a..cd1c073320 100644 --- a/esphome/components/nextion/nextion_upload.cpp +++ b/esphome/components/nextion/nextion_upload.cpp @@ -281,12 +281,14 @@ void Nextion::upload_tft() { #endif // NOLINTNEXTLINE(readability-static-accessed-through-instance) ESP_LOGD(TAG, "Allocating buffer size %d, Heap size is %u", chunk_size, ESP.getFreeHeap()); - this->transfer_buffer_ = new (std::nothrow) uint8_t[chunk_size]; // NOLINT(cppcoreguidelines-owning-memory) - if (this->transfer_buffer_ == nullptr) { // Try a smaller size + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + this->transfer_buffer_ = new (std::nothrow) uint8_t[chunk_size]; + if (this->transfer_buffer_ == nullptr) { // Try a smaller size ESP_LOGD(TAG, "Could not allocate buffer size: %d trying 4096 instead", chunk_size); chunk_size = 4096; ESP_LOGD(TAG, "Allocating %d buffer", chunk_size); - this->transfer_buffer_ = new (std::nothrow) uint8_t[chunk_size]; // NOLINT(cppcoreguidelines-owning-memory) + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + this->transfer_buffer_ = new uint8_t[chunk_size]; if (!this->transfer_buffer_) this->upload_end_(); @@ -330,7 +332,8 @@ void Nextion::upload_end_() { WiFiClient *Nextion::get_wifi_client_() { if (this->tft_url_.compare(0, 6, "https:") == 0) { if (this->wifi_client_secure_ == nullptr) { - this->wifi_client_secure_ = new BearSSL::WiFiClientSecure(); // NOLINT(cppcoreguidelines-owning-memory) + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + this->wifi_client_secure_ = new BearSSL::WiFiClientSecure(); this->wifi_client_secure_->setInsecure(); this->wifi_client_secure_->setBufferSizes(512, 512); } @@ -338,7 +341,8 @@ WiFiClient *Nextion::get_wifi_client_() { } if (this->wifi_client_ == nullptr) { - this->wifi_client_ = new WiFiClient(); // NOLINT(cppcoreguidelines-owning-memory) + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + this->wifi_client_ = new WiFiClient(); } return this->wifi_client_; } diff --git a/esphome/components/nextion/sensor/nextion_sensor.cpp b/esphome/components/nextion/sensor/nextion_sensor.cpp index e983ebcc6f..4b7532d32d 100644 --- a/esphome/components/nextion/sensor/nextion_sensor.cpp +++ b/esphome/components/nextion/sensor/nextion_sensor.cpp @@ -34,7 +34,7 @@ void NextionSensor::update() { return; if (this->wave_chan_id_ == UINT8_MAX) { - this->nextion_->add_to_get_queue(shared_from_this()); + this->nextion_->add_to_get_queue(this); } else { if (this->send_last_value_) { this->add_to_wave_buffer(this->last_value_); @@ -62,9 +62,9 @@ void NextionSensor::set_state(float state, bool publish, bool send_to_nextion) { double to_multiply = pow(10, this->precision_); int state_value = (int) (state * to_multiply); - this->nextion_->add_no_result_to_queue_with_set(shared_from_this(), (int) state_value); + this->nextion_->add_no_result_to_queue_with_set(this, (int) state_value); } else { - this->nextion_->add_no_result_to_queue_with_set(shared_from_this(), (int) state); + this->nextion_->add_no_result_to_queue_with_set(this, (int) state); } } } @@ -103,7 +103,7 @@ void NextionSensor::wave_update_() { buffer_to_send, this->wave_buffer_.size(), this->component_id_, this->wave_chan_id_); #endif - this->nextion_->add_addt_command_to_queue(shared_from_this()); + this->nextion_->add_addt_command_to_queue(this); } } // namespace nextion diff --git a/esphome/components/nextion/sensor/nextion_sensor.h b/esphome/components/nextion/sensor/nextion_sensor.h index 068ff0451b..e4dde9a513 100644 --- a/esphome/components/nextion/sensor/nextion_sensor.h +++ b/esphome/components/nextion/sensor/nextion_sensor.h @@ -8,10 +8,7 @@ namespace esphome { namespace nextion { class NextionSensor; -class NextionSensor : public NextionComponent, - public sensor::Sensor, - public PollingComponent, - public std::enable_shared_from_this { +class NextionSensor : public NextionComponent, public sensor::Sensor, public PollingComponent { public: NextionSensor(NextionBase *nextion) { this->nextion_ = nextion; } void send_state_to_nextion() override { this->set_state(this->state, false, true); }; diff --git a/esphome/components/nextion/switch/nextion_switch.cpp b/esphome/components/nextion/switch/nextion_switch.cpp index 0bd958e0d8..1f32ad3425 100644 --- a/esphome/components/nextion/switch/nextion_switch.cpp +++ b/esphome/components/nextion/switch/nextion_switch.cpp @@ -20,7 +20,7 @@ void NextionSwitch::process_bool(const std::string &variable_name, bool on) { void NextionSwitch::update() { if (!this->nextion_->is_setup()) return; - this->nextion_->add_to_get_queue(shared_from_this()); + this->nextion_->add_to_get_queue(this); } void NextionSwitch::set_state(bool state, bool publish, bool send_to_nextion) { @@ -32,7 +32,7 @@ void NextionSwitch::set_state(bool state, bool publish, bool send_to_nextion) { this->needs_to_send_update_ = true; } else { this->needs_to_send_update_ = false; - this->nextion_->add_no_result_to_queue_with_set(shared_from_this(), (int) state); + this->nextion_->add_no_result_to_queue_with_set(this, (int) state); } } if (publish) { diff --git a/esphome/components/nextion/switch/nextion_switch.h b/esphome/components/nextion/switch/nextion_switch.h index d7783e5c51..1548287473 100644 --- a/esphome/components/nextion/switch/nextion_switch.h +++ b/esphome/components/nextion/switch/nextion_switch.h @@ -8,10 +8,7 @@ namespace esphome { namespace nextion { class NextionSwitch; -class NextionSwitch : public NextionComponent, - public switch_::Switch, - public PollingComponent, - public std::enable_shared_from_this { +class NextionSwitch : public NextionComponent, public switch_::Switch, public PollingComponent { public: NextionSwitch(NextionBase *nextion) { this->nextion_ = nextion; } diff --git a/esphome/components/nextion/text_sensor/nextion_textsensor.cpp b/esphome/components/nextion/text_sensor/nextion_textsensor.cpp index fa7cb35025..08f032df74 100644 --- a/esphome/components/nextion/text_sensor/nextion_textsensor.cpp +++ b/esphome/components/nextion/text_sensor/nextion_textsensor.cpp @@ -18,7 +18,7 @@ void NextionTextSensor::process_text(const std::string &variable_name, const std void NextionTextSensor::update() { if (!this->nextion_->is_setup()) return; - this->nextion_->add_to_get_queue(shared_from_this()); + this->nextion_->add_to_get_queue(this); } void NextionTextSensor::set_state(const std::string &state, bool publish, bool send_to_nextion) { @@ -29,7 +29,7 @@ void NextionTextSensor::set_state(const std::string &state, bool publish, bool s if (this->nextion_->is_sleeping() || !this->visible_) { this->needs_to_send_update_ = true; } else { - this->nextion_->add_no_result_to_queue_with_set(shared_from_this(), state); + this->nextion_->add_no_result_to_queue_with_set(this, state); } } diff --git a/esphome/components/nextion/text_sensor/nextion_textsensor.h b/esphome/components/nextion/text_sensor/nextion_textsensor.h index 762797727d..5716d0a008 100644 --- a/esphome/components/nextion/text_sensor/nextion_textsensor.h +++ b/esphome/components/nextion/text_sensor/nextion_textsensor.h @@ -8,10 +8,7 @@ namespace esphome { namespace nextion { class NextionTextSensor; -class NextionTextSensor : public NextionComponent, - public text_sensor::TextSensor, - public PollingComponent, - public std::enable_shared_from_this { +class NextionTextSensor : public NextionComponent, public text_sensor::TextSensor, public PollingComponent { public: NextionTextSensor(NextionBase *nextion) { this->nextion_ = nextion; } void update() override; From 56cc31e8e752572f3233c6e68ef4f832d6b3132d Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 21 Oct 2021 07:32:27 +1300 Subject: [PATCH 0041/1729] Bump version to 2021.10.0b11 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index f0951cbdaa..2c1bd219d7 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2021.10.0b10" +__version__ = "2021.10.0b11" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From 625463d87168bcc563896355e531c4b9a18bef34 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 21 Oct 2021 07:57:10 +1300 Subject: [PATCH 0042/1729] Bump version to 2021.10.0 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 2c1bd219d7..59441e23c8 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2021.10.0b11" +__version__ = "2021.10.0" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From 332c9e891b4abb7c72c324f02d0b8750e6199b1f Mon Sep 17 00:00:00 2001 From: Maurice Makaay Date: Thu, 21 Oct 2021 12:23:21 +0200 Subject: [PATCH 0043/1729] Fix MDNS for ESP8266 devices (#2571) Co-authored-by: Maurice Makaay Co-authored-by: Otto winter Co-authored-by: Maurice Makaay --- esphome/components/mdns/mdns_component.cpp | 6 +++--- esphome/components/mdns/mdns_component.h | 4 ++++ esphome/components/mdns/mdns_esp8266.cpp | 16 ++++++++++++++-- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/esphome/components/mdns/mdns_component.cpp b/esphome/components/mdns/mdns_component.cpp index 372d980eb0..631af9eba9 100644 --- a/esphome/components/mdns/mdns_component.cpp +++ b/esphome/components/mdns/mdns_component.cpp @@ -23,7 +23,7 @@ std::vector MDNSComponent::compile_services_() { #ifdef USE_API if (api::global_api_server != nullptr) { MDNSService service{}; - service.service_type = "esphomelib"; + service.service_type = "_esphomelib"; service.proto = "_tcp"; service.port = api::global_api_server->get_port(); service.txt_records.push_back({"version", ESPHOME_VERSION}); @@ -57,7 +57,7 @@ std::vector MDNSComponent::compile_services_() { #ifdef USE_PROMETHEUS { MDNSService service{}; - service.service_type = "prometheus-http"; + service.service_type = "_prometheus-http"; service.proto = "_tcp"; service.port = WEBSERVER_PORT; res.push_back(service); @@ -68,7 +68,7 @@ std::vector MDNSComponent::compile_services_() { // Publish "http" service if not using native API // This is just to have *some* mDNS service so that .local resolution works MDNSService service{}; - service.service_type = "http"; + service.service_type = "_http"; service.proto = "_tcp"; service.port = WEBSERVER_PORT; service.txt_records.push_back({"version", ESPHOME_VERSION}); diff --git a/esphome/components/mdns/mdns_component.h b/esphome/components/mdns/mdns_component.h index 985947d99c..679fb1a768 100644 --- a/esphome/components/mdns/mdns_component.h +++ b/esphome/components/mdns/mdns_component.h @@ -13,7 +13,11 @@ struct MDNSTXTRecord { }; struct MDNSService { + // service name _including_ underscore character prefix + // as defined in RFC6763 Section 7 std::string service_type; + // second label indicating protocol _including_ underscore character prefix + // as defined in RFC6763 Section 7, like "_tcp" or "_udp" std::string proto; uint16_t port; std::vector txt_records; diff --git a/esphome/components/mdns/mdns_esp8266.cpp b/esphome/components/mdns/mdns_esp8266.cpp index 48f31f1bbf..1a73e4b5b3 100644 --- a/esphome/components/mdns/mdns_esp8266.cpp +++ b/esphome/components/mdns/mdns_esp8266.cpp @@ -17,9 +17,21 @@ void MDNSComponent::setup() { auto services = compile_services_(); for (const auto &service : services) { - MDNS.addService(service.service_type.c_str(), service.proto.c_str(), service.port); + // Strip the leading underscore from the proto and service_type. While it is + // part of the wire protocol to have an underscore, and for example ESP-IDF + // expects the underscore to be there, the ESP8266 implementation always adds + // the underscore itself. + auto proto = service.proto.c_str(); + while (*proto == '_') { + proto++; + } + auto service_type = service.service_type.c_str(); + while (*service_type == '_') { + service_type++; + } + MDNS.addService(service_type, proto, service.port); for (const auto &record : service.txt_records) { - MDNS.addServiceTxt(service.service_type.c_str(), service.proto.c_str(), record.key.c_str(), record.value.c_str()); + MDNS.addServiceTxt(service_type, proto, record.key.c_str(), record.value.c_str()); } } } From f6935a4b4b61769d8b13320985753e97278d345f Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Thu, 21 Oct 2021 12:23:37 +0200 Subject: [PATCH 0044/1729] Fix ESP8266 GPIO0 Pullup Validation (#2572) --- esphome/components/esp8266/gpio.py | 4 ++-- tests/test3.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/esp8266/gpio.py b/esphome/components/esp8266/gpio.py index 0ebfbd6f69..fa5c94dff5 100644 --- a/esphome/components/esp8266/gpio.py +++ b/esphome/components/esp8266/gpio.py @@ -107,9 +107,9 @@ def validate_supports(value): raise cv.Invalid( "Open-drain only works with output mode", [CONF_MODE, CONF_OPEN_DRAIN] ) - if is_pullup and num == 0: + if is_pullup and num == 16: raise cv.Invalid( - "GPIO Pin 0 does not support pullup pin mode. " + "GPIO Pin 16 does not support pullup pin mode. " "Please choose another pin.", [CONF_MODE, CONF_PULLUP], ) diff --git a/tests/test3.yaml b/tests/test3.yaml index 73e314c94c..f7cba5e787 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -1150,7 +1150,7 @@ servo: ttp229_lsf: ttp229_bsf: - sdo_pin: D0 + sdo_pin: D2 scl_pin: D1 sim800l: From 2f32833a22e7d91b11221688f09c2a5c06aa8651 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Thu, 21 Oct 2021 12:24:01 +0200 Subject: [PATCH 0045/1729] Fix wifi ble coexistence check (#2573) --- esphome/components/wifi/__init__.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 19e4046711..faf3cca280 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -159,8 +159,15 @@ def final_validate_power_esp32_ble(value): "esp32_ble_server", "esp32_ble_tracker", ]: + if conflicting not in fv.full_config.get(): + continue + try: - cv.require_framework_version(esp32_arduino=cv.Version(1, 0, 5))(None) + # Only arduino 1.0.5+ and esp-idf impacted + cv.require_framework_version( + esp32_arduino=cv.Version(1, 0, 5), + esp_idf=cv.Version(4, 0, 0), + )(None) except cv.Invalid: pass else: From e7baa42e6309949db5b1f7b22813f04f96ec3060 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Thu, 21 Oct 2021 14:20:23 +0200 Subject: [PATCH 0046/1729] Arduino global delay/millis/... symbols workaround (#2575) --- esphome/core/config.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/esphome/core/config.py b/esphome/core/config.py index c495fefddd..3c53d81784 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -206,6 +206,31 @@ def include_file(path, basename): cg.add_global(cg.RawStatement(f'#include "{basename}"')) +ARDUINO_GLUE_CODE = """\ +#define yield() esphome::yield() +#define millis() esphome::millis() +#define delay(x) esphome::delay(x) +#define delayMicroseconds(x) esphome::delayMicroseconds(x) +""" + + +@coroutine_with_priority(-999.0) +async def add_arduino_global_workaround(): + # The Arduino framework defined these itself in the global + # namespace. For the esphome codebase that is not a problem, + # but when custom code + # 1. writes `millis()` for example AND + # 2. has `using namespace esphome;` like our guides suggest + # Then the compiler will complain that the call is ambiguous + # Define a hacky macro so that the call is never ambiguous + # and always uses the esphome namespace one. + # See also https://github.com/esphome/issues/issues/2510 + # Priority -999 so that it runs before adding includes, as those + # also might reference these symbols + for line in ARDUINO_GLUE_CODE.splitlines(): + cg.add_global(cg.RawStatement(line)) + + @coroutine_with_priority(-1000.0) async def add_includes(includes): # Add includes at the very end, so that the included files can access global variables @@ -287,6 +312,9 @@ async def to_code(config): cg.add_build_flag("-Wno-unused-but-set-variable") cg.add_build_flag("-Wno-sign-compare") + if CORE.using_arduino: + CORE.add_job(add_arduino_global_workaround) + if config[CONF_INCLUDES]: CORE.add_job(add_includes, config[CONF_INCLUDES]) From 7b8d826704715792f5fba2d8e8e9540b3cb86d46 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Thu, 21 Oct 2021 14:20:57 +0200 Subject: [PATCH 0047/1729] Fix ESP8266 OTA adds unnecessary Update library (#2579) --- esphome/components/ota/__init__.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/esphome/components/ota/__init__.py b/esphome/components/ota/__init__.py index 53b282c43e..b3d3b7ad23 100644 --- a/esphome/components/ota/__init__.py +++ b/esphome/components/ota/__init__.py @@ -90,9 +90,7 @@ async def to_code(config): ) cg.add(RawExpression(f"if ({condition}) return")) - if CORE.is_esp8266: - cg.add_library("Update", None) - elif CORE.is_esp32 and CORE.using_arduino: + if CORE.is_esp32 and CORE.using_arduino: cg.add_library("Update", None) use_state_callback = False From eae3d72a4da02f03929ddc2cc6d40e7b0c81c0d4 Mon Sep 17 00:00:00 2001 From: Otto winter Date: Thu, 21 Oct 2021 14:23:08 +0200 Subject: [PATCH 0048/1729] Bump version to 2021.10.1 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 59441e23c8..f07c83b32d 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2021.10.0" +__version__ = "2021.10.1" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From ab07ee57c6f96025c2dbaefc5a642a05f689b0b3 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Thu, 21 Oct 2021 14:39:36 +0200 Subject: [PATCH 0049/1729] Fix ESP8266 dallas GPIO16 INPUT_PULLUP (#2581) --- esphome/components/esp8266/gpio.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/esphome/components/esp8266/gpio.cpp b/esphome/components/esp8266/gpio.cpp index cb703c18e1..7805889b40 100644 --- a/esphome/components/esp8266/gpio.cpp +++ b/esphome/components/esp8266/gpio.cpp @@ -50,6 +50,13 @@ void ESP8266GPIOPin::pin_mode(gpio::Flags flags) { mode = OUTPUT; } else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_PULLUP)) { mode = INPUT_PULLUP; + if (pin_ == 16) { + // GPIO16 doesn't have a pullup, so pinMode would fail. + // However, sometimes this method is called with pullup mode anyway + // for example from dallas one_wire. For those cases convert this + // to a INPUT mode. + mode = INPUT; + } } else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_PULLDOWN)) { mode = INPUT_PULLDOWN_16; } else if (flags == (gpio::FLAG_OUTPUT | gpio::FLAG_OPEN_DRAIN)) { From f0aba6ceb282155f69c2928e9e896eea0e924ac8 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Thu, 21 Oct 2021 14:53:08 +0200 Subject: [PATCH 0050/1729] Fix platformio version in Dockerfile doesn't match requirements (#2582) --- docker/Dockerfile | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index e66c3e1d95..7928ff8901 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -43,7 +43,7 @@ RUN \ # Ubuntu python3-pip is missing wheel pip3 install --no-cache-dir \ wheel==0.36.2 \ - platformio==5.2.0 \ + platformio==5.2.1 \ # Change some platformio settings && platformio settings set enable_telemetry No \ && platformio settings set check_libraries_interval 1000000 \ diff --git a/requirements.txt b/requirements.txt index 63804e89e8..d0ee047f3a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ tornado==6.1 tzlocal==3.0 # from time tzdata>=2021.1 # from time pyserial==3.5 -platformio==5.2.1 +platformio==5.2.1 # When updating platformio, also update Dockerfile esptool==3.1 click==8.0.3 esphome-dashboard==20211021.0 From 7d9d9fcf3601e1058e4c47b43481d1bab8dc63ae Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Thu, 21 Oct 2021 15:29:32 +0200 Subject: [PATCH 0051/1729] Fix platformio_install_deps no longer installing all lib_deps (#2584) --- docker/platformio_install_deps.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/docker/platformio_install_deps.py b/docker/platformio_install_deps.py index 5625bd4d01..c7b11cf321 100755 --- a/docker/platformio_install_deps.py +++ b/docker/platformio_install_deps.py @@ -8,6 +8,23 @@ import sys config = configparser.ConfigParser(inline_comment_prefixes=(';', )) config.read(sys.argv[1]) -libs = [x for x in config['common']['lib_deps'].splitlines() if len(x) != 0] + +libs = [] +# Extract from every lib_deps key in all sections +for section in config.sections(): + conf = config[section] + if "lib_deps" not in conf: + continue + for lib_dep in conf["lib_deps"].splitlines(): + if not lib_dep: + # Empty line or comment + continue + if lib_dep.startswith("${"): + # Extending from another section + continue + if "@" not in lib_dep: + # No version pinned, this is an internal lib + continue + libs.append(lib_dep) subprocess.check_call(['platformio', 'lib', '-g', 'install', *libs]) From eed0c18d65e2f732128e48661218abc6a499b6dd Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Thu, 21 Oct 2021 18:57:03 +0200 Subject: [PATCH 0052/1729] Fix HeatpumpIR pin (#2585) --- platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index 9cc7477d51..6a8b342314 100644 --- a/platformio.ini +++ b/platformio.ini @@ -49,7 +49,7 @@ lib_deps = glmnet/Dsmr@0.5 ; dsmr rweather/Crypto@0.2.0 ; dsmr dudanov/MideaUART@1.1.8 ; midea - tonia/HeatpumpIR@^1.0.15 ; heatpumpir + tonia/HeatpumpIR@1.0.15 ; heatpumpir build_flags = ${common.build_flags} -DUSE_ARDUINO From ab3440142112e956ab40676088c8e7fb2bc584c7 Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Thu, 21 Oct 2021 18:59:49 +0200 Subject: [PATCH 0053/1729] Fix PlatformIO version for latest Arduino framework (#2590) --- esphome/components/esp8266/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index a5323db8bf..ddaeee6ab7 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -65,7 +65,7 @@ RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(2, 7, 4) # - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif8266 ARDUINO_2_PLATFORM_VERSION = cv.Version(2, 6, 2) # for arduino 3 framework versions -ARDUINO_3_PLATFORM_VERSION = cv.Version(3, 0, 2) +ARDUINO_3_PLATFORM_VERSION = cv.Version(3, 2, 0) def _arduino_check_versions(value): From ed0b34b2fe03d71cc28694c9ec39446fd888c9bc Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Thu, 21 Oct 2021 19:55:19 +0200 Subject: [PATCH 0054/1729] Fix pin/component switchup in SX1509 pin configuration (#2593) --- esphome/components/sx1509/__init__.py | 4 ++-- esphome/core/helpers.cpp | 1 + tests/test4.yaml | 10 ++++++++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/esphome/components/sx1509/__init__.py b/esphome/components/sx1509/__init__.py index f1b7d5f424..879ced2fb3 100644 --- a/esphome/components/sx1509/__init__.py +++ b/esphome/components/sx1509/__init__.py @@ -80,8 +80,8 @@ def validate_mode(value): CONF_SX1509 = "sx1509" SX1509_PIN_SCHEMA = cv.All( { - cv.GenerateID(): cv.declare_id(SX1509Component), - cv.Required(CONF_SX1509): cv.use_id(SX1509GPIOPin), + cv.GenerateID(): cv.declare_id(SX1509GPIOPin), + cv.Required(CONF_SX1509): cv.use_id(SX1509Component), cv.Required(CONF_NUMBER): cv.int_range(min=0, max=15), cv.Optional(CONF_MODE, default={}): cv.All( { diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 780df3ca6d..bc97259a71 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -358,6 +358,7 @@ template T clamp(const T val, const T min, const T max) { return max; return val; } +template uint8_t clamp(uint8_t, uint8_t, uint8_t); template float clamp(float, float, float); template int clamp(int, int, int); diff --git a/tests/test4.yaml b/tests/test4.yaml index 4f2025ad74..bc249c5ecb 100644 --- a/tests/test4.yaml +++ b/tests/test4.yaml @@ -59,6 +59,10 @@ tuya: pipsolar: id: inverter0 +sx1509: + - id: sx1509_hub + address: 0x3E + sensor: - platform: homeassistant entity_id: sensor.hello_world @@ -209,6 +213,7 @@ sensor: - or: - throttle: "20min" - delta: 0.02 + # # platform sensor.apds9960 requires component apds9960 # @@ -308,6 +313,11 @@ binary_sensor: y_max: 212 on_state: - lambda: 'ESP_LOGI("main", "key0: %s", (x ? "touch" : "release"));' + - platform: gpio + name: GPIO SX1509 test + pin: + sx1509: sx1509_hub + number: 3 climate: - platform: tuya From 115bca98f16ef99c718d563ec25f395d0aef744b Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Thu, 21 Oct 2021 19:56:47 +0200 Subject: [PATCH 0055/1729] Fix old-style `arduino_version` on ESP8266 and with magic values (#2591) --- esphome/const.py | 5 ++++- esphome/core/config.py | 9 +++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/esphome/const.py b/esphome/const.py index f07c83b32d..ce401b9f73 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,10 @@ __version__ = "2021.10.1" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" -TARGET_PLATFORMS = ["esp32", "esp8266"] +PLATFORM_ESP32 = "esp32" +PLATFORM_ESP8266 = "esp8266" + +TARGET_PLATFORMS = [PLATFORM_ESP32, PLATFORM_ESP8266] TARGET_FRAMEWORKS = ["arduino", "esp-idf"] # See also https://github.com/platformio/platform-espressif8266/releases diff --git a/esphome/core/config.py b/esphome/core/config.py index 3c53d81784..235e0eeb84 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -29,6 +29,7 @@ from esphome.const import ( CONF_VERSION, KEY_CORE, TARGET_PLATFORMS, + PLATFORM_ESP8266, ) from esphome.core import CORE, coroutine_with_priority from esphome.helpers import copy_file_if_changed, walk_files @@ -182,9 +183,13 @@ def preload_core_config(config, result): 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] = {CONF_TYPE: "arduino"} + plat_conf[CONF_FRAMEWORK] = {} + if plat != PLATFORM_ESP8266: + plat_conf[CONF_FRAMEWORK][CONF_TYPE] = "arduino" + try: - cv.Version.parse(conf[CONF_ARDUINO_VERSION]) + 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) From 68cbe58d005903c88736725f44991c718b355109 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 21 Oct 2021 20:01:03 +0200 Subject: [PATCH 0056/1729] Bump esphome-dashboard from 20211021.0 to 20211021.1 (#2594) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d0ee047f3a..51bbb445d7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ pyserial==3.5 platformio==5.2.1 # When updating platformio, also update Dockerfile esptool==3.1 click==8.0.3 -esphome-dashboard==20211021.0 +esphome-dashboard==20211021.1 aioesphomeapi==9.1.5 # esp-idf requires this, but doesn't bundle it by default From bbcd52396745f3a33da705c403da66f322228d52 Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Thu, 21 Oct 2021 20:07:37 +0200 Subject: [PATCH 0057/1729] Fix validation of addressable light IDs (#2588) --- esphome/components/light/addressable_light.h | 6 ++++++ esphome/components/light/types.py | 5 ++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/esphome/components/light/addressable_light.h b/esphome/components/light/addressable_light.h index fea7508515..2b2c0ca7e4 100644 --- a/esphome/components/light/addressable_light.h +++ b/esphome/components/light/addressable_light.h @@ -22,6 +22,12 @@ using ESPColor ESPDEPRECATED("esphome::light::ESPColor is deprecated, use esphom /// Convert the color information from a `LightColorValues` object to a `Color` object (does not apply brightness). Color color_from_light_color_values(LightColorValues val); +/// Use a custom state class for addressable lights, to allow type system to discriminate between addressable and +/// non-addressable lights. +class AddressableLightState : public LightState { + using LightState::LightState; +}; + class AddressableLight : public LightOutput, public Component { public: virtual int32_t size() const = 0; diff --git a/esphome/components/light/types.py b/esphome/components/light/types.py index cf544e5435..bc20cd5555 100644 --- a/esphome/components/light/types.py +++ b/esphome/components/light/types.py @@ -4,10 +4,9 @@ from esphome import automation # Base light_ns = cg.esphome_ns.namespace("light") LightState = light_ns.class_("LightState", cg.EntityBase, cg.Component) -# Fake class for addressable lights -AddressableLightState = light_ns.class_("LightState", LightState) +AddressableLightState = light_ns.class_("AddressableLightState", LightState) LightOutput = light_ns.class_("LightOutput") -AddressableLight = light_ns.class_("AddressableLight", cg.Component) +AddressableLight = light_ns.class_("AddressableLight", LightOutput, cg.Component) AddressableLightRef = AddressableLight.operator("ref") Color = cg.esphome_ns.class_("Color") From f93e7d4e3a63f946ecaef103bf239d9295dbaae1 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Fri, 22 Oct 2021 10:46:44 +0200 Subject: [PATCH 0058/1729] Fix socket connection closed not detected (#2587) --- esphome/components/api/api_connection.cpp | 2 + esphome/components/api/api_frame_helper.cpp | 48 ++++++++++++++----- esphome/components/api/api_frame_helper.h | 1 + esphome/components/ota/ota_component.cpp | 9 ++++ .../components/socket/lwip_raw_tcp_impl.cpp | 8 +++- 5 files changed, 53 insertions(+), 15 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 47171ba50f..c87ccf4dc0 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -78,6 +78,8 @@ void APIConnection::loop() { on_fatal_error(); if (err == APIError::SOCKET_READ_FAILED && errno == ECONNRESET) { ESP_LOGW(TAG, "%s: Connection reset", client_info_.c_str()); + } else if (err == APIError::CONNECTION_CLOSED) { + ESP_LOGW(TAG, "%s: Connection closed", client_info_.c_str()); } else { ESP_LOGW(TAG, "%s: Reading failed: %s errno=%d", client_info_.c_str(), api_error_to_str(err), errno); } diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp index 4971272f41..c0e37ec90d 100644 --- a/esphome/components/api/api_frame_helper.cpp +++ b/esphome/components/api/api_frame_helper.cpp @@ -10,7 +10,7 @@ namespace api { static const char *const TAG = "api.socket"; -/// Is the given return value (from read/write syscalls) a wouldblock error? +/// Is the given return value (from write syscalls) a wouldblock error? bool is_would_block(ssize_t ret) { if (ret == -1) { return errno == EWOULDBLOCK || errno == EAGAIN; @@ -64,6 +64,8 @@ const char *api_error_to_str(APIError err) { return "HANDSHAKESTATE_SPLIT_FAILED"; } else if (err == APIError::BAD_HANDSHAKE_ERROR_BYTE) { return "BAD_HANDSHAKE_ERROR_BYTE"; + } else if (err == APIError::CONNECTION_CLOSED) { + return "CONNECTION_CLOSED"; } return "UNKNOWN"; } @@ -185,12 +187,17 @@ APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) { // no header information yet size_t to_read = 3 - rx_header_buf_len_; ssize_t received = socket_->read(&rx_header_buf_[rx_header_buf_len_], to_read); - if (is_would_block(received)) { - return APIError::WOULD_BLOCK; - } else if (received == -1) { + if (received == -1) { + if (errno == EWOULDBLOCK || errno == EAGAIN) { + return APIError::WOULD_BLOCK; + } state_ = State::FAILED; HELPER_LOG("Socket read failed with errno %d", errno); return APIError::SOCKET_READ_FAILED; + } else if (received == 0) { + state_ = State::FAILED; + HELPER_LOG("Connection closed"); + return APIError::CONNECTION_CLOSED; } rx_header_buf_len_ += received; if (received != to_read) { @@ -227,12 +234,17 @@ APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) { // more data to read size_t to_read = msg_size - rx_buf_len_; ssize_t received = socket_->read(&rx_buf_[rx_buf_len_], to_read); - if (is_would_block(received)) { - return APIError::WOULD_BLOCK; - } else if (received == -1) { + if (received == -1) { + if (errno == EWOULDBLOCK || errno == EAGAIN) { + return APIError::WOULD_BLOCK; + } state_ = State::FAILED; HELPER_LOG("Socket read failed with errno %d", errno); return APIError::SOCKET_READ_FAILED; + } else if (received == 0) { + state_ = State::FAILED; + HELPER_LOG("Connection closed"); + return APIError::CONNECTION_CLOSED; } rx_buf_len_ += received; if (received != to_read) { @@ -778,12 +790,17 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) { while (!rx_header_parsed_) { uint8_t data; ssize_t received = socket_->read(&data, 1); - if (is_would_block(received)) { - return APIError::WOULD_BLOCK; - } else if (received == -1) { + if (received == -1) { + if (errno == EWOULDBLOCK || errno == EAGAIN) { + return APIError::WOULD_BLOCK; + } state_ = State::FAILED; HELPER_LOG("Socket read failed with errno %d", errno); return APIError::SOCKET_READ_FAILED; + } else if (received == 0) { + state_ = State::FAILED; + HELPER_LOG("Connection closed"); + return APIError::CONNECTION_CLOSED; } rx_header_buf_.push_back(data); @@ -824,12 +841,17 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) { // more data to read size_t to_read = rx_header_parsed_len_ - rx_buf_len_; ssize_t received = socket_->read(&rx_buf_[rx_buf_len_], to_read); - if (is_would_block(received)) { - return APIError::WOULD_BLOCK; - } else if (received == -1) { + if (received == -1) { + if (errno == EWOULDBLOCK || errno == EAGAIN) { + return APIError::WOULD_BLOCK; + } state_ = State::FAILED; HELPER_LOG("Socket read failed with errno %d", errno); return APIError::SOCKET_READ_FAILED; + } else if (received == 0) { + state_ = State::FAILED; + HELPER_LOG("Connection closed"); + return APIError::CONNECTION_CLOSED; } rx_buf_len_ += received; if (received != to_read) { diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index 7fdb26fd40..57e3c961d5 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -53,6 +53,7 @@ enum class APIError : int { HANDSHAKESTATE_SETUP_FAILED = 1019, HANDSHAKESTATE_SPLIT_FAILED = 1020, BAD_HANDSHAKE_ERROR_BYTE = 1021, + CONNECTION_CLOSED = 1022, }; const char *api_error_to_str(APIError err); diff --git a/esphome/components/ota/ota_component.cpp b/esphome/components/ota/ota_component.cpp index 89bee17452..6d51087882 100644 --- a/esphome/components/ota/ota_component.cpp +++ b/esphome/components/ota/ota_component.cpp @@ -275,6 +275,12 @@ void OTAComponent::handle_() { } ESP_LOGW(TAG, "Error receiving data for update, errno: %d", errno); goto error; + } else if (read == 0) { + // $ man recv + // "When a stream socket peer has performed an orderly shutdown, the return value will + // be 0 (the traditional "end-of-file" return)." + ESP_LOGW(TAG, "Remote end closed connection"); + goto error; } error_code = backend->write(buf, read); @@ -362,6 +368,9 @@ bool OTAComponent::readall_(uint8_t *buf, size_t len) { } ESP_LOGW(TAG, "Failed to read %d bytes of data, errno: %d", len, errno); return false; + } else if (read == 0) { + ESP_LOGW(TAG, "Remote closed connection"); + return false; } else { at += read; } diff --git a/esphome/components/socket/lwip_raw_tcp_impl.cpp b/esphome/components/socket/lwip_raw_tcp_impl.cpp index 54dfddac3f..922d895ff4 100644 --- a/esphome/components/socket/lwip_raw_tcp_impl.cpp +++ b/esphome/components/socket/lwip_raw_tcp_impl.cpp @@ -320,8 +320,7 @@ class LWIPRawImpl : public Socket { return -1; } if (rx_closed_ && rx_buf_ == nullptr) { - errno = ECONNRESET; - return -1; + return 0; } if (len == 0) { return 0; @@ -366,6 +365,11 @@ class LWIPRawImpl : public Socket { read += copysize; } + if (read == 0) { + errno = EWOULDBLOCK; + return -1; + } + return read; } ssize_t readv(const struct iovec *iov, int iovcnt) override { From 42873dd37ca231dc1dd33f8f8cc72ac4bc10f541 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Fri, 22 Oct 2021 12:12:07 +0200 Subject: [PATCH 0059/1729] Bump noise-c from 0.1.3 to 0.1.4 (#2602) --- esphome/components/api/__init__.py | 2 +- platformio.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index b0608a69dd..6b2e7fd06b 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -121,7 +121,7 @@ async def to_code(config): decoded = base64.b64decode(conf[CONF_KEY]) cg.add(var.set_noise_psk(list(decoded))) cg.add_define("USE_API_NOISE") - cg.add_library("esphome/noise-c", "0.1.3") + cg.add_library("esphome/noise-c", "0.1.4") else: cg.add_define("USE_API_PLAINTEXT") diff --git a/platformio.ini b/platformio.ini index 6a8b342314..ac5144fc37 100644 --- a/platformio.ini +++ b/platformio.ini @@ -26,7 +26,7 @@ build_flags = [common] lib_deps = - esphome/noise-c@0.1.3 ; api + esphome/noise-c@0.1.4 ; api makuna/NeoPixelBus@2.6.7 ; neopixelbus build_flags = -DESPHOME_LOG_LEVEL=ESPHOME_LOG_LEVEL_VERY_VERBOSE From 5be52f71f9c036d78fa1fe6799650899a6180794 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Fri, 22 Oct 2021 13:02:55 +0200 Subject: [PATCH 0060/1729] Add OTA upload compression for ESP8266 (#2601) --- esphome/components/ota/ota_backend.h | 1 + .../ota/ota_backend_arduino_esp32.h | 1 + .../ota/ota_backend_arduino_esp8266.h | 1 + esphome/components/ota/ota_backend_esp_idf.h | 1 + esphome/components/ota/ota_component.cpp | 9 +++- esphome/components/ota/ota_component.h | 1 + esphome/espota2.py | 44 ++++++++++++------- 7 files changed, 42 insertions(+), 16 deletions(-) diff --git a/esphome/components/ota/ota_backend.h b/esphome/components/ota/ota_backend.h index c253e009c6..5c5b61a278 100644 --- a/esphome/components/ota/ota_backend.h +++ b/esphome/components/ota/ota_backend.h @@ -12,6 +12,7 @@ class OTABackend { virtual OTAResponseTypes write(uint8_t *data, size_t len) = 0; virtual OTAResponseTypes end() = 0; virtual void abort() = 0; + virtual bool supports_compression() = 0; }; } // namespace ota diff --git a/esphome/components/ota/ota_backend_arduino_esp32.h b/esphome/components/ota/ota_backend_arduino_esp32.h index 6b712502fb..f86a70d678 100644 --- a/esphome/components/ota/ota_backend_arduino_esp32.h +++ b/esphome/components/ota/ota_backend_arduino_esp32.h @@ -15,6 +15,7 @@ class ArduinoESP32OTABackend : public OTABackend { OTAResponseTypes write(uint8_t *data, size_t len) override; OTAResponseTypes end() override; void abort() override; + bool supports_compression() override { return false; } }; } // namespace ota diff --git a/esphome/components/ota/ota_backend_arduino_esp8266.h b/esphome/components/ota/ota_backend_arduino_esp8266.h index d1195af911..cf29a90fc1 100644 --- a/esphome/components/ota/ota_backend_arduino_esp8266.h +++ b/esphome/components/ota/ota_backend_arduino_esp8266.h @@ -16,6 +16,7 @@ class ArduinoESP8266OTABackend : public OTABackend { OTAResponseTypes write(uint8_t *data, size_t len) override; OTAResponseTypes end() override; void abort() override; + bool supports_compression() override { return true; } }; } // namespace ota diff --git a/esphome/components/ota/ota_backend_esp_idf.h b/esphome/components/ota/ota_backend_esp_idf.h index 49c6e124fa..af09d0d693 100644 --- a/esphome/components/ota/ota_backend_esp_idf.h +++ b/esphome/components/ota/ota_backend_esp_idf.h @@ -17,6 +17,7 @@ class IDFOTABackend : public OTABackend { OTAResponseTypes write(uint8_t *data, size_t len) override; OTAResponseTypes end() override; void abort() override; + bool supports_compression() override { return false; } private: esp_ota_handle_t update_handle_{0}; diff --git a/esphome/components/ota/ota_component.cpp b/esphome/components/ota/ota_component.cpp index 6d51087882..e49c108320 100644 --- a/esphome/components/ota/ota_component.cpp +++ b/esphome/components/ota/ota_component.cpp @@ -104,6 +104,8 @@ void OTAComponent::loop() { } } +static const uint8_t FEATURE_SUPPORTS_COMPRESSION = 0x01; + void OTAComponent::handle_() { OTAResponseTypes error_code = OTA_RESPONSE_ERROR_UNKNOWN; bool update_started = false; @@ -154,6 +156,8 @@ void OTAComponent::handle_() { buf[1] = OTA_VERSION_1_0; this->writeall_(buf, 2); + backend = make_ota_backend(); + // Read features - 1 byte if (!this->readall_(buf, 1)) { ESP_LOGW(TAG, "Reading features failed!"); @@ -164,6 +168,10 @@ void OTAComponent::handle_() { // Acknowledge header - 1 byte buf[0] = OTA_RESPONSE_HEADER_OK; + if ((ota_features & FEATURE_SUPPORTS_COMPRESSION) != 0 && backend->supports_compression()) { + buf[0] = OTA_RESPONSE_SUPPORTS_COMPRESSION; + } + this->writeall_(buf, 1); #ifdef USE_OTA_PASSWORD @@ -241,7 +249,6 @@ void OTAComponent::handle_() { } ESP_LOGV(TAG, "OTA size is %u bytes", ota_size); - backend = make_ota_backend(); error_code = backend->begin(ota_size); if (error_code != OTA_RESPONSE_OK) goto error; diff --git a/esphome/components/ota/ota_component.h b/esphome/components/ota/ota_component.h index e08e187df6..5647d52eeb 100644 --- a/esphome/components/ota/ota_component.h +++ b/esphome/components/ota/ota_component.h @@ -19,6 +19,7 @@ enum OTAResponseTypes { OTA_RESPONSE_BIN_MD5_OK = 67, OTA_RESPONSE_RECEIVE_OK = 68, OTA_RESPONSE_UPDATE_END_OK = 69, + OTA_RESPONSE_SUPPORTS_COMPRESSION = 70, OTA_RESPONSE_ERROR_MAGIC = 128, OTA_RESPONSE_ERROR_UPDATE_PREPARE = 129, diff --git a/esphome/espota2.py b/esphome/espota2.py index f8a2fab94c..8f299395dd 100644 --- a/esphome/espota2.py +++ b/esphome/espota2.py @@ -4,6 +4,7 @@ import random import socket import sys import time +import gzip from esphome.core import EsphomeError from esphome.helpers import is_ip_address, resolve_ip_address @@ -17,6 +18,7 @@ RESPONSE_UPDATE_PREPARE_OK = 66 RESPONSE_BIN_MD5_OK = 67 RESPONSE_RECEIVE_OK = 68 RESPONSE_UPDATE_END_OK = 69 +RESPONSE_SUPPORTS_COMPRESSION = 70 RESPONSE_ERROR_MAGIC = 128 RESPONSE_ERROR_UPDATE_PREPARE = 129 @@ -34,6 +36,8 @@ OTA_VERSION_1_0 = 1 MAGIC_BYTES = [0x6C, 0x26, 0xF7, 0x5C, 0x45] +FEATURE_SUPPORTS_COMPRESSION = 0x01 + _LOGGER = logging.getLogger(__name__) @@ -170,11 +174,9 @@ def send_check(sock, data, msg): def perform_ota(sock, password, file_handle, filename): - file_md5 = hashlib.md5(file_handle.read()).hexdigest() - file_size = file_handle.tell() + file_contents = file_handle.read() + file_size = len(file_contents) _LOGGER.info("Uploading %s (%s bytes)", filename, file_size) - file_handle.seek(0) - _LOGGER.debug("MD5 of binary is %s", file_md5) # Enable nodelay, we need it for phase 1 sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) @@ -185,8 +187,16 @@ def perform_ota(sock, password, file_handle, filename): raise OTAError(f"Unsupported OTA version {version}") # Features - send_check(sock, 0x00, "features") - receive_exactly(sock, 1, "features", RESPONSE_HEADER_OK) + send_check(sock, FEATURE_SUPPORTS_COMPRESSION, "features") + features = receive_exactly( + sock, 1, "features", [RESPONSE_HEADER_OK, RESPONSE_SUPPORTS_COMPRESSION] + )[0] + + if features == RESPONSE_SUPPORTS_COMPRESSION: + upload_contents = gzip.compress(file_contents, compresslevel=9) + _LOGGER.info("Compressed to %s bytes", len(upload_contents)) + else: + upload_contents = file_contents (auth,) = receive_exactly( sock, 1, "auth", [RESPONSE_REQUEST_AUTH, RESPONSE_AUTH_OK] @@ -213,16 +223,20 @@ def perform_ota(sock, password, file_handle, filename): send_check(sock, result, "auth result") receive_exactly(sock, 1, "auth result", RESPONSE_AUTH_OK) - file_size_encoded = [ - (file_size >> 24) & 0xFF, - (file_size >> 16) & 0xFF, - (file_size >> 8) & 0xFF, - (file_size >> 0) & 0xFF, + upload_size = len(upload_contents) + upload_size_encoded = [ + (upload_size >> 24) & 0xFF, + (upload_size >> 16) & 0xFF, + (upload_size >> 8) & 0xFF, + (upload_size >> 0) & 0xFF, ] - send_check(sock, file_size_encoded, "binary size") + send_check(sock, upload_size_encoded, "binary size") receive_exactly(sock, 1, "binary size", RESPONSE_UPDATE_PREPARE_OK) - send_check(sock, file_md5, "file checksum") + upload_md5 = hashlib.md5(upload_contents).hexdigest() + _LOGGER.debug("MD5 of upload is %s", upload_md5) + + send_check(sock, upload_md5, "file checksum") receive_exactly(sock, 1, "file checksum", RESPONSE_BIN_MD5_OK) # Disable nodelay for transfer @@ -236,7 +250,7 @@ def perform_ota(sock, password, file_handle, filename): offset = 0 progress = ProgressBar() while True: - chunk = file_handle.read(1024) + chunk = upload_contents[offset : offset + 1024] if not chunk: break offset += len(chunk) @@ -247,7 +261,7 @@ def perform_ota(sock, password, file_handle, filename): sys.stderr.write("\n") raise OTAError(f"Error sending data: {err}") from err - progress.update(offset / float(file_size)) + progress.update(offset / upload_size) progress.done() # Enable nodelay for last checks From ff2c316b1830571c50b69e6bfc91962a0b0850c5 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Fri, 22 Oct 2021 14:14:07 +0200 Subject: [PATCH 0061/1729] Re-raise keyboardinterrupt (#2603) --- esphome/util.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/esphome/util.py b/esphome/util.py index 0f168cade3..937635fa43 100644 --- a/esphome/util.py +++ b/esphome/util.py @@ -192,8 +192,8 @@ def run_external_command( sys.argv = list(cmd) sys.exit = mock_exit return func() or 0 - except KeyboardInterrupt: - return 1 + except KeyboardInterrupt: # pylint: disable=try-except-raise + raise except SystemExit as err: return err.args[0] except Exception as err: # pylint: disable=broad-except @@ -227,6 +227,8 @@ def run_external_process(*cmd, **kwargs): try: return subprocess.call(cmd, stdout=sub_stdout, stderr=sub_stderr) + except KeyboardInterrupt: # pylint: disable=try-except-raise + raise except Exception as err: # pylint: disable=broad-except _LOGGER.error("Running command failed: %s", err) _LOGGER.error("Please try running %s locally.", full_cmd) From dfb96e4b7fb23ed00707d5401facf8970b930168 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Fri, 22 Oct 2021 14:14:14 +0200 Subject: [PATCH 0062/1729] Add owner to all libraries used (#2604) --- esphome/components/bme680_bsec/__init__.py | 2 +- esphome/components/mqtt/__init__.py | 2 +- esphome/components/web_server_base/__init__.py | 2 +- platformio.ini | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/esphome/components/bme680_bsec/__init__.py b/esphome/components/bme680_bsec/__init__.py index 38da18d702..2f844fa666 100644 --- a/esphome/components/bme680_bsec/__init__.py +++ b/esphome/components/bme680_bsec/__init__.py @@ -67,4 +67,4 @@ async def to_code(config): cg.add_library("SPI", None) cg.add_define("USE_BSEC") - cg.add_library("BSEC Software Library", "1.6.1480") + cg.add_library("boschsensortec/BSEC Software Library", "1.6.1480") diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index 8f02f8d437..3d52dab67f 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -215,7 +215,7 @@ async def to_code(config): await cg.register_component(var, config) # https://github.com/OttoWinter/async-mqtt-client/blob/master/library.json - cg.add_library("ottowinter/AsyncMqttClient-esphome", "0.8.4") + cg.add_library("ottowinter/AsyncMqttClient-esphome", "0.8.6") cg.add_define("USE_MQTT") cg.add_global(mqtt_ns.using) diff --git a/esphome/components/web_server_base/__init__.py b/esphome/components/web_server_base/__init__.py index 95d59a863e..019f31954f 100644 --- a/esphome/components/web_server_base/__init__.py +++ b/esphome/components/web_server_base/__init__.py @@ -28,4 +28,4 @@ async def to_code(config): cg.add_library("FS", None) cg.add_library("Update", None) # https://github.com/esphome/ESPAsyncWebServer/blob/master/library.json - cg.add_library("esphome/ESPAsyncWebServer-esphome", "1.3.0") + cg.add_library("esphome/ESPAsyncWebServer-esphome", "1.3.1") diff --git a/platformio.ini b/platformio.ini index ac5144fc37..3d720e24ac 100644 --- a/platformio.ini +++ b/platformio.ini @@ -39,9 +39,9 @@ src_filter = extends = common lib_deps = ${common.lib_deps} - ottowinter/AsyncMqttClient-esphome@0.8.4 ; mqtt + ottowinter/AsyncMqttClient-esphome@0.8.6 ; mqtt ottowinter/ArduinoJson-esphomelib@5.13.3 ; json - esphome/ESPAsyncWebServer-esphome@1.3.0 ; web_server_base + esphome/ESPAsyncWebServer-esphome@1.3.1 ; web_server_base fastled/FastLED@3.3.2 ; fastled_base mikalhart/TinyGPSPlus@1.0.2 ; gps freekode/TM1651@1.0.1 ; tm1651 From 6bdae55ee16f638cd80d43d8a1a79b466ca510bf Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Fri, 22 Oct 2021 16:52:43 +0200 Subject: [PATCH 0063/1729] Fix compiler warnings and update platformio line filter (#2607) --- esphome/components/climate/climate.cpp | 4 ++++ esphome/components/mqtt/mqtt_fan.cpp | 6 ++++++ esphome/components/web_server/web_server.cpp | 6 ++++++ esphome/components/web_server_base/__init__.py | 2 +- esphome/platformio_api.py | 17 ++++++++++++----- platformio.ini | 2 +- 6 files changed, 30 insertions(+), 7 deletions(-) diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 34e6328d8a..ebea20ed1f 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -440,7 +440,11 @@ void Climate::set_visual_max_temperature_override(float visual_max_temperature_o void Climate::set_visual_temperature_step_override(float visual_temperature_step_override) { this->visual_temperature_step_override_ = visual_temperature_step_override; } +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" Climate::Climate(const std::string &name) : EntityBase(name) {} +#pragma GCC diagnostic pop + Climate::Climate() : Climate("") {} ClimateCall Climate::make_call() { return ClimateCall(this); } diff --git a/esphome/components/mqtt/mqtt_fan.cpp b/esphome/components/mqtt/mqtt_fan.cpp index 898183cc58..1703343a77 100644 --- a/esphome/components/mqtt/mqtt_fan.cpp +++ b/esphome/components/mqtt/mqtt_fan.cpp @@ -88,9 +88,12 @@ void MQTTFanComponent::setup() { if (this->state_->get_traits().supports_speed()) { this->subscribe(this->get_speed_command_topic(), [this](const std::string &topic, const std::string &payload) { +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" this->state_->make_call() .set_speed(payload.c_str()) // NOLINT(clang-diagnostic-deprecated-declarations) .perform(); +#pragma GCC diagnostic pop }); } @@ -145,6 +148,8 @@ bool MQTTFanComponent::publish_state() { } if (traits.supports_speed()) { const char *payload; +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" // NOLINTNEXTLINE(clang-diagnostic-deprecated-declarations) switch (fan::speed_level_to_enum(this->state_->speed, traits.supported_speed_count())) { case FAN_SPEED_LOW: { // NOLINT(clang-diagnostic-deprecated-declarations) @@ -161,6 +166,7 @@ bool MQTTFanComponent::publish_state() { break; } } +#pragma GCC diagnostic pop bool success = this->publish(this->get_speed_state_topic(), payload); failed = failed || !success; } diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index e99431be36..44ace38990 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -414,6 +414,8 @@ std::string WebServer::fan_json(fan::FanState *obj) { const auto traits = obj->get_traits(); if (traits.supports_speed()) { root["speed_level"] = obj->speed; +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" // NOLINTNEXTLINE(clang-diagnostic-deprecated-declarations) switch (fan::speed_level_to_enum(obj->speed, traits.supported_speed_count())) { case fan::FAN_SPEED_LOW: // NOLINT(clang-diagnostic-deprecated-declarations) @@ -426,6 +428,7 @@ std::string WebServer::fan_json(fan::FanState *obj) { root["speed"] = "high"; break; } +#pragma GCC diagnostic pop } if (obj->get_traits().supports_oscillation()) root["oscillation"] = obj->oscillating; @@ -448,7 +451,10 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc auto call = obj->turn_on(); if (request->hasParam("speed")) { String speed = request->getParam("speed")->value(); +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" call.set_speed(speed.c_str()); // NOLINT(clang-diagnostic-deprecated-declarations) +#pragma GCC diagnostic pop } if (request->hasParam("speed_level")) { String speed_level = request->getParam("speed_level")->value(); diff --git a/esphome/components/web_server_base/__init__.py b/esphome/components/web_server_base/__init__.py index 019f31954f..4da94d990a 100644 --- a/esphome/components/web_server_base/__init__.py +++ b/esphome/components/web_server_base/__init__.py @@ -28,4 +28,4 @@ async def to_code(config): cg.add_library("FS", None) cg.add_library("Update", None) # https://github.com/esphome/ESPAsyncWebServer/blob/master/library.json - cg.add_library("esphome/ESPAsyncWebServer-esphome", "1.3.1") + cg.add_library("esphome/ESPAsyncWebServer-esphome", "2.0.0") diff --git a/esphome/platformio_api.py b/esphome/platformio_api.py index 054c0cb1b0..70e4430e71 100644 --- a/esphome/platformio_api.py +++ b/esphome/platformio_api.py @@ -46,24 +46,31 @@ IGNORE_LIB_WARNINGS = f"(?:{'|'.join(['Hash', 'Update'])})" FILTER_PLATFORMIO_LINES = [ r"Verbose mode can be enabled via `-v, --verbose` option.*", r"CONFIGURATION: https://docs.platformio.org/.*", - r"PLATFORM: .*", r"DEBUG: Current.*", - r"PACKAGES: .*", + r"LDF Modes:.*", r"LDF: Library Dependency Finder -> http://bit.ly/configure-pio-ldf.*", - r"LDF Modes: Finder ~ chain, Compatibility ~ soft.*", f"Looking for {IGNORE_LIB_WARNINGS} library in registry", f"Warning! Library `.*'{IGNORE_LIB_WARNINGS}.*` has not been found in PlatformIO Registry.", f"You can ignore this message, if `.*{IGNORE_LIB_WARNINGS}.*` is a built-in library.*", r"Scanning dependencies...", r"Found \d+ compatible libraries", r"Memory Usage -> http://bit.ly/pio-memory-usage", - r"esptool.py v.*", r"Found: https://platformio.org/lib/show/.*", r"Using cache: .*", r"Installing dependencies", - r".* @ .* is already installed", + r"Library Manager: Already installed, built-in library", r"Building in .* mode", r"Advanced Memory Usage is available via .*", + r"Merged .* ELF section", + r"esptool.py v.*", + r"Checking size .*", + r"Retrieving maximum program size .*", + r"PLATFORM: .*", + r"PACKAGES:.*", + r" - framework-arduinoespressif.* \(.*\)", + r" - tool-esptool.* \(.*\)", + r" - toolchain-.* \(.*\)", + r"Creating BIN file .*", ] diff --git a/platformio.ini b/platformio.ini index 3d720e24ac..ee895ed882 100644 --- a/platformio.ini +++ b/platformio.ini @@ -41,7 +41,7 @@ lib_deps = ${common.lib_deps} ottowinter/AsyncMqttClient-esphome@0.8.6 ; mqtt ottowinter/ArduinoJson-esphomelib@5.13.3 ; json - esphome/ESPAsyncWebServer-esphome@1.3.1 ; web_server_base + esphome/ESPAsyncWebServer-esphome@2.0.0 ; web_server_base fastled/FastLED@3.3.2 ; fastled_base mikalhart/TinyGPSPlus@1.0.2 ; gps freekode/TM1651@1.0.1 ; tm1651 From 901ec918b1f0575bdfbced07aa7f275e9f75cef0 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Fri, 22 Oct 2021 17:23:31 +0200 Subject: [PATCH 0064/1729] Fix ESP8266 OTA compression only starting framework v2.7.0 (#2610) --- esphome/components/ota/ota_backend_arduino_esp8266.h | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/esphome/components/ota/ota_backend_arduino_esp8266.h b/esphome/components/ota/ota_backend_arduino_esp8266.h index cf29a90fc1..329f2cf0f2 100644 --- a/esphome/components/ota/ota_backend_arduino_esp8266.h +++ b/esphome/components/ota/ota_backend_arduino_esp8266.h @@ -5,6 +5,7 @@ #include "ota_component.h" #include "ota_backend.h" +#include "esphome/core/macros.h" namespace esphome { namespace ota { @@ -16,7 +17,11 @@ class ArduinoESP8266OTABackend : public OTABackend { OTAResponseTypes write(uint8_t *data, size_t len) override; OTAResponseTypes end() override; void abort() override; +#if ARDUINO_VERSION_CODE >= VERSION_CODE(2, 7, 0) bool supports_compression() override { return true; } +#else + bool supports_compression() override { return false; } +#endif }; } // namespace ota From c7ef18fbc4ace4dbf4ba2d0f7bf84ddec77d5356 Mon Sep 17 00:00:00 2001 From: Andreas Hergert <36455093+andreashergert1984@users.noreply.github.com> Date: Fri, 22 Oct 2021 18:20:57 +0200 Subject: [PATCH 0065/1729] Bugfix tca9548a and idf refactor anh (#2612) Co-authored-by: Andreas Hergert --- esphome/components/tca9548a/tca9548a.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/tca9548a/tca9548a.cpp b/esphome/components/tca9548a/tca9548a.cpp index 5117ad8969..e902eb5ed4 100644 --- a/esphome/components/tca9548a/tca9548a.cpp +++ b/esphome/components/tca9548a/tca9548a.cpp @@ -22,7 +22,7 @@ i2c::ErrorCode TCA9548AChannel::writev(uint8_t address, i2c::WriteBuffer *buffer void TCA9548AComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up TCA9548A..."); uint8_t status = 0; - if (!this->read_register(0x00, &status, 1)) { + if (this->read_register(0x00, &status, 1) != i2c::ERROR_OK) { ESP_LOGI(TAG, "TCA9548A failed"); this->mark_failed(); return; From eda1c471ad247c1f94e7fd1b7d33e80ef430eeaf Mon Sep 17 00:00:00 2001 From: Otto winter Date: Fri, 22 Oct 2021 18:26:24 +0200 Subject: [PATCH 0066/1729] Bump version to 2021.10.2 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index ce401b9f73..37c325465b 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2021.10.1" +__version__ = "2021.10.2" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From cbfbcf7f1b74969152cbcf69ab9446a3f7bf6dc9 Mon Sep 17 00:00:00 2001 From: Andreas Hergert <36455093+andreashergert1984@users.noreply.github.com> Date: Fri, 22 Oct 2021 18:52:47 +0200 Subject: [PATCH 0067/1729] fixed dependency for pca9685 component (#2614) Co-authored-by: Otto Winter Co-authored-by: Andreas --- esphome/components/pca9685/pca9685_output.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/pca9685/pca9685_output.cpp b/esphome/components/pca9685/pca9685_output.cpp index 1ad6f4a665..957f4062fc 100644 --- a/esphome/components/pca9685/pca9685_output.cpp +++ b/esphome/components/pca9685/pca9685_output.cpp @@ -1,6 +1,7 @@ #include "pca9685_output.h" #include "esphome/core/log.h" #include "esphome/core/helpers.h" +#include "esphome/core/hal.h" namespace esphome { namespace pca9685 { From 9aaaf4dd4be4b88e295d6ee8ffbcacb8eab231fe Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sat, 23 Oct 2021 12:37:50 +0200 Subject: [PATCH 0068/1729] Bump platform-espressif8266 from 2.6.2 to 2.6.3 (#2620) --- esphome/components/esp8266/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index ddaeee6ab7..b2706bd4fb 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -63,7 +63,7 @@ RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(2, 7, 4) # The platformio/espressif8266 version to use for arduino 2 framework versions # - https://github.com/platformio/platform-espressif8266/releases # - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif8266 -ARDUINO_2_PLATFORM_VERSION = cv.Version(2, 6, 2) +ARDUINO_2_PLATFORM_VERSION = cv.Version(2, 6, 3) # for arduino 3 framework versions ARDUINO_3_PLATFORM_VERSION = cv.Version(3, 2, 0) From 2abe09529a0a18fba35dfc05d4253a93f640552f Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Sat, 23 Oct 2021 13:25:46 +0200 Subject: [PATCH 0069/1729] Autodetect flash size (#2615) --- esphome/__main__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/__main__.py b/esphome/__main__.py index 97059154fd..c2a6dd343f 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -226,6 +226,8 @@ def upload_using_esptool(config, port): mcu, "write_flash", "-z", + "--flash_size", + "detect", ] for img in flash_images: cmd += [img.offset, img.path] From b34eed125dc604417c313891e37420624b3218b6 Mon Sep 17 00:00:00 2001 From: 0hax <43876620+0hax@users.noreply.github.com> Date: Sat, 23 Oct 2021 19:01:23 +0200 Subject: [PATCH 0070/1729] Teleinfo ptec (#2599) * teleinfo: handle historical mode correctly. In historical mode, tags like PTEC leads to an issue where we detect a timestamp wheras this is not possible in historical mode. PTEC teleinfo tag looks like: PTEC HP.. Instead of the usual format IINST1 001 I This make our data parsing fails. While at here, make sure we continue parsing other tags even if parsing one of the tag fails. Signed-off-by: 0hax <0hax@protonmail.com> * teleinfo: fix compilation with loglevel set to debug. Signed-off-by: 0hax <0hax@protonmail.com> --- .../teleinfo/sensor/teleinfo_sensor.cpp | 2 +- esphome/components/teleinfo/teleinfo.cpp | 17 +++++++++-------- .../text_sensor/teleinfo_text_sensor.cpp | 2 +- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/esphome/components/teleinfo/sensor/teleinfo_sensor.cpp b/esphome/components/teleinfo/sensor/teleinfo_sensor.cpp index 661c149c09..4e4cd9f9e6 100644 --- a/esphome/components/teleinfo/sensor/teleinfo_sensor.cpp +++ b/esphome/components/teleinfo/sensor/teleinfo_sensor.cpp @@ -9,6 +9,6 @@ void TeleInfoSensor::publish_val(const std::string &val) { auto newval = parse_float(val); publish_state(*newval); } -void TeleInfoSensor::dump_config() { LOG_SENSOR(" ", tag.c_str(), this); } +void TeleInfoSensor::dump_config() { LOG_SENSOR(" ", "Teleinfo Sensor", this); } } // namespace teleinfo } // namespace esphome diff --git a/esphome/components/teleinfo/teleinfo.cpp b/esphome/components/teleinfo/teleinfo.cpp index badd66ae83..5a1e44ac8b 100644 --- a/esphome/components/teleinfo/teleinfo.cpp +++ b/esphome/components/teleinfo/teleinfo.cpp @@ -141,21 +141,22 @@ void TeleInfo::loop() { field_len = get_field(tag_, buf_finger, grp_end, separator_, MAX_TAG_SIZE); if (!field_len || field_len >= MAX_TAG_SIZE) { ESP_LOGE(TAG, "Invalid tag."); - break; + continue; } /* Advance buf_finger to after the tag and the separator. */ buf_finger += field_len + 1; /* - * If there is two separators and the tag is not equal to "DATE", - * it means there is a timestamp to read first. + * If there is two separators and the tag is not equal to "DATE" or + * historical mode is not in use (separator_ != 0x20), it means there is a + * timestamp to read first. */ - if (std::count(buf_finger, grp_end, separator_) == 2 && strcmp(tag_, "DATE") != 0) { + if (std::count(buf_finger, grp_end, separator_) == 2 && strcmp(tag_, "DATE") != 0 && separator_ != 0x20) { field_len = get_field(timestamp_, buf_finger, grp_end, separator_, MAX_TIMESTAMP_SIZE); if (!field_len || field_len >= MAX_TIMESTAMP_SIZE) { - ESP_LOGE(TAG, "Invalid Timestamp"); - break; + ESP_LOGE(TAG, "Invalid timestamp for tag %s", timestamp_); + continue; } /* Advance buf_finger to after the first data and the separator. */ @@ -164,8 +165,8 @@ void TeleInfo::loop() { field_len = get_field(val_, buf_finger, grp_end, separator_, MAX_VAL_SIZE); if (!field_len || field_len >= MAX_VAL_SIZE) { - ESP_LOGE(TAG, "Invalid Value"); - break; + ESP_LOGE(TAG, "Invalid value for tag %s", tag_); + continue; } /* Advance buf_finger to end of group */ diff --git a/esphome/components/teleinfo/text_sensor/teleinfo_text_sensor.cpp b/esphome/components/teleinfo/text_sensor/teleinfo_text_sensor.cpp index 1adbd9ce13..87cf0dea17 100644 --- a/esphome/components/teleinfo/text_sensor/teleinfo_text_sensor.cpp +++ b/esphome/components/teleinfo/text_sensor/teleinfo_text_sensor.cpp @@ -6,6 +6,6 @@ namespace teleinfo { static const char *const TAG = "teleinfo_text_sensor"; TeleInfoTextSensor::TeleInfoTextSensor(const char *tag) { this->tag = std::string(tag); } void TeleInfoTextSensor::publish_val(const std::string &val) { publish_state(val); } -void TeleInfoTextSensor::dump_config() { LOG_TEXT_SENSOR(" ", tag.c_str(), this); } +void TeleInfoTextSensor::dump_config() { LOG_TEXT_SENSOR(" ", "Teleinfo Text Sensor", this); } } // namespace teleinfo } // namespace esphome From 91999a38ca1608a234d42d2079263a6d53064692 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sat, 23 Oct 2021 19:25:53 +0200 Subject: [PATCH 0071/1729] Fix glue code missing micros() (#2623) --- esphome/core/config.py | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/core/config.py b/esphome/core/config.py index 235e0eeb84..262451df88 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -214,6 +214,7 @@ def include_file(path, basename): ARDUINO_GLUE_CODE = """\ #define yield() esphome::yield() #define millis() esphome::millis() +#define micros() esphome::micros() #define delay(x) esphome::delay(x) #define delayMicroseconds(x) esphome::delayMicroseconds(x) """ From c6adaaea9726fbdcbc8414144291048e556fb2ef Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 26 Oct 2021 21:55:20 +1300 Subject: [PATCH 0072/1729] Remove power and energy from sensors that are not true power (#2628) --- esphome/components/dsmr/sensor.py | 16 ++++++++-------- esphome/components/havells_solar/sensor.py | 4 +--- esphome/components/pipsolar/sensor/__init__.py | 4 ++-- esphome/components/sdm_meter/sensor.py | 4 ---- esphome/components/selec_meter/sensor.py | 8 -------- 5 files changed, 11 insertions(+), 25 deletions(-) diff --git a/esphome/components/dsmr/sensor.py b/esphome/components/dsmr/sensor.py index 761009c766..d809d0d105 100644 --- a/esphome/components/dsmr/sensor.py +++ b/esphome/components/dsmr/sensor.py @@ -75,14 +75,14 @@ CONFIG_SCHEMA = cv.Schema( UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, ICON_EMPTY, 3, - DEVICE_CLASS_ENERGY, + DEVICE_CLASS_EMPTY, STATE_CLASS_NONE, ), cv.Optional("total_exported_energy"): sensor.sensor_schema( UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, ICON_EMPTY, 3, - DEVICE_CLASS_ENERGY, + DEVICE_CLASS_EMPTY, STATE_CLASS_NONE, ), cv.Optional("power_delivered"): sensor.sensor_schema( @@ -166,42 +166,42 @@ CONFIG_SCHEMA = cv.Schema( UNIT_KILOVOLT_AMPS_REACTIVE, ICON_EMPTY, 3, - DEVICE_CLASS_POWER, + DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT, ), cv.Optional("reactive_power_delivered_l2"): sensor.sensor_schema( UNIT_KILOVOLT_AMPS_REACTIVE, ICON_EMPTY, 3, - DEVICE_CLASS_POWER, + DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT, ), cv.Optional("reactive_power_delivered_l3"): sensor.sensor_schema( UNIT_KILOVOLT_AMPS_REACTIVE, ICON_EMPTY, 3, - DEVICE_CLASS_POWER, + DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT, ), cv.Optional("reactive_power_returned_l1"): sensor.sensor_schema( UNIT_KILOVOLT_AMPS_REACTIVE, ICON_EMPTY, 3, - DEVICE_CLASS_POWER, + DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT, ), cv.Optional("reactive_power_returned_l2"): sensor.sensor_schema( UNIT_KILOVOLT_AMPS_REACTIVE, ICON_EMPTY, 3, - DEVICE_CLASS_POWER, + DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT, ), cv.Optional("reactive_power_returned_l3"): sensor.sensor_schema( UNIT_KILOVOLT_AMPS_REACTIVE, ICON_EMPTY, 3, - DEVICE_CLASS_POWER, + DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT, ), cv.Optional("voltage_l1"): sensor.sensor_schema( diff --git a/esphome/components/havells_solar/sensor.py b/esphome/components/havells_solar/sensor.py index 3ec12d5b83..d7c8d544f9 100644 --- a/esphome/components/havells_solar/sensor.py +++ b/esphome/components/havells_solar/sensor.py @@ -93,13 +93,12 @@ PV_SENSORS = { CONF_VOLTAGE_SAMPLED_BY_SECONDARY_CPU: sensor.sensor_schema( unit_of_measurement=UNIT_VOLT, accuracy_decimals=0, - device_class=DEVICE_CLASS_POWER, + device_class=DEVICE_CLASS_VOLTAGE, state_class=STATE_CLASS_MEASUREMENT, ), CONF_INSULATION_OF_P_TO_GROUND: sensor.sensor_schema( unit_of_measurement=UNIT_KOHM, accuracy_decimals=0, - device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, ), } @@ -135,7 +134,6 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_REACTIVE_POWER): sensor.sensor_schema( unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE, accuracy_decimals=2, - device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_ENERGY_PRODUCTION_DAY): sensor.sensor_schema( diff --git a/esphome/components/pipsolar/sensor/__init__.py b/esphome/components/pipsolar/sensor/__init__.py index 5e4dd6c40c..a206e41988 100644 --- a/esphome/components/pipsolar/sensor/__init__.py +++ b/esphome/components/pipsolar/sensor/__init__.py @@ -89,7 +89,7 @@ TYPES = { UNIT_AMPERE, ICON_EMPTY, 1, DEVICE_CLASS_CURRENT ), CONF_AC_OUTPUT_RATING_APPARENT_POWER: sensor.sensor_schema( - UNIT_VOLT_AMPS, ICON_EMPTY, 1, DEVICE_CLASS_POWER + UNIT_VOLT_AMPS, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY ), CONF_AC_OUTPUT_RATING_ACTIVE_POWER: sensor.sensor_schema( UNIT_WATT, ICON_EMPTY, 1, DEVICE_CLASS_POWER @@ -159,7 +159,7 @@ TYPES = { UNIT_HERTZ, ICON_CURRENT_AC, 1, DEVICE_CLASS_EMPTY ), CONF_AC_OUTPUT_APPARENT_POWER: sensor.sensor_schema( - UNIT_VOLT_AMPS, ICON_EMPTY, 1, DEVICE_CLASS_POWER + UNIT_VOLT_AMPS, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY ), CONF_AC_OUTPUT_ACTIVE_POWER: sensor.sensor_schema( UNIT_WATT, ICON_EMPTY, 1, DEVICE_CLASS_POWER diff --git a/esphome/components/sdm_meter/sensor.py b/esphome/components/sdm_meter/sensor.py index 8a0d9674a7..87c99c9152 100644 --- a/esphome/components/sdm_meter/sensor.py +++ b/esphome/components/sdm_meter/sensor.py @@ -64,13 +64,11 @@ PHASE_SENSORS = { CONF_APPARENT_POWER: sensor.sensor_schema( unit_of_measurement=UNIT_VOLT_AMPS, accuracy_decimals=2, - device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, ), CONF_REACTIVE_POWER: sensor.sensor_schema( unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE, accuracy_decimals=2, - device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, ), CONF_POWER_FACTOR: sensor.sensor_schema( @@ -115,13 +113,11 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_IMPORT_REACTIVE_ENERGY): sensor.sensor_schema( unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, accuracy_decimals=2, - device_class=DEVICE_CLASS_ENERGY, state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional(CONF_EXPORT_REACTIVE_ENERGY): sensor.sensor_schema( unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, accuracy_decimals=2, - device_class=DEVICE_CLASS_ENERGY, state_class=STATE_CLASS_TOTAL_INCREASING, ), } diff --git a/esphome/components/selec_meter/sensor.py b/esphome/components/selec_meter/sensor.py index 168d3a3db2..e698255c25 100644 --- a/esphome/components/selec_meter/sensor.py +++ b/esphome/components/selec_meter/sensor.py @@ -71,25 +71,21 @@ SENSORS = { CONF_TOTAL_REACTIVE_ENERGY: sensor.sensor_schema( unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, accuracy_decimals=2, - device_class=DEVICE_CLASS_ENERGY, state_class=STATE_CLASS_TOTAL_INCREASING, ), CONF_IMPORT_REACTIVE_ENERGY: sensor.sensor_schema( unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, accuracy_decimals=2, - device_class=DEVICE_CLASS_ENERGY, state_class=STATE_CLASS_TOTAL_INCREASING, ), CONF_EXPORT_REACTIVE_ENERGY: sensor.sensor_schema( unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, accuracy_decimals=2, - device_class=DEVICE_CLASS_ENERGY, state_class=STATE_CLASS_TOTAL_INCREASING, ), CONF_APPARENT_ENERGY: sensor.sensor_schema( unit_of_measurement=UNIT_KILOVOLT_AMPS_HOURS, accuracy_decimals=2, - device_class=DEVICE_CLASS_ENERGY, state_class=STATE_CLASS_TOTAL_INCREASING, ), CONF_ACTIVE_POWER: sensor.sensor_schema( @@ -101,13 +97,11 @@ SENSORS = { CONF_REACTIVE_POWER: sensor.sensor_schema( unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE, accuracy_decimals=3, - device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, ), CONF_APPARENT_POWER: sensor.sensor_schema( unit_of_measurement=UNIT_VOLT_AMPS, accuracy_decimals=3, - device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, ), CONF_VOLTAGE: sensor.sensor_schema( @@ -142,13 +136,11 @@ SENSORS = { CONF_MAXIMUM_DEMAND_REACTIVE_POWER: sensor.sensor_schema( unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE, accuracy_decimals=3, - device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, ), CONF_MAXIMUM_DEMAND_APPARENT_POWER: sensor.sensor_schema( unit_of_measurement=UNIT_VOLT_AMPS, accuracy_decimals=3, - device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, ), } From 72108684ea20bc6b8527726a27c5c298919f3e24 Mon Sep 17 00:00:00 2001 From: Martin <25747549+martgras@users.noreply.github.com> Date: Tue, 26 Oct 2021 11:30:25 +0200 Subject: [PATCH 0073/1729] fix modbus output (#2630) --- .../components/modbus_controller/__init__.py | 15 +++++++++++++++ .../modbus_controller/number/__init__.py | 17 +---------------- .../modbus_controller/output/__init__.py | 13 ++++++++++++- .../modbus_controller/output/modbus_output.h | 3 ++- .../modbus_controller/sensor/__init__.py | 18 +----------------- 5 files changed, 31 insertions(+), 35 deletions(-) diff --git a/esphome/components/modbus_controller/__init__.py b/esphome/components/modbus_controller/__init__.py index 6b452ea25c..8499cec561 100644 --- a/esphome/components/modbus_controller/__init__.py +++ b/esphome/components/modbus_controller/__init__.py @@ -61,6 +61,21 @@ SENSOR_VALUE_TYPE = { "FP32_R": SensorValueType.FP32_R, } +TYPE_REGISTER_MAP = { + "RAW": 1, + "U_WORD": 1, + "S_WORD": 1, + "U_DWORD": 2, + "U_DWORD_R": 2, + "S_DWORD": 2, + "S_DWORD_R": 2, + "U_QWORD": 4, + "U_QWORDU_R": 4, + "S_QWORD": 4, + "U_QWORD_R": 4, + "FP32": 2, + "FP32_R": 2, +} MULTI_CONF = True diff --git a/esphome/components/modbus_controller/number/__init__.py b/esphome/components/modbus_controller/number/__init__.py index c7919bb972..afb69f8798 100644 --- a/esphome/components/modbus_controller/number/__init__.py +++ b/esphome/components/modbus_controller/number/__init__.py @@ -17,6 +17,7 @@ from .. import ( ModbusController, SENSOR_VALUE_TYPE, SensorItem, + TYPE_REGISTER_MAP, ) @@ -39,22 +40,6 @@ ModbusNumber = modbus_controller_ns.class_( "ModbusNumber", cg.Component, number.Number, SensorItem ) -TYPE_REGISTER_MAP = { - "RAW": 1, - "U_WORD": 1, - "S_WORD": 1, - "U_DWORD": 2, - "U_DWORD_R": 2, - "S_DWORD": 2, - "S_DWORD_R": 2, - "U_QWORD": 4, - "U_QWORDU_R": 4, - "S_QWORD": 4, - "U_QWORD_R": 4, - "FP32": 2, - "FP32_R": 2, -} - def validate_min_max(config): if config[CONF_MAX_VALUE] <= config[CONF_MIN_VALUE]: diff --git a/esphome/components/modbus_controller/output/__init__.py b/esphome/components/modbus_controller/output/__init__.py index 9c41fc011c..4aca4db64f 100644 --- a/esphome/components/modbus_controller/output/__init__.py +++ b/esphome/components/modbus_controller/output/__init__.py @@ -13,11 +13,13 @@ from .. import ( SensorItem, modbus_controller_ns, ModbusController, + TYPE_REGISTER_MAP, ) from ..const import ( CONF_BYTE_OFFSET, CONF_MODBUS_CONTROLLER_ID, + CONF_REGISTER_COUNT, CONF_VALUE_TYPE, CONF_WRITE_LAMBDA, ) @@ -40,6 +42,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_OFFSET, default=0): cv.positive_int, cv.Optional(CONF_BYTE_OFFSET): cv.positive_int, cv.Optional(CONF_VALUE_TYPE, default="U_WORD"): cv.enum(SENSOR_VALUE_TYPE), + cv.Optional(CONF_REGISTER_COUNT, default=0): cv.positive_int, cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda, cv.Optional(CONF_MULTIPLY, default=1.0): cv.float_, } @@ -54,8 +57,16 @@ async def to_code(config): # A CONF_BYTE_OFFSET setting overrides CONF_OFFSET if CONF_BYTE_OFFSET in config: byte_offset = config[CONF_BYTE_OFFSET] + value_type = config[CONF_VALUE_TYPE] + reg_count = config[CONF_REGISTER_COUNT] + if reg_count == 0: + reg_count = TYPE_REGISTER_MAP[value_type] var = cg.new_Pvariable( - config[CONF_ID], config[CONF_ADDRESS], byte_offset, config[CONF_VALUE_TYPE] + config[CONF_ID], + config[CONF_ADDRESS], + byte_offset, + value_type, + reg_count, ) await output.register_output(var, config) cg.add(var.set_write_multiply(config[CONF_MULTIPLY])) diff --git a/esphome/components/modbus_controller/output/modbus_output.h b/esphome/components/modbus_controller/output/modbus_output.h index 053186a321..6e8521854b 100644 --- a/esphome/components/modbus_controller/output/modbus_output.h +++ b/esphome/components/modbus_controller/output/modbus_output.h @@ -11,12 +11,13 @@ using value_to_data_t = std::function(float); class ModbusOutput : public output::FloatOutput, public Component, public SensorItem { public: - ModbusOutput(uint16_t start_address, uint8_t offset, SensorValueType value_type) + ModbusOutput(uint16_t start_address, uint8_t offset, SensorValueType value_type, int register_count) : output::FloatOutput(), Component() { this->register_type = ModbusRegisterType::HOLDING; this->start_address = start_address; this->offset = offset; this->bitmask = bitmask; + this->register_count = register_count; this->sensor_value_type = value_type; this->skip_updates = 0; this->start_address += offset; diff --git a/esphome/components/modbus_controller/sensor/__init__.py b/esphome/components/modbus_controller/sensor/__init__.py index 687f3d82fb..82acfe120b 100644 --- a/esphome/components/modbus_controller/sensor/__init__.py +++ b/esphome/components/modbus_controller/sensor/__init__.py @@ -9,6 +9,7 @@ from .. import ( ModbusController, MODBUS_REGISTER_TYPE, SENSOR_VALUE_TYPE, + TYPE_REGISTER_MAP, ) from ..const import ( CONF_BITMASK, @@ -29,23 +30,6 @@ ModbusSensor = modbus_controller_ns.class_( "ModbusSensor", cg.Component, sensor.Sensor, SensorItem ) -TYPE_REGISTER_MAP = { - "RAW": 1, - "U_WORD": 1, - "S_WORD": 1, - "U_DWORD": 2, - "U_DWORD_R": 2, - "S_DWORD": 2, - "S_DWORD_R": 2, - "U_QWORD": 4, - "U_QWORDU_R": 4, - "S_QWORD": 4, - "U_QWORD_R": 4, - "FP32": 2, - "FP32_R": 2, -} - - CONFIG_SCHEMA = cv.All( sensor.SENSOR_SCHEMA.extend( { From f1377b560e696330e1a6faa902c38ddf81032be1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20=C4=8Cerm=C3=A1k?= Date: Tue, 26 Oct 2021 18:10:45 +0200 Subject: [PATCH 0074/1729] Fix pin number validation for sn74hc595 (#2621) --- esphome/components/sn74hc595/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/sn74hc595/__init__.py b/esphome/components/sn74hc595/__init__.py index 0d1ff6ecba..630abc8bca 100644 --- a/esphome/components/sn74hc595/__init__.py +++ b/esphome/components/sn74hc595/__init__.py @@ -60,7 +60,7 @@ SN74HC595_PIN_SCHEMA = cv.All( { cv.GenerateID(): cv.declare_id(SN74HC595GPIOPin), cv.Required(CONF_SN74HC595): cv.use_id(SN74HC595Component), - cv.Required(CONF_NUMBER): cv.int_range(min=0, max=7), + cv.Required(CONF_NUMBER): cv.int_range(min=0, max=31), cv.Optional(CONF_MODE, default={}): cv.All( { cv.Optional(CONF_OUTPUT, default=True): cv.All( From 23560e608c5848b6029c76847f94144cb8fc9b04 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 27 Oct 2021 08:27:51 +1300 Subject: [PATCH 0075/1729] Fix select.set using lambda (#2633) --- esphome/components/select/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/select/__init__.py b/esphome/components/select/__init__.py index c156a63a86..7e4047d3c8 100644 --- a/esphome/components/select/__init__.py +++ b/esphome/components/select/__init__.py @@ -90,6 +90,6 @@ async def to_code(config): async def select_set_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_OPTION], args, str) + template_ = await cg.templatable(config[CONF_OPTION], args, cg.std_string) cg.add(var.set_option(template_)) return var From bd782fc82809a7f77734db05b146caab3d568d93 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 27 Oct 2021 10:49:11 +1300 Subject: [PATCH 0076/1729] Bump version to 2021.10.3 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 37c325465b..d90ff04e7f 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2021.10.2" +__version__ = "2021.10.3" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From b226215593449487dba12f2d27b0f02efa650c36 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 11 Nov 2021 10:10:05 +1300 Subject: [PATCH 0077/1729] Bump version to 2021.11.0b1 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index f0df8c562d..26fd514173 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2021.10.0b11" +__version__ = "2021.11.0b1" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From 8cbb3798986013d4af3b1fcf1cf499243444a50f Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 11 Nov 2021 10:35:18 +1300 Subject: [PATCH 0078/1729] Remove import (not sure how it got there) --- esphome/components/bme680_bsec/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/esphome/components/bme680_bsec/__init__.py b/esphome/components/bme680_bsec/__init__.py index b27d477ea0..83e519f8aa 100644 --- a/esphome/components/bme680_bsec/__init__.py +++ b/esphome/components/bme680_bsec/__init__.py @@ -2,7 +2,6 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c from esphome.const import CONF_ID -from esphome.core import CORE CODEOWNERS = ["@trvrnrth"] DEPENDENCIES = ["i2c"] From 4b7fe202ec2fc7501268da01c65e75544a9fb285 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 11 Nov 2021 11:24:48 +1300 Subject: [PATCH 0079/1729] Fix template number initial value being NaN (#2692) --- esphome/components/template/number/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/esphome/components/template/number/__init__.py b/esphome/components/template/number/__init__.py index 887f6b15ad..3dec7066d3 100644 --- a/esphome/components/template/number/__init__.py +++ b/esphome/components/template/number/__init__.py @@ -35,6 +35,9 @@ def validate(config): raise cv.Invalid("initial_value cannot be used with lambda") if CONF_RESTORE_VALUE in config: raise cv.Invalid("restore_value cannot be used with lambda") + elif CONF_INITIAL_VALUE not in config: + config[CONF_INITIAL_VALUE] = config[CONF_MIN_VALUE] + if not config[CONF_OPTIMISTIC] and CONF_SET_ACTION not in config: raise cv.Invalid( "Either optimistic mode must be enabled, or set_action must be set, to handle the number being set." @@ -80,8 +83,7 @@ async def to_code(config): else: cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) - if CONF_INITIAL_VALUE in config: - cg.add(var.set_initial_value(config[CONF_INITIAL_VALUE])) + cg.add(var.set_initial_value(config[CONF_INITIAL_VALUE])) if CONF_RESTORE_VALUE in config: cg.add(var.set_restore_value(config[CONF_RESTORE_VALUE])) From 21c896d8f83be31dc1a6ff8067f59a04d0326a54 Mon Sep 17 00:00:00 2001 From: Carlos Garcia Saura Date: Wed, 10 Nov 2021 23:28:45 +0100 Subject: [PATCH 0080/1729] [remote_transmitter] accurate pulse timing for ESP8266 (#2476) --- .../remote_transmitter/remote_transmitter.h | 3 + .../remote_transmitter_esp8266.cpp | 78 ++++++++++--------- 2 files changed, 46 insertions(+), 35 deletions(-) diff --git a/esphome/components/remote_transmitter/remote_transmitter.h b/esphome/components/remote_transmitter/remote_transmitter.h index 733ac5e50d..a4235e875f 100644 --- a/esphome/components/remote_transmitter/remote_transmitter.h +++ b/esphome/components/remote_transmitter/remote_transmitter.h @@ -32,6 +32,9 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, void mark_(uint32_t on_time, uint32_t off_time, uint32_t usec); void space_(uint32_t usec); + + void await_target_time_(); + uint32_t target_time_; #endif #ifdef USE_ESP32 diff --git a/esphome/components/remote_transmitter/remote_transmitter_esp8266.cpp b/esphome/components/remote_transmitter/remote_transmitter_esp8266.cpp index 74e62d4e3b..39752cac5b 100644 --- a/esphome/components/remote_transmitter/remote_transmitter_esp8266.cpp +++ b/esphome/components/remote_transmitter/remote_transmitter_esp8266.cpp @@ -33,56 +33,64 @@ void RemoteTransmitterComponent::calculate_on_off_time_(uint32_t carrier_frequen *off_time_period = period - *on_time_period; } +void RemoteTransmitterComponent::await_target_time_() { + const uint32_t current_time = micros(); + if (this->target_time_ == 0) + this->target_time_ = current_time; + else if (this->target_time_ > current_time) + delayMicroseconds(this->target_time_ - current_time); +} + void RemoteTransmitterComponent::mark_(uint32_t on_time, uint32_t off_time, uint32_t usec) { - if (this->carrier_duty_percent_ == 100 || (on_time == 0 && off_time == 0)) { - this->pin_->digital_write(true); - delayMicroseconds(usec); - this->pin_->digital_write(false); - return; - } - - const uint32_t start_time = micros(); - uint32_t current_time = start_time; - - while (current_time - start_time < usec) { - const uint32_t elapsed = current_time - start_time; - this->pin_->digital_write(true); - - delayMicroseconds(std::min(on_time, usec - elapsed)); - this->pin_->digital_write(false); - if (elapsed + on_time >= usec) - return; - - delayMicroseconds(std::min(usec - elapsed - on_time, off_time)); - - current_time = micros(); + this->await_target_time_(); + this->pin_->digital_write(true); + + const uint32_t target = this->target_time_ + usec; + if (this->carrier_duty_percent_ < 100 && (on_time > 0 || off_time > 0)) { + while (true) { // Modulate with carrier frequency + this->target_time_ += on_time; + if (this->target_time_ >= target) + break; + this->await_target_time_(); + this->pin_->digital_write(false); + + this->target_time_ += off_time; + if (this->target_time_ >= target) + break; + this->await_target_time_(); + this->pin_->digital_write(true); + } } + this->target_time_ = target; } + void RemoteTransmitterComponent::space_(uint32_t usec) { + this->await_target_time_(); this->pin_->digital_write(false); - delayMicroseconds(usec); + this->target_time_ += usec; } + void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) { ESP_LOGD(TAG, "Sending remote code..."); uint32_t on_time, off_time; this->calculate_on_off_time_(this->temp_.get_carrier_frequency(), &on_time, &off_time); + this->target_time_ = 0; for (uint32_t i = 0; i < send_times; i++) { - { - InterruptLock lock; - for (int32_t item : this->temp_.get_data()) { - if (item > 0) { - const auto length = uint32_t(item); - this->mark_(on_time, off_time, length); - } else { - const auto length = uint32_t(-item); - this->space_(length); - } - App.feed_wdt(); + for (int32_t item : this->temp_.get_data()) { + if (item > 0) { + const auto length = uint32_t(item); + this->mark_(on_time, off_time, length); + } else { + const auto length = uint32_t(-item); + this->space_(length); } + App.feed_wdt(); } + this->await_target_time_(); // wait for duration of last pulse + this->pin_->digital_write(false); if (i + 1 < send_times) - delayMicroseconds(send_wait); + this->target_time_ += send_wait; } } From 755289331146f9cd53dc002a69e693513cd4c323 Mon Sep 17 00:00:00 2001 From: Maurice Makaay Date: Wed, 10 Nov 2021 23:34:17 +0100 Subject: [PATCH 0081/1729] Uart debugging support (#2478) Co-authored-by: Maurice Makaay Co-authored-by: Maurice Makaay Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/uart/__init__.py | 69 +++++++ esphome/components/uart/uart.h | 6 +- esphome/components/uart/uart_component.h | 21 ++ .../uart/uart_component_esp32_arduino.cpp | 12 +- .../uart/uart_component_esp8266.cpp | 9 +- .../uart/uart_component_esp_idf.cpp | 12 +- esphome/components/uart/uart_debugger.cpp | 193 ++++++++++++++++++ esphome/components/uart/uart_debugger.h | 101 +++++++++ esphome/const.py | 7 + esphome/core/defines.h | 1 + tests/test1.yaml | 12 ++ 11 files changed, 430 insertions(+), 13 deletions(-) create mode 100644 esphome/components/uart/uart_debugger.cpp create mode 100644 esphome/components/uart/uart_debugger.h diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py index 35af3eedf7..53209dfc7b 100644 --- a/esphome/components/uart/__init__.py +++ b/esphome/components/uart/__init__.py @@ -14,6 +14,16 @@ from esphome.const import ( CONF_DATA, CONF_RX_BUFFER_SIZE, CONF_INVERT, + CONF_TRIGGER_ID, + CONF_SEQUENCE, + CONF_TIMEOUT, + CONF_DEBUG, + CONF_DIRECTION, + CONF_AFTER, + CONF_BYTES, + CONF_DELIMITER, + CONF_DUMMY_RECEIVER, + CONF_DUMMY_RECEIVER_ID, ) from esphome.core import CORE @@ -31,6 +41,8 @@ ESP8266UartComponent = uart_ns.class_( UARTDevice = uart_ns.class_("UARTDevice") UARTWriteAction = uart_ns.class_("UARTWriteAction", automation.Action) +UARTDebugger = uart_ns.class_("UARTDebugger", cg.Component, automation.Action) +UARTDummyReceiver = uart_ns.class_("UARTDummyReceiver", cg.Component) MULTI_CONF = True @@ -75,6 +87,34 @@ CONF_STOP_BITS = "stop_bits" CONF_DATA_BITS = "data_bits" CONF_PARITY = "parity" +UARTDirection = uart_ns.enum("UARTDirection") +UART_DIRECTIONS = { + "RX": UARTDirection.UART_DIRECTION_RX, + "TX": UARTDirection.UART_DIRECTION_TX, + "BOTH": UARTDirection.UART_DIRECTION_BOTH, +} + +DEBUG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(UARTDebugger), + cv.Optional(CONF_DIRECTION, default="BOTH"): cv.enum( + UART_DIRECTIONS, upper=True + ), + cv.Optional(CONF_AFTER): cv.Schema( + { + cv.Optional(CONF_BYTES, default=256): cv.validate_bytes, + cv.Optional( + CONF_TIMEOUT, default="100ms" + ): cv.positive_time_period_milliseconds, + cv.Optional(CONF_DELIMITER): cv.templatable(validate_raw_data), + } + ), + cv.Required(CONF_SEQUENCE): automation.validate_automation(), + cv.Optional(CONF_DUMMY_RECEIVER, default=False): cv.boolean, + cv.GenerateID(CONF_DUMMY_RECEIVER_ID): cv.declare_id(UARTDummyReceiver), + } +) + CONFIG_SCHEMA = cv.All( cv.Schema( { @@ -91,12 +131,38 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_INVERT): cv.invalid( "This option has been removed. Please instead use invert in the tx/rx pin schemas." ), + cv.Optional(CONF_DEBUG): DEBUG_SCHEMA, } ).extend(cv.COMPONENT_SCHEMA), cv.has_at_least_one_key(CONF_TX_PIN, CONF_RX_PIN), ) +async def debug_to_code(config, parent): + trigger = cg.new_Pvariable(config[CONF_TRIGGER_ID], parent) + await cg.register_component(trigger, config) + for action in config[CONF_SEQUENCE]: + await automation.build_automation( + trigger, + [(UARTDirection, "direction"), (cg.std_vector.template(cg.uint8), "bytes")], + action, + ) + cg.add(trigger.set_direction(config[CONF_DIRECTION])) + after = config[CONF_AFTER] + cg.add(trigger.set_after_bytes(after[CONF_BYTES])) + cg.add(trigger.set_after_timeout(after[CONF_TIMEOUT])) + if CONF_DELIMITER in after: + data = after[CONF_DELIMITER] + if isinstance(data, bytes): + data = list(data) + for byte in after[CONF_DELIMITER]: + cg.add(trigger.add_delimiter_byte(byte)) + if config[CONF_DUMMY_RECEIVER]: + dummy = cg.new_Pvariable(config[CONF_DUMMY_RECEIVER_ID], parent) + await cg.register_component(dummy, {}) + cg.add_define("USE_UART_DEBUGGER") + + async def to_code(config): cg.add_global(uart_ns.using) var = cg.new_Pvariable(config[CONF_ID]) @@ -115,6 +181,9 @@ async def to_code(config): cg.add(var.set_data_bits(config[CONF_DATA_BITS])) cg.add(var.set_parity(config[CONF_PARITY])) + if CONF_DEBUG in config: + await debug_to_code(config[CONF_DEBUG], var) + # A schema to use for all UART devices, all UART integrations must extend this! UART_DEVICE_SCHEMA = cv.Schema( diff --git a/esphome/components/uart/uart.h b/esphome/components/uart/uart.h index c368f9ed6b..d41dbe26e6 100644 --- a/esphome/components/uart/uart.h +++ b/esphome/components/uart/uart.h @@ -45,17 +45,17 @@ class UARTDevice { // Compat APIs int read() { uint8_t data; - if (!read_byte(&data)) + if (!this->read_byte(&data)) return -1; return data; } size_t write(uint8_t data) { - write_byte(data); + this->write_byte(data); return 1; } int peek() { uint8_t data; - if (!peek_byte(&data)) + if (!this->peek_byte(&data)) return -1; return data; } diff --git a/esphome/components/uart/uart_component.h b/esphome/components/uart/uart_component.h index de85cd2ca3..73694d3db7 100644 --- a/esphome/components/uart/uart_component.h +++ b/esphome/components/uart/uart_component.h @@ -2,9 +2,13 @@ #include #include +#include "esphome/core/defines.h" #include "esphome/core/component.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" +#ifdef USE_UART_DEBUGGER +#include "esphome/core/automation.h" +#endif namespace esphome { namespace uart { @@ -15,6 +19,14 @@ enum UARTParityOptions { UART_CONFIG_PARITY_ODD, }; +#ifdef USE_UART_DEBUGGER +enum UARTDirection { + UART_DIRECTION_RX, + UART_DIRECTION_TX, + UART_DIRECTION_BOTH, +}; +#endif + const LogString *parity_to_str(UARTParityOptions parity); class UARTComponent { @@ -50,6 +62,12 @@ class UARTComponent { void set_baud_rate(uint32_t baud_rate) { baud_rate_ = baud_rate; } uint32_t get_baud_rate() const { return baud_rate_; } +#ifdef USE_UART_DEBUGGER + void add_debug_callback(std::function &&callback) { + this->debug_callback_.add(std::move(callback)); + } +#endif + protected: virtual void check_logger_conflict() = 0; bool check_read_timeout_(size_t len = 1); @@ -61,6 +79,9 @@ class UARTComponent { uint8_t stop_bits_; uint8_t data_bits_; UARTParityOptions parity_; +#ifdef USE_UART_DEBUGGER + CallbackManager debug_callback_{}; +#endif }; } // namespace uart diff --git a/esphome/components/uart/uart_component_esp32_arduino.cpp b/esphome/components/uart/uart_component_esp32_arduino.cpp index 1b1ce382f2..95cdde4a43 100644 --- a/esphome/components/uart/uart_component_esp32_arduino.cpp +++ b/esphome/components/uart/uart_component_esp32_arduino.cpp @@ -117,26 +117,32 @@ void ESP32ArduinoUARTComponent::dump_config() { void ESP32ArduinoUARTComponent::write_array(const uint8_t *data, size_t len) { this->hw_serial_->write(data, len); +#ifdef USE_UART_DEBUGGER for (size_t i = 0; i < len; i++) { - ESP_LOGVV(TAG, " Wrote 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", BYTE_TO_BINARY(data[i]), data[i]); + this->debug_callback_.call(UART_DIRECTION_TX, data[i]); } +#endif } + bool ESP32ArduinoUARTComponent::peek_byte(uint8_t *data) { if (!this->check_read_timeout_()) return false; *data = this->hw_serial_->peek(); return true; } + bool ESP32ArduinoUARTComponent::read_array(uint8_t *data, size_t len) { if (!this->check_read_timeout_(len)) return false; this->hw_serial_->readBytes(data, len); +#ifdef USE_UART_DEBUGGER for (size_t i = 0; i < len; i++) { - ESP_LOGVV(TAG, " Read 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", BYTE_TO_BINARY(data[i]), data[i]); + this->debug_callback_.call(UART_DIRECTION_RX, data[i]); } - +#endif return true; } + int ESP32ArduinoUARTComponent::available() { return this->hw_serial_->available(); } void ESP32ArduinoUARTComponent::flush() { ESP_LOGVV(TAG, " Flushing..."); diff --git a/esphome/components/uart/uart_component_esp8266.cpp b/esphome/components/uart/uart_component_esp8266.cpp index 973306cde2..c367de05bb 100644 --- a/esphome/components/uart/uart_component_esp8266.cpp +++ b/esphome/components/uart/uart_component_esp8266.cpp @@ -130,9 +130,11 @@ void ESP8266UartComponent::write_array(const uint8_t *data, size_t len) { for (size_t i = 0; i < len; i++) this->sw_serial_->write_byte(data[i]); } +#ifdef USE_UART_DEBUGGER for (size_t i = 0; i < len; i++) { - ESP_LOGVV(TAG, " Wrote 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", BYTE_TO_BINARY(data[i]), data[i]); + this->debug_callback_.call(UART_DIRECTION_TX, data[i]); } +#endif } bool ESP8266UartComponent::peek_byte(uint8_t *data) { if (!this->check_read_timeout_()) @@ -153,10 +155,11 @@ bool ESP8266UartComponent::read_array(uint8_t *data, size_t len) { for (size_t i = 0; i < len; i++) data[i] = this->sw_serial_->read_byte(); } +#ifdef USE_UART_DEBUGGER for (size_t i = 0; i < len; i++) { - ESP_LOGVV(TAG, " Read 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", BYTE_TO_BINARY(data[i]), data[i]); + this->debug_callback_.call(UART_DIRECTION_RX, data[i]); } - +#endif return true; } int ESP8266UartComponent::available() { diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index 1cccd5821e..4d6a6af0fc 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -130,10 +130,13 @@ void IDFUARTComponent::write_array(const uint8_t *data, size_t len) { xSemaphoreTake(this->lock_, portMAX_DELAY); uart_write_bytes(this->uart_num_, data, len); xSemaphoreGive(this->lock_); +#ifdef USE_UART_DEBUGGER for (size_t i = 0; i < len; i++) { - ESP_LOGVV(TAG, " Wrote 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", BYTE_TO_BINARY(data[i]), data[i]); + this->debug_callback_.call(UART_DIRECTION_TX, data[i]); } +#endif } + bool IDFUARTComponent::peek_byte(uint8_t *data) { if (!this->check_read_timeout_()) return false; @@ -152,6 +155,7 @@ bool IDFUARTComponent::peek_byte(uint8_t *data) { xSemaphoreGive(this->lock_); return true; } + bool IDFUARTComponent::read_array(uint8_t *data, size_t len) { size_t length_to_read = len; if (!this->check_read_timeout_(len)) @@ -165,12 +169,12 @@ bool IDFUARTComponent::read_array(uint8_t *data, size_t len) { } if (length_to_read > 0) uart_read_bytes(this->uart_num_, data, length_to_read, 20 / portTICK_RATE_MS); - xSemaphoreGive(this->lock_); +#ifdef USE_UART_DEBUGGER for (size_t i = 0; i < len; i++) { - ESP_LOGVV(TAG, " Read 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", BYTE_TO_BINARY(data[i]), data[i]); + this->debug_callback_.call(UART_DIRECTION_RX, data[i]); } - +#endif return true; } diff --git a/esphome/components/uart/uart_debugger.cpp b/esphome/components/uart/uart_debugger.cpp new file mode 100644 index 0000000000..9a535656a2 --- /dev/null +++ b/esphome/components/uart/uart_debugger.cpp @@ -0,0 +1,193 @@ +#include "esphome/core/defines.h" +#ifdef USE_UART_DEBUGGER + +#include +#include "uart_debugger.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace uart { + +static const char *const TAG = "uart_debug"; + +UARTDebugger::UARTDebugger(UARTComponent *parent) { + parent->add_debug_callback([this](UARTDirection direction, uint8_t byte) { + if (!this->is_my_direction_(direction) || this->is_recursive_()) { + return; + } + this->trigger_after_direction_change_(direction); + this->store_byte_(direction, byte); + this->trigger_after_delimiter_(byte); + this->trigger_after_bytes_(); + }); +} + +void UARTDebugger::loop() { this->trigger_after_timeout_(); } + +bool UARTDebugger::is_my_direction_(UARTDirection direction) { + return this->for_direction_ == UART_DIRECTION_BOTH || this->for_direction_ == direction; +} + +bool UARTDebugger::is_recursive_() { return this->is_triggering_; } + +void UARTDebugger::trigger_after_direction_change_(UARTDirection direction) { + if (this->has_buffered_bytes_() && this->for_direction_ == UART_DIRECTION_BOTH && + this->last_direction_ != direction) { + this->fire_trigger_(); + } +} + +void UARTDebugger::store_byte_(UARTDirection direction, uint8_t byte) { + this->bytes_.push_back(byte); + this->last_direction_ = direction; + this->last_time_ = millis(); +} + +void UARTDebugger::trigger_after_delimiter_(uint8_t byte) { + if (this->after_delimiter_.empty() || !this->has_buffered_bytes_()) { + return; + } + if (this->after_delimiter_[this->after_delimiter_pos_] != byte) { + this->after_delimiter_pos_ = 0; + return; + } + this->after_delimiter_pos_++; + if (this->after_delimiter_pos_ == this->after_delimiter_.size()) { + this->fire_trigger_(); + this->after_delimiter_pos_ = 0; + } +} + +void UARTDebugger::trigger_after_bytes_() { + if (this->has_buffered_bytes_() && this->after_bytes_ > 0 && this->bytes_.size() >= this->after_bytes_) { + this->fire_trigger_(); + } +} + +void UARTDebugger::trigger_after_timeout_() { + if (this->has_buffered_bytes_() && this->after_timeout_ > 0 && millis() - this->last_time_ >= this->after_timeout_) { + this->fire_trigger_(); + } +} + +bool UARTDebugger::has_buffered_bytes_() { return !this->bytes_.empty(); } + +void UARTDebugger::fire_trigger_() { + this->is_triggering_ = true; + trigger(this->last_direction_, this->bytes_); + this->bytes_.clear(); + this->is_triggering_ = false; +} + +void UARTDummyReceiver::loop() { + // Reading up to a limited number of bytes, to make sure that this loop() + // won't lock up the system on a continuous incoming stream of bytes. + uint8_t data; + int count = 50; + while (this->available() && count--) { + this->read_byte(&data); + } +} + +void UARTDebug::log_hex(UARTDirection direction, std::vector bytes, uint8_t separator) { + std::string res; + if (direction == UART_DIRECTION_RX) { + res += "<<< "; + } else { + res += ">>> "; + } + size_t len = bytes.size(); + char buf[5]; + for (size_t i = 0; i < len; i++) { + if (i > 0) { + res += separator; + } + sprintf(buf, "%02X", bytes[i]); + res += buf; + } + ESP_LOGD(TAG, "%s", res.c_str()); +} + +void UARTDebug::log_string(UARTDirection direction, std::vector bytes) { + std::string res; + if (direction == UART_DIRECTION_RX) { + res += "<<< \""; + } else { + res += ">>> \""; + } + size_t len = bytes.size(); + char buf[5]; + for (size_t i = 0; i < len; i++) { + if (bytes[i] == 7) { + res += "\\a"; + } else if (bytes[i] == 8) { + res += "\\b"; + } else if (bytes[i] == 9) { + res += "\\t"; + } else if (bytes[i] == 10) { + res += "\\n"; + } else if (bytes[i] == 11) { + res += "\\v"; + } else if (bytes[i] == 12) { + res += "\\f"; + } else if (bytes[i] == 13) { + res += "\\r"; + } else if (bytes[i] == 27) { + res += "\\e"; + } else if (bytes[i] == 34) { + res += "\\\""; + } else if (bytes[i] == 39) { + res += "\\'"; + } else if (bytes[i] == 92) { + res += "\\\\"; + } else if (bytes[i] < 32 || bytes[i] > 127) { + sprintf(buf, "\\x%02X", bytes[i]); + res += buf; + } else { + res += bytes[i]; + } + } + res += '"'; + ESP_LOGD(TAG, "%s", res.c_str()); +} + +void UARTDebug::log_int(UARTDirection direction, std::vector bytes, uint8_t separator) { + std::string res; + size_t len = bytes.size(); + if (direction == UART_DIRECTION_RX) { + res += "<<< "; + } else { + res += ">>> "; + } + for (size_t i = 0; i < len; i++) { + if (i > 0) { + res += separator; + } + res += to_string(bytes[i]); + } + ESP_LOGD(TAG, "%s", res.c_str()); +} + +void UARTDebug::log_binary(UARTDirection direction, std::vector bytes, uint8_t separator) { + std::string res; + size_t len = bytes.size(); + if (direction == UART_DIRECTION_RX) { + res += "<<< "; + } else { + res += ">>> "; + } + char buf[20]; + for (size_t i = 0; i < len; i++) { + if (i > 0) { + res += separator; + } + sprintf(buf, "0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", BYTE_TO_BINARY(bytes[i]), bytes[i]); + res += buf; + } + ESP_LOGD(TAG, "%s", res.c_str()); +} + +} // namespace uart +} // namespace esphome +#endif diff --git a/esphome/components/uart/uart_debugger.h b/esphome/components/uart/uart_debugger.h new file mode 100644 index 0000000000..6e84bbe450 --- /dev/null +++ b/esphome/components/uart/uart_debugger.h @@ -0,0 +1,101 @@ +#pragma once +#include "esphome/core/defines.h" +#ifdef USE_UART_DEBUGGER + +#include +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "uart.h" +#include "uart_component.h" + +namespace esphome { +namespace uart { + +/// The UARTDebugger class adds debugging support to a UART bus. +/// +/// It accumulates bytes that travel over the UART bus and triggers one or +/// more actions that can log the data at an appropriate time. What +/// 'appropriate time' means exactly, is determined by a number of +/// configurable constraints. E.g. when a given number of bytes is gathered +/// and/or when no more data has been seen for a given time interval. +class UARTDebugger : public Component, public Trigger> { + public: + explicit UARTDebugger(UARTComponent *parent); + void loop() override; + + /// Set the direction in which to inspect the bytes: incoming, outgoing + /// or both. When debugging in both directions, logging will be triggered + /// when the direction of the data stream changes. + void set_direction(UARTDirection direction) { this->for_direction_ = direction; } + + /// Set the maximum number of bytes to accumulate. When the number of bytes + /// is reached, logging will be triggered. + void set_after_bytes(size_t size) { this->after_bytes_ = size; } + + /// Set a timeout for the data stream. When no new bytes are seen during + /// this timeout, logging will be triggered. + void set_after_timeout(uint32_t timeout) { this->after_timeout_ = timeout; } + + /// Add a delimiter byte. This can be called multiple times to setup a + /// multi-byte delimiter (a typical example would be '\r\n'). + /// When the constructued byte sequence is found in the data stream, + /// logging will be triggered. + void add_delimiter_byte(uint8_t byte) { this->after_delimiter_.push_back(byte); } + + protected: + UARTDirection for_direction_; + UARTDirection last_direction_{}; + std::vector bytes_{}; + size_t after_bytes_; + uint32_t after_timeout_; + uint32_t last_time_{}; + std::vector after_delimiter_{}; + size_t after_delimiter_pos_{}; + bool is_triggering_{false}; + + bool is_my_direction_(UARTDirection direction); + bool is_recursive_(); + void store_byte_(UARTDirection direction, uint8_t byte); + void trigger_after_direction_change_(UARTDirection direction); + void trigger_after_delimiter_(uint8_t byte); + void trigger_after_bytes_(); + void trigger_after_timeout_(); + bool has_buffered_bytes_(); + void fire_trigger_(); +}; + +/// This UARTDevice is used by the serial debugger to read data from a +/// serial interface when the 'dummy_receiver' option is enabled. +/// The data are not stored, nor processed. This is most useful when the +/// debugger is used to reverse engineer a serial protocol, for which no +/// specific UARTDevice implementation exists (yet), but for which the +/// incoming bytes must be read to drive the debugger. +class UARTDummyReceiver : public Component, public UARTDevice { + public: + UARTDummyReceiver(UARTComponent *parent) : UARTDevice(parent) {} + void loop() override; +}; + +/// This class contains some static methods, that can be used to easily +/// create a logging action for the debugger. +class UARTDebug { + public: + /// Log the bytes as hex values, separated by the provided separator + /// character. + static void log_hex(UARTDirection direction, std::vector bytes, uint8_t separator); + + /// Log the bytes as string values, escaping unprintable characters. + static void log_string(UARTDirection direction, std::vector bytes); + + /// Log the bytes as integer values, separated by the provided separator + /// character. + static void log_int(UARTDirection direction, std::vector bytes, uint8_t separator); + + /// Log the bytes as ' ()' values, separated by the provided + /// separator. + static void log_binary(UARTDirection direction, std::vector bytes, uint8_t separator); +}; + +} // namespace uart +} // namespace esphome +#endif diff --git a/esphome/const.py b/esphome/const.py index 26fd514173..af079ac69d 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -34,6 +34,7 @@ ARDUINO_VERSION_ESP8266 = { SOURCE_FILE_EXTENSIONS = {".cpp", ".hpp", ".h", ".c", ".tcc", ".ino"} HEADER_FILE_EXTENSIONS = {".h", ".hpp", ".tcc"} + CONF_ABOVE = "above" CONF_ACCELERATION = "acceleration" CONF_ACCELERATION_X = "acceleration_x" @@ -47,6 +48,7 @@ CONF_ACTIVE_POWER = "active_power" CONF_ADDRESS = "address" CONF_ADDRESSABLE_LIGHT_ID = "addressable_light_id" CONF_ADVANCED = "advanced" +CONF_AFTER = "after" CONF_ALPHA = "alpha" CONF_ALTITUDE = "altitude" CONF_AND = "and" @@ -93,6 +95,7 @@ CONF_BUFFER_SIZE = "buffer_size" CONF_BUILD_PATH = "build_path" CONF_BUS_VOLTAGE = "bus_voltage" CONF_BUSY_PIN = "busy_pin" +CONF_BYTES = "bytes" CONF_CALCULATED_LUX = "calculated_lux" CONF_CALIBRATE_LINEAR = "calibrate_linear" CONF_CALIBRATION = "calibration" @@ -164,6 +167,7 @@ CONF_DAYS_OF_WEEK = "days_of_week" CONF_DC_PIN = "dc_pin" CONF_DEASSERT_RTS_DTR = "deassert_rts_dtr" CONF_DEBOUNCE = "debounce" +CONF_DEBUG = "debug" CONF_DECAY_MODE = "decay_mode" CONF_DECELERATION = "deceleration" CONF_DEFAULT_MODE = "default_mode" @@ -171,6 +175,7 @@ CONF_DEFAULT_TARGET_TEMPERATURE_HIGH = "default_target_temperature_high" CONF_DEFAULT_TARGET_TEMPERATURE_LOW = "default_target_temperature_low" CONF_DEFAULT_TRANSITION_LENGTH = "default_transition_length" CONF_DELAY = "delay" +CONF_DELIMITER = "delimiter" CONF_DELTA = "delta" CONF_DEVICE = "device" CONF_DEVICE_CLASS = "device_class" @@ -192,6 +197,8 @@ CONF_DNS2 = "dns2" CONF_DOMAIN = "domain" CONF_DRY_ACTION = "dry_action" CONF_DRY_MODE = "dry_mode" +CONF_DUMMY_RECEIVER = "dummy_receiver" +CONF_DUMMY_RECEIVER_ID = "dummy_receiver_id" CONF_DUMP = "dump" CONF_DURATION = "duration" CONF_EAP = "eap" diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 94fac73906..e679fe1cef 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -37,6 +37,7 @@ #define USE_SWITCH #define USE_TEXT_SENSOR #define USE_TIME +#define USE_UART_DEBUGGER #define USE_WEBSERVER #define USE_WIFI diff --git a/tests/test1.yaml b/tests/test1.yaml index dee7493bf2..04d928e1b8 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -193,6 +193,18 @@ uart: data_bits: 8 stop_bits: 1 rx_buffer_size: 512 + debug: + dummy_receiver: true + direction: both + after: + bytes: 50 + timeout: 500ms + delimiter: "\r\n" + sequence: + - lambda: UARTDebug::log_hex(direction, bytes, ':'); + - lambda: UARTDebug::log_string(direction, bytes); + - lambda: UARTDebug::log_int(direction, bytes, ','); + - lambda: UARTDebug::log_binary(direction, bytes, ';'); - id: adalight_uart tx_pin: GPIO25 From b4cd8d21a517c19dfdb7e432899db4fcaec10428 Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Wed, 10 Nov 2021 23:53:25 +0100 Subject: [PATCH 0082/1729] Enable addressable light power supply based on raw values (#2690) --- esphome/components/light/addressable_light.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/light/addressable_light.h b/esphome/components/light/addressable_light.h index 2b2c0ca7e4..8302239d6a 100644 --- a/esphome/components/light/addressable_light.h +++ b/esphome/components/light/addressable_light.h @@ -87,7 +87,7 @@ class AddressableLight : public LightOutput, public Component { void mark_shown_() { #ifdef USE_POWER_SUPPLY for (const auto &c : *this) { - if (c.get().is_on()) { + if (c.get_red_raw() > 0 || c.get_green_raw() > 0 || c.get_blue_raw() > 0 || c.get_white_raw() > 0) { this->power_.request(); return; } From 78026e766f9bb1b8a567ec7fdfdb3f44393fb515 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 11 Nov 2021 12:25:41 +1300 Subject: [PATCH 0083/1729] Bump version to 2021.11.0b2 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index af079ac69d..065145761b 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2021.11.0b1" +__version__ = "2021.11.0b2" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From 38cb9888094ce71ccba8c29b97483146fcd432dd Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 11 Nov 2021 15:15:37 +1300 Subject: [PATCH 0084/1729] Remove my.ha links from improv (#2695) --- .../components/esp32_improv/esp32_improv_component.cpp | 10 ++++++++-- .../improv_serial/improv_serial_component.cpp | 3 +-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/esphome/components/esp32_improv/esp32_improv_component.cpp b/esphome/components/esp32_improv/esp32_improv_component.cpp index faa9ab7df6..22bebdfe98 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.cpp +++ b/esphome/components/esp32_improv/esp32_improv_component.cpp @@ -11,6 +11,7 @@ namespace esphome { namespace esp32_improv { static const char *const TAG = "esp32_improv.component"; +static const char *const ESPHOME_MY_LINK = "https://my.home-assistant.io/redirect/config_flow_start?domain=esphome"; ESP32ImprovComponent::ESP32ImprovComponent() { global_improv_component = this; } @@ -124,8 +125,13 @@ void ESP32ImprovComponent::loop() { this->cancel_timeout("wifi-connect-timeout"); this->set_state_(improv::STATE_PROVISIONED); - std::string url = "https://my.home-assistant.io/redirect/config_flow_start?domain=esphome"; - std::vector data = improv::build_rpc_response(improv::WIFI_SETTINGS, {url}); + std::vector urls = {ESPHOME_MY_LINK}; +#ifdef USE_WEBSERVER + auto ip = wifi::global_wifi_component->wifi_sta_ip(); + std::string webserver_url = "http://" + ip.str() + ":" + to_string(WEBSERVER_PORT); + urls.push_back(webserver_url); +#endif + std::vector data = improv::build_rpc_response(improv::WIFI_SETTINGS, urls); this->send_response_(data); this->set_timeout("end-service", 1000, [this] { this->service_->stop(); diff --git a/esphome/components/improv_serial/improv_serial_component.cpp b/esphome/components/improv_serial/improv_serial_component.cpp index a12f1bd83b..abbb76ab11 100644 --- a/esphome/components/improv_serial/improv_serial_component.cpp +++ b/esphome/components/improv_serial/improv_serial_component.cpp @@ -92,8 +92,7 @@ void ImprovSerialComponent::loop() { } std::vector ImprovSerialComponent::build_rpc_settings_response_(improv::Command command) { - std::string url = "https://my.home-assistant.io/redirect/config_flow_start?domain=esphome"; - std::vector urls = {url}; + std::vector urls; #ifdef USE_WEBSERVER auto ip = wifi::global_wifi_component->wifi_sta_ip(); std::string webserver_url = "http://" + ip.str() + ":" + to_string(WEBSERVER_PORT); From 62c3f301e7459c57db7195416cab93ba9f0a93a6 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 11 Nov 2021 22:56:35 +1300 Subject: [PATCH 0085/1729] Only allow prometheus when using arduino (#2697) --- esphome/components/prometheus/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/prometheus/__init__.py b/esphome/components/prometheus/__init__.py index f5f166d085..45345f06e8 100644 --- a/esphome/components/prometheus/__init__.py +++ b/esphome/components/prometheus/__init__.py @@ -15,7 +15,8 @@ CONFIG_SCHEMA = cv.Schema( cv.GenerateID(CONF_WEB_SERVER_BASE_ID): cv.use_id( web_server_base.WebServerBase ), - } + }, + cv.only_with_arduino, ).extend(cv.COMPONENT_SCHEMA) From b526155cce6611c908e1da8ff562c92c42458313 Mon Sep 17 00:00:00 2001 From: lcavalli Date: Fri, 12 Nov 2021 01:17:10 +0100 Subject: [PATCH 0086/1729] Update device classes for binary sensors (#2703) --- esphome/components/binary_sensor/__init__.py | 6 ++++-- esphome/const.py | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index faafcddd06..3f11e18e45 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -44,10 +44,11 @@ from esphome.const import ( DEVICE_CLASS_POWER, DEVICE_CLASS_PRESENCE, DEVICE_CLASS_PROBLEM, + DEVICE_CLASS_RUNNING, DEVICE_CLASS_SAFETY, DEVICE_CLASS_SMOKE, DEVICE_CLASS_SOUND, - DEVICE_CLASS_UPDATE, + DEVICE_CLASS_TAMPER, DEVICE_CLASS_VIBRATION, DEVICE_CLASS_WINDOW, ) @@ -76,10 +77,11 @@ DEVICE_CLASSES = [ DEVICE_CLASS_POWER, DEVICE_CLASS_PRESENCE, DEVICE_CLASS_PROBLEM, + DEVICE_CLASS_RUNNING, DEVICE_CLASS_SAFETY, DEVICE_CLASS_SMOKE, DEVICE_CLASS_SOUND, - DEVICE_CLASS_UPDATE, + DEVICE_CLASS_TAMPER, DEVICE_CLASS_VIBRATION, DEVICE_CLASS_WINDOW, ] diff --git a/esphome/const.py b/esphome/const.py index 065145761b..32773f06bd 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -878,10 +878,11 @@ DEVICE_CLASS_OPENING = "opening" DEVICE_CLASS_PLUG = "plug" DEVICE_CLASS_PRESENCE = "presence" DEVICE_CLASS_PROBLEM = "problem" +DEVICE_CLASS_RUNNING = "running" DEVICE_CLASS_SAFETY = "safety" DEVICE_CLASS_SMOKE = "smoke" DEVICE_CLASS_SOUND = "sound" -DEVICE_CLASS_UPDATE = "update" +DEVICE_CLASS_TAMPER = "tamper" DEVICE_CLASS_VIBRATION = "vibration" DEVICE_CLASS_WINDOW = "window" # device classes of both binary_sensor and sensor component From f1c5e2ef8130fb2b737724bc0033247f3e7fac4f Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 12 Nov 2021 16:12:31 +1300 Subject: [PATCH 0087/1729] Bump version to 2021.11.0b3 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 32773f06bd..f45c6022dd 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2021.11.0b2" +__version__ = "2021.11.0b3" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From 81a36146efb4b686d2eb49d2ae73d16be7e5b6a6 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Sat, 13 Nov 2021 21:22:32 +1300 Subject: [PATCH 0088/1729] Bump ESPAsyncWebServer to 2.1.0 (#2686) --- esphome/components/web_server_base/__init__.py | 2 +- platformio.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/web_server_base/__init__.py b/esphome/components/web_server_base/__init__.py index dc1a2bc2f0..14fb033a56 100644 --- a/esphome/components/web_server_base/__init__.py +++ b/esphome/components/web_server_base/__init__.py @@ -28,4 +28,4 @@ async def to_code(config): cg.add_library("FS", None) cg.add_library("Update", None) # https://github.com/esphome/ESPAsyncWebServer/blob/master/library.json - cg.add_library("esphome/ESPAsyncWebServer-esphome", "2.0.1") + cg.add_library("esphome/ESPAsyncWebServer-esphome", "2.1.0") diff --git a/platformio.ini b/platformio.ini index 0dd32268e0..5e89afe8e6 100644 --- a/platformio.ini +++ b/platformio.ini @@ -41,7 +41,7 @@ lib_deps = ${common.lib_deps} ottowinter/AsyncMqttClient-esphome@0.8.6 ; mqtt ottowinter/ArduinoJson-esphomelib@5.13.3 ; json - esphome/ESPAsyncWebServer-esphome@2.0.1 ; web_server_base + esphome/ESPAsyncWebServer-esphome@2.1.0 ; web_server_base fastled/FastLED@3.3.2 ; fastled_base mikalhart/TinyGPSPlus@1.0.2 ; gps freekode/TM1651@1.0.1 ; tm1651 From 87e1cdeedb39a01353d683c31082fbd13e24d70a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Bia=C5=82ek?= Date: Sun, 14 Nov 2021 14:59:34 +0100 Subject: [PATCH 0089/1729] Allow setting custom command_topic for Select and Number components (#2714) --- esphome/components/number/__init__.py | 2 +- esphome/components/select/__init__.py | 2 +- tests/test1.yaml | 20 ++++++++++++++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/esphome/components/number/__init__.py b/esphome/components/number/__init__.py index bb3427e4bd..1da25caafe 100644 --- a/esphome/components/number/__init__.py +++ b/esphome/components/number/__init__.py @@ -41,7 +41,7 @@ NumberInRangeCondition = number_ns.class_( icon = cv.icon -NUMBER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).extend( +NUMBER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( { cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTNumberComponent), cv.GenerateID(): cv.declare_id(Number), diff --git a/esphome/components/select/__init__.py b/esphome/components/select/__init__.py index 8ea159d657..c15036e9f9 100644 --- a/esphome/components/select/__init__.py +++ b/esphome/components/select/__init__.py @@ -30,7 +30,7 @@ SelectSetAction = select_ns.class_("SelectSetAction", automation.Action) icon = cv.icon -SELECT_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).extend( +SELECT_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( { cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTSelectComponent), cv.GenerateID(): cv.declare_id(Select), diff --git a/tests/test1.yaml b/tests/test1.yaml index 04d928e1b8..a7f2e24465 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -2512,3 +2512,23 @@ teleinfo: uart_id: uart0 update_interval: 60s historical_mode: true + +number: + - platform: template + id: test_number + state_topic: livingroom/custom_state_topic + command_topic: livingroom/custom_command_topic + min_value: 0 + step: 1 + max_value: 10 + optimistic: true + +select: + - platform: template + id: test_select + state_topic: livingroom/custom_state_topic + command_topic: livingroom/custom_command_topic + options: + - one + - two + optimistic: true From ab506b09fe66c62123aa624e7e31791752a11b75 Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Sun, 14 Nov 2021 20:05:11 +0100 Subject: [PATCH 0090/1729] Restore InterruptLock on wifi-less ESP8266 (#2712) --- esphome/core/helpers.cpp | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index daca3ffd32..27608a84c1 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -9,6 +9,7 @@ #ifdef USE_WIFI #include #endif +#include #include #elif defined(USE_ESP32_FRAMEWORK_ARDUINO) #include @@ -430,13 +431,8 @@ void hsv_to_rgb(int hue, float saturation, float value, float &red, float &green } #ifdef USE_ESP8266 -#ifdef USE_WIFI IRAM_ATTR InterruptLock::InterruptLock() { xt_state_ = xt_rsil(15); } IRAM_ATTR InterruptLock::~InterruptLock() { xt_wsr_ps(xt_state_); } -#else -IRAM_ATTR InterruptLock::InterruptLock() {} -IRAM_ATTR InterruptLock::~InterruptLock() {} -#endif #endif #ifdef USE_ESP32 IRAM_ATTR InterruptLock::InterruptLock() { portDISABLE_INTERRUPTS(); } From f4a140e126fcbd3649d5a9569131dda66687a4ce Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Sun, 14 Nov 2021 21:45:25 +0100 Subject: [PATCH 0091/1729] Feed WDT between doing ESP32 touchpad measurements (#2720) --- esphome/components/esp32_touch/esp32_touch.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/esp32_touch/esp32_touch.cpp b/esphome/components/esp32_touch/esp32_touch.cpp index cb72820900..85f4058eee 100644 --- a/esphome/components/esp32_touch/esp32_touch.cpp +++ b/esphome/components/esp32_touch/esp32_touch.cpp @@ -1,6 +1,7 @@ #ifdef USE_ESP32 #include "esp32_touch.h" +#include "esphome/core/application.h" #include "esphome/core/log.h" #include "esphome/core/hal.h" @@ -125,6 +126,8 @@ void ESP32TouchComponent::loop() { if (should_print) { ESP_LOGD(TAG, "Touch Pad '%s' (T%u): %u", child->get_name().c_str(), child->get_touch_pad(), value); } + + App.feed_wdt(); } if (should_print) { From c2f57baec2cef35e1fde9b4f1781e639dab37208 Mon Sep 17 00:00:00 2001 From: "Sergey V. DUDANOV" Date: Mon, 15 Nov 2021 01:40:35 +0400 Subject: [PATCH 0092/1729] RemoteTransmitter fix. Bug from version 2021.10. Some changes. (#2706) --- .../midea/{adapter.cpp => ac_adapter.cpp} | 4 +- .../midea/{adapter.h => ac_adapter.h} | 6 +- .../midea/{automations.h => ac_automations.h} | 2 + esphome/components/midea/air_conditioner.cpp | 13 +-- esphome/components/midea/air_conditioner.h | 21 +++- esphome/components/midea/appliance_base.h | 109 ++++++++++-------- esphome/components/midea/climate.py | 20 ++-- .../midea/{midea_ir.h => ir_transmitter.h} | 15 +++ 8 files changed, 122 insertions(+), 68 deletions(-) rename esphome/components/midea/{adapter.cpp => ac_adapter.cpp} (98%) rename esphome/components/midea/{adapter.h => ac_adapter.h} (95%) rename esphome/components/midea/{automations.h => ac_automations.h} (97%) rename esphome/components/midea/{midea_ir.h => ir_transmitter.h} (73%) diff --git a/esphome/components/midea/adapter.cpp b/esphome/components/midea/ac_adapter.cpp similarity index 98% rename from esphome/components/midea/adapter.cpp rename to esphome/components/midea/ac_adapter.cpp index a3f19dbda8..2837713c35 100644 --- a/esphome/components/midea/adapter.cpp +++ b/esphome/components/midea/ac_adapter.cpp @@ -1,10 +1,11 @@ #ifdef USE_ARDUINO #include "esphome/core/log.h" -#include "adapter.h" +#include "ac_adapter.h" namespace esphome { namespace midea { +namespace ac { const char *const Constants::TAG = "midea"; const std::string Constants::FREEZE_PROTECTION = "freeze protection"; @@ -171,6 +172,7 @@ void Converters::to_climate_traits(ClimateTraits &traits, const dudanov::midea:: traits.add_supported_custom_preset(Constants::FREEZE_PROTECTION); } +} // namespace ac } // namespace midea } // namespace esphome diff --git a/esphome/components/midea/adapter.h b/esphome/components/midea/ac_adapter.h similarity index 95% rename from esphome/components/midea/adapter.h rename to esphome/components/midea/ac_adapter.h index 2497cbbe5b..c17894ae31 100644 --- a/esphome/components/midea/adapter.h +++ b/esphome/components/midea/ac_adapter.h @@ -2,12 +2,15 @@ #ifdef USE_ARDUINO +// MideaUART #include + #include "esphome/components/climate/climate_traits.h" -#include "appliance_base.h" +#include "air_conditioner.h" namespace esphome { namespace midea { +namespace ac { using MideaMode = dudanov::midea::ac::Mode; using MideaSwingMode = dudanov::midea::ac::SwingMode; @@ -41,6 +44,7 @@ class Converters { static void to_climate_traits(ClimateTraits &traits, const dudanov::midea::ac::Capabilities &capabilities); }; +} // namespace ac } // namespace midea } // namespace esphome diff --git a/esphome/components/midea/automations.h b/esphome/components/midea/ac_automations.h similarity index 97% rename from esphome/components/midea/automations.h rename to esphome/components/midea/ac_automations.h index 5b638286ac..d4ed2e7168 100644 --- a/esphome/components/midea/automations.h +++ b/esphome/components/midea/ac_automations.h @@ -7,6 +7,7 @@ namespace esphome { namespace midea { +namespace ac { template class MideaActionBase : public Action { public: @@ -55,6 +56,7 @@ template class PowerOffAction : public MideaActionBase { void play(Ts... x) override { this->parent_->do_power_off(); } }; +} // namespace ac } // namespace midea } // namespace esphome diff --git a/esphome/components/midea/air_conditioner.cpp b/esphome/components/midea/air_conditioner.cpp index 103b852936..dd48f640a2 100644 --- a/esphome/components/midea/air_conditioner.cpp +++ b/esphome/components/midea/air_conditioner.cpp @@ -2,13 +2,11 @@ #include "esphome/core/log.h" #include "air_conditioner.h" -#include "adapter.h" -#ifdef USE_REMOTE_TRANSMITTER -#include "midea_ir.h" -#endif +#include "ac_adapter.h" namespace esphome { namespace midea { +namespace ac { static void set_sensor(Sensor *sensor, float value) { if (sensor != nullptr && (!sensor->has_state() || sensor->get_raw_state() != value)) @@ -122,7 +120,7 @@ void AirConditioner::dump_config() { void AirConditioner::do_follow_me(float temperature, bool beeper) { #ifdef USE_REMOTE_TRANSMITTER IrFollowMeData data(static_cast(lroundf(temperature)), beeper); - this->transmit_ir(data); + this->transmitter_.transmit(data); #else ESP_LOGW(Constants::TAG, "Action needs remote_transmitter component"); #endif @@ -131,7 +129,7 @@ void AirConditioner::do_follow_me(float temperature, bool beeper) { void AirConditioner::do_swing_step() { #ifdef USE_REMOTE_TRANSMITTER IrSpecialData data(0x01); - this->transmit_ir(data); + this->transmitter_.transmit(data); #else ESP_LOGW(Constants::TAG, "Action needs remote_transmitter component"); #endif @@ -143,13 +141,14 @@ void AirConditioner::do_display_toggle() { } else { #ifdef USE_REMOTE_TRANSMITTER IrSpecialData data(0x08); - this->transmit_ir(data); + this->transmitter_.transmit(data); #else ESP_LOGW(Constants::TAG, "Action needs remote_transmitter component"); #endif } } +} // namespace ac } // namespace midea } // namespace esphome diff --git a/esphome/components/midea/air_conditioner.h b/esphome/components/midea/air_conditioner.h index 8dfb9dcb3d..a6023b78bb 100644 --- a/esphome/components/midea/air_conditioner.h +++ b/esphome/components/midea/air_conditioner.h @@ -2,17 +2,25 @@ #ifdef USE_ARDUINO +// MideaUART #include + #include "appliance_base.h" #include "esphome/components/sensor/sensor.h" namespace esphome { namespace midea { +namespace ac { using sensor::Sensor; using climate::ClimateCall; +using climate::ClimatePreset; +using climate::ClimateTraits; +using climate::ClimateMode; +using climate::ClimateSwingMode; +using climate::ClimateFanMode; -class AirConditioner : public ApplianceBase { +class AirConditioner : public ApplianceBase, public climate::Climate { public: void dump_config() override; void set_outdoor_temperature_sensor(Sensor *sensor) { this->outdoor_sensor_ = sensor; } @@ -31,15 +39,26 @@ class AirConditioner : public ApplianceBase void do_beeper_off() { this->set_beeper_feedback(false); } void do_power_on() { this->base_.setPowerState(true); } void do_power_off() { this->base_.setPowerState(false); } + void set_supported_modes(const std::set &modes) { this->supported_modes_ = modes; } + void set_supported_swing_modes(const std::set &modes) { this->supported_swing_modes_ = modes; } + void set_supported_presets(const std::set &presets) { this->supported_presets_ = presets; } + void set_custom_presets(const std::set &presets) { this->supported_custom_presets_ = presets; } + void set_custom_fan_modes(const std::set &modes) { this->supported_custom_fan_modes_ = modes; } protected: void control(const ClimateCall &call) override; ClimateTraits traits() override; + std::set supported_modes_{}; + std::set supported_swing_modes_{}; + std::set supported_presets_{}; + std::set supported_custom_presets_{}; + std::set supported_custom_fan_modes_{}; Sensor *outdoor_sensor_{nullptr}; Sensor *humidity_sensor_{nullptr}; Sensor *power_sensor_{nullptr}; }; +} // namespace ac } // namespace midea } // namespace esphome diff --git a/esphome/components/midea/appliance_base.h b/esphome/components/midea/appliance_base.h index 88a722e389..060cbd996b 100644 --- a/esphome/components/midea/appliance_base.h +++ b/esphome/components/midea/appliance_base.h @@ -2,84 +2,97 @@ #ifdef USE_ARDUINO +// MideaUART +#include +#include + +// Include global defines +#include "esphome/core/defines.h" + #include "esphome/core/component.h" #include "esphome/core/log.h" #include "esphome/components/uart/uart.h" #include "esphome/components/climate/climate.h" -#ifdef USE_REMOTE_TRANSMITTER -#include "esphome/components/remote_base/midea_protocol.h" -#include "esphome/components/remote_transmitter/remote_transmitter.h" -#endif -#include -#include +#include "ir_transmitter.h" namespace esphome { namespace midea { -using climate::ClimatePreset; -using climate::ClimateTraits; -using climate::ClimateMode; -using climate::ClimateSwingMode; -using climate::ClimateFanMode; +/* Stream from UART component */ +class UARTStream : public Stream { + public: + void set_uart(uart::UARTComponent *uart) { this->uart_ = uart; } -template -class ApplianceBase : public Component, public uart::UARTDevice, public climate::Climate, public Stream { + /* Stream interface implementation */ + + int available() override { return this->uart_->available(); } + int read() override { + uint8_t data; + this->uart_->read_byte(&data); + return data; + } + int peek() override { + uint8_t data; + this->uart_->peek_byte(&data); + return data; + } + size_t write(uint8_t data) override { + this->uart_->write_byte(data); + return 1; + } + size_t write(const uint8_t *data, size_t size) override { + this->uart_->write_array(data, size); + return size; + } + void flush() override { this->uart_->flush(); } + + protected: + uart::UARTComponent *uart_; +}; + +template class ApplianceBase : public Component { static_assert(std::is_base_of::value, "T must derive from dudanov::midea::ApplianceBase class"); public: ApplianceBase() { - this->base_.setStream(this); + this->base_.setStream(&this->stream_); this->base_.addOnStateCallback(std::bind(&ApplianceBase::on_status_change, this)); dudanov::midea::ApplianceBase::setLogger( [](int level, const char *tag, int line, const String &format, va_list args) { esp_log_vprintf_(level, tag, line, format.c_str(), args); }); } - bool can_proceed() override { - return this->base_.getAutoconfStatus() != dudanov::midea::AutoconfStatus::AUTOCONF_PROGRESS; - } - float get_setup_priority() const override { return setup_priority::BEFORE_CONNECTION; } - void setup() override { this->base_.setup(); } - void loop() override { this->base_.loop(); } + +#ifdef USE_REMOTE_TRANSMITTER + void set_transmitter(RemoteTransmitterBase *transmitter) { this->transmitter_.set_transmitter(transmitter); } +#endif + + /* UART communication */ + + void set_uart_parent(uart::UARTComponent *parent) { this->stream_.set_uart(parent); } void set_period(uint32_t ms) { this->base_.setPeriod(ms); } void set_response_timeout(uint32_t ms) { this->base_.setTimeout(ms); } void set_request_attempts(uint32_t attempts) { this->base_.setNumAttempts(attempts); } + + /* Component methods */ + + void setup() override { this->base_.setup(); } + void loop() override { this->base_.loop(); } + float get_setup_priority() const override { return setup_priority::BEFORE_CONNECTION; } + bool can_proceed() override { + return this->base_.getAutoconfStatus() != dudanov::midea::AutoconfStatus::AUTOCONF_PROGRESS; + } + void set_beeper_feedback(bool state) { this->base_.setBeeper(state); } void set_autoconf(bool value) { this->base_.setAutoconf(value); } - void set_supported_modes(const std::set &modes) { this->supported_modes_ = modes; } - void set_supported_swing_modes(const std::set &modes) { this->supported_swing_modes_ = modes; } - void set_supported_presets(const std::set &presets) { this->supported_presets_ = presets; } - void set_custom_presets(const std::set &presets) { this->supported_custom_presets_ = presets; } - void set_custom_fan_modes(const std::set &modes) { this->supported_custom_fan_modes_ = modes; } virtual void on_status_change() = 0; -#ifdef USE_REMOTE_TRANSMITTER - void set_transmitter(remote_transmitter::RemoteTransmitterComponent *transmitter) { - this->transmitter_ = transmitter; - } - void transmit_ir(remote_base::MideaData &data) { - data.finalize(); - auto transmit = this->transmitter_->transmit(); - remote_base::MideaProtocol().encode(transmit.get_data(), data); - transmit.perform(); - } -#endif - - int available() override { return uart::UARTDevice::available(); } - int read() override { return uart::UARTDevice::read(); } - int peek() override { return uart::UARTDevice::peek(); } - void flush() override { uart::UARTDevice::flush(); } - size_t write(uint8_t data) override { return uart::UARTDevice::write(data); } protected: T base_; - std::set supported_modes_{}; - std::set supported_swing_modes_{}; - std::set supported_presets_{}; - std::set supported_custom_presets_{}; - std::set supported_custom_fan_modes_{}; + UARTStream stream_; #ifdef USE_REMOTE_TRANSMITTER - remote_transmitter::RemoteTransmitterComponent *transmitter_{nullptr}; + IrTransmitter transmitter_; #endif }; diff --git a/esphome/components/midea/climate.py b/esphome/components/midea/climate.py index 08e82025b6..46c0019efa 100644 --- a/esphome/components/midea/climate.py +++ b/esphome/components/midea/climate.py @@ -40,9 +40,9 @@ AUTO_LOAD = ["sensor"] CONF_OUTDOOR_TEMPERATURE = "outdoor_temperature" CONF_POWER_USAGE = "power_usage" CONF_HUMIDITY_SETPOINT = "humidity_setpoint" -midea_ns = cg.esphome_ns.namespace("midea") -AirConditioner = midea_ns.class_("AirConditioner", climate.Climate, cg.Component) -Capabilities = midea_ns.namespace("Constants") +midea_ac_ns = cg.esphome_ns.namespace("midea").namespace("ac") +AirConditioner = midea_ac_ns.class_("AirConditioner", climate.Climate, cg.Component) +Capabilities = midea_ac_ns.namespace("Constants") def templatize(value): @@ -156,13 +156,13 @@ CONFIG_SCHEMA = cv.All( ) # Actions -FollowMeAction = midea_ns.class_("FollowMeAction", automation.Action) -DisplayToggleAction = midea_ns.class_("DisplayToggleAction", automation.Action) -SwingStepAction = midea_ns.class_("SwingStepAction", automation.Action) -BeeperOnAction = midea_ns.class_("BeeperOnAction", automation.Action) -BeeperOffAction = midea_ns.class_("BeeperOffAction", automation.Action) -PowerOnAction = midea_ns.class_("PowerOnAction", automation.Action) -PowerOffAction = midea_ns.class_("PowerOffAction", automation.Action) +FollowMeAction = midea_ac_ns.class_("FollowMeAction", automation.Action) +DisplayToggleAction = midea_ac_ns.class_("DisplayToggleAction", automation.Action) +SwingStepAction = midea_ac_ns.class_("SwingStepAction", automation.Action) +BeeperOnAction = midea_ac_ns.class_("BeeperOnAction", automation.Action) +BeeperOffAction = midea_ac_ns.class_("BeeperOffAction", automation.Action) +PowerOnAction = midea_ac_ns.class_("PowerOnAction", automation.Action) +PowerOffAction = midea_ac_ns.class_("PowerOffAction", automation.Action) MIDEA_ACTION_BASE_SCHEMA = cv.Schema( { diff --git a/esphome/components/midea/midea_ir.h b/esphome/components/midea/ir_transmitter.h similarity index 73% rename from esphome/components/midea/midea_ir.h rename to esphome/components/midea/ir_transmitter.h index abd4324bcc..34a9f8498e 100644 --- a/esphome/components/midea/midea_ir.h +++ b/esphome/components/midea/ir_transmitter.h @@ -7,6 +7,7 @@ namespace esphome { namespace midea { +using remote_base::RemoteTransmitterBase; using IrData = remote_base::MideaData; class IrFollowMeData : public IrData { @@ -38,6 +39,20 @@ class IrSpecialData : public IrData { IrSpecialData(uint8_t code) : IrData({MIDEA_TYPE_SPECIAL, code, 0xFF, 0xFF, 0xFF}) {} }; +class IrTransmitter { + public: + void set_transmitter(RemoteTransmitterBase *transmitter) { this->transmitter_ = transmitter; } + void transmit(IrData &data) { + data.finalize(); + auto transmit = this->transmitter_->transmit(); + remote_base::MideaProtocol().encode(transmit.get_data(), data); + transmit.perform(); + } + + protected: + RemoteTransmitterBase *transmitter_{nullptr}; +}; + } // namespace midea } // namespace esphome From fea3c4809893a6b3a94ebac7a32e7dc7214d2996 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 15 Nov 2021 10:59:48 +1300 Subject: [PATCH 0093/1729] Fix indentation of write_lambda for modbus_controller number (#2722) --- .../modbus_controller/number/__init__.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/esphome/components/modbus_controller/number/__init__.py b/esphome/components/modbus_controller/number/__init__.py index afb69f8798..4de0ffbcea 100644 --- a/esphome/components/modbus_controller/number/__init__.py +++ b/esphome/components/modbus_controller/number/__init__.py @@ -129,14 +129,14 @@ async def to_code(config): return_type=cg.optional.template(float), ) cg.add(var.set_template(template_)) - if CONF_WRITE_LAMBDA in config: - template_ = await cg.process_lambda( - config[CONF_WRITE_LAMBDA], - [ - (ModbusNumber.operator("ptr"), "item"), - (cg.float_, "x"), - (cg.std_vector.template(cg.uint16).operator("ref"), "payload"), - ], - return_type=cg.optional.template(float), - ) - cg.add(var.set_write_template(template_)) + if CONF_WRITE_LAMBDA in config: + template_ = await cg.process_lambda( + config[CONF_WRITE_LAMBDA], + [ + (ModbusNumber.operator("ptr"), "item"), + (cg.float_, "x"), + (cg.std_vector.template(cg.uint16).operator("ref"), "payload"), + ], + return_type=cg.optional.template(float), + ) + cg.add(var.set_write_template(template_)) From 194f922312119556cef465d4b9b89ce7477a9a33 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 15 Nov 2021 11:03:40 +1300 Subject: [PATCH 0094/1729] Bump version to 2021.11.0b4 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index f45c6022dd..324356ee0b 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2021.11.0b3" +__version__ = "2021.11.0b4" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From 09e87823180bd37366391f5cd89338ae63d64f2a Mon Sep 17 00:00:00 2001 From: Alexandre-Jacques St-Jacques Date: Sun, 14 Nov 2021 17:58:22 -0500 Subject: [PATCH 0095/1729] Remove unnecessary duplicate touch_pad_filter_start (#2724) --- esphome/components/esp32_touch/esp32_touch.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/esphome/components/esp32_touch/esp32_touch.cpp b/esphome/components/esp32_touch/esp32_touch.cpp index 85f4058eee..b225ae1a8a 100644 --- a/esphome/components/esp32_touch/esp32_touch.cpp +++ b/esphome/components/esp32_touch/esp32_touch.cpp @@ -94,7 +94,6 @@ void ESP32TouchComponent::dump_config() { if (this->iir_filter_enabled_()) { ESP_LOGCONFIG(TAG, " IIR Filter: %ums", this->iir_filter_); - touch_pad_filter_start(this->iir_filter_); } else { ESP_LOGCONFIG(TAG, " IIR Filter DISABLED"); } From 687a7e9b2f3a8813e195f7b5e5cba3a27ad92241 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 15 Nov 2021 12:02:18 +1300 Subject: [PATCH 0096/1729] Bump version to 2021.11.0b5 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 324356ee0b..a47673b085 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2021.11.0b4" +__version__ = "2021.11.0b5" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From 0f2df599988c5421972bf33a6b297b15d7391b49 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 16 Nov 2021 09:53:52 +1300 Subject: [PATCH 0097/1729] Add zeroconf as a direct dependency and lock the version (#2729) --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 6e1fe56057..c4b211283d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,6 +11,7 @@ esptool==3.2 click==8.0.3 esphome-dashboard==20211021.1 aioesphomeapi==10.2.0 +zeroconf==0.36.13 # esp-idf requires this, but doesn't bundle it by default # https://github.com/espressif/esp-idf/blob/220590d599e134d7a5e7f1e683cc4550349ffbf8/requirements.txt#L24 From 0a545a28b9d7e6844695d005cc21515fc8436d58 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 16 Nov 2021 09:59:00 +1300 Subject: [PATCH 0098/1729] Bump version to 2021.11.0b6 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index a47673b085..e2bfc81208 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2021.11.0b5" +__version__ = "2021.11.0b6" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From b0a0a153f3bf5182ce0b7d5f61e29021184d5e5f Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 16 Nov 2021 11:02:45 +1300 Subject: [PATCH 0099/1729] Improv serial/checksum changes (#2731) Co-authored-by: Paulus Schoutsen --- esphome/components/improv/improv.cpp | 54 ++++++++++--------- esphome/components/improv/improv.h | 9 ++-- .../improv_serial/improv_serial_component.cpp | 35 ++++++++---- 3 files changed, 61 insertions(+), 37 deletions(-) diff --git a/esphome/components/improv/improv.cpp b/esphome/components/improv/improv.cpp index 94068bc626..759962b51a 100644 --- a/esphome/components/improv/improv.cpp +++ b/esphome/components/improv/improv.cpp @@ -2,30 +2,32 @@ namespace improv { -ImprovCommand parse_improv_data(const std::vector &data) { - return parse_improv_data(data.data(), data.size()); +ImprovCommand parse_improv_data(const std::vector &data, bool check_checksum) { + return parse_improv_data(data.data(), data.size(), check_checksum); } -ImprovCommand parse_improv_data(const uint8_t *data, size_t length) { +ImprovCommand parse_improv_data(const uint8_t *data, size_t length, bool check_checksum) { ImprovCommand improv_command; Command command = (Command) data[0]; uint8_t data_length = data[1]; - if (data_length != length - 3) { + if (data_length != length - 2 - check_checksum) { improv_command.command = UNKNOWN; return improv_command; } - uint8_t checksum = data[length - 1]; + if (check_checksum) { + uint8_t checksum = data[length - 1]; - uint32_t calculated_checksum = 0; - for (uint8_t i = 0; i < length - 1; i++) { - calculated_checksum += data[i]; - } + uint32_t calculated_checksum = 0; + for (uint8_t i = 0; i < length - 1; i++) { + calculated_checksum += data[i]; + } - if ((uint8_t) calculated_checksum != checksum) { - improv_command.command = BAD_CHECKSUM; - return improv_command; + if ((uint8_t) calculated_checksum != checksum) { + improv_command.command = BAD_CHECKSUM; + return improv_command; + } } if (command == WIFI_SETTINGS) { @@ -46,7 +48,7 @@ ImprovCommand parse_improv_data(const uint8_t *data, size_t length) { return improv_command; } -std::vector build_rpc_response(Command command, const std::vector &datum) { +std::vector build_rpc_response(Command command, const std::vector &datum, bool add_checksum) { std::vector out; uint32_t length = 0; out.push_back(command); @@ -58,17 +60,19 @@ std::vector build_rpc_response(Command command, const std::vector build_rpc_response(Command command, const std::vector &datum) { +#ifdef ARDUINO +std::vector build_rpc_response(Command command, const std::vector &datum, bool add_checksum) { std::vector out; uint32_t length = 0; out.push_back(command); @@ -80,14 +84,16 @@ std::vector build_rpc_response(Command command, const std::vector &data); -ImprovCommand parse_improv_data(const uint8_t *data, size_t length); +ImprovCommand parse_improv_data(const std::vector &data, bool check_checksum = true); +ImprovCommand parse_improv_data(const uint8_t *data, size_t length, bool check_checksum = true); -std::vector build_rpc_response(Command command, const std::vector &datum); +std::vector build_rpc_response(Command command, const std::vector &datum, + bool add_checksum = true); #ifdef ARDUINO -std::vector build_rpc_response(Command command, const std::vector &datum); +std::vector build_rpc_response(Command command, const std::vector &datum, bool add_checksum = true); #endif // ARDUINO } // namespace improv diff --git a/esphome/components/improv_serial/improv_serial_component.cpp b/esphome/components/improv_serial/improv_serial_component.cpp index abbb76ab11..a9a7467125 100644 --- a/esphome/components/improv_serial/improv_serial_component.cpp +++ b/esphome/components/improv_serial/improv_serial_component.cpp @@ -98,13 +98,13 @@ std::vector ImprovSerialComponent::build_rpc_settings_response_(improv: std::string webserver_url = "http://" + ip.str() + ":" + to_string(WEBSERVER_PORT); urls.push_back(webserver_url); #endif - std::vector data = improv::build_rpc_response(command, urls); + std::vector data = improv::build_rpc_response(command, urls, false); return data; } std::vector ImprovSerialComponent::build_version_info_() { std::vector infos = {"ESPHome", ESPHOME_VERSION, ESPHOME_VARIANT, App.get_name()}; - std::vector data = improv::build_rpc_response(improv::GET_DEVICE_INFO, infos); + std::vector data = improv::build_rpc_response(improv::GET_DEVICE_INFO, infos, false); return data; }; @@ -140,22 +140,33 @@ bool ImprovSerialComponent::parse_improv_serial_byte_(uint8_t byte) { if (at < 8 + data_len) return true; - if (at == 8 + data_len) { + if (at == 8 + data_len) + return true; + + if (at == 8 + data_len + 1) { + uint8_t checksum = 0x00; + for (uint8_t i = 0; i < at; i++) + checksum += raw[i]; + + if (checksum != byte) { + ESP_LOGW(TAG, "Error decoding Improv payload"); + this->set_error_(improv::ERROR_INVALID_RPC); + return false; + } + if (type == TYPE_RPC) { this->set_error_(improv::ERROR_NONE); - auto command = improv::parse_improv_data(&raw[9], data_len); + auto command = improv::parse_improv_data(&raw[9], data_len, false); return this->parse_improv_payload_(command); } } - return true; + + // If we got here then the command coming is is improv, but not an RPC command + return false; } bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command) { switch (command.command) { - case improv::BAD_CHECKSUM: - ESP_LOGW(TAG, "Error decoding Improv payload"); - this->set_error_(improv::ERROR_INVALID_RPC); - return false; case improv::WIFI_SETTINGS: { wifi::WiFiAP sta{}; sta.set_ssid(command.ssid); @@ -232,6 +243,12 @@ void ImprovSerialComponent::send_response_(std::vector &response) { data[7] = TYPE_RPC_RESPONSE; data[8] = response.size(); data.insert(data.end(), response.begin(), response.end()); + + uint8_t checksum = 0x00; + for (uint8_t d : data) + checksum += d; + data.push_back(checksum); + this->write_data_(data); } From d7432f7c10b7be33a5b7d974cb91eb61a378515a Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 16 Nov 2021 11:05:51 +1300 Subject: [PATCH 0100/1729] Bump version to 2021.11.0b7 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index e2bfc81208..48b1e8aa96 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2021.11.0b6" +__version__ = "2021.11.0b7" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From c75566b374bb7dbf38f05e331f6b3a3f2f09885c Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 16 Nov 2021 12:47:06 +1300 Subject: [PATCH 0101/1729] Fix zeroconf time comparisons (#2733) Co-authored-by: J. Nick Koston --- esphome/zeroconf.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/esphome/zeroconf.py b/esphome/zeroconf.py index a19fc143ec..1fbdf7e93f 100644 --- a/esphome/zeroconf.py +++ b/esphome/zeroconf.py @@ -13,8 +13,9 @@ from zeroconf import ( RecordUpdateListener, Zeroconf, ServiceBrowser, + ServiceStateChange, + current_time_millis, ) -from zeroconf._services import ServiceStateChange _CLASS_IN = 1 _FLAGS_QR_QUERY = 0x0000 # query @@ -88,7 +89,7 @@ class DashboardStatus(threading.Thread): entries = self.zc.cache.entries_with_name(key) if not entries: return False - now = time.time() * 1000 + now = current_time_millis() return any( (entry.created + DashboardStatus.OFFLINE_AFTER) >= now for entry in entries @@ -99,7 +100,7 @@ class DashboardStatus(threading.Thread): self.on_update( {key: self.host_status(host) for key, host in self.key_to_host.items()} ) - now = time.time() * 1000 + now = current_time_millis() for host in self.query_hosts: entries = self.zc.cache.entries_with_name(host) if not entries or all( From cbbafbcca2a340aa6701c63fe719e71c25a53cbc Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 16 Nov 2021 12:53:56 +1300 Subject: [PATCH 0102/1729] Bump version to 2021.11.0b8 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 48b1e8aa96..38ef0e60ee 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2021.11.0b7" +__version__ = "2021.11.0b8" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From 0d47d41c85fce14c36cb144a7446260496032b30 Mon Sep 17 00:00:00 2001 From: Ryan Hoffman Date: Tue, 16 Nov 2021 12:53:36 -0500 Subject: [PATCH 0103/1729] Use as_reversed_hex_array in ble_sensor to fix UUID parsing (#2737) #1627 renamed as_hex_array to as_reversed_hex_array but forgot to rename these users. --- esphome/components/ble_client/sensor/__init__.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/esphome/components/ble_client/sensor/__init__.py b/esphome/components/ble_client/sensor/__init__.py index efe4bf0e9a..4aa6a92ba5 100644 --- a/esphome/components/ble_client/sensor/__init__.py +++ b/esphome/components/ble_client/sensor/__init__.py @@ -67,7 +67,7 @@ async def to_code(config): var.set_service_uuid32(esp32_ble_tracker.as_hex(config[CONF_SERVICE_UUID])) ) elif len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid128_format): - uuid128 = esp32_ble_tracker.as_hex_array(config[CONF_SERVICE_UUID]) + uuid128 = esp32_ble_tracker.as_reversed_hex_array(config[CONF_SERVICE_UUID]) cg.add(var.set_service_uuid128(uuid128)) if len(config[CONF_CHARACTERISTIC_UUID]) == len(esp32_ble_tracker.bt_uuid16_format): @@ -87,7 +87,9 @@ async def to_code(config): elif len(config[CONF_CHARACTERISTIC_UUID]) == len( esp32_ble_tracker.bt_uuid128_format ): - uuid128 = esp32_ble_tracker.as_hex_array(config[CONF_CHARACTERISTIC_UUID]) + uuid128 = esp32_ble_tracker.as_reversed_hex_array( + config[CONF_CHARACTERISTIC_UUID] + ) cg.add(var.set_char_uuid128(uuid128)) if CONF_DESCRIPTOR_UUID in config: @@ -108,7 +110,9 @@ async def to_code(config): elif len(config[CONF_DESCRIPTOR_UUID]) == len( esp32_ble_tracker.bt_uuid128_format ): - uuid128 = esp32_ble_tracker.as_hex_array(config[CONF_DESCRIPTOR_UUID]) + uuid128 = esp32_ble_tracker.as_reversed_hex_array( + config[CONF_DESCRIPTOR_UUID] + ) cg.add(var.set_descr_uuid128(uuid128)) if CONF_LAMBDA in config: From c41547fd4a631eae1018acdd93dbd58020476553 Mon Sep 17 00:00:00 2001 From: rotarykite Date: Wed, 17 Nov 2021 02:57:03 +0800 Subject: [PATCH 0104/1729] Fix senseair component uart read timeout (#2658) Co-authored-by: DAVe3283 Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Co-authored-by: Chua Jun Chieh --- esphome/components/senseair/senseair.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/esphome/components/senseair/senseair.cpp b/esphome/components/senseair/senseair.cpp index 610892dd9e..50b9e01f17 100644 --- a/esphome/components/senseair/senseair.cpp +++ b/esphome/components/senseair/senseair.cpp @@ -141,12 +141,16 @@ void SenseAirComponent::abc_get_period() { } bool SenseAirComponent::senseair_write_command_(const uint8_t *command, uint8_t *response, uint8_t response_length) { + // Verify we have somewhere to store the response + if (response == nullptr) { + return false; + } + // Write wake up byte required by some S8 sensor models + this->write_byte(0); this->flush(); + delay(5); this->write_array(command, SENSEAIR_REQUEST_LENGTH); - if (response == nullptr) - return true; - bool ret = this->read_array(response, response_length); this->flush(); return ret; From 7e495a5e278345237135d3fb77a867ec7e655f39 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 17 Nov 2021 08:00:26 +1300 Subject: [PATCH 0105/1729] Bump version to 2021.11.0b9 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 38ef0e60ee..5d25ba1688 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2021.11.0b8" +__version__ = "2021.11.0b9" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From 58a0b28a39a3b78ef41e530bc796499541115277 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 17 Nov 2021 21:58:30 +1300 Subject: [PATCH 0106/1729] Bump version to 2021.11.0 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 5d25ba1688..128d12aad5 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2021.11.0b9" +__version__ = "2021.11.0" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From 67558bec472237495410a610f9457d04971c5e66 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Wed, 17 Nov 2021 09:52:40 +0100 Subject: [PATCH 0107/1729] Fix HM3301 AQI index calculator (#2739) --- esphome/components/hm3301/aqi_calculator.h | 2 +- esphome/components/hm3301/caqi_calculator.h | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/esphome/components/hm3301/aqi_calculator.h b/esphome/components/hm3301/aqi_calculator.h index 1410eac72b..a3839b643c 100644 --- a/esphome/components/hm3301/aqi_calculator.h +++ b/esphome/components/hm3301/aqi_calculator.h @@ -33,7 +33,7 @@ class AQICalculator : public AbstractAQICalculator { int conc_lo = array[grid_index][0]; int conc_hi = array[grid_index][1]; - return ((aqi_hi - aqi_lo) / (conc_hi - conc_lo)) * (value - conc_lo) + aqi_lo; + return (value - conc_lo) * (aqi_hi - aqi_lo) / (conc_hi - conc_lo) + aqi_lo; } int get_grid_index_(uint16_t value, int array[AMOUNT_OF_LEVELS][2]) { diff --git a/esphome/components/hm3301/caqi_calculator.h b/esphome/components/hm3301/caqi_calculator.h index 51158454d0..a7f5460e0a 100644 --- a/esphome/components/hm3301/caqi_calculator.h +++ b/esphome/components/hm3301/caqi_calculator.h @@ -37,9 +37,7 @@ class CAQICalculator : public AbstractAQICalculator { int conc_lo = array[grid_index][0]; int conc_hi = array[grid_index][1]; - int aqi = ((aqi_hi - aqi_lo) / (conc_hi - conc_lo)) * (value - conc_lo) + aqi_lo; - - return aqi; + return (value - conc_lo) * (aqi_hi - aqi_lo) / (conc_hi - conc_lo) + aqi_lo; } int get_grid_index_(uint16_t value, int array[AMOUNT_OF_LEVELS][2]) { From 8294d10d5bb55c6590117e303ad92a9d071670a2 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 17 Nov 2021 11:28:31 +0100 Subject: [PATCH 0108/1729] Re-instate device class update for binary sensors (#2743) --- esphome/components/binary_sensor/__init__.py | 2 ++ esphome/const.py | 1 + 2 files changed, 3 insertions(+) diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index 3f11e18e45..1eab76d54e 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -49,6 +49,7 @@ from esphome.const import ( DEVICE_CLASS_SMOKE, DEVICE_CLASS_SOUND, DEVICE_CLASS_TAMPER, + DEVICE_CLASS_UPDATE, DEVICE_CLASS_VIBRATION, DEVICE_CLASS_WINDOW, ) @@ -82,6 +83,7 @@ DEVICE_CLASSES = [ DEVICE_CLASS_SMOKE, DEVICE_CLASS_SOUND, DEVICE_CLASS_TAMPER, + DEVICE_CLASS_UPDATE, DEVICE_CLASS_VIBRATION, DEVICE_CLASS_WINDOW, ] diff --git a/esphome/const.py b/esphome/const.py index 128d12aad5..4c540ccb11 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -883,6 +883,7 @@ DEVICE_CLASS_SAFETY = "safety" DEVICE_CLASS_SMOKE = "smoke" DEVICE_CLASS_SOUND = "sound" DEVICE_CLASS_TAMPER = "tamper" +DEVICE_CLASS_UPDATE = "update" DEVICE_CLASS_VIBRATION = "vibration" DEVICE_CLASS_WINDOW = "window" # device classes of both binary_sensor and sensor component From 9c6a475a6e29d47f0f52f079bcb57d79cfa8d5e7 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 17 Nov 2021 23:31:38 +1300 Subject: [PATCH 0109/1729] Bump version to 2021.11.1 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 4c540ccb11..4bd02022d1 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2021.11.0" +__version__ = "2021.11.1" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From d30e2f2a4f0313bbe1bc1b4d30279be0cb70d86a Mon Sep 17 00:00:00 2001 From: Maurice Makaay Date: Thu, 18 Nov 2021 22:41:26 +0100 Subject: [PATCH 0110/1729] Allow UART debug configuration with no after: definition (#2753) --- esphome/components/uart/__init__.py | 10 +++++++--- tests/test2.yaml | 6 ++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py index 53209dfc7b..159b08d2d9 100644 --- a/esphome/components/uart/__init__.py +++ b/esphome/components/uart/__init__.py @@ -94,17 +94,21 @@ UART_DIRECTIONS = { "BOTH": UARTDirection.UART_DIRECTION_BOTH, } +AFTER_DEFAULTS = {CONF_BYTES: 256, CONF_TIMEOUT: "100ms"} + DEBUG_SCHEMA = cv.Schema( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(UARTDebugger), cv.Optional(CONF_DIRECTION, default="BOTH"): cv.enum( UART_DIRECTIONS, upper=True ), - cv.Optional(CONF_AFTER): cv.Schema( + cv.Optional(CONF_AFTER, default=AFTER_DEFAULTS): cv.Schema( { - cv.Optional(CONF_BYTES, default=256): cv.validate_bytes, cv.Optional( - CONF_TIMEOUT, default="100ms" + CONF_BYTES, default=AFTER_DEFAULTS[CONF_BYTES] + ): cv.validate_bytes, + cv.Optional( + CONF_TIMEOUT, default=AFTER_DEFAULTS[CONF_TIMEOUT] ): cv.positive_time_period_milliseconds, cv.Optional(CONF_DELIMITER): cv.templatable(validate_raw_data), } diff --git a/tests/test2.yaml b/tests/test2.yaml index f90e522b1e..3afef9501d 100644 --- a/tests/test2.yaml +++ b/tests/test2.yaml @@ -39,6 +39,12 @@ uart: tx_pin: GPIO22 rx_pin: GPIO23 baud_rate: 115200 + # Specifically added for testing debug with no after: definition. + debug: + dummy_receiver: false + direction: rx + sequence: + - lambda: UARTDebug::log_hex(direction, bytes, ':'); ota: safe_mode: True From 3178243811627b1068f31ac74adec062e79c2fd6 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Thu, 18 Nov 2021 22:20:32 +0000 Subject: [PATCH 0111/1729] Fix frame scaling for animated gifs (#2750) --- esphome/components/animation/__init__.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/esphome/components/animation/__init__.py b/esphome/components/animation/__init__.py index 3f03e5c185..1780bdf72e 100644 --- a/esphome/components/animation/__init__.py +++ b/esphome/components/animation/__init__.py @@ -60,6 +60,10 @@ async def to_code(config): image.seek(frameIndex) frame = image.convert("L", dither=Image.NONE) pixels = list(frame.getdata()) + if len(pixels) != height * width: + raise core.EsphomeError( + f"Unexpected number of pixels in frame {frameIndex}: {len(pixels)} != {height*width}" + ) for pix in pixels: data[pos] = pix pos += 1 @@ -69,8 +73,14 @@ async def to_code(config): pos = 0 for frameIndex in range(frames): image.seek(frameIndex) + if CONF_RESIZE in config: + image.thumbnail(config[CONF_RESIZE]) frame = image.convert("RGB") pixels = list(frame.getdata()) + if len(pixels) != height * width: + raise core.EsphomeError( + f"Unexpected number of pixels in frame {frameIndex}: {len(pixels)} != {height*width}" + ) for pix in pixels: data[pos] = pix[0] pos += 1 From 3a72dd5cb6f9595615c6dca4d778b6adac925b94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Trzci=C5=84ski?= Date: Mon, 22 Nov 2021 00:09:11 +0100 Subject: [PATCH 0112/1729] esp32_camera_web_server: Improve support for MotionEye (#2777) --- .../camera_web_server.cpp | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/esphome/components/esp32_camera_web_server/camera_web_server.cpp b/esphome/components/esp32_camera_web_server/camera_web_server.cpp index c9a684c7e5..653a274bf4 100644 --- a/esphome/components/esp32_camera_web_server/camera_web_server.cpp +++ b/esphome/components/esp32_camera_web_server/camera_web_server.cpp @@ -21,12 +21,19 @@ static const char *const TAG = "esp32_camera_web_server"; #define CONTENT_TYPE "image/jpeg" #define CONTENT_LENGTH "Content-Length" -static const char *const STREAM_HEADER = - "HTTP/1.1 200\r\nAccess-Control-Allow-Origin: *\r\nContent-Type: multipart/x-mixed-replace;boundary=" PART_BOUNDARY - "\r\n"; -static const char *const STREAM_500 = "HTTP/1.1 500\r\nContent-Type: text/plain\r\n\r\nNo frames send.\r\n"; -static const char *const STREAM_BOUNDARY = "\r\n--" PART_BOUNDARY "\r\n"; +static const char *const STREAM_HEADER = "HTTP/1.0 200 OK\r\n" + "Access-Control-Allow-Origin: *\r\n" + "Connection: close\r\n" + "Content-Type: multipart/x-mixed-replace;boundary=" PART_BOUNDARY "\r\n" + "\r\n" + "--" PART_BOUNDARY "\r\n"; +static const char *const STREAM_ERROR = "Content-Type: text/plain\r\n" + "\r\n" + "No frames send.\r\n" + "--" PART_BOUNDARY "\r\n"; static const char *const STREAM_PART = "Content-Type: " CONTENT_TYPE "\r\n" CONTENT_LENGTH ": %u\r\n\r\n"; +static const char *const STREAM_BOUNDARY = "\r\n" + "--" PART_BOUNDARY "\r\n"; CameraWebServer::CameraWebServer() {} @@ -45,6 +52,7 @@ void CameraWebServer::setup() { config.ctrl_port = this->port_; config.max_open_sockets = 1; config.backlog_conn = 2; + config.lru_purge_enable = true; if (httpd_start(&this->httpd_, &config) != ESP_OK) { mark_failed(); @@ -172,9 +180,6 @@ esp_err_t CameraWebServer::streaming_handler_(struct httpd_req *req) { ESP_LOGW(TAG, "STREAM: failed to acquire frame"); res = ESP_FAIL; } - if (res == ESP_OK) { - res = httpd_send_all(req, STREAM_BOUNDARY, strlen(STREAM_BOUNDARY)); - } if (res == ESP_OK) { size_t hlen = snprintf(part_buf, 64, STREAM_PART, image->get_data_length()); res = httpd_send_all(req, part_buf, hlen); @@ -182,6 +187,9 @@ esp_err_t CameraWebServer::streaming_handler_(struct httpd_req *req) { if (res == ESP_OK) { res = httpd_send_all(req, (const char *) image->get_data_buffer(), image->get_data_length()); } + if (res == ESP_OK) { + res = httpd_send_all(req, STREAM_BOUNDARY, strlen(STREAM_BOUNDARY)); + } if (res == ESP_OK) { frames++; int64_t frame_time = millis() - last_frame; @@ -193,7 +201,7 @@ esp_err_t CameraWebServer::streaming_handler_(struct httpd_req *req) { } if (!frames) { - res = httpd_send_all(req, STREAM_500, strlen(STREAM_500)); + res = httpd_send_all(req, STREAM_ERROR, strlen(STREAM_ERROR)); } ESP_LOGI(TAG, "STREAM: closed. Frames: %u", frames); From 980b7cda8f519ffbe6aeddafae32eb37e6e2c008 Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Sun, 21 Nov 2021 15:11:36 -0800 Subject: [PATCH 0113/1729] Remove floating point ops from the ISR (#2751) Co-authored-by: Samuel Sieb --- esphome/components/zyaura/zyaura.cpp | 40 +++++++++++++++++----------- esphome/components/zyaura/zyaura.h | 8 +++--- 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/esphome/components/zyaura/zyaura.cpp b/esphome/components/zyaura/zyaura.cpp index 11643a5c23..621439aa0c 100644 --- a/esphome/components/zyaura/zyaura.cpp +++ b/esphome/components/zyaura/zyaura.cpp @@ -57,38 +57,46 @@ void IRAM_ATTR ZaSensorStore::interrupt(ZaSensorStore *arg) { void IRAM_ATTR ZaSensorStore::set_data_(ZaMessage *message) { switch (message->type) { case HUMIDITY: - this->humidity = (message->value > 10000) ? NAN : (message->value / 100.0f); + this->humidity = message->value; break; - case TEMPERATURE: - this->temperature = (message->value > 5970) ? NAN : (message->value / 16.0f - 273.15f); + this->temperature = message->value; break; - case CO2: - this->co2 = (message->value > 10000) ? NAN : message->value; - break; - - default: + this->co2 = message->value; break; } } -bool ZyAuraSensor::publish_state_(sensor::Sensor *sensor, float *value) { - // Sensor doesn't added to configuration +bool ZyAuraSensor::publish_state_(ZaDataType data_type, sensor::Sensor *sensor, uint16_t *data_value) { + // Sensor wasn't added to configuration if (sensor == nullptr) { return true; } - sensor->publish_state(*value); + float value = NAN; + switch (data_type) { + case HUMIDITY: + value = (*data_value > 10000) ? NAN : (*data_value / 100.0f); + break; + case TEMPERATURE: + value = (*data_value > 5970) ? NAN : (*data_value / 16.0f - 273.15f); + break; + case CO2: + value = (*data_value > 10000) ? NAN : *data_value; + break; + } + + sensor->publish_state(value); // Sensor reported wrong value - if (std::isnan(*value)) { + if (std::isnan(value)) { ESP_LOGW(TAG, "Sensor reported invalid data. Is the update interval too small?"); this->status_set_warning(); return false; } - *value = NAN; + *data_value = -1; return true; } @@ -104,9 +112,9 @@ void ZyAuraSensor::dump_config() { } void ZyAuraSensor::update() { - bool co2_result = this->publish_state_(this->co2_sensor_, &this->store_.co2); - bool temperature_result = this->publish_state_(this->temperature_sensor_, &this->store_.temperature); - bool humidity_result = this->publish_state_(this->humidity_sensor_, &this->store_.humidity); + bool co2_result = this->publish_state_(CO2, this->co2_sensor_, &this->store_.co2); + bool temperature_result = this->publish_state_(TEMPERATURE, this->temperature_sensor_, &this->store_.temperature); + bool humidity_result = this->publish_state_(HUMIDITY, this->humidity_sensor_, &this->store_.humidity); if (co2_result && temperature_result && humidity_result) { this->status_clear_warning(); diff --git a/esphome/components/zyaura/zyaura.h b/esphome/components/zyaura/zyaura.h index 2b9e3fbb35..85c31ec75a 100644 --- a/esphome/components/zyaura/zyaura.h +++ b/esphome/components/zyaura/zyaura.h @@ -42,9 +42,9 @@ class ZaDataProcessor { class ZaSensorStore { public: - float co2 = NAN; - float temperature = NAN; - float humidity = NAN; + uint16_t co2 = -1; + uint16_t temperature = -1; + uint16_t humidity = -1; void setup(InternalGPIOPin *pin_clock, InternalGPIOPin *pin_data); static void interrupt(ZaSensorStore *arg); @@ -79,7 +79,7 @@ class ZyAuraSensor : public PollingComponent { sensor::Sensor *temperature_sensor_{nullptr}; sensor::Sensor *humidity_sensor_{nullptr}; - bool publish_state_(sensor::Sensor *sensor, float *value); + bool publish_state_(ZaDataType data_type, sensor::Sensor *sensor, uint16_t *data_value); }; } // namespace zyaura From 8e1c9f50427890beb7d62da8319910ff9f56f409 Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Thu, 25 Nov 2021 21:00:49 +0100 Subject: [PATCH 0114/1729] Fix parsing numbers from null-terminated buffers (#2755) --- esphome/components/anova/anova_base.cpp | 23 ++++++++++++----------- esphome/components/anova/anova_base.h | 1 - esphome/components/ezo/ezo.cpp | 4 ++-- esphome/core/helpers.h | 22 ++++++++++++---------- 4 files changed, 26 insertions(+), 24 deletions(-) diff --git a/esphome/components/anova/anova_base.cpp b/esphome/components/anova/anova_base.cpp index d55404089e..dcef75e483 100644 --- a/esphome/components/anova/anova_base.cpp +++ b/esphome/components/anova/anova_base.cpp @@ -73,51 +73,52 @@ AnovaPacket *AnovaCodec::get_stop_request() { } void AnovaCodec::decode(const uint8_t *data, uint16_t length) { - memset(this->buf_, 0, 32); - strncpy(this->buf_, (char *) data, length); + char buf[32]; + memset(buf, 0, sizeof(buf)); + strncpy(buf, (char *) data, std::min(length, sizeof(buf) - 1)); this->has_target_temp_ = this->has_current_temp_ = this->has_unit_ = this->has_running_ = false; switch (this->current_query_) { case READ_DEVICE_STATUS: { - if (!strncmp(this->buf_, "stopped", 7)) { + if (!strncmp(buf, "stopped", 7)) { this->has_running_ = true; this->running_ = false; } - if (!strncmp(this->buf_, "running", 7)) { + if (!strncmp(buf, "running", 7)) { this->has_running_ = true; this->running_ = true; } break; } case START: { - if (!strncmp(this->buf_, "start", 5)) { + if (!strncmp(buf, "start", 5)) { this->has_running_ = true; this->running_ = true; } break; } case STOP: { - if (!strncmp(this->buf_, "stop", 4)) { + if (!strncmp(buf, "stop", 4)) { this->has_running_ = true; this->running_ = false; } break; } case READ_TARGET_TEMPERATURE: { - this->target_temp_ = parse_number(this->buf_, sizeof(this->buf_)).value_or(0.0f); + this->target_temp_ = parse_number(buf, sizeof(buf)).value_or(0.0f); if (this->fahrenheit_) this->target_temp_ = ftoc(this->target_temp_); this->has_target_temp_ = true; break; } case SET_TARGET_TEMPERATURE: { - this->target_temp_ = parse_number(this->buf_, sizeof(this->buf_)).value_or(0.0f); + this->target_temp_ = parse_number(buf, sizeof(buf)).value_or(0.0f); if (this->fahrenheit_) this->target_temp_ = ftoc(this->target_temp_); this->has_target_temp_ = true; break; } case READ_CURRENT_TEMPERATURE: { - this->current_temp_ = parse_number(this->buf_, sizeof(this->buf_)).value_or(0.0f); + this->current_temp_ = parse_number(buf, sizeof(buf)).value_or(0.0f); if (this->fahrenheit_) this->current_temp_ = ftoc(this->current_temp_); this->has_current_temp_ = true; @@ -125,8 +126,8 @@ void AnovaCodec::decode(const uint8_t *data, uint16_t length) { } case SET_UNIT: case READ_UNIT: { - this->unit_ = this->buf_[0]; - this->fahrenheit_ = this->buf_[0] == 'f'; + this->unit_ = buf[0]; + this->fahrenheit_ = buf[0] == 'f'; this->has_unit_ = true; break; } diff --git a/esphome/components/anova/anova_base.h b/esphome/components/anova/anova_base.h index 7c1383512d..b831157849 100644 --- a/esphome/components/anova/anova_base.h +++ b/esphome/components/anova/anova_base.h @@ -70,7 +70,6 @@ class AnovaCodec { bool has_current_temp_; bool has_unit_; bool has_running_; - char buf_[32]; bool fahrenheit_; CurrentQuery current_query_; diff --git a/esphome/components/ezo/ezo.cpp b/esphome/components/ezo/ezo.cpp index 7f7a41fb41..426f2807c1 100644 --- a/esphome/components/ezo/ezo.cpp +++ b/esphome/components/ezo/ezo.cpp @@ -32,7 +32,7 @@ void EZOSensor::update() { } void EZOSensor::loop() { - uint8_t buf[20]; + uint8_t buf[21]; if (!(this->state_ & EZO_STATE_WAIT)) { if (this->state_ & EZO_STATE_SEND_TEMP) { int len = sprintf((char *) buf, "T,%0.3f", this->tempcomp_); @@ -74,7 +74,7 @@ void EZOSensor::loop() { if (buf[0] != 1) return; - float val = parse_number((char *) &buf[1], sizeof(buf) - 1).value_or(0); + float val = parse_number((char *) &buf[1], sizeof(buf) - 2).value_or(0); this->publish_state(val); } diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index c67ad8eea3..9cdbf7ca16 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -362,45 +362,47 @@ std::string str_sanitize(const std::string &str); /// @name Parsing & formatting ///@{ -/// Parse a unsigned decimal number. +/// Parse an unsigned decimal number (requires null-terminated string). template::value && std::is_unsigned::value), int> = 0> optional parse_number(const char *str, size_t len) { char *end = nullptr; unsigned long value = ::strtoul(str, &end, 10); // NOLINT(google-runtime-int) - if (end == nullptr || end != str + len || value > std::numeric_limits::max()) + if (end == str || *end != '\0' || value > std::numeric_limits::max()) return {}; return value; } +/// Parse an unsigned decimal number. template::value && std::is_unsigned::value), int> = 0> optional parse_number(const std::string &str) { - return parse_number(str.c_str(), str.length()); + return parse_number(str.c_str(), str.length() + 1); } -/// Parse a signed decimal number. +/// Parse a signed decimal number (requires null-terminated string). template::value && std::is_signed::value), int> = 0> optional parse_number(const char *str, size_t len) { char *end = nullptr; signed long value = ::strtol(str, &end, 10); // NOLINT(google-runtime-int) - if (end == nullptr || end != str + len || value < std::numeric_limits::min() || - value > std::numeric_limits::max()) + if (end == str || *end != '\0' || value < std::numeric_limits::min() || value > std::numeric_limits::max()) return {}; return value; } +/// Parse a signed decimal number. template::value && std::is_signed::value), int> = 0> optional parse_number(const std::string &str) { - return parse_number(str.c_str(), str.length()); + return parse_number(str.c_str(), str.length() + 1); } -/// Parse a decimal floating-point number. +/// Parse a decimal floating-point number (requires null-terminated string). template::value), int> = 0> optional parse_number(const char *str, size_t len) { char *end = nullptr; float value = ::strtof(str, &end); - if (end == nullptr || end != str + len || value == HUGE_VALF) + if (end == str || *end != '\0' || value == HUGE_VALF) return {}; return value; } +/// Parse a decimal floating-point number. template::value), int> = 0> optional parse_number(const std::string &str) { - return parse_number(str.c_str(), str.length()); + return parse_number(str.c_str(), str.length() + 1); } ///@} From 7d03823afd9d24092c27b9b44ee99af56c7aa495 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 26 Nov 2021 09:02:54 +1300 Subject: [PATCH 0115/1729] Bump version to 2021.11.2 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 4bd02022d1..6af44197ff 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2021.11.1" +__version__ = "2021.11.2" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From 57a029189ca5309d7ddc3fddfbd30a43683d07d5 Mon Sep 17 00:00:00 2001 From: Maurice Makaay Date: Fri, 26 Nov 2021 21:25:58 +0100 Subject: [PATCH 0116/1729] Add missing nvs_flash_init() to ESP32 preferences code (#2805) Co-authored-by: Maurice Makaay --- esphome/components/esp32/preferences.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/esp32/preferences.cpp b/esphome/components/esp32/preferences.cpp index 96b7e7809e..8c2b67a942 100644 --- a/esphome/components/esp32/preferences.cpp +++ b/esphome/components/esp32/preferences.cpp @@ -76,6 +76,7 @@ class ESP32Preferences : public ESPPreferences { uint32_t current_offset = 0; void open() { + nvs_flash_init(); esp_err_t err = nvs_open("esphome", NVS_READWRITE, &nvs_handle); if (err == 0) return; From 5009b3029ff569bd7df35cce6fb265e51a515cad Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Sat, 27 Nov 2021 21:13:01 +1300 Subject: [PATCH 0117/1729] Bump version to 2021.11.3 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 6af44197ff..cb6ea8addd 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2021.11.2" +__version__ = "2021.11.3" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From db2128a3447daaf19ad26f4cb792f07b4075627c Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Sun, 28 Nov 2021 20:00:29 +0100 Subject: [PATCH 0118/1729] Fix parsing numbers in Anova (#2816) --- esphome/components/anova/anova_base.cpp | 6 +++--- esphome/core/helpers.cpp | 5 +++++ esphome/core/helpers.h | 6 ++++++ 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/esphome/components/anova/anova_base.cpp b/esphome/components/anova/anova_base.cpp index dcef75e483..cb877bef35 100644 --- a/esphome/components/anova/anova_base.cpp +++ b/esphome/components/anova/anova_base.cpp @@ -104,21 +104,21 @@ void AnovaCodec::decode(const uint8_t *data, uint16_t length) { break; } case READ_TARGET_TEMPERATURE: { - this->target_temp_ = parse_number(buf, sizeof(buf)).value_or(0.0f); + this->target_temp_ = parse_number(str_until(buf, '\r')).value_or(0.0f); if (this->fahrenheit_) this->target_temp_ = ftoc(this->target_temp_); this->has_target_temp_ = true; break; } case SET_TARGET_TEMPERATURE: { - this->target_temp_ = parse_number(buf, sizeof(buf)).value_or(0.0f); + this->target_temp_ = parse_number(str_until(buf, '\r')).value_or(0.0f); if (this->fahrenheit_) this->target_temp_ = ftoc(this->target_temp_); this->has_target_temp_ = true; break; } case READ_CURRENT_TEMPERATURE: { - this->current_temp_ = parse_number(buf, sizeof(buf)).value_or(0.0f); + this->current_temp_ = parse_number(str_until(buf, '\r')).value_or(0.0f); if (this->fahrenheit_) this->current_temp_ = ftoc(this->current_temp_); this->has_current_temp_ = true; diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 27608a84c1..cfc1c74145 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -444,6 +444,11 @@ IRAM_ATTR InterruptLock::~InterruptLock() { portENABLE_INTERRUPTS(); } std::string str_truncate(const std::string &str, size_t length) { return str.length() > length ? str.substr(0, length) : str; } +std::string str_until(const char *str, char ch) { + char *pos = strchr(str, ch); + return pos == nullptr ? std::string(str) : std::string(str, pos - str); +} +std::string str_until(const std::string &str, char ch) { return str.substr(0, str.find(ch)); } std::string str_snake_case(const std::string &str) { std::string result; result.resize(str.length()); diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 9cdbf7ca16..7718c5f1b2 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -351,6 +351,12 @@ template::value, int> = 0> constexpr /// Truncate a string to a specific length. std::string str_truncate(const std::string &str, size_t length); +/// Extract the part of the string until either the first occurence of the specified character, or the end (requires str +/// to be null-terminated). +std::string str_until(const char *str, char ch); +/// Extract the part of the string until either the first occurence of the specified character, or the end. +std::string str_until(const std::string &str, char ch); + /// Convert the string to snake case (lowercase with underscores). std::string str_snake_case(const std::string &str); From 3d5e1d8d91a117d4a26e5fb0205c7eef280bb26c Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Sun, 28 Nov 2021 20:02:10 +0100 Subject: [PATCH 0119/1729] Fix parsing of multiple values in EZO sensor (#2814) Co-authored-by: Lydia Sevelt --- esphome/components/ezo/ezo.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/esphome/components/ezo/ezo.cpp b/esphome/components/ezo/ezo.cpp index 426f2807c1..ca6f121dbb 100644 --- a/esphome/components/ezo/ezo.cpp +++ b/esphome/components/ezo/ezo.cpp @@ -74,6 +74,11 @@ void EZOSensor::loop() { if (buf[0] != 1) return; + // some sensors return multiple comma-separated values, terminate string after first one + for (int i = 1; i < sizeof(buf) - 1; i++) + if (buf[i] == ',') + buf[i] = '\0'; + float val = parse_number((char *) &buf[1], sizeof(buf) - 2).value_or(0); this->publish_state(val); } From 50ec1d0445d4a67d08b3ab0b903733b7aa0c9d9c Mon Sep 17 00:00:00 2001 From: Carlos Garcia Saura Date: Sun, 28 Nov 2021 20:06:53 +0100 Subject: [PATCH 0120/1729] Fix compilation error for WPA enterprise in ESP-IDF (#2815) --- esphome/components/wifi/wifi_component_esp_idf.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 7f71b7078c..1d346c0a8e 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -375,8 +375,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_password failed! %d", err); } } - esp_wpa2_config_t wpa2_config = WPA2_CONFIG_INIT_DEFAULT(); - err = esp_wifi_sta_wpa2_ent_enable(&wpa2_config); + err = esp_wifi_sta_wpa2_ent_enable(); if (err != ESP_OK) { ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_enable failed! %d", err); } From e55506f9dbbb4cceec2345c8e37bf6b0bc95b15c Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Sun, 28 Nov 2021 19:12:40 +0000 Subject: [PATCH 0121/1729] Correct bitmask for third color (blue) scaling. (#2817) --- esphome/components/display/display_color_utils.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/display/display_color_utils.h b/esphome/components/display/display_color_utils.h index 8fc3b0adb9..202de912de 100644 --- a/esphome/components/display/display_color_utils.h +++ b/esphome/components/display/display_color_utils.h @@ -42,7 +42,7 @@ class ColorUtil { ? esp_scale(((colorcode >> third_bits) & ((1 << second_bits) - 1)), ((1 << second_bits) - 1)) : esp_scale(((colorcode >> 8) & 0xFF), ((1 << second_bits) - 1)); - third_color = (right_bit_aligned ? esp_scale(((colorcode >> 0) & 0xFF), ((1 << third_bits) - 1)) + third_color = (right_bit_aligned ? esp_scale(((colorcode >> 0) & ((1 << third_bits) - 1)), ((1 << third_bits) - 1)) : esp_scale(((colorcode >> 0) & 0xFF), (1 << third_bits) - 1)); Color color_return; From a5fb0360118d163f31273732c7e1ae27ed3ed74b Mon Sep 17 00:00:00 2001 From: Conclusio Date: Sun, 28 Nov 2021 20:13:42 +0100 Subject: [PATCH 0122/1729] Add delay to improve stability (#2793) --- esphome/components/scd30/scd30.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/scd30/scd30.cpp b/esphome/components/scd30/scd30.cpp index e6d6ec1c1a..272ee75e30 100644 --- a/esphome/components/scd30/scd30.cpp +++ b/esphome/components/scd30/scd30.cpp @@ -200,6 +200,7 @@ bool SCD30Component::is_data_ready_() { if (!this->write_command_(SCD30_CMD_GET_DATA_READY_STATUS)) { return false; } + delay(4); uint16_t is_data_ready; if (!this->read_data_(&is_data_ready, 1)) { return false; From ea9e75039baa83636e498a87f019ee67bd3512ff Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 29 Nov 2021 10:18:49 +1300 Subject: [PATCH 0123/1729] Bump version to 2021.11.4 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index cb6ea8addd..c2df6ea922 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2021.11.3" +__version__ = "2021.11.4" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From c6414138c723ff18492f1ce75a8ae7b2a2b3281a Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 2 Dec 2021 19:38:49 +1300 Subject: [PATCH 0124/1729] Bump version to 2021.12.0b1 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index f3189a6918..27feacb296 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2021.12.0-dev" +__version__ = "2021.12.0b1" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From 86c205fe43089b7a7f3d1e23eb31978385150f15 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 2 Dec 2021 21:08:11 +1300 Subject: [PATCH 0125/1729] Remove blank line --- esphome/const.py | 1 - 1 file changed, 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 27feacb296..925e198fd7 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -14,7 +14,6 @@ HEADER_FILE_EXTENSIONS = {".h", ".hpp", ".tcc"} SECRETS_FILES = {"secrets.yaml", "secrets.yml"} - CONF_ABOVE = "above" CONF_ACCELERATION = "acceleration" CONF_ACCELERATION_X = "acceleration_x" From 9dcd3d18a04d9d50805ec67743457bdcccb0e023 Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Thu, 2 Dec 2021 19:52:56 +0100 Subject: [PATCH 0126/1729] Update ota_component.cpp (#2852) --- esphome/components/ota/ota_component.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/esphome/components/ota/ota_component.cpp b/esphome/components/ota/ota_component.cpp index 79edd91173..0cf5ea242c 100644 --- a/esphome/components/ota/ota_component.cpp +++ b/esphome/components/ota/ota_component.cpp @@ -277,6 +277,7 @@ void OTAComponent::handle_() { ssize_t read = this->client_->read(buf, requested); if (read == -1) { if (errno == EAGAIN || errno == EWOULDBLOCK) { + App.feed_wdt(); delay(1); continue; } @@ -305,8 +306,9 @@ void OTAComponent::handle_() { #ifdef USE_OTA_STATE_CALLBACK this->state_callback_.call(OTA_IN_PROGRESS, percentage, 0); #endif - // slow down OTA update to avoid getting killed by task watchdog (task_wdt) - delay(10); + // feed watchdog and give other tasks a chance to run + App.feed_wdt(); + yield(); } } From 329bf861d674c9b1d23c85afa5e98738a52ba050 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 3 Dec 2021 07:54:34 +1300 Subject: [PATCH 0127/1729] Bump version to 2021.12.0b2 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 925e198fd7..40ed60cd6f 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2021.12.0b1" +__version__ = "2021.12.0b2" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From 5ac88de985c23c9fb2d195ad3dbe4457ffca8db8 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 6 Dec 2021 19:42:49 +1300 Subject: [PATCH 0128/1729] Bump esphome-dashboard to 20211206.0 (#2870) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 6061476802..22cfeecd5d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ pyserial==3.5 platformio==5.2.2 # When updating platformio, also update Dockerfile esptool==3.2 click==8.0.3 -esphome-dashboard==20211201.0 +esphome-dashboard==20211206.0 aioesphomeapi==10.6.0 zeroconf==0.36.13 From f72abc6f3d3d7e09a2c95c8c3f631ea92b292c92 Mon Sep 17 00:00:00 2001 From: Martin <25747549+martgras@users.noreply.github.com> Date: Mon, 6 Dec 2021 07:54:46 +0100 Subject: [PATCH 0129/1729] tlc59208f : fix compilation error (#2867) --- esphome/components/tlc59208f/tlc59208f_output.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/tlc59208f/tlc59208f_output.cpp b/esphome/components/tlc59208f/tlc59208f_output.cpp index 59fb9f98ed..bd62f8de6d 100644 --- a/esphome/components/tlc59208f/tlc59208f_output.cpp +++ b/esphome/components/tlc59208f/tlc59208f_output.cpp @@ -1,6 +1,7 @@ #include "tlc59208f_output.h" #include "esphome/core/log.h" #include "esphome/core/helpers.h" +#include "esphome/core/hal.h" namespace esphome { namespace tlc59208f { From 1bc757ad0677cac4eb3cb445dd8990b86fc091f9 Mon Sep 17 00:00:00 2001 From: Carlos Garcia Saura Date: Mon, 6 Dec 2021 07:56:53 +0100 Subject: [PATCH 0130/1729] ADC: Turn verbose the debugging "got voltage" (#2863) --- esphome/components/adc/adc_sensor.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/adc/adc_sensor.cpp b/esphome/components/adc/adc_sensor.cpp index c8242ce008..0a439f8b8d 100644 --- a/esphome/components/adc/adc_sensor.cpp +++ b/esphome/components/adc/adc_sensor.cpp @@ -91,7 +91,7 @@ void ADCSensor::dump_config() { float ADCSensor::get_setup_priority() const { return setup_priority::DATA; } void ADCSensor::update() { float value_v = this->sample(); - ESP_LOGD(TAG, "'%s': Got voltage=%.4fV", this->get_name().c_str(), value_v); + ESP_LOGV(TAG, "'%s': Got voltage=%.4fV", this->get_name().c_str(), value_v); this->publish_state(value_v); } From 3ac720df476fef4316fcdefb4209329e2476921f Mon Sep 17 00:00:00 2001 From: Martin <25747549+martgras@users.noreply.github.com> Date: Mon, 6 Dec 2021 07:58:26 +0100 Subject: [PATCH 0131/1729] SPS30 : fix i2c read size (#2866) --- esphome/components/sps30/sps30.cpp | 26 ++++++++++++++++---------- esphome/components/sps30/sps30.h | 1 + 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/esphome/components/sps30/sps30.cpp b/esphome/components/sps30/sps30.cpp index 472b7606ed..6160120564 100644 --- a/esphome/components/sps30/sps30.cpp +++ b/esphome/components/sps30/sps30.cpp @@ -32,14 +32,11 @@ void SPS30Component::setup() { return; } - uint16_t raw_firmware_version[4]; - if (!this->read_data_(raw_firmware_version, 4)) { + if (!this->read_data_(&raw_firmware_version_, 1)) { this->error_code_ = FIRMWARE_VERSION_READ_FAILED; this->mark_failed(); return; } - ESP_LOGD(TAG, " Firmware version v%0d.%02d", (raw_firmware_version[0] >> 8), - uint16_t(raw_firmware_version[0] & 0xFF)); /// Serial number identification if (!this->write_command_(SPS30_CMD_GET_SERIAL_NUMBER)) { this->error_code_ = SERIAL_NUMBER_REQUEST_FAILED; @@ -59,6 +56,8 @@ void SPS30Component::setup() { this->serial_number_[i * 2 + 1] = uint16_t(uint16_t(raw_serial_number[i] & 0xFF)); } ESP_LOGD(TAG, " Serial Number: '%s'", this->serial_number_); + this->status_clear_warning(); + this->skipped_data_read_cycles_ = 0; this->start_continuous_measurement_(); }); } @@ -93,10 +92,17 @@ void SPS30Component::dump_config() { } LOG_UPDATE_INTERVAL(this); ESP_LOGCONFIG(TAG, " Serial Number: '%s'", this->serial_number_); - LOG_SENSOR(" ", "PM1.0", this->pm_1_0_sensor_); - LOG_SENSOR(" ", "PM2.5", this->pm_2_5_sensor_); - LOG_SENSOR(" ", "PM4", this->pm_4_0_sensor_); - LOG_SENSOR(" ", "PM10", this->pm_10_0_sensor_); + ESP_LOGCONFIG(TAG, " Firmware version v%0d.%0d", (raw_firmware_version_ >> 8), + uint16_t(raw_firmware_version_ & 0xFF)); + LOG_SENSOR(" ", "PM1.0 Weight Concentration", this->pm_1_0_sensor_); + LOG_SENSOR(" ", "PM2.5 Weight Concentration", this->pm_2_5_sensor_); + LOG_SENSOR(" ", "PM4 Weight Concentration", this->pm_4_0_sensor_); + LOG_SENSOR(" ", "PM10 Weight Concentration", this->pm_10_0_sensor_); + LOG_SENSOR(" ", "PM1.0 Number Concentration", this->pmc_1_0_sensor_); + LOG_SENSOR(" ", "PM2.5 Number Concentration", this->pmc_2_5_sensor_); + LOG_SENSOR(" ", "PM4 Number Concentration", this->pmc_4_0_sensor_); + LOG_SENSOR(" ", "PM10 Number Concentration", this->pmc_10_0_sensor_); + LOG_SENSOR(" ", "PM typical size", this->pm_size_sensor_); } void SPS30Component::update() { @@ -123,8 +129,8 @@ void SPS30Component::update() { return; } - uint16_t raw_read_status[1]; - if (!this->read_data_(raw_read_status, 1) || raw_read_status[0] == 0x00) { + uint16_t raw_read_status; + if (!this->read_data_(&raw_read_status, 1) || raw_read_status == 0x00) { ESP_LOGD(TAG, "Sensor measurement not ready yet."); this->skipped_data_read_cycles_++; /// The following logic is required to address the cases when a sensor is quickly replaced before it's marked diff --git a/esphome/components/sps30/sps30.h b/esphome/components/sps30/sps30.h index 2f977252a5..bae33a46e1 100644 --- a/esphome/components/sps30/sps30.h +++ b/esphome/components/sps30/sps30.h @@ -33,6 +33,7 @@ class SPS30Component : public PollingComponent, public i2c::I2CDevice { bool read_data_(uint16_t *data, uint8_t len); uint8_t sht_crc_(uint8_t data1, uint8_t data2); char serial_number_[17] = {0}; /// Terminating NULL character + uint16_t raw_firmware_version_; bool start_continuous_measurement_(); uint8_t skipped_data_read_cycles_ = 0; From 56870ed4a8cbe82db4c51acc8ec7d9fd9ed741a2 Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Mon, 6 Dec 2021 07:59:50 +0100 Subject: [PATCH 0132/1729] Fix MCP23x17 not disabling pullup after config change (#2855) --- esphome/components/mcp23x17_base/mcp23x17_base.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/mcp23x17_base/mcp23x17_base.cpp b/esphome/components/mcp23x17_base/mcp23x17_base.cpp index e975670faa..744f2fbe9c 100644 --- a/esphome/components/mcp23x17_base/mcp23x17_base.cpp +++ b/esphome/components/mcp23x17_base/mcp23x17_base.cpp @@ -24,6 +24,7 @@ void MCP23X17Base::pin_mode(uint8_t pin, gpio::Flags flags) { uint8_t gppu = pin < 8 ? mcp23x17_base::MCP23X17_GPPUA : mcp23x17_base::MCP23X17_GPPUB; if (flags == gpio::FLAG_INPUT) { this->update_reg(pin, true, iodir); + this->update_reg(pin, false, gppu); } else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_PULLUP)) { this->update_reg(pin, true, iodir); this->update_reg(pin, true, gppu); From a66e94a0b00110bc011b6eda718adebc60af65c8 Mon Sep 17 00:00:00 2001 From: Massimiliano Ravelli Date: Mon, 6 Dec 2021 08:01:50 +0100 Subject: [PATCH 0133/1729] Ignore already stopped dhcp for ethernet (#2862) Co-authored-by: Oxan van Leeuwen Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/ethernet/ethernet_component.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index 00f68df2b4..384a31ed2f 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -184,7 +184,9 @@ void EthernetComponent::start_connect_() { } err = tcpip_adapter_dhcpc_stop(TCPIP_ADAPTER_IF_ETH); - ESPHL_ERROR_CHECK(err, "DHCPC stop error"); + if (err != ESP_ERR_TCPIP_ADAPTER_DHCP_ALREADY_STOPPED) { + ESPHL_ERROR_CHECK(err, "DHCPC stop error"); + } err = tcpip_adapter_set_ip_info(TCPIP_ADAPTER_IF_ETH, &info); ESPHL_ERROR_CHECK(err, "DHCPC set IP info error"); From c128880033c05012b47db901366db85433219486 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 6 Dec 2021 20:15:34 +1300 Subject: [PATCH 0134/1729] Add endpoint to fetch secrets keys (#2873) --- esphome/dashboard/dashboard.py | 25 ++++++++++++++++++++++++- esphome/yaml_util.py | 7 ++++--- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index c98047d9e5..5e5cc4ecd2 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -27,7 +27,7 @@ import tornado.process import tornado.web import tornado.websocket -from esphome import const, platformio_api, util +from esphome import const, platformio_api, util, yaml_util from esphome.helpers import mkdir_p, get_bool_env, run_system_command from esphome.storage_json import ( EsphomeStorageJSON, @@ -836,6 +836,28 @@ class LogoutHandler(BaseHandler): self.redirect("./login") +class SecretKeysRequestHandler(BaseHandler): + @authenticated + def get(self): + + filename = None + + for secret_filename in const.SECRETS_FILES: + relative_filename = settings.rel_path(secret_filename) + if os.path.isfile(relative_filename): + filename = relative_filename + break + + if filename is None: + self.send_error(404) + return + + secret_keys = list(yaml_util.load_yaml(filename, clear_secrets=False)) + + self.set_header("content-type", "application/json") + self.write(json.dumps(secret_keys)) + + def get_base_frontend_path(): if ENV_DEV not in os.environ: import esphome_dashboard @@ -939,6 +961,7 @@ def make_app(debug=get_bool_env(ENV_DEV)): (f"{rel}static/(.*)", StaticFileHandler, {"path": get_static_path()}), (f"{rel}devices", ListDevicesHandler), (f"{rel}import", ImportRequestHandler), + (f"{rel}secret_keys", SecretKeysRequestHandler), ], **app_settings, ) diff --git a/esphome/yaml_util.py b/esphome/yaml_util.py index bdadbbd43a..57009be57e 100644 --- a/esphome/yaml_util.py +++ b/esphome/yaml_util.py @@ -329,9 +329,10 @@ ESPHomeLoader.add_constructor("!lambda", ESPHomeLoader.construct_lambda) ESPHomeLoader.add_constructor("!force", ESPHomeLoader.construct_force) -def load_yaml(fname): - _SECRET_VALUES.clear() - _SECRET_CACHE.clear() +def load_yaml(fname, clear_secrets=True): + if clear_secrets: + _SECRET_VALUES.clear() + _SECRET_CACHE.clear() return _load_yaml_internal(fname) From 24874f4c3cb9e9efa3d70758ccd1d4b31e03a7c3 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 6 Dec 2021 20:57:56 +1300 Subject: [PATCH 0135/1729] Adopt using wifi secrets that should exist at this point (#2874) --- esphome/components/dashboard_import/__init__.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/esphome/components/dashboard_import/__init__.py b/esphome/components/dashboard_import/__init__.py index d483c77c61..4c47c32ccc 100644 --- a/esphome/components/dashboard_import/__init__.py +++ b/esphome/components/dashboard_import/__init__.py @@ -29,12 +29,11 @@ CONFIG_SCHEMA = cv.Schema( } ) -WIFI_MESSAGE = """ +WIFI_CONFIG = """ -# Do not forget to add your own wifi configuration before installing this configuration -# wifi: -# ssid: !secret wifi_ssid -# password: !secret wifi_password +wifi: + ssid: !secret wifi_ssid + password: !secret wifi_password """ @@ -55,6 +54,6 @@ def import_config(path: str, name: str, project_name: str, import_url: str) -> N "esphome": {"name_add_mac_suffix": False}, } p.write_text( - dump(config) + WIFI_MESSAGE, + dump(config) + WIFI_CONFIG, encoding="utf8", ) From 7ee4bb621c37b58ef037a565d06d0642c1623e2c Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 6 Dec 2021 20:58:51 +1300 Subject: [PATCH 0136/1729] Allow wizard to specify secrets (#2875) --- esphome/wizard.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/esphome/wizard.py b/esphome/wizard.py index 6c87b66453..5f4f347ba7 100644 --- a/esphome/wizard.py +++ b/esphome/wizard.py @@ -86,12 +86,11 @@ def wizard_file(**kwargs): config += "\n\nwifi:\n" if "ssid" in kwargs: - # pylint: disable=consider-using-f-string - config += """ ssid: "{ssid}" - password: "{psk}" -""".format( - **kwargs - ) + if kwargs["ssid"].startswith("!secret"): + template = " ssid: {ssid}\n password: {psk}\n" + else: + template = """ ssid: "{ssid}"\n password: "{psk}"\n""" + config += template.format(**kwargs) else: config += """ # ssid: "My SSID" # password: "mypassword" From df315a1f5182a4ab18fc4c147dc370209c9f3704 Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Mon, 6 Dec 2021 19:24:20 +0100 Subject: [PATCH 0137/1729] Feed watchdog when no component loops (#2857) --- esphome/core/application.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 1bef99e868..a423397453 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -37,6 +37,7 @@ void Application::setup() { component->call(); this->scheduler.process_to_add(); + this->feed_wdt(); if (component->can_proceed()) continue; @@ -46,14 +47,15 @@ void Application::setup() { do { uint32_t new_app_state = STATUS_LED_WARNING; this->scheduler.call(); + this->feed_wdt(); for (uint32_t j = 0; j <= i; j++) { this->components_[j]->call(); new_app_state |= this->components_[j]->get_component_state(); this->app_state_ |= new_app_state; + this->feed_wdt(); } this->app_state_ = new_app_state; yield(); - this->feed_wdt(); } while (!component->can_proceed()); } @@ -65,6 +67,7 @@ void Application::loop() { uint32_t new_app_state = 0; this->scheduler.call(); + this->feed_wdt(); for (Component *component : this->looping_components_) { { WarnIfComponentBlockingGuard guard{component}; From 09b7c6f55092501be2283b451be00132908deced Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 7 Dec 2021 07:41:40 +1300 Subject: [PATCH 0138/1729] Bump esphome-dashboard to 20211207.0 (#2877) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 22cfeecd5d..e27ae0f625 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ pyserial==3.5 platformio==5.2.2 # When updating platformio, also update Dockerfile esptool==3.2 click==8.0.3 -esphome-dashboard==20211206.0 +esphome-dashboard==20211207.0 aioesphomeapi==10.6.0 zeroconf==0.36.13 From ed5e2dd332643b201b1c9854556c6260bd91561e Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 7 Dec 2021 07:47:48 +1300 Subject: [PATCH 0139/1729] Bump version to 2021.12.0b3 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 40ed60cd6f..a77629ad35 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2021.12.0b2" +__version__ = "2021.12.0b3" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From e763469af89fbd22d293424eed89dd393c78df9c Mon Sep 17 00:00:00 2001 From: Carlos Garcia Saura Date: Mon, 6 Dec 2021 23:26:06 +0100 Subject: [PATCH 0140/1729] Feed watchdog while setting up OTA (#2876) --- esphome/components/ota/ota_component.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/esphome/components/ota/ota_component.cpp b/esphome/components/ota/ota_component.cpp index 0cf5ea242c..92256eb1b6 100644 --- a/esphome/components/ota/ota_component.cpp +++ b/esphome/components/ota/ota_component.cpp @@ -372,6 +372,7 @@ bool OTAComponent::readall_(uint8_t *buf, size_t len) { ssize_t read = this->client_->read(buf + at, len - at); if (read == -1) { if (errno == EAGAIN || errno == EWOULDBLOCK) { + App.feed_wdt(); delay(1); continue; } @@ -383,6 +384,7 @@ bool OTAComponent::readall_(uint8_t *buf, size_t len) { } else { at += read; } + App.feed_wdt(); delay(1); } @@ -401,6 +403,7 @@ bool OTAComponent::writeall_(const uint8_t *buf, size_t len) { ssize_t written = this->client_->write(buf + at, len - at); if (written == -1) { if (errno == EAGAIN || errno == EWOULDBLOCK) { + App.feed_wdt(); delay(1); continue; } @@ -409,6 +412,7 @@ bool OTAComponent::writeall_(const uint8_t *buf, size_t len) { } else { at += written; } + App.feed_wdt(); delay(1); } return true; From fbc84861c7fba05e40603b513c65d73a37f27136 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 8 Dec 2021 09:22:03 +1300 Subject: [PATCH 0141/1729] Use new platform component config blocks for wizard (#2885) --- esphome/wizard.py | 24 ++++++++++++++++++++++-- tests/unit_tests/test_wizard.py | 9 +++++---- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/esphome/wizard.py b/esphome/wizard.py index 5f4f347ba7..f2632caf71 100644 --- a/esphome/wizard.py +++ b/esphome/wizard.py @@ -45,9 +45,9 @@ OTA_BIG = r""" ____ _______ BASE_CONFIG = """esphome: name: {name} - platform: {platform} - board: {board} +""" +LOGGER_API_CONFIG = """ # Enable logging logger: @@ -55,6 +55,18 @@ logger: api: """ +ESP8266_CONFIG = """ +esp8266: + board: {board} +""" + +ESP32_CONFIG = """ +esp32: + board: {board} + framework: + type: arduino +""" + def sanitize_double_quotes(value): return value.replace("\\", "\\\\").replace('"', '\\"') @@ -71,6 +83,14 @@ def wizard_file(**kwargs): config = BASE_CONFIG.format(**kwargs) + config += ( + ESP8266_CONFIG.format(**kwargs) + if kwargs["platform"] == "ESP8266" + else ESP32_CONFIG.format(**kwargs) + ) + + config += LOGGER_API_CONFIG + # Configure API if "password" in kwargs: config += f" password: \"{kwargs['password']}\"\n" diff --git a/tests/unit_tests/test_wizard.py b/tests/unit_tests/test_wizard.py index 18e040b0a6..59fcfbff60 100644 --- a/tests/unit_tests/test_wizard.py +++ b/tests/unit_tests/test_wizard.py @@ -11,7 +11,7 @@ def default_config(): return { "name": "test-name", "platform": "test_platform", - "board": "test_board", + "board": "esp01_1m", "ssid": "test_ssid", "psk": "test_psk", "password": "", @@ -105,6 +105,7 @@ def test_wizard_write_sets_platform(default_config, tmp_path, monkeypatch): If the platform is not explicitly set, use "ESP8266" if the board is one of the ESP8266 boards """ # Given + del default_config["platform"] monkeypatch.setattr(wz, "write_file", MagicMock()) # When @@ -112,7 +113,7 @@ def test_wizard_write_sets_platform(default_config, tmp_path, monkeypatch): # Then generated_config = wz.write_file.call_args.args[1] - assert f"platform: {default_config['platform']}" in generated_config + assert "esp8266:" in generated_config def test_wizard_write_defaults_platform_from_board_esp8266( @@ -132,7 +133,7 @@ def test_wizard_write_defaults_platform_from_board_esp8266( # Then generated_config = wz.write_file.call_args.args[1] - assert "platform: ESP8266" in generated_config + assert "esp8266:" in generated_config def test_wizard_write_defaults_platform_from_board_esp32( @@ -152,7 +153,7 @@ def test_wizard_write_defaults_platform_from_board_esp32( # Then generated_config = wz.write_file.call_args.args[1] - assert "platform: ESP32" in generated_config + assert "esp32:" in generated_config def test_safe_print_step_prints_step_number_and_description(monkeypatch): From 090e10730cfcfd22df5a3252def5c09df89d1fb9 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 8 Dec 2021 12:42:50 +1300 Subject: [PATCH 0142/1729] Bump esphome-dashboard to 20211208.0 (#2887) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e27ae0f625..f7b1a6a1fe 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ pyserial==3.5 platformio==5.2.2 # When updating platformio, also update Dockerfile esptool==3.2 click==8.0.3 -esphome-dashboard==20211207.0 +esphome-dashboard==20211208.0 aioesphomeapi==10.6.0 zeroconf==0.36.13 From f3d9d707b6dca605382f74bf855e2ec12139bc47 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 8 Dec 2021 12:58:14 +1300 Subject: [PATCH 0143/1729] Bump version to 2021.12.0b4 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index a77629ad35..67bbcba853 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2021.12.0b3" +__version__ = "2021.12.0b4" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From 649366ff446f1812681e76c3de38b2834fac1a69 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 10 Dec 2021 09:32:34 +1300 Subject: [PATCH 0144/1729] Fix published state for modbus number (#2894) --- .../modbus_controller/number/modbus_number.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/esphome/components/modbus_controller/number/modbus_number.cpp b/esphome/components/modbus_controller/number/modbus_number.cpp index ba2ffdd09f..70be9eaa1f 100644 --- a/esphome/components/modbus_controller/number/modbus_number.cpp +++ b/esphome/components/modbus_controller/number/modbus_number.cpp @@ -27,6 +27,7 @@ void ModbusNumber::parse_and_publish(const std::vector &data) { void ModbusNumber::control(float value) { std::vector data; + float write_value = value; // Is there are lambda configured? if (this->write_transform_func_.has_value()) { // data is passed by reference @@ -35,23 +36,23 @@ void ModbusNumber::control(float value) { auto val = (*this->write_transform_func_)(this, value, data); if (val.has_value()) { ESP_LOGV(TAG, "Value overwritten by lambda"); - value = val.value(); + write_value = val.value(); } else { ESP_LOGV(TAG, "Communication handled by lambda - exiting control"); return; } } else { - value = multiply_by_ * value; + write_value = multiply_by_ * write_value; } // lambda didn't set payload if (data.empty()) { - data = float_to_payload(value, this->sensor_value_type); + data = float_to_payload(write_value, this->sensor_value_type); } ESP_LOGD(TAG, "Updating register: connected Sensor=%s start address=0x%X register count=%d new value=%.02f (val=%.02f)", - this->get_name().c_str(), this->start_address, this->register_count, value, value); + this->get_name().c_str(), this->start_address, this->register_count, write_value, write_value); // Create and send the write command auto write_cmd = ModbusCommandItem::create_write_multiple_command(parent_, this->start_address + this->offset, From 708b928c73e7a1a5eb9173a7b74c1decf6d6ed9d Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Thu, 9 Dec 2021 17:44:43 -0300 Subject: [PATCH 0145/1729] Modbus number/output use write single (#2896) Co-authored-by: Martin <25747549+martgras@users.noreply.github.com> --- esphome/components/modbus_controller/const.py | 1 + .../components/modbus_controller/number/__init__.py | 3 +++ .../modbus_controller/number/modbus_number.cpp | 10 +++++++--- .../modbus_controller/number/modbus_number.h | 2 ++ .../components/modbus_controller/output/__init__.py | 3 +++ .../modbus_controller/output/modbus_output.cpp | 10 ++++++++-- .../modbus_controller/output/modbus_output.h | 2 ++ .../components/modbus_controller/switch/__init__.py | 2 +- 8 files changed, 27 insertions(+), 6 deletions(-) diff --git a/esphome/components/modbus_controller/const.py b/esphome/components/modbus_controller/const.py index 8d1676dd38..baf72efb94 100644 --- a/esphome/components/modbus_controller/const.py +++ b/esphome/components/modbus_controller/const.py @@ -10,5 +10,6 @@ CONF_REGISTER_COUNT = "register_count" CONF_REGISTER_TYPE = "register_type" CONF_RESPONSE_SIZE = "response_size" CONF_SKIP_UPDATES = "skip_updates" +CONF_USE_WRITE_MULTIPLE = "use_write_multiple" CONF_VALUE_TYPE = "value_type" CONF_WRITE_LAMBDA = "write_lambda" diff --git a/esphome/components/modbus_controller/number/__init__.py b/esphome/components/modbus_controller/number/__init__.py index 3c5db9b9c8..4ad6601fee 100644 --- a/esphome/components/modbus_controller/number/__init__.py +++ b/esphome/components/modbus_controller/number/__init__.py @@ -25,6 +25,7 @@ from ..const import ( CONF_FORCE_NEW_RANGE, CONF_MODBUS_CONTROLLER_ID, CONF_SKIP_UPDATES, + CONF_USE_WRITE_MULTIPLE, CONF_VALUE_TYPE, CONF_WRITE_LAMBDA, ) @@ -69,6 +70,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_MIN_VALUE, default=-16777215.0): cv.float_, cv.Optional(CONF_STEP, default=1): cv.positive_float, cv.Optional(CONF_MULTIPLY, default=1.0): cv.float_, + cv.Optional(CONF_USE_WRITE_MULTIPLE, default=False): cv.boolean, } ) .extend(cv.polling_component_schema("60s")), @@ -105,6 +107,7 @@ async def to_code(config): cg.add(var.set_parent(parent)) cg.add(parent.add_sensor_item(var)) await add_modbus_base_properties(var, config, ModbusNumber) + cg.add(var.set_use_write_mutiple(config[CONF_USE_WRITE_MULTIPLE])) if CONF_WRITE_LAMBDA in config: template_ = await cg.process_lambda( config[CONF_WRITE_LAMBDA], diff --git a/esphome/components/modbus_controller/number/modbus_number.cpp b/esphome/components/modbus_controller/number/modbus_number.cpp index 70be9eaa1f..e5afd0c611 100644 --- a/esphome/components/modbus_controller/number/modbus_number.cpp +++ b/esphome/components/modbus_controller/number/modbus_number.cpp @@ -55,9 +55,13 @@ void ModbusNumber::control(float value) { this->get_name().c_str(), this->start_address, this->register_count, write_value, write_value); // Create and send the write command - auto write_cmd = ModbusCommandItem::create_write_multiple_command(parent_, this->start_address + this->offset, - this->register_count, data); - + ModbusCommandItem write_cmd; + if (this->register_count == 1 && !this->use_write_multiple_) { + write_cmd = ModbusCommandItem::create_write_single_command(parent_, this->start_address + this->offset, data[0]); + } else { + write_cmd = ModbusCommandItem::create_write_multiple_command(parent_, this->start_address + this->offset, + this->register_count, data); + } // publish new value write_cmd.on_data_func = [this, write_cmd, value](ModbusRegisterType register_type, uint16_t start_address, const std::vector &data) { diff --git a/esphome/components/modbus_controller/number/modbus_number.h b/esphome/components/modbus_controller/number/modbus_number.h index 271bbfac50..c678cd00cc 100644 --- a/esphome/components/modbus_controller/number/modbus_number.h +++ b/esphome/components/modbus_controller/number/modbus_number.h @@ -35,6 +35,7 @@ class ModbusNumber : public number::Number, public Component, public SensorItem using write_transform_func_t = std::function(ModbusNumber *, float, std::vector &)>; void set_template(transform_func_t &&f) { this->transform_func_ = f; } void set_write_template(write_transform_func_t &&f) { this->write_transform_func_ = f; } + void set_use_write_mutiple(bool use_write_multiple) { this->use_write_multiple_ = use_write_multiple; } protected: void control(float value) override; @@ -42,6 +43,7 @@ class ModbusNumber : public number::Number, public Component, public SensorItem optional write_transform_func_; ModbusController *parent_; float multiply_by_{1.0}; + bool use_write_multiple_{false}; }; } // namespace modbus_controller diff --git a/esphome/components/modbus_controller/output/__init__.py b/esphome/components/modbus_controller/output/__init__.py index eacd96579f..a26d05a18b 100644 --- a/esphome/components/modbus_controller/output/__init__.py +++ b/esphome/components/modbus_controller/output/__init__.py @@ -18,6 +18,7 @@ from .. import ( from ..const import ( CONF_MODBUS_CONTROLLER_ID, + CONF_USE_WRITE_MULTIPLE, CONF_VALUE_TYPE, CONF_WRITE_LAMBDA, ) @@ -36,6 +37,7 @@ CONFIG_SCHEMA = cv.All( cv.GenerateID(): cv.declare_id(ModbusOutput), cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda, cv.Optional(CONF_MULTIPLY, default=1.0): cv.float_, + cv.Optional(CONF_USE_WRITE_MULTIPLE, default=False): cv.boolean, } ), validate_modbus_register, @@ -54,6 +56,7 @@ async def to_code(config): await output.register_output(var, config) cg.add(var.set_write_multiply(config[CONF_MULTIPLY])) parent = await cg.get_variable(config[CONF_MODBUS_CONTROLLER_ID]) + cg.add(var.set_use_write_mutiple(config[CONF_USE_WRITE_MULTIPLE])) cg.add(var.set_parent(parent)) if CONF_WRITE_LAMBDA in config: template_ = await cg.process_lambda( diff --git a/esphome/components/modbus_controller/output/modbus_output.cpp b/esphome/components/modbus_controller/output/modbus_output.cpp index d2b5d02bda..4c2e5775b9 100644 --- a/esphome/components/modbus_controller/output/modbus_output.cpp +++ b/esphome/components/modbus_controller/output/modbus_output.cpp @@ -40,8 +40,14 @@ void ModbusOutput::write_state(float value) { this->start_address, this->register_count, value, original_value); // Create and send the write command - auto write_cmd = - ModbusCommandItem::create_write_multiple_command(parent_, this->start_address, this->register_count, data); + // Create and send the write command + ModbusCommandItem write_cmd; + if (this->register_count == 1 && !this->use_write_multiple_) { + write_cmd = ModbusCommandItem::create_write_single_command(parent_, this->start_address + this->offset, data[0]); + } else { + write_cmd = ModbusCommandItem::create_write_multiple_command(parent_, this->start_address + this->offset, + this->register_count, data); + } parent_->queue_command(write_cmd); } diff --git a/esphome/components/modbus_controller/output/modbus_output.h b/esphome/components/modbus_controller/output/modbus_output.h index 6e8521854b..78d3474ad6 100644 --- a/esphome/components/modbus_controller/output/modbus_output.h +++ b/esphome/components/modbus_controller/output/modbus_output.h @@ -33,6 +33,7 @@ class ModbusOutput : public output::FloatOutput, public Component, public Sensor using write_transform_func_t = std::function(ModbusOutput *, float, std::vector &)>; void set_write_template(write_transform_func_t &&f) { this->write_transform_func_ = f; } + void set_use_write_mutiple(bool use_write_multiple) { this->use_write_multiple_ = use_write_multiple; } protected: void write_state(float value) override; @@ -40,6 +41,7 @@ class ModbusOutput : public output::FloatOutput, public Component, public Sensor ModbusController *parent_; float multiply_by_{1.0}; + bool use_write_multiple_; }; } // namespace modbus_controller diff --git a/esphome/components/modbus_controller/switch/__init__.py b/esphome/components/modbus_controller/switch/__init__.py index df11b268ac..9858d45617 100644 --- a/esphome/components/modbus_controller/switch/__init__.py +++ b/esphome/components/modbus_controller/switch/__init__.py @@ -18,10 +18,10 @@ from ..const import ( CONF_FORCE_NEW_RANGE, CONF_MODBUS_CONTROLLER_ID, CONF_REGISTER_TYPE, + CONF_USE_WRITE_MULTIPLE, CONF_WRITE_LAMBDA, ) -CONF_USE_WRITE_MULTIPLE = "use_write_multiple" DEPENDENCIES = ["modbus_controller"] CODEOWNERS = ["@martgras"] From 3bf632003056dd93c0fbc22cbcbeb199340a9dbd Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 10 Dec 2021 09:55:48 +1300 Subject: [PATCH 0146/1729] Bump version to 2021.12.0b5 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 67bbcba853..e284ee6414 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2021.12.0b4" +__version__ = "2021.12.0b5" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From d504daef9102b0eccfa1a3d2d9f5c7dd16c09160 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Sat, 11 Dec 2021 01:03:22 -0600 Subject: [PATCH 0147/1729] Fix for two points setting when fan_only_cooling is disabled (#2903) Co-authored-by: Paulus Schoutsen Co-authored-by: Keith Burzinski --- esphome/components/thermostat/climate.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/thermostat/climate.py b/esphome/components/thermostat/climate.py index 7b5ee7c624..20565e811c 100644 --- a/esphome/components/thermostat/climate.py +++ b/esphome/components/thermostat/climate.py @@ -431,7 +431,8 @@ async def to_code(config): heat_cool_mode_available = CONF_HEAT_ACTION in config and CONF_COOL_ACTION in config two_points_available = CONF_HEAT_ACTION in config and ( - CONF_COOL_ACTION in config or CONF_FAN_ONLY_ACTION in config + CONF_COOL_ACTION in config + or (config[CONF_FAN_ONLY_COOLING] and CONF_FAN_ONLY_ACTION in config) ) sens = await cg.get_variable(config[CONF_SENSOR]) From bfaa64883794d1167d80e4d12ba8c42862861d0e Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Sat, 11 Dec 2021 21:03:41 +1300 Subject: [PATCH 0148/1729] Bump esphome-dashboard to 20211211.0 (#2904) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f7b1a6a1fe..c45797a71f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ pyserial==3.5 platformio==5.2.2 # When updating platformio, also update Dockerfile esptool==3.2 click==8.0.3 -esphome-dashboard==20211208.0 +esphome-dashboard==20211211.0 aioesphomeapi==10.6.0 zeroconf==0.36.13 From 08057720b81b894e74e1afea9278afb9631755dd Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Sat, 11 Dec 2021 21:07:07 +1300 Subject: [PATCH 0149/1729] Bump version to 2021.12.0b6 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index e284ee6414..a078ca56a6 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2021.12.0b5" +__version__ = "2021.12.0b6" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From 8c9e0e552dab8ac7c04fc8b1142fe650671b8bfb Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Sun, 12 Dec 2021 07:10:51 +1300 Subject: [PATCH 0150/1729] Bump version to 2021.12.0 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index a078ca56a6..1508f9b78a 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2021.12.0b6" +__version__ = "2021.12.0" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From e32a999cd0bb8a4dec5c8dc266407bfad93e7197 Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Mon, 13 Dec 2021 03:21:09 +0100 Subject: [PATCH 0151/1729] Set text sensor state property to filter output (#2893) --- esphome/components/text_sensor/text_sensor.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/text_sensor/text_sensor.cpp b/esphome/components/text_sensor/text_sensor.cpp index 0bcab90843..5d47e7465a 100644 --- a/esphome/components/text_sensor/text_sensor.cpp +++ b/esphome/components/text_sensor/text_sensor.cpp @@ -62,7 +62,7 @@ void TextSensor::add_on_raw_state_callback(std::function call std::string TextSensor::get_state() const { return this->state; } std::string TextSensor::get_raw_state() const { return this->raw_state; } void TextSensor::internal_send_state_to_frontend(const std::string &state) { - this->state = this->raw_state; + this->state = state; this->has_state_ = true; ESP_LOGD(TAG, "'%s': Sending state '%s'", this->name_.c_str(), state.c_str()); this->callback_.call(state); From 386a5b63629775e74fc8ddd85598c2eed8fb1f57 Mon Sep 17 00:00:00 2001 From: wilberforce Date: Tue, 14 Dec 2021 15:08:01 +1300 Subject: [PATCH 0152/1729] Allow button POST on press from web server (#2913) --- esphome/components/web_server/web_server.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 29cb4827bd..1e7696edfb 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -390,8 +390,6 @@ void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlM #ifdef USE_BUTTON void WebServer::handle_button_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (button::Button *obj : App.get_buttons()) { - if (obj->is_internal()) - continue; if (obj->get_object_id() != match.id) continue; From 4bb779d9a565caf562e16a0169cf35c099282b51 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 15 Dec 2021 14:57:32 +1300 Subject: [PATCH 0153/1729] Bump version to 2021.12.1 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 1508f9b78a..a47d90f401 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2021.12.0" +__version__ = "2021.12.1" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From 6d39f64be714ca4b94b6e2a6219fdd7270339d93 Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Mon, 6 Dec 2021 08:01:14 +0100 Subject: [PATCH 0154/1729] Don't disable idle task WDT when it's not enabled (#2856) --- esphome/components/esp32/core.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/esp32/core.cpp b/esphome/components/esp32/core.cpp index a9756b41cd..6123d83a34 100644 --- a/esphome/components/esp32/core.cpp +++ b/esphome/components/esp32/core.cpp @@ -42,11 +42,11 @@ void arch_init() { // Idle task watchdog is disabled on ESP-IDF #elif defined(USE_ARDUINO) enableLoopWDT(); - // Disable idle task watchdog on the core we're using (Arduino pins the process to a core) -#if CONFIG_ARDUINO_RUNNING_CORE == 0 + // Disable idle task watchdog on the core we're using (Arduino pins the task to a core) +#if defined(CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0) && CONFIG_ARDUINO_RUNNING_CORE == 0 disableCore0WDT(); #endif -#if CONFIG_ARDUINO_RUNNING_CORE == 1 +#if defined(CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1) && CONFIG_ARDUINO_RUNNING_CORE == 1 disableCore1WDT(); #endif #endif From 9471df0a1b7b4933610bed6c6b79838f38b9370b Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Mon, 20 Dec 2021 20:19:20 +0100 Subject: [PATCH 0155/1729] Fix MQTT button press action (#2917) --- esphome/components/mqtt/mqtt_button.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/mqtt/mqtt_button.cpp b/esphome/components/mqtt/mqtt_button.cpp index 25ff327cf9..5f3aaa1dd9 100644 --- a/esphome/components/mqtt/mqtt_button.cpp +++ b/esphome/components/mqtt/mqtt_button.cpp @@ -17,7 +17,7 @@ MQTTButtonComponent::MQTTButtonComponent(button::Button *button) : MQTTComponent void MQTTButtonComponent::setup() { this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &payload) { - if (payload == "press") { + if (payload == "PRESS") { this->button_->press(); } else { ESP_LOGW(TAG, "'%s': Received unknown status payload: %s", this->friendly_name().c_str(), payload.c_str()); @@ -31,6 +31,7 @@ void MQTTButtonComponent::dump_config() { } void MQTTButtonComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) { + config.state_topic = false; if (!this->button_->get_device_class().empty()) root[MQTT_DEVICE_CLASS] = this->button_->get_device_class(); } From 5516f65971da8a0a3c6e073bb31392b6f5d8766e Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 21 Dec 2021 08:24:08 +1300 Subject: [PATCH 0156/1729] Bump version to 2021.12.2 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index a47d90f401..614288ba51 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2021.12.1" +__version__ = "2021.12.2" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From ffd4280d6c7af4ab7e0ee8f5fe8373531419d413 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 22 Dec 2021 20:08:54 +1300 Subject: [PATCH 0157/1729] Require arduino in webserver for better validation (#2941) --- esphome/components/web_server/__init__.py | 57 +++++++++++++---------- 1 file changed, 32 insertions(+), 25 deletions(-) diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index d9ff84d501..62d5ec6f14 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -22,31 +22,38 @@ AUTO_LOAD = ["json", "web_server_base"] web_server_ns = cg.esphome_ns.namespace("web_server") WebServer = web_server_ns.class_("WebServer", cg.Component, cg.Controller) -CONFIG_SCHEMA = cv.Schema( - { - cv.GenerateID(): cv.declare_id(WebServer), - cv.Optional(CONF_PORT, default=80): cv.port, - cv.Optional( - CONF_CSS_URL, default="https://esphome.io/_static/webserver-v1.min.css" - ): cv.string, - cv.Optional(CONF_CSS_INCLUDE): cv.file_, - cv.Optional( - CONF_JS_URL, default="https://esphome.io/_static/webserver-v1.min.js" - ): cv.string, - cv.Optional(CONF_JS_INCLUDE): cv.file_, - cv.Optional(CONF_AUTH): cv.Schema( - { - cv.Required(CONF_USERNAME): cv.All(cv.string_strict, cv.Length(min=1)), - cv.Required(CONF_PASSWORD): cv.All(cv.string_strict, cv.Length(min=1)), - } - ), - cv.GenerateID(CONF_WEB_SERVER_BASE_ID): cv.use_id( - web_server_base.WebServerBase - ), - cv.Optional(CONF_INCLUDE_INTERNAL, default=False): cv.boolean, - cv.Optional(CONF_OTA, default=True): cv.boolean, - } -).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(WebServer), + cv.Optional(CONF_PORT, default=80): cv.port, + cv.Optional( + CONF_CSS_URL, default="https://esphome.io/_static/webserver-v1.min.css" + ): cv.string, + cv.Optional(CONF_CSS_INCLUDE): cv.file_, + cv.Optional( + CONF_JS_URL, default="https://esphome.io/_static/webserver-v1.min.js" + ): cv.string, + cv.Optional(CONF_JS_INCLUDE): cv.file_, + cv.Optional(CONF_AUTH): cv.Schema( + { + cv.Required(CONF_USERNAME): cv.All( + cv.string_strict, cv.Length(min=1) + ), + cv.Required(CONF_PASSWORD): cv.All( + cv.string_strict, cv.Length(min=1) + ), + } + ), + cv.GenerateID(CONF_WEB_SERVER_BASE_ID): cv.use_id( + web_server_base.WebServerBase + ), + cv.Optional(CONF_INCLUDE_INTERNAL, default=False): cv.boolean, + cv.Optional(CONF_OTA, default=True): cv.boolean, + }, + ).extend(cv.COMPONENT_SCHEMA), + cv.only_with_arduino, +) @coroutine_with_priority(40.0) From fc0a6546a221984b6a7e124d497aa8a669955c94 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 23 Dec 2021 08:31:56 +1300 Subject: [PATCH 0158/1729] Only allow internal pins for dht sensor (#2940) --- esphome/components/dht/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/dht/sensor.py b/esphome/components/dht/sensor.py index 1334f0270c..cd1886728e 100644 --- a/esphome/components/dht/sensor.py +++ b/esphome/components/dht/sensor.py @@ -33,7 +33,7 @@ DHT = dht_ns.class_("DHT", cg.PollingComponent) CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(DHT), - cv.Required(CONF_PIN): pins.gpio_input_pin_schema, + cv.Required(CONF_PIN): pins.internal_gpio_input_pin_schema, cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( unit_of_measurement=UNIT_CELSIUS, accuracy_decimals=1, From 41879e41e6227dc2273ca083831da80a4938241f Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 23 Dec 2021 08:43:17 +1300 Subject: [PATCH 0159/1729] Workaround installing as editable package not working (#2936) --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 62a64c851d..7df2fbf3d8 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -64,7 +64,7 @@ RUN \ # Copy esphome and install COPY . /esphome -RUN pip3 install --no-cache-dir -e /esphome +RUN pip3 install --no-cache-dir /esphome # Settings for dashboard ENV USERNAME="" PASSWORD="" From 28f87dc8045e0f1af7794c121f20ca943f2f2ad0 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 30 Dec 2021 10:42:22 +1300 Subject: [PATCH 0160/1729] Remove -e for hassio images (#2964) --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 7df2fbf3d8..eece2108f7 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -112,7 +112,7 @@ RUN \ # Copy esphome and install COPY . /esphome -RUN pip3 install --no-cache-dir -e /esphome +RUN pip3 install --no-cache-dir /esphome # Labels LABEL \ From b37739eec2fb39bb97602411a71a949da82902ee Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 30 Dec 2021 13:58:47 +1300 Subject: [PATCH 0161/1729] Bump version to 2021.12.3 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 614288ba51..9ed0975cd4 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2021.12.2" +__version__ = "2021.12.3" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From d32633b3c7604d5a3415db6c4d14a52fba5c96e5 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 22 Dec 2021 15:27:34 +1300 Subject: [PATCH 0162/1729] Update curl package version in docker (#2939) --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index eece2108f7..25f2cf85d2 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -27,7 +27,7 @@ RUN \ python3-cryptography=3.3.2-1 \ iputils-ping=3:20210202-1 \ git=1:2.30.2-1 \ - curl=7.74.0-1.3+b1 \ + curl=7.74.0-1.3+deb11u1 \ && rm -rf \ /tmp/* \ /var/{cache,log}/* \ From 961c27f1c27ff8e9496d669254f2fb522e3361f6 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 13 Jan 2022 11:02:07 +1300 Subject: [PATCH 0163/1729] Bump version to 2022.1.0b1 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index fdc880caf3..a26e3f050f 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2022.1.0-dev" +__version__ = "2022.1.0b1" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From d6009453df6d79c693ae8cb1c5cc554aa570ed28 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 12 Jan 2022 22:35:30 -0800 Subject: [PATCH 0164/1729] Add factory to download name (#3040) --- esphome/dashboard/dashboard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index ca257d93b4..f9ae3a4fc8 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -416,7 +416,7 @@ class DownloadBinaryRequestHandler(BaseHandler): if storage_json is None: self.send_error(404) return - filename = f"{storage_json.name}.bin" + filename = f"{storage_json.name}-factory.bin" path = storage_json.firmware_bin_path.replace( "firmware.bin", "firmware-factory.bin" ) From 6dfe3039d03e585dc22c23c35c597580b60f62b3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 12 Jan 2022 23:22:19 -0800 Subject: [PATCH 0165/1729] Bump dashboard to 20220113.2 (#3041) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index b21218f511..05636da805 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ pyserial==3.5 platformio==5.2.4 # When updating platformio, also update Dockerfile esptool==3.2 click==8.0.3 -esphome-dashboard==20220113.1 +esphome-dashboard==20220113.2 aioesphomeapi==10.6.0 zeroconf==0.37.0 From cbe30924049be5caa16a44fbf959ef8e0ae72afe Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 13 Jan 2022 21:28:45 +1300 Subject: [PATCH 0166/1729] Bump version to 2022.1.0b2 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index a26e3f050f..daa61ebd9b 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2022.1.0b1" +__version__ = "2022.1.0b2" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From d274545e770c11784a20ec14ad697d03ec907583 Mon Sep 17 00:00:00 2001 From: Ohad Lutzky Date: Sun, 16 Jan 2022 22:14:45 +0000 Subject: [PATCH 0167/1729] Disable caching for binary download (#3054) --- esphome/dashboard/dashboard.py | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index f9ae3a4fc8..c68d037fe6 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -445,6 +445,7 @@ class DownloadBinaryRequestHandler(BaseHandler): self.set_header("Content-Type", "application/octet-stream") self.set_header("Content-Disposition", f'attachment; filename="{filename}"') + self.set_header("Cache-Control", "no-cache") if not Path(path).is_file(): self.send_error(404) return From 282313ab52f4cf6433767e95eddcfa486e1ed5c6 Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Sun, 16 Jan 2022 23:15:11 +0100 Subject: [PATCH 0168/1729] Rename post_build scripts to fix codeowners script (#3057) --- esphome/components/esp32/__init__.py | 2 +- .../components/esp32/{post_build.py => post_build.py.script} | 0 esphome/components/esp8266/__init__.py | 2 +- .../components/esp8266/{post_build.py => post_build.py.script} | 0 4 files changed, 2 insertions(+), 2 deletions(-) rename esphome/components/esp32/{post_build.py => post_build.py.script} (100%) rename esphome/components/esp8266/{post_build.py => post_build.py.script} (100%) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 161803eaf4..8214886f8c 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -417,7 +417,7 @@ def copy_files(): ) dir = os.path.dirname(__file__) - post_build_file = os.path.join(dir, "post_build.py") + post_build_file = os.path.join(dir, "post_build.py.script") copy_file_if_changed( post_build_file, CORE.relative_build_path("post_build.py"), diff --git a/esphome/components/esp32/post_build.py b/esphome/components/esp32/post_build.py.script similarity index 100% rename from esphome/components/esp32/post_build.py rename to esphome/components/esp32/post_build.py.script diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index 34a4a2fadb..7182042770 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -220,7 +220,7 @@ async def to_code(config): def copy_files(): dir = os.path.dirname(__file__) - post_build_file = os.path.join(dir, "post_build.py") + post_build_file = os.path.join(dir, "post_build.py.script") copy_file_if_changed( post_build_file, CORE.relative_build_path("post_build.py"), diff --git a/esphome/components/esp8266/post_build.py b/esphome/components/esp8266/post_build.py.script similarity index 100% rename from esphome/components/esp8266/post_build.py rename to esphome/components/esp8266/post_build.py.script From c5eba0451771620ee75d6a0f5cc757b205867ff7 Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Sun, 16 Jan 2022 23:40:15 +0100 Subject: [PATCH 0169/1729] Remove deprecated attribute from virtual entity methods (#3056) --- .../components/binary_sensor/binary_sensor.h | 6 +++-- esphome/components/cover/cover.h | 6 ++++- esphome/components/sensor/sensor.h | 24 ++++++++++++------- 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/esphome/components/binary_sensor/binary_sensor.h b/esphome/components/binary_sensor/binary_sensor.h index ecf68de74c..591f444387 100644 --- a/esphome/components/binary_sensor/binary_sensor.h +++ b/esphome/components/binary_sensor/binary_sensor.h @@ -74,8 +74,10 @@ class BinarySensor : public EntityBase { // ========== OVERRIDE METHODS ========== // (You'll only need this when creating your own custom binary sensor) - /// Get the default device class for this sensor, or empty string for no default. - ESPDEPRECATED("device_class() is deprecated, set property during config validation instead.", "2022.01") + /** Override this to set the default device class. + * + * @deprecated This method is deprecated, set the property during config validation instead. (2022.1) + */ virtual std::string device_class(); protected: diff --git a/esphome/components/cover/cover.h b/esphome/components/cover/cover.h index 779e4a2a46..1b5d3a8fa1 100644 --- a/esphome/components/cover/cover.h +++ b/esphome/components/cover/cover.h @@ -169,7 +169,11 @@ class Cover : public EntityBase { friend CoverCall; virtual void control(const CoverCall &call) = 0; - ESPDEPRECATED("device_class() is deprecated, set property during config validation instead.", "2022.01") + + /** Override this to set the default device class. + * + * @deprecated This method is deprecated, set the property during config validation instead. (2022.1) + */ virtual std::string device_class(); optional restore_state_(); diff --git a/esphome/components/sensor/sensor.h b/esphome/components/sensor/sensor.h index 794aecca95..d31fe9d834 100644 --- a/esphome/components/sensor/sensor.h +++ b/esphome/components/sensor/sensor.h @@ -150,20 +150,28 @@ class Sensor : public EntityBase { void internal_send_state_to_frontend(float state); protected: - /// Override this to set the default unit of measurement. - ESPDEPRECATED("unit_of_measurement() is deprecated, set property during config validation instead.", "2022.01") + /** Override this to set the default unit of measurement. + * + * @deprecated This method is deprecated, set the property during config validation instead. (2022.1) + */ virtual std::string unit_of_measurement(); // NOLINT - /// Override this to set the default accuracy in decimals. - ESPDEPRECATED("accuracy_decimals() is deprecated, set property during config validation instead.", "2022.01") + /** Override this to set the default accuracy in decimals. + * + * @deprecated This method is deprecated, set the property during config validation instead. (2022.1) + */ virtual int8_t accuracy_decimals(); // NOLINT - /// Override this to set the default device class. - ESPDEPRECATED("device_class() is deprecated, set property during config validation instead.", "2022.01") + /** Override this to set the default device class. + * + * @deprecated This method is deprecated, set the property during config validation instead. (2022.1) + */ virtual std::string device_class(); // NOLINT - /// Override this to set the default state class. - ESPDEPRECATED("state_class() is deprecated, set property during config validation instead.", "2022.01") + /** Override this to set the default state class. + * + * @deprecated This method is deprecated, set the property during config validation instead. (2022.1) + */ virtual StateClass state_class(); // NOLINT uint32_t hash_base() override; From 01b62a16c3eb081edf34a99196e0c7f17de0525f Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 17 Jan 2022 12:31:44 +1300 Subject: [PATCH 0170/1729] Add number setting to web_server/rest_api (#3055) --- esphome/components/web_server/web_server.cpp | 45 +++++++++++++++++--- 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 4cc77da256..7413af67c4 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -236,7 +236,18 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) { #ifdef USE_NUMBER for (auto *obj : App.get_numbers()) if (this->include_internal_ || !obj->is_internal()) - write_row(stream, obj, "number", ""); + write_row(stream, obj, "number", "", [](AsyncResponseStream &stream, EntityBase *obj) { + number::Number *number = (number::Number *) obj; + stream.print(R"(traits.get_min_value()); + stream.print(R"(" max=")"); + stream.print(number->traits.get_max_value()); + stream.print(R"(" step=")"); + stream.print(number->traits.get_step()); + stream.print(R"(" value=")"); + stream.print(number->state); + stream.print(R"("/>)"); + }); #endif #ifdef USE_SELECT @@ -652,8 +663,29 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM for (auto *obj : App.get_numbers()) { if (obj->get_object_id() != match.id) continue; - std::string data = this->number_json(obj, obj->state); - request->send(200, "text/json", data.c_str()); + + if (request->method() == HTTP_GET) { + std::string data = this->number_json(obj, obj->state); + request->send(200, "text/json", data.c_str()); + return; + } + + if (match.method != "set") { + request->send(404); + return; + } + + auto call = obj->make_call(); + + if (request->hasParam("value")) { + String value = request->getParam("value")->value(); + optional value_f = parse_number(value.c_str()); + if (value_f.has_value()) + call.set_value(*value_f); + } + + this->defer([call]() mutable { call.perform(); }); + request->send(200); return; } request->send(404); @@ -661,9 +693,8 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM std::string WebServer::number_json(number::Number *obj, float value) { return json::build_json([obj, value](JsonObject root) { root["id"] = "number-" + obj->get_object_id(); - char buffer[64]; - snprintf(buffer, sizeof(buffer), "%f", value); - root["state"] = buffer; + std::string state = str_sprintf("%f", value); + root["state"] = state; root["value"] = value; }); } @@ -769,7 +800,7 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) { #endif #ifdef USE_NUMBER - if (request->method() == HTTP_GET && match.domain == "number") + if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "number") return true; #endif From afbf9897154776c77444a788679847060da908e6 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 17 Jan 2022 12:40:07 +1300 Subject: [PATCH 0171/1729] Bump version to 2022.1.0b3 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index daa61ebd9b..9005cc927a 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2022.1.0b2" +__version__ = "2022.1.0b3" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From 348f880e150d76aca5ff0c2a0dfd68fee32010b4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 17 Jan 2022 11:44:18 -0800 Subject: [PATCH 0172/1729] bump dashboard to 20220116.0 (#3061) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 05636da805..9add417bdf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ pyserial==3.5 platformio==5.2.4 # When updating platformio, also update Dockerfile esptool==3.2 click==8.0.3 -esphome-dashboard==20220113.2 +esphome-dashboard==20220116.0 aioesphomeapi==10.6.0 zeroconf==0.37.0 From 7b03e07908497c98069ca864e13eaf81403a1101 Mon Sep 17 00:00:00 2001 From: Martin <25747549+martgras@users.noreply.github.com> Date: Mon, 17 Jan 2022 21:05:13 +0100 Subject: [PATCH 0173/1729] [modbus_controller] add missing skip_updates (#3063) --- esphome/components/modbus_controller/switch/__init__.py | 2 ++ esphome/components/modbus_controller/switch/modbus_switch.h | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/esphome/components/modbus_controller/switch/__init__.py b/esphome/components/modbus_controller/switch/__init__.py index 9858d45617..0dfbd83cb8 100644 --- a/esphome/components/modbus_controller/switch/__init__.py +++ b/esphome/components/modbus_controller/switch/__init__.py @@ -18,6 +18,7 @@ from ..const import ( CONF_FORCE_NEW_RANGE, CONF_MODBUS_CONTROLLER_ID, CONF_REGISTER_TYPE, + CONF_SKIP_UPDATES, CONF_USE_WRITE_MULTIPLE, CONF_WRITE_LAMBDA, ) @@ -53,6 +54,7 @@ async def to_code(config): config[CONF_ADDRESS], byte_offset, config[CONF_BITMASK], + config[CONF_SKIP_UPDATES], config[CONF_FORCE_NEW_RANGE], ) await cg.register_component(var, config) diff --git a/esphome/components/modbus_controller/switch/modbus_switch.h b/esphome/components/modbus_controller/switch/modbus_switch.h index 5ac2af01a1..6732c01eef 100644 --- a/esphome/components/modbus_controller/switch/modbus_switch.h +++ b/esphome/components/modbus_controller/switch/modbus_switch.h @@ -10,14 +10,14 @@ namespace modbus_controller { class ModbusSwitch : public Component, public switch_::Switch, public SensorItem { public: ModbusSwitch(ModbusRegisterType register_type, uint16_t start_address, uint8_t offset, uint32_t bitmask, - bool force_new_range) + uint8_t skip_updates, bool force_new_range) : Component(), switch_::Switch() { this->register_type = register_type; this->start_address = start_address; this->offset = offset; this->bitmask = bitmask; this->sensor_value_type = SensorValueType::BIT; - this->skip_updates = 0; + this->skip_updates = skip_updates; this->register_count = 1; if (register_type == ModbusRegisterType::HOLDING || register_type == ModbusRegisterType::COIL) { this->start_address += offset; From 869743a7427f33e8e61fa9c4429b040ab0bf2561 Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Tue, 18 Jan 2022 02:29:57 +0100 Subject: [PATCH 0174/1729] Fail hard if no random bytes available for encryption (#3067) --- esphome/components/api/api_frame_helper.cpp | 8 +++++++- esphome/core/helpers.cpp | 7 +++---- esphome/core/helpers.h | 2 +- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp index 094dd67e33..d9eadb2aaa 100644 --- a/esphome/components/api/api_frame_helper.cpp +++ b/esphome/components/api/api_frame_helper.cpp @@ -1,6 +1,7 @@ #include "api_frame_helper.h" #include "esphome/core/log.h" +#include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "proto.h" #include @@ -721,7 +722,12 @@ APIError APINoiseFrameHelper::shutdown(int how) { } extern "C" { // declare how noise generates random bytes (here with a good HWRNG based on the RF system) -void noise_rand_bytes(void *output, size_t len) { esphome::random_bytes(reinterpret_cast(output), len); } +void noise_rand_bytes(void *output, size_t len) { + if (!esphome::random_bytes(reinterpret_cast(output), len)) { + ESP_LOGE(TAG, "Failed to acquire random bytes, rebooting!"); + arch_restart(); + } +} } #endif // USE_API_NOISE diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index e15e3a8ea3..5f29abe579 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -287,13 +287,12 @@ uint32_t random_uint32() { #endif } float random_float() { return static_cast(random_uint32()) / static_cast(UINT32_MAX); } -void random_bytes(uint8_t *data, size_t len) { +bool random_bytes(uint8_t *data, size_t len) { #ifdef USE_ESP32 esp_fill_random(data, len); + return true; #elif defined(USE_ESP8266) - if (os_get_random(data, len) != 0) { - ESP_LOGE(TAG, "Failed to generate random bytes!"); - } + return os_get_random(data, len) == 0; #else #error "No random source available for this configuration." #endif diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index f071b4a814..c9a27a2fab 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -311,7 +311,7 @@ uint32_t random_uint32(); /// Return a random float between 0 and 1. float random_float(); /// Generate \p len number of random bytes. -void random_bytes(uint8_t *data, size_t len); +bool random_bytes(uint8_t *data, size_t len); ///@} From 72d60f30f79a89c1b2506a32a22832d6a91c3e19 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 18 Jan 2022 15:49:31 +1300 Subject: [PATCH 0175/1729] Bump version to 2022.1.0b4 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 9005cc927a..a94902d946 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2022.1.0b3" +__version__ = "2022.1.0b4" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From 9296a078a7fea5c837b8c4c6b0d75096b7ed0e63 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 19 Jan 2022 16:08:27 +1300 Subject: [PATCH 0176/1729] Bump version to 2022.1.0 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index a94902d946..637cf43928 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2022.1.0b4" +__version__ = "2022.1.0" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From c19458696e78327516ae1559e2336f07aeb30114 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 20 Jan 2022 08:33:13 +1300 Subject: [PATCH 0177/1729] Add *.py.script files to distributions (#3074) --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) diff --git a/MANIFEST.in b/MANIFEST.in index 0fe80762b3..a3126404f2 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -4,4 +4,5 @@ include requirements.txt include esphome/dashboard/templates/*.html recursive-include esphome/dashboard/static *.ico *.js *.css *.woff* LICENSE recursive-include esphome *.cpp *.h *.tcc +recursive-include esphome *.py.script recursive-include esphome LICENSE.txt From cd6f4fb93fbad3df8dc2539611931da33d7d323a Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 20 Jan 2022 08:34:18 +1300 Subject: [PATCH 0178/1729] Bump version to 2022.1.1 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 637cf43928..a9f4f0900a 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2022.1.0" +__version__ = "2022.1.1" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From 76a238912b2b474c928e5ce0bceba1e05b15ebdd Mon Sep 17 00:00:00 2001 From: Martin <25747549+martgras@users.noreply.github.com> Date: Wed, 19 Jan 2022 21:19:24 +0100 Subject: [PATCH 0179/1729] [modbus_controller] fix incorrect start address for number write (#3073) --- .../components/modbus_controller/number/modbus_number.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/esphome/components/modbus_controller/number/modbus_number.cpp b/esphome/components/modbus_controller/number/modbus_number.cpp index 5e977f5df4..a0e990d272 100644 --- a/esphome/components/modbus_controller/number/modbus_number.cpp +++ b/esphome/components/modbus_controller/number/modbus_number.cpp @@ -57,9 +57,11 @@ void ModbusNumber::control(float value) { // Create and send the write command ModbusCommandItem write_cmd; if (this->register_count == 1 && !this->use_write_multiple_) { - write_cmd = ModbusCommandItem::create_write_single_command(parent_, this->start_address + this->offset, data[0]); + // since offset is in bytes and a register is 16 bits we get the start by adding offset/2 + write_cmd = + ModbusCommandItem::create_write_single_command(parent_, this->start_address + this->offset / 2, data[0]); } else { - write_cmd = ModbusCommandItem::create_write_multiple_command(parent_, this->start_address + this->offset, + write_cmd = ModbusCommandItem::create_write_multiple_command(parent_, this->start_address + this->offset / 2, this->register_count, data); } // publish new value From 46af4cad6ef8cceddd6787a7ddc41e1c39dc63a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pl=C3=A1cido=20Revilla?= Date: Sat, 22 Jan 2022 12:04:36 -0800 Subject: [PATCH 0180/1729] Set the wrapped single light in light partition to internal (#3092) --- esphome/components/partition/light.py | 1 - 1 file changed, 1 deletion(-) diff --git a/esphome/components/partition/light.py b/esphome/components/partition/light.py index 822b7ac306..73cda2c926 100644 --- a/esphome/components/partition/light.py +++ b/esphome/components/partition/light.py @@ -104,7 +104,6 @@ async def to_code(config): ) light_state = cg.new_Pvariable(conf[CONF_LIGHT_ID], "", wrapper) await cg.register_component(light_state, conf) - cg.add(cg.App.register_light(light_state)) segments.append(AddressableSegment(light_state, 0, 1, False)) else: From bdb9546ca3d9743cd382ce1031b9ddff4dee14a1 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 25 Jan 2022 09:11:20 +1300 Subject: [PATCH 0181/1729] Bump version to 2022.1.2 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index a9f4f0900a..e97e0ac41a 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2022.1.1" +__version__ = "2022.1.2" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From 7479e0aadaeb4bd62294f9d82af1cfb48b077139 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 31 Jan 2022 10:58:27 +1300 Subject: [PATCH 0182/1729] Fix backwards string case helpers (#3126) --- esphome/components/dallas/dallas_component.cpp | 2 +- esphome/core/helpers.cpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/dallas/dallas_component.cpp b/esphome/components/dallas/dallas_component.cpp index 3610e79447..17412b08c9 100644 --- a/esphome/components/dallas/dallas_component.cpp +++ b/esphome/components/dallas/dallas_component.cpp @@ -235,7 +235,7 @@ float DallasTemperatureSensor::get_temp_c() { return temp / 128.0f; } -std::string DallasTemperatureSensor::unique_id() { return "dallas-" + str_upper_case(format_hex(this->address_)); } +std::string DallasTemperatureSensor::unique_id() { return "dallas-" + str_lower_case(format_hex(this->address_)); } } // namespace dallas } // namespace esphome diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 5f29abe579..11a2b1acb0 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -316,8 +316,8 @@ template std::string str_ctype_transform(const std::string &str) std::transform(str.begin(), str.end(), result.begin(), [](unsigned char ch) { return fn(ch); }); return result; } -std::string str_lower_case(const std::string &str) { return str_ctype_transform(str); } -std::string str_upper_case(const std::string &str) { return str_ctype_transform(str); } +std::string str_lower_case(const std::string &str) { return str_ctype_transform(str); } +std::string str_upper_case(const std::string &str) { return str_ctype_transform(str); } std::string str_snake_case(const std::string &str) { std::string result; result.resize(str.length()); From c7f091ab10effc0e386163e2832777109b7b89d0 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 2 Feb 2022 22:16:24 +1300 Subject: [PATCH 0183/1729] Bump version to 2022.1.3 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index e97e0ac41a..243b3d84b1 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2022.1.2" +__version__ = "2022.1.3" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From 21803607e79ecb799171c59081c53979c1bfc84a Mon Sep 17 00:00:00 2001 From: Keilin Bickar Date: Thu, 3 Feb 2022 13:24:31 -0500 Subject: [PATCH 0184/1729] Add new Lock core component (#2958) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/api/api.proto | 59 ++++ esphome/components/api/api_connection.cpp | 43 +++ esphome/components/api/api_connection.h | 5 + esphome/components/api/api_pb2.cpp | 262 +++++++++++++++++- esphome/components/api/api_pb2.h | 65 +++++ esphome/components/api/api_pb2_service.cpp | 42 +++ esphome/components/api/api_pb2_service.h | 15 + esphome/components/api/api_server.cpp | 9 + esphome/components/api/api_server.h | 3 + esphome/components/api/list_entities.cpp | 3 + esphome/components/api/list_entities.h | 3 + esphome/components/api/subscribe_state.cpp | 3 + esphome/components/api/subscribe_state.h | 3 + esphome/components/api/util.cpp | 15 + esphome/components/api/util.h | 6 + esphome/components/cover/__init__.py | 2 +- esphome/components/lock/__init__.py | 102 +++++++ esphome/components/lock/automation.h | 87 ++++++ esphome/components/lock/lock.cpp | 109 ++++++++ esphome/components/lock/lock.h | 178 ++++++++++++ esphome/components/mqtt/__init__.py | 1 + esphome/components/mqtt/mqtt_lock.cpp | 55 ++++ esphome/components/mqtt/mqtt_lock.h | 41 +++ esphome/components/output/lock/__init__.py | 23 ++ .../components/output/lock/output_lock.cpp | 22 ++ esphome/components/output/lock/output_lock.h | 24 ++ .../prometheus/prometheus_handler.cpp | 30 ++ .../prometheus/prometheus_handler.h | 7 + esphome/components/template/lock/__init__.py | 103 +++++++ .../template/lock/template_lock.cpp | 59 ++++ .../components/template/lock/template_lock.h | 38 +++ esphome/components/web_server/web_server.cpp | 70 +++++ esphome/components/web_server/web_server.h | 10 + esphome/const.py | 5 + esphome/core/application.h | 19 ++ esphome/core/controller.cpp | 6 + esphome/core/controller.h | 6 + esphome/core/defines.h | 1 + script/ci-custom.py | 1 + tests/test1.yaml | 25 ++ 41 files changed, 1558 insertions(+), 3 deletions(-) create mode 100644 esphome/components/lock/__init__.py create mode 100644 esphome/components/lock/automation.h create mode 100644 esphome/components/lock/lock.cpp create mode 100644 esphome/components/lock/lock.h create mode 100644 esphome/components/mqtt/mqtt_lock.cpp create mode 100644 esphome/components/mqtt/mqtt_lock.h create mode 100644 esphome/components/output/lock/__init__.py create mode 100644 esphome/components/output/lock/output_lock.cpp create mode 100644 esphome/components/output/lock/output_lock.h create mode 100644 esphome/components/template/lock/__init__.py create mode 100644 esphome/components/template/lock/template_lock.cpp create mode 100644 esphome/components/template/lock/template_lock.h diff --git a/CODEOWNERS b/CODEOWNERS index f075cc8649..5fa3090aaf 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -89,6 +89,7 @@ esphome/components/json/* @OttoWinter esphome/components/kalman_combinator/* @Cat-Ion esphome/components/ledc/* @OttoWinter esphome/components/light/* @esphome/core +esphome/components/lock/* @esphome/core esphome/components/logger/* @esphome/core esphome/components/ltr390/* @sjtrny esphome/components/max7219digit/* @rspaargaren diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 9d43e22497..3ab426979e 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -41,6 +41,7 @@ service APIConnection { rpc number_command (NumberCommandRequest) returns (void) {} rpc select_command (SelectCommandRequest) returns (void) {} rpc button_command (ButtonCommandRequest) returns (void) {} + rpc lock_command (LockCommandRequest) returns (void) {} } @@ -956,6 +957,63 @@ message SelectCommandRequest { string state = 2; } + +// ==================== LOCK ==================== +enum LockState { + LOCK_STATE_NONE = 0; + LOCK_STATE_LOCKED = 1; + LOCK_STATE_UNLOCKED = 2; + LOCK_STATE_JAMMED = 3; + LOCK_STATE_LOCKING = 4; + LOCK_STATE_UNLOCKING = 5; +} +enum LockCommand { + LOCK_UNLOCK = 0; + LOCK_LOCK = 1; + LOCK_OPEN = 2; +} +message ListEntitiesLockResponse { + option (id) = 58; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_LOCK"; + + string object_id = 1; + fixed32 key = 2; + string name = 3; + string unique_id = 4; + + string icon = 5; + bool disabled_by_default = 6; + EntityCategory entity_category = 7; + bool assumed_state = 8; + + bool supports_open = 9; + bool requires_code = 10; + + # Not yet implemented: + string code_format = 11; +} +message LockStateResponse { + option (id) = 59; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_LOCK"; + option (no_delay) = true; + fixed32 key = 1; + LockState state = 2; +} +message LockCommandRequest { + option (id) = 60; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_LOCK"; + option (no_delay) = true; + fixed32 key = 1; + LockCommand command = 2; + + # Not yet implemented: + bool has_code = 3; + string code = 4; +} + // ==================== BUTTON ==================== message ListEntitiesButtonResponse { option (id) = 61; @@ -980,3 +1038,4 @@ message ButtonCommandRequest { fixed32 key = 1; } + diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 8a106dc39c..21388b547e 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -700,6 +700,49 @@ void APIConnection::button_command(const ButtonCommandRequest &msg) { } #endif +#ifdef USE_LOCK +bool APIConnection::send_lock_state(lock::Lock *a_lock, lock::LockState state) { + if (!this->state_subscription_) + return false; + + LockStateResponse resp{}; + resp.key = a_lock->get_object_id_hash(); + resp.state = static_cast(state); + return this->send_lock_state_response(resp); +} +bool APIConnection::send_lock_info(lock::Lock *a_lock) { + ListEntitiesLockResponse msg; + msg.key = a_lock->get_object_id_hash(); + msg.object_id = a_lock->get_object_id(); + msg.name = a_lock->get_name(); + msg.unique_id = get_default_unique_id("lock", a_lock); + msg.icon = a_lock->get_icon(); + msg.assumed_state = a_lock->traits.get_assumed_state(); + msg.disabled_by_default = a_lock->is_disabled_by_default(); + msg.entity_category = static_cast(a_lock->get_entity_category()); + msg.supports_open = a_lock->traits.get_supports_open(); + msg.requires_code = a_lock->traits.get_requires_code(); + return this->send_list_entities_lock_response(msg); +} +void APIConnection::lock_command(const LockCommandRequest &msg) { + lock::Lock *a_lock = App.get_lock_by_key(msg.key); + if (a_lock == nullptr) + return; + + switch (msg.command) { + case enums::LOCK_UNLOCK: + a_lock->unlock(); + break; + case enums::LOCK_LOCK: + a_lock->lock(); + break; + case enums::LOCK_OPEN: + a_lock->open(); + break; + } +} +#endif + #ifdef USE_ESP32_CAMERA void APIConnection::send_camera_state(std::shared_ptr image) { if (!this->state_subscription_) diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index c1f520c83b..10f0becc54 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -77,6 +77,11 @@ class APIConnection : public APIServerConnection { #ifdef USE_BUTTON bool send_button_info(button::Button *button); void button_command(const ButtonCommandRequest &msg) override; +#endif +#ifdef USE_LOCK + bool send_lock_state(lock::Lock *a_lock, lock::LockState state); + bool send_lock_info(lock::Lock *a_lock); + void lock_command(const LockCommandRequest &msg) override; #endif bool send_log_message(int level, const char *tag, const char *line); void send_homeassistant_service_call(const HomeassistantServiceResponse &call) { diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 4ecd727f29..e7e0476afc 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -278,6 +278,36 @@ template<> const char *proto_enum_to_string(enums::NumberMode return "UNKNOWN"; } } +template<> const char *proto_enum_to_string(enums::LockState value) { + switch (value) { + case enums::LOCK_STATE_NONE: + return "LOCK_STATE_NONE"; + case enums::LOCK_STATE_LOCKED: + return "LOCK_STATE_LOCKED"; + case enums::LOCK_STATE_UNLOCKED: + return "LOCK_STATE_UNLOCKED"; + case enums::LOCK_STATE_JAMMED: + return "LOCK_STATE_JAMMED"; + case enums::LOCK_STATE_LOCKING: + return "LOCK_STATE_LOCKING"; + case enums::LOCK_STATE_UNLOCKING: + return "LOCK_STATE_UNLOCKING"; + default: + return "UNKNOWN"; + } +} +template<> const char *proto_enum_to_string(enums::LockCommand value) { + switch (value) { + case enums::LOCK_UNLOCK: + return "LOCK_UNLOCK"; + case enums::LOCK_LOCK: + return "LOCK_LOCK"; + case enums::LOCK_OPEN: + return "LOCK_OPEN"; + default: + return "UNKNOWN"; + } +} bool HelloRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { case 1: { @@ -4186,6 +4216,234 @@ void SelectCommandRequest::dump_to(std::string &out) const { out.append("}"); } #endif +bool ListEntitiesLockResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 6: { + this->disabled_by_default = value.as_bool(); + return true; + } + case 7: { + this->entity_category = value.as_enum(); + return true; + } + case 8: { + this->assumed_state = value.as_bool(); + return true; + } + case 9: { + this->supports_open = value.as_bool(); + return true; + } + case 10: { + this->requires_code = value.as_bool(); + return true; + } + default: + return false; + } +} +bool ListEntitiesLockResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 1: { + this->object_id = value.as_string(); + return true; + } + case 3: { + this->name = value.as_string(); + return true; + } + case 4: { + this->unique_id = value.as_string(); + return true; + } + case 5: { + this->icon = value.as_string(); + return true; + } + case 11: { + this->code_format = value.as_string(); + return true; + } + default: + return false; + } +} +bool ListEntitiesLockResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 2: { + this->key = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void ListEntitiesLockResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_string(1, this->object_id); + buffer.encode_fixed32(2, this->key); + buffer.encode_string(3, this->name); + buffer.encode_string(4, this->unique_id); + buffer.encode_string(5, this->icon); + buffer.encode_bool(6, this->disabled_by_default); + buffer.encode_enum(7, this->entity_category); + buffer.encode_bool(8, this->assumed_state); + buffer.encode_bool(9, this->supports_open); + buffer.encode_bool(10, this->requires_code); + buffer.encode_string(11, this->code_format); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void ListEntitiesLockResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("ListEntitiesLockResponse {\n"); + out.append(" object_id: "); + out.append("'").append(this->object_id).append("'"); + out.append("\n"); + + out.append(" key: "); + sprintf(buffer, "%u", this->key); + out.append(buffer); + out.append("\n"); + + out.append(" name: "); + out.append("'").append(this->name).append("'"); + out.append("\n"); + + out.append(" unique_id: "); + out.append("'").append(this->unique_id).append("'"); + out.append("\n"); + + out.append(" icon: "); + out.append("'").append(this->icon).append("'"); + out.append("\n"); + + out.append(" disabled_by_default: "); + out.append(YESNO(this->disabled_by_default)); + out.append("\n"); + + out.append(" entity_category: "); + out.append(proto_enum_to_string(this->entity_category)); + out.append("\n"); + + out.append(" assumed_state: "); + out.append(YESNO(this->assumed_state)); + out.append("\n"); + + out.append(" supports_open: "); + out.append(YESNO(this->supports_open)); + out.append("\n"); + + out.append(" requires_code: "); + out.append(YESNO(this->requires_code)); + out.append("\n"); + + out.append(" code_format: "); + out.append("'").append(this->code_format).append("'"); + out.append("\n"); + out.append("}"); +} +#endif +bool LockStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 2: { + this->state = value.as_enum(); + return true; + } + default: + return false; + } +} +bool LockStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 1: { + this->key = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void LockStateResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_fixed32(1, this->key); + buffer.encode_enum(2, this->state); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void LockStateResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("LockStateResponse {\n"); + out.append(" key: "); + sprintf(buffer, "%u", this->key); + out.append(buffer); + out.append("\n"); + + out.append(" state: "); + out.append(proto_enum_to_string(this->state)); + out.append("\n"); + out.append("}"); +} +#endif +bool LockCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 2: { + this->command = value.as_enum(); + return true; + } + case 3: { + this->has_code = value.as_bool(); + return true; + } + default: + return false; + } +} +bool LockCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 4: { + this->code = value.as_string(); + return true; + } + default: + return false; + } +} +bool LockCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 1: { + this->key = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void LockCommandRequest::encode(ProtoWriteBuffer buffer) const { + buffer.encode_fixed32(1, this->key); + buffer.encode_enum(2, this->command); + buffer.encode_bool(3, this->has_code); + buffer.encode_string(4, this->code); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void LockCommandRequest::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("LockCommandRequest {\n"); + out.append(" key: "); + sprintf(buffer, "%u", this->key); + out.append(buffer); + out.append("\n"); + + out.append(" command: "); + out.append(proto_enum_to_string(this->command)); + out.append("\n"); + + out.append(" has_code: "); + out.append(YESNO(this->has_code)); + out.append("\n"); + + out.append(" code: "); + out.append("'").append(this->code).append("'"); + out.append("\n"); + out.append("}"); +} +#endif bool ListEntitiesButtonResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 6: { @@ -4248,7 +4506,7 @@ void ListEntitiesButtonResponse::encode(ProtoWriteBuffer buffer) const { } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesButtonResponse::dump_to(std::string &out) const { - char buffer[64]; + __attribute__((unused)) char buffer[64]; out.append("ListEntitiesButtonResponse {\n"); out.append(" object_id: "); out.append("'").append(this->object_id).append("'"); @@ -4298,7 +4556,7 @@ bool ButtonCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { void ButtonCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); } #ifdef HAS_PROTO_MESSAGE_DUMP void ButtonCommandRequest::dump_to(std::string &out) const { - char buffer[64]; + __attribute__((unused)) char buffer[64]; out.append("ButtonCommandRequest {\n"); out.append(" key: "); sprintf(buffer, "%u", this->key); diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 48ecb5f682..4c9a0e9c0f 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -128,6 +128,19 @@ enum NumberMode : uint32_t { NUMBER_MODE_BOX = 1, NUMBER_MODE_SLIDER = 2, }; +enum LockState : uint32_t { + LOCK_STATE_NONE = 0, + LOCK_STATE_LOCKED = 1, + LOCK_STATE_UNLOCKED = 2, + LOCK_STATE_JAMMED = 3, + LOCK_STATE_LOCKING = 4, + LOCK_STATE_UNLOCKING = 5, +}; +enum LockCommand : uint32_t { + LOCK_UNLOCK = 0, + LOCK_LOCK = 1, + LOCK_OPEN = 2, +}; } // namespace enums @@ -1049,6 +1062,58 @@ class SelectCommandRequest : public ProtoMessage { bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; }; +class ListEntitiesLockResponse : public ProtoMessage { + public: + std::string object_id{}; + uint32_t key{0}; + std::string name{}; + std::string unique_id{}; + std::string icon{}; + bool disabled_by_default{false}; + enums::EntityCategory entity_category{}; + bool assumed_state{false}; + bool supports_open{false}; + bool requires_code{false}; + std::string code_format{}; + void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class LockStateResponse : public ProtoMessage { + public: + uint32_t key{0}; + enums::LockState state{}; + void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class LockCommandRequest : public ProtoMessage { + public: + uint32_t key{0}; + enums::LockCommand command{}; + bool has_code{false}; + std::string code{}; + void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; class ListEntitiesButtonResponse : public ProtoMessage { public: std::string object_id{}; diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index 567fbf02c9..d981a3bf4e 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -282,6 +282,24 @@ bool APIServerConnectionBase::send_select_state_response(const SelectStateRespon #endif #ifdef USE_SELECT #endif +#ifdef USE_LOCK +bool APIServerConnectionBase::send_list_entities_lock_response(const ListEntitiesLockResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "send_list_entities_lock_response: %s", msg.dump().c_str()); +#endif + return this->send_message_(msg, 58); +} +#endif +#ifdef USE_LOCK +bool APIServerConnectionBase::send_lock_state_response(const LockStateResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "send_lock_state_response: %s", msg.dump().c_str()); +#endif + return this->send_message_(msg, 59); +} +#endif +#ifdef USE_LOCK +#endif #ifdef USE_BUTTON bool APIServerConnectionBase::send_list_entities_button_response(const ListEntitiesButtonResponse &msg) { #ifdef HAS_PROTO_MESSAGE_DUMP @@ -523,6 +541,17 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, ESP_LOGVV(TAG, "on_select_command_request: %s", msg.dump().c_str()); #endif this->on_select_command_request(msg); +#endif + break; + } + case 60: { +#ifdef USE_LOCK + LockCommandRequest msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "on_lock_command_request: %s", msg.dump().c_str()); +#endif + this->on_lock_command_request(msg); #endif break; } @@ -771,6 +800,19 @@ void APIServerConnection::on_button_command_request(const ButtonCommandRequest & this->button_command(msg); } #endif +#ifdef USE_LOCK +void APIServerConnection::on_lock_command_request(const LockCommandRequest &msg) { + if (!this->is_connection_setup()) { + this->on_no_setup_connection(); + return; + } + if (!this->is_authenticated()) { + this->on_unauthenticated_access(); + return; + } + this->lock_command(msg); +} +#endif } // namespace api } // namespace esphome diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index 50b08d3ec4..5aaf831c91 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -130,6 +130,15 @@ class APIServerConnectionBase : public ProtoService { #ifdef USE_SELECT virtual void on_select_command_request(const SelectCommandRequest &value){}; #endif +#ifdef USE_LOCK + bool send_list_entities_lock_response(const ListEntitiesLockResponse &msg); +#endif +#ifdef USE_LOCK + bool send_lock_state_response(const LockStateResponse &msg); +#endif +#ifdef USE_LOCK + virtual void on_lock_command_request(const LockCommandRequest &value){}; +#endif #ifdef USE_BUTTON bool send_list_entities_button_response(const ListEntitiesButtonResponse &msg); #endif @@ -180,6 +189,9 @@ class APIServerConnection : public APIServerConnectionBase { #endif #ifdef USE_BUTTON virtual void button_command(const ButtonCommandRequest &msg) = 0; +#endif +#ifdef USE_LOCK + virtual void lock_command(const LockCommandRequest &msg) = 0; #endif protected: void on_hello_request(const HelloRequest &msg) override; @@ -221,6 +233,9 @@ class APIServerConnection : public APIServerConnectionBase { #ifdef USE_BUTTON void on_button_command_request(const ButtonCommandRequest &msg) override; #endif +#ifdef USE_LOCK + void on_lock_command_request(const LockCommandRequest &msg) override; +#endif }; } // namespace api diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 93c9209716..4521cc5bfc 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -263,6 +263,15 @@ void APIServer::on_select_update(select::Select *obj, const std::string &state) } #endif +#ifdef USE_LOCK +void APIServer::on_lock_update(lock::Lock *obj) { + if (obj->is_internal()) + return; + for (auto &c : this->clients_) + c->send_lock_state(obj, obj->state); +} +#endif + float APIServer::get_setup_priority() const { return setup_priority::AFTER_WIFI; } void APIServer::set_port(uint16_t port) { this->port_ = port; } APIServer *global_api_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 23b01df375..3214da5b3d 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -66,6 +66,9 @@ class APIServer : public Component, public Controller { #endif #ifdef USE_SELECT void on_select_update(select::Select *obj, const std::string &state) override; +#endif +#ifdef USE_LOCK + void on_lock_update(lock::Lock *obj) override; #endif void send_homeassistant_service_call(const HomeassistantServiceResponse &call); void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); } diff --git a/esphome/components/api/list_entities.cpp b/esphome/components/api/list_entities.cpp index 35a590a828..fb0dfa3d05 100644 --- a/esphome/components/api/list_entities.cpp +++ b/esphome/components/api/list_entities.cpp @@ -35,6 +35,9 @@ bool ListEntitiesIterator::on_text_sensor(text_sensor::TextSensor *text_sensor) return this->client_->send_text_sensor_info(text_sensor); } #endif +#ifdef USE_LOCK +bool ListEntitiesIterator::on_lock(lock::Lock *a_lock) { return this->client_->send_lock_info(a_lock); } +#endif bool ListEntitiesIterator::on_end() { return this->client_->send_list_info_done(); } ListEntitiesIterator::ListEntitiesIterator(APIServer *server, APIConnection *client) diff --git a/esphome/components/api/list_entities.h b/esphome/components/api/list_entities.h index 81b814676a..bfceb39ebf 100644 --- a/esphome/components/api/list_entities.h +++ b/esphome/components/api/list_entities.h @@ -48,6 +48,9 @@ class ListEntitiesIterator : public ComponentIterator { #endif #ifdef USE_SELECT bool on_select(select::Select *select) override; +#endif +#ifdef USE_LOCK + bool on_lock(lock::Lock *a_lock) override; #endif bool on_end() override; diff --git a/esphome/components/api/subscribe_state.cpp b/esphome/components/api/subscribe_state.cpp index 07b3913ff7..10416ecc5c 100644 --- a/esphome/components/api/subscribe_state.cpp +++ b/esphome/components/api/subscribe_state.cpp @@ -47,6 +47,9 @@ bool InitialStateIterator::on_select(select::Select *select) { return this->client_->send_select_state(select, select->state); } #endif +#ifdef USE_LOCK +bool InitialStateIterator::on_lock(lock::Lock *a_lock) { return this->client_->send_lock_state(a_lock, a_lock->state); } +#endif InitialStateIterator::InitialStateIterator(APIServer *server, APIConnection *client) : ComponentIterator(server), client_(client) {} diff --git a/esphome/components/api/subscribe_state.h b/esphome/components/api/subscribe_state.h index 4b83b5e793..caea013f84 100644 --- a/esphome/components/api/subscribe_state.h +++ b/esphome/components/api/subscribe_state.h @@ -45,6 +45,9 @@ class InitialStateIterator : public ComponentIterator { #endif #ifdef USE_SELECT bool on_select(select::Select *select) override; +#endif +#ifdef USE_LOCK + bool on_lock(lock::Lock *a_lock) override; #endif protected: APIConnection *client_; diff --git a/esphome/components/api/util.cpp b/esphome/components/api/util.cpp index f5fd752101..fd55f89f9b 100644 --- a/esphome/components/api/util.cpp +++ b/esphome/components/api/util.cpp @@ -212,6 +212,21 @@ void ComponentIterator::advance() { } } break; +#endif +#ifdef USE_LOCK + case IteratorState::LOCK: + if (this->at_ >= App.get_locks().size()) { + advance_platform = true; + } else { + auto *a_lock = App.get_locks()[this->at_]; + if (a_lock->is_internal()) { + success = true; + break; + } else { + success = this->on_lock(a_lock); + } + } + break; #endif case IteratorState::MAX: if (this->on_end()) { diff --git a/esphome/components/api/util.h b/esphome/components/api/util.h index f329867a4e..9204b0829e 100644 --- a/esphome/components/api/util.h +++ b/esphome/components/api/util.h @@ -56,6 +56,9 @@ class ComponentIterator { #endif #ifdef USE_SELECT virtual bool on_select(select::Select *select) = 0; +#endif +#ifdef USE_LOCK + virtual bool on_lock(lock::Lock *a_lock) = 0; #endif virtual bool on_end(); @@ -99,6 +102,9 @@ class ComponentIterator { #endif #ifdef USE_SELECT SELECT, +#endif +#ifdef USE_LOCK + LOCK, #endif MAX, } state_{IteratorState::NONE}; diff --git a/esphome/components/cover/__init__.py b/esphome/components/cover/__init__.py index 0fd27f3f27..d2421f07d9 100644 --- a/esphome/components/cover/__init__.py +++ b/esphome/components/cover/__init__.py @@ -7,6 +7,7 @@ from esphome.const import ( CONF_ID, CONF_DEVICE_CLASS, CONF_STATE, + CONF_ON_OPEN, CONF_POSITION, CONF_POSITION_COMMAND_TOPIC, CONF_POSITION_STATE_TOPIC, @@ -74,7 +75,6 @@ CoverClosedTrigger = cover_ns.class_( "CoverClosedTrigger", automation.Trigger.template() ) -CONF_ON_OPEN = "on_open" CONF_ON_CLOSED = "on_closed" COVER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( diff --git a/esphome/components/lock/__init__.py b/esphome/components/lock/__init__.py new file mode 100644 index 0000000000..f659c48a6e --- /dev/null +++ b/esphome/components/lock/__init__.py @@ -0,0 +1,102 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.automation import Condition, maybe_simple_id +from esphome.components import mqtt +from esphome.const import ( + CONF_ID, + CONF_ON_LOCK, + CONF_ON_UNLOCK, + CONF_TRIGGER_ID, + CONF_MQTT_ID, +) +from esphome.core import CORE, coroutine_with_priority +from esphome.cpp_helpers import setup_entity + +CODEOWNERS = ["@esphome/core"] +IS_PLATFORM_COMPONENT = True + +lock_ns = cg.esphome_ns.namespace("lock") +Lock = lock_ns.class_("Lock", cg.EntityBase) +LockPtr = Lock.operator("ptr") +LockCall = lock_ns.class_("LockCall") + +UnlockAction = lock_ns.class_("UnlockAction", automation.Action) +LockAction = lock_ns.class_("LockAction", automation.Action) +OpenAction = lock_ns.class_("OpenAction", automation.Action) +LockPublishAction = lock_ns.class_("LockPublishAction", automation.Action) + +LockCondition = lock_ns.class_("LockCondition", Condition) +LockLockTrigger = lock_ns.class_("LockLockTrigger", automation.Trigger.template()) +LockUnlockTrigger = lock_ns.class_("LockUnlockTrigger", automation.Trigger.template()) + +LOCK_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( + { + cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTLockComponent), + cv.Optional(CONF_ON_LOCK): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LockLockTrigger), + } + ), + cv.Optional(CONF_ON_UNLOCK): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LockUnlockTrigger), + } + ), + } +) + + +async def setup_lock_core_(var, config): + await setup_entity(var, config) + + for conf in config.get(CONF_ON_LOCK, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + for conf in config.get(CONF_ON_UNLOCK, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + + if CONF_MQTT_ID in config: + mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var) + await mqtt.register_mqtt_component(mqtt_, config) + + +async def register_lock(var, config): + if not CORE.has_id(config[CONF_ID]): + var = cg.Pvariable(config[CONF_ID], var) + cg.add(cg.App.register_lock(var)) + await setup_lock_core_(var, config) + + +LOCK_ACTION_SCHEMA = maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(Lock), + } +) + + +@automation.register_action("lock.unlock", UnlockAction, LOCK_ACTION_SCHEMA) +@automation.register_action("lock.lock", LockAction, LOCK_ACTION_SCHEMA) +@automation.register_action("lock.open", OpenAction, LOCK_ACTION_SCHEMA) +async def lock_action_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) + + +@automation.register_condition("lock.is_locked", LockCondition, LOCK_ACTION_SCHEMA) +async def lock_is_on_to_code(config, condition_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(condition_id, template_arg, paren, True) + + +@automation.register_condition("lock.is_unlocked", LockCondition, LOCK_ACTION_SCHEMA) +async def lock_is_off_to_code(config, condition_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(condition_id, template_arg, paren, False) + + +@coroutine_with_priority(100.0) +async def to_code(config): + cg.add_global(lock_ns.using) + cg.add_define("USE_LOCK") diff --git a/esphome/components/lock/automation.h b/esphome/components/lock/automation.h new file mode 100644 index 0000000000..74cfbe2ef6 --- /dev/null +++ b/esphome/components/lock/automation.h @@ -0,0 +1,87 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/lock/lock.h" + +namespace esphome { +namespace lock { + +template class LockAction : public Action { + public: + explicit LockAction(Lock *a_lock) : lock_(a_lock) {} + + void play(Ts... x) override { this->lock_->lock(); } + + protected: + Lock *lock_; +}; + +template class UnlockAction : public Action { + public: + explicit UnlockAction(Lock *a_lock) : lock_(a_lock) {} + + void play(Ts... x) override { this->lock_->unlock(); } + + protected: + Lock *lock_; +}; + +template class OpenAction : public Action { + public: + explicit OpenAction(Lock *a_lock) : lock_(a_lock) {} + + void play(Ts... x) override { this->lock_->open(); } + + protected: + Lock *lock_; +}; + +template class LockCondition : public Condition { + public: + LockCondition(Lock *parent, bool state) : parent_(parent), state_(state) {} + bool check(Ts... x) override { + auto check_state = this->state_ ? LockState::LOCK_STATE_LOCKED : LockState::LOCK_STATE_UNLOCKED; + return this->parent_->state == check_state; + } + + protected: + Lock *parent_; + bool state_; +}; + +class LockLockTrigger : public Trigger<> { + public: + LockLockTrigger(Lock *a_lock) { + a_lock->add_on_state_callback([this, a_lock]() { + if (a_lock->state == LockState::LOCK_STATE_LOCKED) { + this->trigger(); + } + }); + } +}; + +class LockUnlockTrigger : public Trigger<> { + public: + LockUnlockTrigger(Lock *a_lock) { + a_lock->add_on_state_callback([this, a_lock]() { + if (a_lock->state == LockState::LOCK_STATE_UNLOCKED) { + this->trigger(); + } + }); + } +}; + +template class LockPublishAction : public Action { + public: + LockPublishAction(Lock *a_lock) : lock_(a_lock) {} + TEMPLATABLE_VALUE(LockState, state) + + void play(Ts... x) override { this->lock_->publish_state(this->state_.value(x...)); } + + protected: + Lock *lock_; +}; + +} // namespace lock +} // namespace esphome diff --git a/esphome/components/lock/lock.cpp b/esphome/components/lock/lock.cpp new file mode 100644 index 0000000000..e32ab6d0a6 --- /dev/null +++ b/esphome/components/lock/lock.cpp @@ -0,0 +1,109 @@ +#include "lock.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace lock { + +static const char *const TAG = "lock"; + +const char *lock_state_to_string(LockState state) { + switch (state) { + case LOCK_STATE_LOCKED: + return "LOCKED"; + case LOCK_STATE_UNLOCKED: + return "UNLOCKED"; + case LOCK_STATE_JAMMED: + return "JAMMED"; + case LOCK_STATE_LOCKING: + return "LOCKING"; + case LOCK_STATE_UNLOCKING: + return "UNLOCKING"; + case LOCK_STATE_NONE: + default: + return "UNKNOWN"; + } +} + +Lock::Lock(const std::string &name) : EntityBase(name), state(LOCK_STATE_NONE) {} +Lock::Lock() : Lock("") {} +LockCall Lock::make_call() { return LockCall(this); } + +void Lock::lock() { + auto call = this->make_call(); + call.set_state(LOCK_STATE_LOCKED); + this->control(call); +} +void Lock::unlock() { + auto call = this->make_call(); + call.set_state(LOCK_STATE_UNLOCKED); + this->control(call); +} +void Lock::open() { + if (traits.get_supports_open()) { + ESP_LOGD(TAG, "'%s' Opening.", this->get_name().c_str()); + this->open_latch(); + } else { + ESP_LOGW(TAG, "'%s' Does not support Open.", this->get_name().c_str()); + } +} +void Lock::publish_state(LockState state) { + if (!this->publish_dedup_.next(state)) + return; + + this->state = state; + this->rtc_.save(&this->state); + ESP_LOGD(TAG, "'%s': Sending state %s", this->name_.c_str(), lock_state_to_string(state)); + this->state_callback_.call(); +} + +void Lock::add_on_state_callback(std::function &&callback) { this->state_callback_.add(std::move(callback)); } +uint32_t Lock::hash_base() { return 856245656UL; } + +void LockCall::perform() { + ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); + this->validate_(); + if (this->state_.has_value()) { + const char *state_s = lock_state_to_string(*this->state_); + ESP_LOGD(TAG, " State: %s", state_s); + } + this->parent_->control(*this); +} +void LockCall::validate_() { + if (this->state_.has_value()) { + auto state = *this->state_; + if (!this->parent_->traits.supports_state(state)) { + ESP_LOGW(TAG, " State %s is not supported by this device!", lock_state_to_string(*this->state_)); + this->state_.reset(); + } + } +} +LockCall &LockCall::set_state(LockState state) { + this->state_ = state; + return *this; +} +LockCall &LockCall::set_state(optional state) { + this->state_ = state; + return *this; +} +LockCall &LockCall::set_state(const std::string &state) { + if (str_equals_case_insensitive(state, "LOCKED")) { + this->set_state(LOCK_STATE_LOCKED); + } else if (str_equals_case_insensitive(state, "UNLOCKED")) { + this->set_state(LOCK_STATE_UNLOCKED); + } else if (str_equals_case_insensitive(state, "JAMMED")) { + this->set_state(LOCK_STATE_JAMMED); + } else if (str_equals_case_insensitive(state, "LOCKING")) { + this->set_state(LOCK_STATE_LOCKING); + } else if (str_equals_case_insensitive(state, "UNLOCKING")) { + this->set_state(LOCK_STATE_UNLOCKING); + } else if (str_equals_case_insensitive(state, "NONE")) { + this->set_state(LOCK_STATE_NONE); + } else { + ESP_LOGW(TAG, "'%s' - Unrecognized state %s", this->parent_->get_name().c_str(), state.c_str()); + } + return *this; +} +const optional &LockCall::get_state() const { return this->state_; } + +} // namespace lock +} // namespace esphome diff --git a/esphome/components/lock/lock.h b/esphome/components/lock/lock.h new file mode 100644 index 0000000000..f11035c03e --- /dev/null +++ b/esphome/components/lock/lock.h @@ -0,0 +1,178 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/entity_base.h" +#include "esphome/core/preferences.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" +#include + +namespace esphome { +namespace lock { + +class Lock; + +#define LOG_LOCK(prefix, type, obj) \ + if ((obj) != nullptr) { \ + ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ + if (!(obj)->get_icon().empty()) { \ + ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon().c_str()); \ + } \ + if ((obj)->traits.get_assumed_state()) { \ + ESP_LOGCONFIG(TAG, "%s Assumed State: YES", prefix); \ + } \ + } +/// Enum for all states a lock can be in. +enum LockState : uint8_t { + LOCK_STATE_NONE = 0, + LOCK_STATE_LOCKED = 1, + LOCK_STATE_UNLOCKED = 2, + LOCK_STATE_JAMMED = 3, + LOCK_STATE_LOCKING = 4, + LOCK_STATE_UNLOCKING = 5 +}; +const char *lock_state_to_string(LockState state); + +class LockTraits { + public: + LockTraits() = default; + + bool get_supports_open() const { return this->supports_open_; } + void set_supports_open(bool supports_open) { this->supports_open_ = supports_open; } + bool get_requires_code() const { return this->requires_code_; } + void set_requires_code(bool requires_code) { this->requires_code_ = requires_code; } + bool get_assumed_state() const { return this->assumed_state_; } + void set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; } + + bool supports_state(LockState state) const { return supported_states_.count(state); } + std::set get_supported_states() const { return supported_states_; } + void set_supported_states(std::set states) { supported_states_ = std::move(states); } + void add_supported_state(LockState state) { supported_states_.insert(state); } + + protected: + bool supports_open_{false}; + bool requires_code_{false}; + bool assumed_state_{false}; + std::set supported_states_ = {LOCK_STATE_NONE, LOCK_STATE_LOCKED, LOCK_STATE_UNLOCKED}; +}; + +/** This class is used to encode all control actions on a lock device. + * + * It is supposed to be used by all code that wishes to control a lock device (mqtt, api, lambda etc). + * Create an instance of this class by calling `id(lock_device).make_call();`. Then set all attributes + * with the `set_x` methods. Finally, to apply the changes call `.perform();`. + * + * The integration that implements the lock device receives this instance with the `control` method. + * It should check all the properties it implements and apply them as needed. It should do so by + * getting all properties it controls with the getter methods in this class. If the optional value is + * set (check with `.has_value()`) that means the user wants to control this property. Get the value + * of the optional with the star operator (`*call.get_state()`) and apply it. + */ +class LockCall { + public: + LockCall(Lock *parent) : parent_(parent) {} + + /// Set the state of the lock device. + LockCall &set_state(LockState state); + /// Set the state of the lock device. + LockCall &set_state(optional state); + /// Set the state of the lock device based on a string. + LockCall &set_state(const std::string &state); + + void perform(); + + const optional &get_state() const; + + protected: + void validate_(); + + Lock *const parent_; + optional state_; +}; + +/** Base class for all locks. + * + * A lock is basically a switch with a combination of a binary sensor (for reporting lock values) + * and a write_state method that writes a state to the hardware. Locks can also have an "open" + * method to unlatch. + * + * For integrations: Integrations must implement the method control(). + * Control will be called with the arguments supplied by the user and should be used + * to control all values of the lock. + */ +class Lock : public EntityBase { + public: + explicit Lock(); + explicit Lock(const std::string &name); + + /** Make a lock device control call, this is used to control the lock device, see the LockCall description + * for more info. + * @return A new LockCall instance targeting this lock device. + */ + LockCall make_call(); + + /** Publish a state to the front-end from the back-end. + * + * Then the internal value member is set and finally the callbacks are called. + * + * @param state The new state. + */ + void publish_state(LockState state); + + /// The current reported state of the lock. + LockState state{LOCK_STATE_NONE}; + + LockTraits traits; + + /** Turn this lock on. This is called by the front-end. + * + * For implementing locks, please override control. + */ + void lock(); + /** Turn this lock off. This is called by the front-end. + * + * For implementing locks, please override control. + */ + void unlock(); + /** Open (unlatch) this lock. This is called by the front-end. + * + * For implementing locks, please override control. + */ + void open(); + + /** Set callback for state changes. + * + * @param callback The void(bool) callback. + */ + void add_on_state_callback(std::function &&callback); + + protected: + friend LockCall; + + /** Perform the open latch action with hardware. This method is optional to implement + * when creating a new lock. + * + * In the implementation of this method, it is recommended you also call + * publish_state with "unlock" to acknowledge that the state was written to the hardware. + */ + virtual void open_latch() { unlock(); }; + + /** Control the lock device, this is a virtual method that each lock integration must implement. + * + * See more info in LockCall. The integration should check all of its values in this method and + * set them accordingly. At the end of the call, the integration must call `publish_state()` to + * notify the frontend of a changed state. + * + * @param call The LockCall instance encoding all attribute changes. + */ + virtual void control(const LockCall &call) = 0; + + uint32_t hash_base() override; + + CallbackManager state_callback_{}; + Deduplicator publish_dedup_; + ESPPreferenceObject rtc_; +}; + +} // namespace lock +} // namespace esphome diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index a7e14bd4a6..88e5d43509 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -97,6 +97,7 @@ MQTTTextSensor = mqtt_ns.class_("MQTTTextSensor", MQTTComponent) MQTTNumberComponent = mqtt_ns.class_("MQTTNumberComponent", MQTTComponent) MQTTSelectComponent = mqtt_ns.class_("MQTTSelectComponent", MQTTComponent) MQTTButtonComponent = mqtt_ns.class_("MQTTButtonComponent", MQTTComponent) +MQTTLockComponent = mqtt_ns.class_("MQTTLockComponent", MQTTComponent) MQTTDiscoveryUniqueIdGenerator = mqtt_ns.enum("MQTTDiscoveryUniqueIdGenerator") MQTT_DISCOVERY_UNIQUE_ID_GENERATOR_OPTIONS = { diff --git a/esphome/components/mqtt/mqtt_lock.cpp b/esphome/components/mqtt/mqtt_lock.cpp new file mode 100644 index 0000000000..197d0c32d4 --- /dev/null +++ b/esphome/components/mqtt/mqtt_lock.cpp @@ -0,0 +1,55 @@ +#include "mqtt_lock.h" +#include "esphome/core/log.h" + +#include "mqtt_const.h" + +#ifdef USE_MQTT +#ifdef USE_LOCK + +namespace esphome { +namespace mqtt { + +static const char *const TAG = "mqtt.lock"; + +using namespace esphome::lock; + +MQTTLockComponent::MQTTLockComponent(lock::Lock *a_lock) : lock_(a_lock) {} + +void MQTTLockComponent::setup() { + this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &payload) { + if (strcasecmp(payload.c_str(), "LOCK") == 0) { + this->lock_->lock(); + } else if (strcasecmp(payload.c_str(), "UNLOCK") == 0) { + this->lock_->unlock(); + } else if (strcasecmp(payload.c_str(), "OPEN") == 0) { + this->lock_->open(); + } else { + ESP_LOGW(TAG, "'%s': Received unknown status payload: %s", this->friendly_name().c_str(), payload.c_str()); + this->status_momentary_warning("state", 5000); + } + }); + this->lock_->add_on_state_callback([this]() { this->defer("send", [this]() { this->publish_state(); }); }); +} +void MQTTLockComponent::dump_config() { + ESP_LOGCONFIG(TAG, "MQTT Lock '%s': ", this->lock_->get_name().c_str()); + LOG_MQTT_COMPONENT(true, true); +} + +std::string MQTTLockComponent::component_type() const { return "lock"; } +const EntityBase *MQTTLockComponent::get_entity() const { return this->lock_; } +void MQTTLockComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { + if (this->lock_->traits.get_assumed_state()) + root[MQTT_OPTIMISTIC] = true; +} +bool MQTTLockComponent::send_initial_state() { return this->publish_state(); } + +bool MQTTLockComponent::publish_state() { + std::string payload = lock_state_to_string(this->lock_->state); + return this->publish(this->get_state_topic_(), payload); +} + +} // namespace mqtt +} // namespace esphome + +#endif +#endif // USE_MQTT diff --git a/esphome/components/mqtt/mqtt_lock.h b/esphome/components/mqtt/mqtt_lock.h new file mode 100644 index 0000000000..789f74c795 --- /dev/null +++ b/esphome/components/mqtt/mqtt_lock.h @@ -0,0 +1,41 @@ +#pragma once + +#include "esphome/core/defines.h" + +#ifdef USE_MQTT +#ifdef USE_LOCK + +#include "esphome/components/lock/lock.h" +#include "mqtt_component.h" + +namespace esphome { +namespace mqtt { + +class MQTTLockComponent : public mqtt::MQTTComponent { + public: + explicit MQTTLockComponent(lock::Lock *a_lock); + + // ========== INTERNAL METHODS ========== + // (In most use cases you won't need these) + void setup() override; + void dump_config() override; + + void send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) override; + + bool send_initial_state() override; + + bool publish_state(); + + protected: + /// "lock" component type. + std::string component_type() const override; + const EntityBase *get_entity() const override; + + lock::Lock *lock_; +}; + +} // namespace mqtt +} // namespace esphome + +#endif +#endif // USE_MQTT diff --git a/esphome/components/output/lock/__init__.py b/esphome/components/output/lock/__init__.py new file mode 100644 index 0000000000..3be2cb09aa --- /dev/null +++ b/esphome/components/output/lock/__init__.py @@ -0,0 +1,23 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import output, lock +from esphome.const import CONF_ID, CONF_OUTPUT +from .. import output_ns + +OutputLock = output_ns.class_("OutputLock", lock.Lock, cg.Component) + +CONFIG_SCHEMA = lock.LOCK_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(OutputLock), + cv.Required(CONF_OUTPUT): cv.use_id(output.BinaryOutput), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await lock.register_lock(var, config) + + output_ = await cg.get_variable(config[CONF_OUTPUT]) + cg.add(var.set_output(output_)) diff --git a/esphome/components/output/lock/output_lock.cpp b/esphome/components/output/lock/output_lock.cpp new file mode 100644 index 0000000000..2545f62481 --- /dev/null +++ b/esphome/components/output/lock/output_lock.cpp @@ -0,0 +1,22 @@ +#include "output_lock.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace output { + +static const char *const TAG = "output.lock"; + +void OutputLock::dump_config() { LOG_LOCK("", "Output Lock", this); } + +void OutputLock::control(const lock::LockCall &call) { + auto state = *call.get_state(); + if (state == lock::LOCK_STATE_LOCKED) { + this->output_->turn_on(); + } else if (state == lock::LOCK_STATE_UNLOCKED) { + this->output_->turn_off(); + } + this->publish_state(state); +} + +} // namespace output +} // namespace esphome diff --git a/esphome/components/output/lock/output_lock.h b/esphome/components/output/lock/output_lock.h new file mode 100644 index 0000000000..c183c3a3ea --- /dev/null +++ b/esphome/components/output/lock/output_lock.h @@ -0,0 +1,24 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/lock/lock.h" +#include "esphome/components/output/binary_output.h" + +namespace esphome { +namespace output { + +class OutputLock : public lock::Lock, public Component { + public: + void set_output(BinaryOutput *output) { output_ = output; } + + float get_setup_priority() const override { return setup_priority::HARDWARE - 1.0f; } + void dump_config() override; + + protected: + void control(const lock::LockCall &call) override; + + output::BinaryOutput *output_; +}; + +} // namespace output +} // namespace esphome diff --git a/esphome/components/prometheus/prometheus_handler.cpp b/esphome/components/prometheus/prometheus_handler.cpp index e65729b184..e4dd6b9043 100644 --- a/esphome/components/prometheus/prometheus_handler.cpp +++ b/esphome/components/prometheus/prometheus_handler.cpp @@ -45,6 +45,12 @@ void PrometheusHandler::handleRequest(AsyncWebServerRequest *req) { this->switch_row_(stream, obj); #endif +#ifdef USE_LOCK + this->lock_type_(stream); + for (auto *obj : App.get_locks()) + this->lock_row_(stream, obj); +#endif + req->send(stream); } @@ -310,6 +316,30 @@ void PrometheusHandler::switch_row_(AsyncResponseStream *stream, switch_::Switch } #endif +#ifdef USE_LOCK +void PrometheusHandler::lock_type_(AsyncResponseStream *stream) { + stream->print(F("#TYPE esphome_lock_value GAUGE\n")); + stream->print(F("#TYPE esphome_lock_failed GAUGE\n")); +} +void PrometheusHandler::lock_row_(AsyncResponseStream *stream, lock::Lock *obj) { + if (obj->is_internal()) + return; + stream->print(F("esphome_lock_failed{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} 0\n")); + // Data itself + stream->print(F("esphome_lock_value{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} ")); + stream->print(obj->state); + stream->print('\n'); +} +#endif + } // namespace prometheus } // namespace esphome diff --git a/esphome/components/prometheus/prometheus_handler.h b/esphome/components/prometheus/prometheus_handler.h index 82e3fe28e0..5c8d51c60f 100644 --- a/esphome/components/prometheus/prometheus_handler.h +++ b/esphome/components/prometheus/prometheus_handler.h @@ -76,6 +76,13 @@ class PrometheusHandler : public AsyncWebHandler, public Component { void switch_row_(AsyncResponseStream *stream, switch_::Switch *obj); #endif +#ifdef USE_LOCK + /// Return the type for prometheus + void lock_type_(AsyncResponseStream *stream); + /// Return the lock Values state as prometheus data point + void lock_row_(AsyncResponseStream *stream, lock::Lock *obj); +#endif + web_server_base::WebServerBase *base_; }; diff --git a/esphome/components/template/lock/__init__.py b/esphome/components/template/lock/__init__.py new file mode 100644 index 0000000000..24709ff4f2 --- /dev/null +++ b/esphome/components/template/lock/__init__.py @@ -0,0 +1,103 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.components import lock +from esphome.const import ( + CONF_ASSUMED_STATE, + CONF_ID, + CONF_LAMBDA, + CONF_LOCK_ACTION, + CONF_OPEN_ACTION, + CONF_OPTIMISTIC, + CONF_STATE, + CONF_UNLOCK_ACTION, +) +from .. import template_ns + +TemplateLock = template_ns.class_("TemplateLock", lock.Lock, cg.Component) + +LockState = lock.lock_ns.enum("LockState") + +LOCK_STATES = { + "LOCKED": LockState.LOCK_STATE_LOCKED, + "UNLOCKED": LockState.LOCK_STATE_UNLOCKED, + "JAMMED": LockState.LOCK_STATE_JAMMED, + "LOCKING": LockState.LOCK_STATE_LOCKING, + "UNLOCKING": LockState.LOCK_STATE_UNLOCKING, +} + +validate_lock_state = cv.enum(LOCK_STATES, upper=True) + + +def validate(config): + if not config[CONF_OPTIMISTIC] and ( + CONF_LOCK_ACTION not in config or CONF_UNLOCK_ACTION not in config + ): + raise cv.Invalid( + "Either optimistic mode must be enabled, or lock_action and unlock_action must be set, " + "to handle the lock being changed." + ) + return config + + +CONFIG_SCHEMA = cv.All( + lock.LOCK_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(TemplateLock), + cv.Optional(CONF_LAMBDA): cv.returning_lambda, + cv.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, + cv.Optional(CONF_ASSUMED_STATE, default=False): cv.boolean, + cv.Optional(CONF_UNLOCK_ACTION): automation.validate_automation( + single=True + ), + cv.Optional(CONF_LOCK_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_OPEN_ACTION): automation.validate_automation(single=True), + } + ).extend(cv.COMPONENT_SCHEMA), + validate, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await lock.register_lock(var, config) + + if CONF_LAMBDA in config: + template_ = await cg.process_lambda( + config[CONF_LAMBDA], [], return_type=cg.optional.template(LockState) + ) + cg.add(var.set_state_lambda(template_)) + if CONF_UNLOCK_ACTION in config: + await automation.build_automation( + var.get_unlock_trigger(), [], config[CONF_UNLOCK_ACTION] + ) + if CONF_LOCK_ACTION in config: + await automation.build_automation( + var.get_lock_trigger(), [], config[CONF_LOCK_ACTION] + ) + if CONF_OPEN_ACTION in config: + await automation.build_automation( + var.get_open_trigger(), [], config[CONF_OPEN_ACTION] + ) + cg.add(var.traits.set_supports_open(CONF_OPEN_ACTION in config)) + cg.add(var.traits.set_assumed_state(config[CONF_ASSUMED_STATE])) + cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) + + +@automation.register_action( + "lock.template.publish", + lock.LockPublishAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(lock.Lock), + cv.Required(CONF_STATE): cv.templatable(validate_lock_state), + } + ), +) +async def lock_template_publish_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_STATE], args, LockState) + cg.add(var.set_state(template_)) + return var diff --git a/esphome/components/template/lock/template_lock.cpp b/esphome/components/template/lock/template_lock.cpp new file mode 100644 index 0000000000..87ba1046eb --- /dev/null +++ b/esphome/components/template/lock/template_lock.cpp @@ -0,0 +1,59 @@ +#include "template_lock.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace template_ { + +using namespace esphome::lock; + +static const char *const TAG = "template.lock"; + +TemplateLock::TemplateLock() + : lock_trigger_(new Trigger<>()), unlock_trigger_(new Trigger<>()), open_trigger_(new Trigger<>()) {} + +void TemplateLock::loop() { + if (!this->f_.has_value()) + return; + auto val = (*this->f_)(); + if (!val.has_value()) + return; + + this->publish_state(*val); +} +void TemplateLock::control(const lock::LockCall &call) { + if (this->prev_trigger_ != nullptr) { + this->prev_trigger_->stop_action(); + } + + auto state = *call.get_state(); + if (state == LOCK_STATE_LOCKED) { + this->prev_trigger_ = this->lock_trigger_; + this->lock_trigger_->trigger(); + } else if (state == LOCK_STATE_UNLOCKED) { + this->prev_trigger_ = this->unlock_trigger_; + this->unlock_trigger_->trigger(); + } + + if (this->optimistic_) + this->publish_state(state); +} +void TemplateLock::open_latch() { + if (this->prev_trigger_ != nullptr) { + this->prev_trigger_->stop_action(); + } + this->prev_trigger_ = this->open_trigger_; + this->open_trigger_->trigger(); +} +void TemplateLock::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } +void TemplateLock::set_state_lambda(std::function()> &&f) { this->f_ = f; } +float TemplateLock::get_setup_priority() const { return setup_priority::HARDWARE; } +Trigger<> *TemplateLock::get_lock_trigger() const { return this->lock_trigger_; } +Trigger<> *TemplateLock::get_unlock_trigger() const { return this->unlock_trigger_; } +Trigger<> *TemplateLock::get_open_trigger() const { return this->open_trigger_; } +void TemplateLock::dump_config() { + LOG_LOCK("", "Template Lock", this); + ESP_LOGCONFIG(TAG, " Optimistic: %s", YESNO(this->optimistic_)); +} + +} // namespace template_ +} // namespace esphome diff --git a/esphome/components/template/lock/template_lock.h b/esphome/components/template/lock/template_lock.h new file mode 100644 index 0000000000..4f798eca81 --- /dev/null +++ b/esphome/components/template/lock/template_lock.h @@ -0,0 +1,38 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/lock/lock.h" + +namespace esphome { +namespace template_ { + +class TemplateLock : public lock::Lock, public Component { + public: + TemplateLock(); + + void dump_config() override; + + void set_state_lambda(std::function()> &&f); + Trigger<> *get_lock_trigger() const; + Trigger<> *get_unlock_trigger() const; + Trigger<> *get_open_trigger() const; + void set_optimistic(bool optimistic); + void loop() override; + + float get_setup_priority() const override; + + protected: + void control(const lock::LockCall &call) override; + void open_latch() override; + + optional()>> f_; + bool optimistic_{false}; + Trigger<> *lock_trigger_; + Trigger<> *unlock_trigger_; + Trigger<> *open_trigger_; + Trigger<> *prev_trigger_{nullptr}; +}; + +} // namespace template_ +} // namespace esphome diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 83b6ca1e2f..44d044750e 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -152,6 +152,13 @@ void WebServer::setup() { client->send(this->select_json(obj, obj->state).c_str(), "state"); } #endif + +#ifdef USE_LOCK + for (auto *obj : App.get_locks()) { + if (this->include_internal_ || !obj->is_internal()) + client->send(this->lock_json(obj, obj->state).c_str(), "state"); + } +#endif }); #ifdef USE_LOGGER @@ -287,6 +294,20 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) { } #endif +#ifdef USE_LOCK + for (auto *obj : App.get_locks()) { + if (this->include_internal_ || !obj->is_internal()) { + write_row(stream, obj, "lock", "", [](AsyncResponseStream &stream, EntityBase *obj) { + lock::Lock *lock = (lock::Lock *) obj; + stream.print(""); + if (lock->traits.get_supports_open()) { + stream.print(""); + } + }); + } + } +#endif + stream->print(F("

See ESPHome Web API for " "REST API documentation.

")); if (this->allow_ota_) { @@ -763,6 +784,43 @@ std::string WebServer::select_json(select::Select *obj, const std::string &value } #endif +#ifdef USE_LOCK +void WebServer::on_lock_update(lock::Lock *obj) { + this->events_.send(this->lock_json(obj, obj->state).c_str(), "state"); +} +std::string WebServer::lock_json(lock::Lock *obj, lock::LockState value) { + return json::build_json([obj, value](JsonObject root) { + root["id"] = "lock-" + obj->get_object_id(); + root["state"] = lock::lock_state_to_string(value); + root["value"] = value; + }); +} +void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMatch &match) { + for (lock::Lock *obj : App.get_locks()) { + if (obj->get_object_id() != match.id) + continue; + + if (request->method() == HTTP_GET) { + std::string data = this->lock_json(obj, obj->state); + request->send(200, "text/json", data.c_str()); + } else if (match.method == "lock") { + this->defer([obj]() { obj->lock(); }); + request->send(200); + } else if (match.method == "unlock") { + this->defer([obj]() { obj->unlock(); }); + request->send(200); + } else if (match.method == "open") { + this->defer([obj]() { obj->open(); }); + request->send(200); + } else { + request->send(404); + } + return; + } + request->send(404); +} +#endif + bool WebServer::canHandle(AsyncWebServerRequest *request) { if (request->url() == "/") return true; @@ -830,6 +888,11 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) { return true; #endif +#ifdef USE_LOCK + if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "lock") + return true; +#endif + return false; } void WebServer::handleRequest(AsyncWebServerRequest *request) { @@ -922,6 +985,13 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) { return; } #endif + +#ifdef USE_LOCK + if (match.domain == "lock") { + this->handle_lock_request(request, match); + return; + } +#endif } bool WebServer::isRequestHandlerTrivial() { return false; } diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index afd4f1d4b5..3dd5c93f59 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -185,6 +185,16 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { std::string select_json(select::Select *obj, const std::string &value); #endif +#ifdef USE_LOCK + void on_lock_update(lock::Lock *obj) override; + + /// Handle a lock request under '/lock//'. + void handle_lock_request(AsyncWebServerRequest *request, const UrlMatch &match); + + /// Dump the lock state with its value as a JSON string. + std::string lock_json(lock::Lock *obj, lock::LockState value); +#endif + /// Override the web handler's canHandle method. bool canHandle(AsyncWebServerRequest *request) override; /// Override the web handler's handleRequest method. diff --git a/esphome/const.py b/esphome/const.py index 2d2f5f1da0..a2dd20269a 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -334,6 +334,7 @@ CONF_LINE_THICKNESS = "line_thickness" CONF_LINE_TYPE = "line_type" CONF_LOADED_INTEGRATIONS = "loaded_integrations" CONF_LOCAL = "local" +CONF_LOCK_ACTION = "lock_action" CONF_LOG_TOPIC = "log_topic" CONF_LOGGER = "logger" CONF_LOGS = "logs" @@ -426,9 +427,11 @@ CONF_ON_ENROLLMENT_SCAN = "on_enrollment_scan" CONF_ON_FINGER_SCAN_MATCHED = "on_finger_scan_matched" CONF_ON_FINGER_SCAN_UNMATCHED = "on_finger_scan_unmatched" CONF_ON_JSON_MESSAGE = "on_json_message" +CONF_ON_LOCK = "on_lock" CONF_ON_LOOP = "on_loop" CONF_ON_MESSAGE = "on_message" CONF_ON_MULTI_CLICK = "on_multi_click" +CONF_ON_OPEN = "on_open" CONF_ON_PRESS = "on_press" CONF_ON_RAW_VALUE = "on_raw_value" CONF_ON_RELEASE = "on_release" @@ -442,6 +445,7 @@ CONF_ON_TIME_SYNC = "on_time_sync" CONF_ON_TOUCH = "on_touch" CONF_ON_TURN_OFF = "on_turn_off" CONF_ON_TURN_ON = "on_turn_on" +CONF_ON_UNLOCK = "on_unlock" CONF_ON_VALUE = "on_value" CONF_ON_VALUE_RANGE = "on_value_range" CONF_ONE = "one" @@ -709,6 +713,7 @@ CONF_UART_ID = "uart_id" CONF_UID = "uid" CONF_UNIQUE = "unique" CONF_UNIT_OF_MEASUREMENT = "unit_of_measurement" +CONF_UNLOCK_ACTION = "unlock_action" CONF_UPDATE_INTERVAL = "update_interval" CONF_UPDATE_ON_BOOT = "update_on_boot" CONF_URL = "url" diff --git a/esphome/core/application.h b/esphome/core/application.h index 2598a2f4a4..cec6e7baa9 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -42,6 +42,9 @@ #ifdef USE_SELECT #include "esphome/components/select/select.h" #endif +#ifdef USE_LOCK +#include "esphome/components/lock/lock.h" +#endif namespace esphome { @@ -104,6 +107,10 @@ class Application { void register_select(select::Select *select) { this->selects_.push_back(select); } #endif +#ifdef USE_LOCK + void register_lock(lock::Lock *a_lock) { this->locks_.push_back(a_lock); } +#endif + /// Register the component in this Application instance. template C *register_component(C *c) { static_assert(std::is_base_of::value, "Only Component subclasses can be registered"); @@ -257,6 +264,15 @@ class Application { return nullptr; } #endif +#ifdef USE_LOCK + const std::vector &get_locks() { return this->locks_; } + lock::Lock *get_lock_by_key(uint32_t key, bool include_internal = false) { + for (auto *obj : this->locks_) + if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) + return obj; + return nullptr; + } +#endif Scheduler scheduler; @@ -305,6 +321,9 @@ class Application { #ifdef USE_SELECT std::vector selects_{}; #endif +#ifdef USE_LOCK + std::vector locks_{}; +#endif std::string name_; std::string compilation_time_; diff --git a/esphome/core/controller.cpp b/esphome/core/controller.cpp index 6d3a76a292..dfcef5e4c1 100644 --- a/esphome/core/controller.cpp +++ b/esphome/core/controller.cpp @@ -65,6 +65,12 @@ void Controller::setup_controller(bool include_internal) { obj->add_on_state_callback([this, obj](const std::string &state) { this->on_select_update(obj, state); }); } #endif +#ifdef USE_LOCK + for (auto *obj : App.get_locks()) { + if (include_internal || !obj->is_internal()) + obj->add_on_state_callback([this, obj]() { this->on_lock_update(obj); }); + } +#endif } } // namespace esphome diff --git a/esphome/core/controller.h b/esphome/core/controller.h index 49750d1cc4..0be854828b 100644 --- a/esphome/core/controller.h +++ b/esphome/core/controller.h @@ -34,6 +34,9 @@ #ifdef USE_SELECT #include "esphome/components/select/select.h" #endif +#ifdef USE_LOCK +#include "esphome/components/lock/lock.h" +#endif namespace esphome { @@ -70,6 +73,9 @@ class Controller { #ifdef USE_SELECT virtual void on_select_update(select::Select *obj, const std::string &state){}; #endif +#ifdef USE_LOCK + virtual void on_lock_update(lock::Lock *obj){}; +#endif }; } // namespace esphome diff --git a/esphome/core/defines.h b/esphome/core/defines.h index acdc5df815..574a8dcafe 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -26,6 +26,7 @@ #define USE_GRAPH #define USE_HOMEASSISTANT_TIME #define USE_LIGHT +#define USE_LOCK #define USE_LOGGER #define USE_MDNS #define USE_NUMBER diff --git a/script/ci-custom.py b/script/ci-custom.py index 956716e5fa..7bbaaf1c79 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -598,6 +598,7 @@ def lint_inclusive_language(fname, match): "esphome/components/display/display_buffer.h", "esphome/components/fan/fan.h", "esphome/components/i2c/i2c.h", + "esphome/components/lock/lock.h", "esphome/components/mqtt/mqtt_component.h", "esphome/components/number/number.h", "esphome/components/output/binary_output.h", diff --git a/tests/test1.yaml b/tests/test1.yaml index 40cd0d4827..a0c9d03f14 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -2582,3 +2582,28 @@ select: qr_code: - id: homepage_qr value: https://esphome.io/index.html + +lock: + - platform: template + id: test_lock1 + name: "Template Switch" + lambda: |- + if (id(binary_sensor1).state) { + return LOCK_STATE_LOCKED; + }else{ + return LOCK_STATE_UNLOCKED; + } + optimistic: true + assumed_state: no + on_unlock: + - lock.template.publish: + id: test_lock1 + state: !lambda "return LOCK_STATE_UNLOCKED;" + on_lock: + - lock.template.publish: + id: test_lock1 + state: !lambda "return LOCK_STATE_LOCKED;" + - platform: output + name: "Generic Output Lock" + id: test_lock2 + output: pca_6 From e7864a28a112d64a98fecd738ad1658994f2dacb Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 3 Feb 2022 21:04:48 +0100 Subject: [PATCH 0185/1729] Add device class support to Switch (#3012) Co-authored-by: Oxan van Leeuwen --- esphome/components/api/api.proto | 1 + esphome/components/api/api_connection.cpp | 1 + esphome/components/api/api_pb2.cpp | 9 +++++++++ esphome/components/api/api_pb2.h | 1 + esphome/components/switch/__init__.py | 15 ++++++++++++++- esphome/components/switch/switch.cpp | 7 +++++++ esphome/components/switch/switch.h | 9 +++++++++ esphome/const.py | 3 +++ tests/test1.yaml | 4 ++++ 9 files changed, 49 insertions(+), 1 deletion(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 3ab426979e..bd39893825 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -529,6 +529,7 @@ message ListEntitiesSwitchResponse { bool assumed_state = 6; bool disabled_by_default = 7; EntityCategory entity_category = 8; + string device_class = 9; } message SwitchStateResponse { option (id) = 26; diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 21388b547e..d9ce6cd79e 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -462,6 +462,7 @@ bool APIConnection::send_switch_info(switch_::Switch *a_switch) { msg.assumed_state = a_switch->assumed_state(); msg.disabled_by_default = a_switch->is_disabled_by_default(); msg.entity_category = static_cast(a_switch->get_entity_category()); + msg.device_class = a_switch->get_device_class(); return this->send_list_entities_switch_response(msg); } void APIConnection::switch_command(const SwitchCommandRequest &msg) { diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index e7e0476afc..5a78587473 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -2177,6 +2177,10 @@ bool ListEntitiesSwitchResponse::decode_length(uint32_t field_id, ProtoLengthDel this->icon = value.as_string(); return true; } + case 9: { + this->device_class = value.as_string(); + return true; + } default: return false; } @@ -2200,6 +2204,7 @@ void ListEntitiesSwitchResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(6, this->assumed_state); buffer.encode_bool(7, this->disabled_by_default); buffer.encode_enum(8, this->entity_category); + buffer.encode_string(9, this->device_class); } #ifdef HAS_PROTO_MESSAGE_DUMP void ListEntitiesSwitchResponse::dump_to(std::string &out) const { @@ -2237,6 +2242,10 @@ void ListEntitiesSwitchResponse::dump_to(std::string &out) const { out.append(" entity_category: "); out.append(proto_enum_to_string(this->entity_category)); out.append("\n"); + + out.append(" device_class: "); + out.append("'").append(this->device_class).append("'"); + out.append("\n"); out.append("}"); } #endif diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 4c9a0e9c0f..28c0a7ce88 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -580,6 +580,7 @@ class ListEntitiesSwitchResponse : public ProtoMessage { bool assumed_state{false}; bool disabled_by_default{false}; enums::EntityCategory entity_category{}; + std::string device_class{}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; diff --git a/esphome/components/switch/__init__.py b/esphome/components/switch/__init__.py index 08cbccbe35..71a16439cd 100644 --- a/esphome/components/switch/__init__.py +++ b/esphome/components/switch/__init__.py @@ -4,18 +4,27 @@ from esphome import automation from esphome.automation import Condition, maybe_simple_id from esphome.components import mqtt from esphome.const import ( + CONF_DEVICE_CLASS, CONF_ID, CONF_INVERTED, + CONF_MQTT_ID, CONF_ON_TURN_OFF, CONF_ON_TURN_ON, CONF_TRIGGER_ID, - CONF_MQTT_ID, + DEVICE_CLASS_EMPTY, + DEVICE_CLASS_OUTLET, + DEVICE_CLASS_SWITCH, ) from esphome.core import CORE, coroutine_with_priority from esphome.cpp_helpers import setup_entity CODEOWNERS = ["@esphome/core"] IS_PLATFORM_COMPONENT = True +DEVICE_CLASSES = [ + DEVICE_CLASS_EMPTY, + DEVICE_CLASS_OUTLET, + DEVICE_CLASS_SWITCH, +] switch_ns = cg.esphome_ns.namespace("switch_") Switch = switch_ns.class_("Switch", cg.EntityBase) @@ -51,6 +60,7 @@ SWITCH_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).e cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SwitchTurnOffTrigger), } ), + cv.Optional(CONF_DEVICE_CLASS): cv.one_of(*DEVICE_CLASSES, lower=True), } ) @@ -71,6 +81,9 @@ async def setup_switch_core_(var, config): mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var) await mqtt.register_mqtt_component(mqtt_, config) + if CONF_DEVICE_CLASS in config: + cg.add(var.set_device_class(config[CONF_DEVICE_CLASS])) + async def register_switch(var, config): if not CORE.has_id(config[CONF_ID]): diff --git a/esphome/components/switch/switch.cpp b/esphome/components/switch/switch.cpp index b9b99b4147..ca36e6feb9 100644 --- a/esphome/components/switch/switch.cpp +++ b/esphome/components/switch/switch.cpp @@ -46,5 +46,12 @@ void Switch::set_inverted(bool inverted) { this->inverted_ = inverted; } uint32_t Switch::hash_base() { return 3129890955UL; } bool Switch::is_inverted() const { return this->inverted_; } +std::string Switch::get_device_class() { + if (this->device_class_.has_value()) + return *this->device_class_; + return ""; +} +void Switch::set_device_class(const std::string &device_class) { this->device_class_ = device_class; } + } // namespace switch_ } // namespace esphome diff --git a/esphome/components/switch/switch.h b/esphome/components/switch/switch.h index 071393003a..dda24e85fa 100644 --- a/esphome/components/switch/switch.h +++ b/esphome/components/switch/switch.h @@ -20,6 +20,9 @@ namespace switch_ { if ((obj)->is_inverted()) { \ ESP_LOGCONFIG(TAG, "%s Inverted: YES", prefix); \ } \ + if (!(obj)->get_device_class().empty()) { \ + ESP_LOGCONFIG(TAG, "%s Device Class: '%s'", prefix, (obj)->get_device_class().c_str()); \ + } \ } /** Base class for all switches. @@ -88,6 +91,11 @@ class Switch : public EntityBase { bool is_inverted() const; + /// Get the device class for this switch. + std::string get_device_class(); + /// Set the Home Assistant device class for this switch. + void set_device_class(const std::string &device_class); + protected: /** Write the given state to hardware. You should implement this * abstract method if you want to create your own switch. @@ -105,6 +113,7 @@ class Switch : public EntityBase { bool inverted_{false}; Deduplicator publish_dedup_; ESPPreferenceObject rtc_; + optional device_class_; }; } // namespace switch_ diff --git a/esphome/const.py b/esphome/const.py index a2dd20269a..61b152654a 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -920,6 +920,9 @@ DEVICE_CLASS_VOLTAGE = "voltage" DEVICE_CLASS_UPDATE = "update" # device classes of button component DEVICE_CLASS_RESTART = "restart" +# device classes of switch component +DEVICE_CLASS_OUTLET = "outlet" +DEVICE_CLASS_SWITCH = "switch" # state classes diff --git a/tests/test1.yaml b/tests/test1.yaml index a0c9d03f14..e97b3aed73 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -2037,6 +2037,10 @@ switch: - platform: template id: ble1_status optimistic: true + - platform: template + id: outlet_switch + optimistic: true + device_class: outlet fan: - platform: binary From 42984fa72a9541bc3e36d537893360248965f80e Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Thu, 3 Feb 2022 18:50:42 -0800 Subject: [PATCH 0186/1729] Handle Tuya multi-datapoint messages (#3159) Co-authored-by: Samuel Sieb --- esphome/components/tuya/tuya.cpp | 189 ++++++++++++++++--------------- esphome/components/tuya/tuya.h | 2 +- 2 files changed, 98 insertions(+), 93 deletions(-) diff --git a/esphome/components/tuya/tuya.cpp b/esphome/components/tuya/tuya.cpp index f2dceed33f..1fbca7796d 100644 --- a/esphome/components/tuya/tuya.cpp +++ b/esphome/components/tuya/tuya.cpp @@ -199,7 +199,7 @@ void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buff this->set_timeout("datapoint_dump", 1000, [this] { this->dump_config(); }); this->initialized_callback_.call(); } - this->handle_datapoint_(buffer, len); + this->handle_datapoints_(buffer, len); break; case TuyaCommandType::DATAPOINT_QUERY: break; @@ -224,105 +224,110 @@ void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buff } } -void Tuya::handle_datapoint_(const uint8_t *buffer, size_t len) { - if (len < 2) - return; +void Tuya::handle_datapoints_(const uint8_t *buffer, size_t len) { + while (len >= 4) { + TuyaDatapoint datapoint{}; + datapoint.id = buffer[0]; + datapoint.type = (TuyaDatapointType) buffer[1]; + datapoint.value_uint = 0; - TuyaDatapoint datapoint{}; - datapoint.id = buffer[0]; - datapoint.type = (TuyaDatapointType) buffer[1]; - datapoint.value_uint = 0; - - // Drop update if datapoint is in ignore_mcu_datapoint_update list - for (uint8_t i : this->ignore_mcu_update_on_datapoints_) { - if (datapoint.id == i) { - ESP_LOGV(TAG, "Datapoint %u found in ignore_mcu_update_on_datapoints list, dropping MCU update", datapoint.id); + size_t data_size = (buffer[2] << 8) + buffer[3]; + const uint8_t *data = buffer + 4; + size_t data_len = len - 4; + if (data_size > data_len) { + ESP_LOGW(TAG, "Datapoint %u is truncated and cannot be parsed (%zu > %zu)", datapoint.id, data_size, data_len); return; } - } - size_t data_size = (buffer[2] << 8) + buffer[3]; - const uint8_t *data = buffer + 4; - size_t data_len = len - 4; - if (data_size > data_len) { - ESP_LOGW(TAG, "Datapoint %u has extra bytes that will be ignored (%zu > %zu)", datapoint.id, data_size, data_len); - } else if (data_size < data_len) { - ESP_LOGW(TAG, "Datapoint %u is truncated and cannot be parsed (%zu < %zu)", datapoint.id, data_size, data_len); - return; - } - datapoint.len = data_len; + datapoint.len = data_size; - switch (datapoint.type) { - case TuyaDatapointType::RAW: - datapoint.value_raw = std::vector(data, data + data_len); - ESP_LOGD(TAG, "Datapoint %u update to %s", datapoint.id, format_hex_pretty(datapoint.value_raw).c_str()); - break; - case TuyaDatapointType::BOOLEAN: - if (data_len != 1) { - ESP_LOGW(TAG, "Datapoint %u has bad boolean len %zu", datapoint.id, data_len); - return; - } - datapoint.value_bool = data[0]; - ESP_LOGD(TAG, "Datapoint %u update to %s", datapoint.id, ONOFF(datapoint.value_bool)); - break; - case TuyaDatapointType::INTEGER: - if (data_len != 4) { - ESP_LOGW(TAG, "Datapoint %u has bad integer len %zu", datapoint.id, data_len); - return; - } - datapoint.value_uint = encode_uint32(data[0], data[1], data[2], data[3]); - ESP_LOGD(TAG, "Datapoint %u update to %d", datapoint.id, datapoint.value_int); - break; - case TuyaDatapointType::STRING: - datapoint.value_string = std::string(reinterpret_cast(data), data_len); - ESP_LOGD(TAG, "Datapoint %u update to %s", datapoint.id, datapoint.value_string.c_str()); - break; - case TuyaDatapointType::ENUM: - if (data_len != 1) { - ESP_LOGW(TAG, "Datapoint %u has bad enum len %zu", datapoint.id, data_len); - return; - } - datapoint.value_enum = data[0]; - ESP_LOGD(TAG, "Datapoint %u update to %d", datapoint.id, datapoint.value_enum); - break; - case TuyaDatapointType::BITMASK: - switch (data_len) { - case 1: - datapoint.value_bitmask = encode_uint32(0, 0, 0, data[0]); - break; - case 2: - datapoint.value_bitmask = encode_uint32(0, 0, data[0], data[1]); - break; - case 4: - datapoint.value_bitmask = encode_uint32(data[0], data[1], data[2], data[3]); - break; - default: - ESP_LOGW(TAG, "Datapoint %u has bad bitmask len %zu", datapoint.id, data_len); + switch (datapoint.type) { + case TuyaDatapointType::RAW: + datapoint.value_raw = std::vector(data, data + data_size); + ESP_LOGD(TAG, "Datapoint %u update to %s", datapoint.id, format_hex_pretty(datapoint.value_raw).c_str()); + break; + case TuyaDatapointType::BOOLEAN: + if (data_size != 1) { + ESP_LOGW(TAG, "Datapoint %u has bad boolean len %zu", datapoint.id, data_size); return; - } - ESP_LOGD(TAG, "Datapoint %u update to %#08X", datapoint.id, datapoint.value_bitmask); - break; - default: - ESP_LOGW(TAG, "Datapoint %u has unknown type %#02hhX", datapoint.id, static_cast(datapoint.type)); - return; - } - - // Update internal datapoints - bool found = false; - for (auto &other : this->datapoints_) { - if (other.id == datapoint.id) { - other = datapoint; - found = true; + } + datapoint.value_bool = data[0]; + ESP_LOGD(TAG, "Datapoint %u update to %s", datapoint.id, ONOFF(datapoint.value_bool)); + break; + case TuyaDatapointType::INTEGER: + if (data_size != 4) { + ESP_LOGW(TAG, "Datapoint %u has bad integer len %zu", datapoint.id, data_size); + return; + } + datapoint.value_uint = encode_uint32(data[0], data[1], data[2], data[3]); + ESP_LOGD(TAG, "Datapoint %u update to %d", datapoint.id, datapoint.value_int); + break; + case TuyaDatapointType::STRING: + datapoint.value_string = std::string(reinterpret_cast(data), data_size); + ESP_LOGD(TAG, "Datapoint %u update to %s", datapoint.id, datapoint.value_string.c_str()); + break; + case TuyaDatapointType::ENUM: + if (data_size != 1) { + ESP_LOGW(TAG, "Datapoint %u has bad enum len %zu", datapoint.id, data_size); + return; + } + datapoint.value_enum = data[0]; + ESP_LOGD(TAG, "Datapoint %u update to %d", datapoint.id, datapoint.value_enum); + break; + case TuyaDatapointType::BITMASK: + switch (data_size) { + case 1: + datapoint.value_bitmask = encode_uint32(0, 0, 0, data[0]); + break; + case 2: + datapoint.value_bitmask = encode_uint32(0, 0, data[0], data[1]); + break; + case 4: + datapoint.value_bitmask = encode_uint32(data[0], data[1], data[2], data[3]); + break; + default: + ESP_LOGW(TAG, "Datapoint %u has bad bitmask len %zu", datapoint.id, data_size); + return; + } + ESP_LOGD(TAG, "Datapoint %u update to %#08X", datapoint.id, datapoint.value_bitmask); + break; + default: + ESP_LOGW(TAG, "Datapoint %u has unknown type %#02hhX", datapoint.id, static_cast(datapoint.type)); + return; } - } - if (!found) { - this->datapoints_.push_back(datapoint); - } - // Run through listeners - for (auto &listener : this->listeners_) { - if (listener.datapoint_id == datapoint.id) - listener.on_datapoint(datapoint); + len -= data_size + 4; + buffer = data + data_size; + + // drop update if datapoint is in ignore_mcu_datapoint_update list + bool skip = false; + for (auto i : this->ignore_mcu_update_on_datapoints_) { + if (datapoint.id == i) { + ESP_LOGV(TAG, "Datapoint %u found in ignore_mcu_update_on_datapoints list, dropping MCU update", datapoint.id); + skip = true; + break; + } + } + if (skip) + continue; + + // Update internal datapoints + bool found = false; + for (auto &other : this->datapoints_) { + if (other.id == datapoint.id) { + other = datapoint; + found = true; + } + } + if (!found) { + this->datapoints_.push_back(datapoint); + } + + // Run through listeners + for (auto &listener : this->listeners_) { + if (listener.datapoint_id == datapoint.id) + listener.on_datapoint(datapoint); + } } } diff --git a/esphome/components/tuya/tuya.h b/esphome/components/tuya/tuya.h index c46d61119e..3828c49b48 100644 --- a/esphome/components/tuya/tuya.h +++ b/esphome/components/tuya/tuya.h @@ -101,7 +101,7 @@ class Tuya : public Component, public uart::UARTDevice { protected: void handle_char_(uint8_t c); - void handle_datapoint_(const uint8_t *buffer, size_t len); + void handle_datapoints_(const uint8_t *buffer, size_t len); optional get_datapoint_(uint8_t datapoint_id); bool validate_message_(); From ab47e201c71efb94e38de1097d6cb3ebb177dbfc Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 4 Feb 2022 19:15:00 +1300 Subject: [PATCH 0187/1729] Bump improv library to 1.2.1 (#3160) --- esphome/components/esp32_improv/__init__.py | 2 +- esphome/components/improv_serial/__init__.py | 2 +- platformio.ini | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/esp32_improv/__init__.py b/esphome/components/esp32_improv/__init__.py index c95d4075bc..9f8438f785 100644 --- a/esphome/components/esp32_improv/__init__.py +++ b/esphome/components/esp32_improv/__init__.py @@ -56,7 +56,7 @@ async def to_code(config): cg.add(ble_server.register_service_component(var)) cg.add_define("USE_IMPROV") - cg.add_library("esphome/Improv", "1.2.0") + cg.add_library("esphome/Improv", "1.2.1") cg.add(var.set_identify_duration(config[CONF_IDENTIFY_DURATION])) cg.add(var.set_authorized_duration(config[CONF_AUTHORIZED_DURATION])) diff --git a/esphome/components/improv_serial/__init__.py b/esphome/components/improv_serial/__init__.py index 4de4fe66a7..21073a8ab3 100644 --- a/esphome/components/improv_serial/__init__.py +++ b/esphome/components/improv_serial/__init__.py @@ -30,4 +30,4 @@ FINAL_VALIDATE_SCHEMA = validate_logger_baud_rate async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) - cg.add_library("esphome/Improv", "1.2.0") + cg.add_library("esphome/Improv", "1.2.1") diff --git a/platformio.ini b/platformio.ini index d63f28d05e..70cfb11bf2 100644 --- a/platformio.ini +++ b/platformio.ini @@ -35,7 +35,7 @@ build_flags = lib_deps = esphome/noise-c@0.1.4 ; api makuna/NeoPixelBus@2.6.9 ; neopixelbus - esphome/Improv@1.2.0 ; improv_serial / esp32_improv + esphome/Improv@1.2.1 ; improv_serial / esp32_improv bblanchon/ArduinoJson@6.18.5 ; json wjtje/qr-code-generator-library@1.7.0 ; qr_code build_flags = From 253161d3d0a3d69a8d83c6e39b5dbc6d95085ad0 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Mon, 7 Feb 2022 21:26:16 +0100 Subject: [PATCH 0188/1729] Fix copy_file_if_changed src permissions copied too (#3161) --- esphome/helpers.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/esphome/helpers.py b/esphome/helpers.py index 1193d61eaa..289abe5459 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -1,4 +1,5 @@ import codecs +from contextlib import suppress import logging import os @@ -233,8 +234,20 @@ def copy_file_if_changed(src: os.PathLike, dst: os.PathLike) -> None: return mkdir_p(os.path.dirname(dst)) try: - shutil.copy(src, dst) + shutil.copyfile(src, dst) except OSError as err: + if isinstance(err, PermissionError): + # Older esphome versions copied over the src file permissions too. + # So when the dst file had 444 permissions, the dst file would have those + # too and subsequent writes would fail + + # -> delete file (it would be overwritten anyway), and try again + # if that fails, use normal error handler + with suppress(OSError): + os.unlink(dst) + shutil.copyfile(src, dst) + return + from esphome.core import EsphomeError raise EsphomeError(f"Error copying file {src} to {dst}: {err}") from err From 1e5004f495a1bb10d9b70b9ec96f2250bbe59f3e Mon Sep 17 00:00:00 2001 From: mknjc Date: Tue, 8 Feb 2022 00:45:27 +0100 Subject: [PATCH 0189/1729] [debug] Refactor debug sensors to use the normal sensor model. (#3162) --- esphome/components/debug/__init__.py | 50 +++++--------------- esphome/components/debug/debug_component.cpp | 12 ++++- esphome/components/debug/debug_component.h | 20 ++++++-- esphome/components/debug/sensor.py | 49 +++++++++++++++++++ esphome/components/debug/text_sensor.py | 29 ++++++++++++ 5 files changed, 119 insertions(+), 41 deletions(-) create mode 100644 esphome/components/debug/sensor.py create mode 100644 esphome/components/debug/text_sensor.py diff --git a/esphome/components/debug/__init__.py b/esphome/components/debug/__init__.py index 98ad9e2b10..ff9b9c5314 100644 --- a/esphome/components/debug/__init__.py +++ b/esphome/components/debug/__init__.py @@ -1,6 +1,5 @@ import esphome.config_validation as cv import esphome.codegen as cg -from esphome.components import sensor, text_sensor from esphome.const import ( CONF_ID, CONF_DEVICE, @@ -8,16 +7,12 @@ from esphome.const import ( CONF_FRAGMENTATION, CONF_BLOCK, CONF_LOOP_TIME, - UNIT_MILLISECOND, - UNIT_PERCENT, - UNIT_BYTES, - ICON_COUNTER, - ICON_TIMER, ) CODEOWNERS = ["@OttoWinter"] DEPENDENCIES = ["logger"] +CONF_DEBUG_ID = "debug_id" debug_ns = cg.esphome_ns.namespace("debug") DebugComponent = debug_ns.class_("DebugComponent", cg.PollingComponent) @@ -25,18 +20,20 @@ DebugComponent = debug_ns.class_("DebugComponent", cg.PollingComponent) CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(DebugComponent), - cv.Optional(CONF_DEVICE): text_sensor.TEXT_SENSOR_SCHEMA.extend( - {cv.GenerateID(): cv.declare_id(text_sensor.TextSensor)} + cv.Optional(CONF_DEVICE): cv.invalid( + "The 'device' option has been moved to the 'debug' text_sensor component" ), - cv.Optional(CONF_FREE): sensor.sensor_schema(UNIT_BYTES, ICON_COUNTER, 0), - cv.Optional(CONF_BLOCK): sensor.sensor_schema(UNIT_BYTES, ICON_COUNTER, 0), - cv.Optional(CONF_FRAGMENTATION): cv.All( - cv.only_on_esp8266, - cv.require_framework_version(esp8266_arduino=cv.Version(2, 5, 2)), - sensor.sensor_schema(UNIT_PERCENT, ICON_COUNTER, 1), + cv.Optional(CONF_FREE): cv.invalid( + "The 'free' option has been moved to the 'debug' sensor component" ), - cv.Optional(CONF_LOOP_TIME): sensor.sensor_schema( - UNIT_MILLISECOND, ICON_TIMER, 1 + cv.Optional(CONF_BLOCK): cv.invalid( + "The 'block' option has been moved to the 'debug' sensor component" + ), + cv.Optional(CONF_FRAGMENTATION): cv.invalid( + "The 'fragmentation' option has been moved to the 'debug' sensor component" + ), + cv.Optional(CONF_LOOP_TIME): cv.invalid( + "The 'loop_time' option has been moved to the 'debug' sensor component" ), } ).extend(cv.polling_component_schema("60s")) @@ -45,24 +42,3 @@ CONFIG_SCHEMA = cv.Schema( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) - - if CONF_DEVICE in config: - sens = cg.new_Pvariable(config[CONF_DEVICE][CONF_ID]) - await text_sensor.register_text_sensor(sens, config[CONF_DEVICE]) - cg.add(var.set_device_info_sensor(sens)) - - if CONF_FREE in config: - sens = await sensor.new_sensor(config[CONF_FREE]) - cg.add(var.set_free_sensor(sens)) - - if CONF_BLOCK in config: - sens = await sensor.new_sensor(config[CONF_BLOCK]) - cg.add(var.set_block_sensor(sens)) - - if CONF_FRAGMENTATION in config: - sens = await sensor.new_sensor(config[CONF_FRAGMENTATION]) - cg.add(var.set_fragmentation_sensor(sens)) - - if CONF_LOOP_TIME in config: - sens = await sensor.new_sensor(config[CONF_LOOP_TIME]) - cg.add(var.set_loop_time_sensor(sens)) diff --git a/esphome/components/debug/debug_component.cpp b/esphome/components/debug/debug_component.cpp index 41bf5f50c7..a2697084bd 100644 --- a/esphome/components/debug/debug_component.cpp +++ b/esphome/components/debug/debug_component.cpp @@ -47,12 +47,16 @@ void DebugComponent::dump_config() { #endif ESP_LOGCONFIG(TAG, "Debug component:"); +#ifdef USE_TEXT_SENSOR LOG_TEXT_SENSOR(" ", "Device info", this->device_info_); +#endif // USE_TEXT_SENSOR +#ifdef USE_SENSOR LOG_SENSOR(" ", "Free space on heap", this->free_sensor_); LOG_SENSOR(" ", "Largest free heap block", this->block_sensor_); #if defined(USE_ESP8266) && ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2) LOG_SENSOR(" ", "Heap fragmentation", this->fragmentation_sensor_); -#endif +#endif // defined(USE_ESP8266) && ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2) +#endif // USE_SENSOR ESP_LOGD(TAG, "ESPHome version %s", ESPHOME_VERSION); device_info += ESPHOME_VERSION; @@ -268,11 +272,13 @@ void DebugComponent::dump_config() { device_info += ESP.getResetInfo().c_str(); #endif +#ifdef USE_TEXT_SENSOR if (this->device_info_ != nullptr) { if (device_info.length() > 255) device_info.resize(255); this->device_info_->publish_state(device_info); } +#endif // USE_TEXT_SENSOR } void DebugComponent::loop() { @@ -284,6 +290,7 @@ void DebugComponent::loop() { this->status_momentary_warning("heap", 1000); } +#ifdef USE_SENSOR // calculate loop time - from last call to this one if (this->loop_time_sensor_ != nullptr) { uint32_t now = millis(); @@ -291,9 +298,11 @@ void DebugComponent::loop() { this->max_loop_time_ = std::max(this->max_loop_time_, loop_time); this->last_loop_timetag_ = now; } +#endif // USE_SENSOR } void DebugComponent::update() { +#ifdef USE_SENSOR if (this->free_sensor_ != nullptr) { this->free_sensor_->publish_state(get_free_heap()); } @@ -318,6 +327,7 @@ void DebugComponent::update() { this->loop_time_sensor_->publish_state(this->max_loop_time_); this->max_loop_time_ = 0; } +#endif // USE_SENSOR } float DebugComponent::get_setup_priority() const { return setup_priority::LATE; } diff --git a/esphome/components/debug/debug_component.h b/esphome/components/debug/debug_component.h index a362c52617..f966b4fafc 100644 --- a/esphome/components/debug/debug_component.h +++ b/esphome/components/debug/debug_component.h @@ -1,10 +1,16 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/defines.h" #include "esphome/core/macros.h" #include "esphome/core/helpers.h" -#include "esphome/components/text_sensor/text_sensor.h" + +#ifdef USE_SENSOR #include "esphome/components/sensor/sensor.h" +#endif +#ifdef USE_TEXT_SENSOR +#include "esphome/components/text_sensor/text_sensor.h" +#endif namespace esphome { namespace debug { @@ -16,27 +22,35 @@ class DebugComponent : public PollingComponent { float get_setup_priority() const override; void dump_config() override; +#ifdef USE_TEXT_SENSOR void set_device_info_sensor(text_sensor::TextSensor *device_info) { device_info_ = device_info; } +#endif // USE_TEXT_SENSOR +#ifdef USE_SENSOR void set_free_sensor(sensor::Sensor *free_sensor) { free_sensor_ = free_sensor; } void set_block_sensor(sensor::Sensor *block_sensor) { block_sensor_ = block_sensor; } #if defined(USE_ESP8266) && ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2) void set_fragmentation_sensor(sensor::Sensor *fragmentation_sensor) { fragmentation_sensor_ = fragmentation_sensor; } #endif void set_loop_time_sensor(sensor::Sensor *loop_time_sensor) { loop_time_sensor_ = loop_time_sensor; } - +#endif // USE_SENSOR protected: uint32_t free_heap_{}; +#ifdef USE_SENSOR uint32_t last_loop_timetag_{0}; uint32_t max_loop_time_{0}; - text_sensor::TextSensor *device_info_{nullptr}; sensor::Sensor *free_sensor_{nullptr}; sensor::Sensor *block_sensor_{nullptr}; #if defined(USE_ESP8266) && ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2) sensor::Sensor *fragmentation_sensor_{nullptr}; #endif sensor::Sensor *loop_time_sensor_{nullptr}; +#endif // USE_SENSOR + +#ifdef USE_TEXT_SENSOR + text_sensor::TextSensor *device_info_{nullptr}; +#endif // USE_TEXT_SENSOR }; } // namespace debug diff --git a/esphome/components/debug/sensor.py b/esphome/components/debug/sensor.py new file mode 100644 index 0000000000..deea6fd5ed --- /dev/null +++ b/esphome/components/debug/sensor.py @@ -0,0 +1,49 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import ( + CONF_FREE, + CONF_FRAGMENTATION, + CONF_BLOCK, + CONF_LOOP_TIME, + UNIT_MILLISECOND, + UNIT_PERCENT, + UNIT_BYTES, + ICON_COUNTER, + ICON_TIMER, +) +from . import CONF_DEBUG_ID, DebugComponent + +DEPENDENCIES = ["debug"] + +CONFIG_SCHEMA = { + cv.GenerateID(CONF_DEBUG_ID): cv.use_id(DebugComponent), + cv.Optional(CONF_FREE): sensor.sensor_schema(UNIT_BYTES, ICON_COUNTER, 0), + cv.Optional(CONF_BLOCK): sensor.sensor_schema(UNIT_BYTES, ICON_COUNTER, 0), + cv.Optional(CONF_FRAGMENTATION): cv.All( + cv.only_on_esp8266, + cv.require_framework_version(esp8266_arduino=cv.Version(2, 5, 2)), + sensor.sensor_schema(UNIT_PERCENT, ICON_COUNTER, 1), + ), + cv.Optional(CONF_LOOP_TIME): sensor.sensor_schema(UNIT_MILLISECOND, ICON_TIMER, 0), +} + + +async def to_code(config): + debug_component = await cg.get_variable(config[CONF_DEBUG_ID]) + + if CONF_FREE in config: + sens = await sensor.new_sensor(config[CONF_FREE]) + cg.add(debug_component.set_free_sensor(sens)) + + if CONF_BLOCK in config: + sens = await sensor.new_sensor(config[CONF_BLOCK]) + cg.add(debug_component.set_block_sensor(sens)) + + if CONF_FRAGMENTATION in config: + sens = await sensor.new_sensor(config[CONF_FRAGMENTATION]) + cg.add(debug_component.set_fragmentation_sensor(sens)) + + if CONF_LOOP_TIME in config: + sens = await sensor.new_sensor(config[CONF_LOOP_TIME]) + cg.add(debug_component.set_loop_time_sensor(sens)) diff --git a/esphome/components/debug/text_sensor.py b/esphome/components/debug/text_sensor.py new file mode 100644 index 0000000000..98abc67245 --- /dev/null +++ b/esphome/components/debug/text_sensor.py @@ -0,0 +1,29 @@ +from esphome.components import text_sensor +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome.const import ( + CONF_ID, + CONF_DEVICE, +) +from . import CONF_DEBUG_ID, DebugComponent + +DEPENDENCIES = ["debug"] + + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_DEBUG_ID): cv.use_id(DebugComponent), + cv.Optional(CONF_DEVICE): text_sensor.TEXT_SENSOR_SCHEMA.extend( + {cv.GenerateID(): cv.declare_id(text_sensor.TextSensor)} + ), + } +) + + +async def to_code(config): + debug_component = await cg.get_variable(config[CONF_DEBUG_ID]) + + if CONF_DEVICE in config: + sens = cg.new_Pvariable(config[CONF_DEVICE][CONF_ID]) + await text_sensor.register_text_sensor(sens, config[CONF_DEVICE]) + cg.add(debug_component.set_device_info_sensor(sens)) From ad43d6a5bcb53441edd18cd3f1907dcc47e97452 Mon Sep 17 00:00:00 2001 From: Jeff Eberl Date: Mon, 7 Feb 2022 20:32:37 -0700 Subject: [PATCH 0190/1729] Added RadonEye RD200 Component (#3119) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 2 + esphome/components/radon_eye_ble/__init__.py | 23 +++ .../radon_eye_ble/radon_eye_listener.cpp | 25 +++ .../radon_eye_ble/radon_eye_listener.h | 19 ++ .../components/radon_eye_rd200/__init__.py | 1 + .../radon_eye_rd200/radon_eye_rd200.cpp | 179 ++++++++++++++++++ .../radon_eye_rd200/radon_eye_rd200.h | 59 ++++++ esphome/components/radon_eye_rd200/sensor.py | 55 ++++++ tests/test2.yaml | 11 ++ 9 files changed, 374 insertions(+) create mode 100644 esphome/components/radon_eye_ble/__init__.py create mode 100644 esphome/components/radon_eye_ble/radon_eye_listener.cpp create mode 100644 esphome/components/radon_eye_ble/radon_eye_listener.h create mode 100644 esphome/components/radon_eye_rd200/__init__.py create mode 100644 esphome/components/radon_eye_rd200/radon_eye_rd200.cpp create mode 100644 esphome/components/radon_eye_rd200/radon_eye_rd200.h create mode 100644 esphome/components/radon_eye_rd200/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 5fa3090aaf..a353906da2 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -139,6 +139,8 @@ esphome/components/psram/* @esphome/core esphome/components/pulse_meter/* @stevebaxter esphome/components/pvvx_mithermometer/* @pasiz esphome/components/qr_code/* @wjtje +esphome/components/radon_eye_ble/* @jeffeb3 +esphome/components/radon_eye_rd200/* @jeffeb3 esphome/components/rc522/* @glmnet esphome/components/rc522_i2c/* @glmnet esphome/components/rc522_spi/* @glmnet diff --git a/esphome/components/radon_eye_ble/__init__.py b/esphome/components/radon_eye_ble/__init__.py new file mode 100644 index 0000000000..ffe434d19b --- /dev/null +++ b/esphome/components/radon_eye_ble/__init__.py @@ -0,0 +1,23 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import esp32_ble_tracker +from esphome.const import CONF_ID + +DEPENDENCIES = ["esp32_ble_tracker"] +CODEOWNERS = ["@jeffeb3"] + +radon_eye_ble_ns = cg.esphome_ns.namespace("radon_eye_ble") +RadonEyeListener = radon_eye_ble_ns.class_( + "RadonEyeListener", esp32_ble_tracker.ESPBTDeviceListener +) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(RadonEyeListener), + } +).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield esp32_ble_tracker.register_ble_device(var, config) diff --git a/esphome/components/radon_eye_ble/radon_eye_listener.cpp b/esphome/components/radon_eye_ble/radon_eye_listener.cpp new file mode 100644 index 0000000000..b10986c9cb --- /dev/null +++ b/esphome/components/radon_eye_ble/radon_eye_listener.cpp @@ -0,0 +1,25 @@ +#include "radon_eye_listener.h" +#include "esphome/core/log.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace radon_eye_ble { + +static const char *const TAG = "radon_eye_ble"; + +bool RadonEyeListener::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + if (not device.get_name().empty()) { + if (device.get_name().rfind("FR:R20:SN", 0) == 0) { + // This is an RD200, I think + ESP_LOGD(TAG, "Found Radon Eye RD200 device Name: %s (MAC: %s)", device.get_name().c_str(), + device.address_str().c_str()); + } + } + return false; +} + +} // namespace radon_eye_ble +} // namespace esphome + +#endif diff --git a/esphome/components/radon_eye_ble/radon_eye_listener.h b/esphome/components/radon_eye_ble/radon_eye_listener.h new file mode 100644 index 0000000000..26d0233c56 --- /dev/null +++ b/esphome/components/radon_eye_ble/radon_eye_listener.h @@ -0,0 +1,19 @@ +#pragma once + +#ifdef USE_ESP32 + +#include "esphome/core/component.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" + +namespace esphome { +namespace radon_eye_ble { + +class RadonEyeListener : public esp32_ble_tracker::ESPBTDeviceListener { + public: + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; +}; + +} // namespace radon_eye_ble +} // namespace esphome + +#endif diff --git a/esphome/components/radon_eye_rd200/__init__.py b/esphome/components/radon_eye_rd200/__init__.py new file mode 100644 index 0000000000..0740f6967b --- /dev/null +++ b/esphome/components/radon_eye_rd200/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@jeffeb3"] diff --git a/esphome/components/radon_eye_rd200/radon_eye_rd200.cpp b/esphome/components/radon_eye_rd200/radon_eye_rd200.cpp new file mode 100644 index 0000000000..6bb17f0508 --- /dev/null +++ b/esphome/components/radon_eye_rd200/radon_eye_rd200.cpp @@ -0,0 +1,179 @@ +#include "radon_eye_rd200.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace radon_eye_rd200 { + +static const char *const TAG = "radon_eye_rd200"; + +void RadonEyeRD200::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) { + switch (event) { + case ESP_GATTC_OPEN_EVT: { + if (param->open.status == ESP_GATT_OK) { + ESP_LOGI(TAG, "Connected successfully!"); + } + break; + } + + case ESP_GATTC_DISCONNECT_EVT: { + ESP_LOGW(TAG, "Disconnected!"); + break; + } + + case ESP_GATTC_SEARCH_CMPL_EVT: { + this->read_handle_ = 0; + auto *chr = this->parent()->get_characteristic(service_uuid_, sensors_read_characteristic_uuid_); + if (chr == nullptr) { + ESP_LOGW(TAG, "No sensor read characteristic found at service %s char %s", service_uuid_.to_string().c_str(), + sensors_read_characteristic_uuid_.to_string().c_str()); + break; + } + this->read_handle_ = chr->handle; + + // Write a 0x50 to the write characteristic. + auto *write_chr = this->parent()->get_characteristic(service_uuid_, sensors_write_characteristic_uuid_); + if (write_chr == nullptr) { + ESP_LOGW(TAG, "No sensor write characteristic found at service %s char %s", service_uuid_.to_string().c_str(), + sensors_read_characteristic_uuid_.to_string().c_str()); + break; + } + this->write_handle_ = write_chr->handle; + + this->node_state = esp32_ble_tracker::ClientState::ESTABLISHED; + + write_query_message_(); + + request_read_values_(); + break; + } + + case ESP_GATTC_READ_CHAR_EVT: { + if (param->read.conn_id != this->parent()->conn_id) + break; + if (param->read.status != ESP_GATT_OK) { + ESP_LOGW(TAG, "Error reading char at handle %d, status=%d", param->read.handle, param->read.status); + break; + } + if (param->read.handle == this->read_handle_) { + read_sensors_(param->read.value, param->read.value_len); + } + break; + } + + default: + break; + } +} + +void RadonEyeRD200::read_sensors_(uint8_t *value, uint16_t value_len) { + if (value_len < 20) { + ESP_LOGD(TAG, "Invalid read"); + return; + } + + // Example data + // [13:08:47][D][radon_eye_rd200:107]: result bytes: 5010 85EBB940 00000000 00000000 2200 2500 0000 + ESP_LOGV(TAG, "result bytes: %02X%02X %02X%02X%02X%02X %02X%02X%02X%02X %02X%02X%02X%02X %02X%02X %02X%02X %02X%02X", + value[0], value[1], value[2], value[3], value[4], value[5], value[6], value[7], value[8], value[9], + value[10], value[11], value[12], value[13], value[14], value[15], value[16], value[17], value[18], + value[19]); + + if (value[0] != 0x50) { + // This isn't a sensor reading. + return; + } + + // Convert from pCi/L to Bq/m³ + constexpr float convert_to_bwpm3 = 37.0; + + RadonValue radon_value; + radon_value.chars[0] = value[2]; + radon_value.chars[1] = value[3]; + radon_value.chars[2] = value[4]; + radon_value.chars[3] = value[5]; + float radon_now = radon_value.number * convert_to_bwpm3; + if (is_valid_radon_value_(radon_now)) { + radon_sensor_->publish_state(radon_now); + } + + radon_value.chars[0] = value[6]; + radon_value.chars[1] = value[7]; + radon_value.chars[2] = value[8]; + radon_value.chars[3] = value[9]; + float radon_day = radon_value.number * convert_to_bwpm3; + + radon_value.chars[0] = value[10]; + radon_value.chars[1] = value[11]; + radon_value.chars[2] = value[12]; + radon_value.chars[3] = value[13]; + float radon_month = radon_value.number * convert_to_bwpm3; + + if (is_valid_radon_value_(radon_month)) { + ESP_LOGV(TAG, "Radon Long Term based on month"); + radon_long_term_sensor_->publish_state(radon_month); + } else if (is_valid_radon_value_(radon_day)) { + ESP_LOGV(TAG, "Radon Long Term based on day"); + radon_long_term_sensor_->publish_state(radon_day); + } + + ESP_LOGV(TAG, " Measurements (Bq/m³) now: %0.03f, day: %0.03f, month: %0.03f", radon_now, radon_day, radon_month); + + ESP_LOGV(TAG, " Measurements (pCi/L) now: %0.03f, day: %0.03f, month: %0.03f", radon_now / convert_to_bwpm3, + radon_day / convert_to_bwpm3, radon_month / convert_to_bwpm3); + + // This instance must not stay connected + // so other clients can connect to it (e.g. the + // mobile app). + parent()->set_enabled(false); +} + +bool RadonEyeRD200::is_valid_radon_value_(float radon) { return radon > 0.0 and radon < 37000; } + +void RadonEyeRD200::update() { + if (this->node_state != esp32_ble_tracker::ClientState::ESTABLISHED) { + if (!parent()->enabled) { + ESP_LOGW(TAG, "Reconnecting to device"); + parent()->set_enabled(true); + parent()->connect(); + } else { + ESP_LOGW(TAG, "Connection in progress"); + } + } +} + +void RadonEyeRD200::write_query_message_() { + ESP_LOGV(TAG, "writing 0x50 to write service"); + int request = 0x50; + auto status = esp_ble_gattc_write_char_descr(this->parent()->gattc_if, this->parent()->conn_id, this->write_handle_, + sizeof(request), (uint8_t *) &request, ESP_GATT_WRITE_TYPE_NO_RSP, + ESP_GATT_AUTH_REQ_NONE); + if (status) { + ESP_LOGW(TAG, "Error sending write request for sensor, status=%d", status); + } +} + +void RadonEyeRD200::request_read_values_() { + auto status = esp_ble_gattc_read_char(this->parent()->gattc_if, this->parent()->conn_id, this->read_handle_, + ESP_GATT_AUTH_REQ_NONE); + if (status) { + ESP_LOGW(TAG, "Error sending read request for sensor, status=%d", status); + } +} + +void RadonEyeRD200::dump_config() { + LOG_SENSOR(" ", "Radon", this->radon_sensor_); + LOG_SENSOR(" ", "Radon Long Term", this->radon_long_term_sensor_); +} + +RadonEyeRD200::RadonEyeRD200() + : PollingComponent(10000), + service_uuid_(esp32_ble_tracker::ESPBTUUID::from_raw(SERVICE_UUID)), + sensors_write_characteristic_uuid_(esp32_ble_tracker::ESPBTUUID::from_raw(WRITE_CHARACTERISTIC_UUID)), + sensors_read_characteristic_uuid_(esp32_ble_tracker::ESPBTUUID::from_raw(READ_CHARACTERISTIC_UUID)) {} + +} // namespace radon_eye_rd200 +} // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/components/radon_eye_rd200/radon_eye_rd200.h b/esphome/components/radon_eye_rd200/radon_eye_rd200.h new file mode 100644 index 0000000000..7b29be7bd8 --- /dev/null +++ b/esphome/components/radon_eye_rd200/radon_eye_rd200.h @@ -0,0 +1,59 @@ +#pragma once + +#ifdef USE_ESP32 + +#include +#include +#include +#include "esphome/components/ble_client/ble_client.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/core/component.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace radon_eye_rd200 { + +static const char *const SERVICE_UUID = "00001523-1212-efde-1523-785feabcd123"; +static const char *const WRITE_CHARACTERISTIC_UUID = "00001524-1212-efde-1523-785feabcd123"; +static const char *const READ_CHARACTERISTIC_UUID = "00001525-1212-efde-1523-785feabcd123"; + +class RadonEyeRD200 : public PollingComponent, public ble_client::BLEClientNode { + public: + RadonEyeRD200(); + + void dump_config() override; + void update() override; + + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) override; + + void set_radon(sensor::Sensor *radon) { radon_sensor_ = radon; } + void set_radon_long_term(sensor::Sensor *radon_long_term) { radon_long_term_sensor_ = radon_long_term; } + + protected: + bool is_valid_radon_value_(float radon); + + void read_sensors_(uint8_t *value, uint16_t value_len); + void write_query_message_(); + void request_read_values_(); + + sensor::Sensor *radon_sensor_{nullptr}; + sensor::Sensor *radon_long_term_sensor_{nullptr}; + + uint16_t read_handle_; + uint16_t write_handle_; + esp32_ble_tracker::ESPBTUUID service_uuid_; + esp32_ble_tracker::ESPBTUUID sensors_write_characteristic_uuid_; + esp32_ble_tracker::ESPBTUUID sensors_read_characteristic_uuid_; + + union RadonValue { + char chars[4]; + float number; + }; +}; + +} // namespace radon_eye_rd200 +} // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/components/radon_eye_rd200/sensor.py b/esphome/components/radon_eye_rd200/sensor.py new file mode 100644 index 0000000000..a9667869b8 --- /dev/null +++ b/esphome/components/radon_eye_rd200/sensor.py @@ -0,0 +1,55 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, ble_client + +from esphome.const import ( + STATE_CLASS_MEASUREMENT, + UNIT_BECQUEREL_PER_CUBIC_METER, + CONF_ID, + CONF_RADON, + CONF_RADON_LONG_TERM, + ICON_RADIOACTIVE, +) + +DEPENDENCIES = ["ble_client"] + +radon_eye_rd200_ns = cg.esphome_ns.namespace("radon_eye_rd200") +RadonEyeRD200 = radon_eye_rd200_ns.class_( + "RadonEyeRD200", cg.PollingComponent, ble_client.BLEClientNode +) + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(RadonEyeRD200), + cv.Optional(CONF_RADON): sensor.sensor_schema( + unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER, + icon=ICON_RADIOACTIVE, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_RADON_LONG_TERM): sensor.sensor_schema( + unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER, + icon=ICON_RADIOACTIVE, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("5min")) + .extend(ble_client.BLE_CLIENT_SCHEMA), +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + await ble_client.register_ble_node(var, config) + + if CONF_RADON in config: + sens = await sensor.new_sensor(config[CONF_RADON]) + cg.add(var.set_radon(sens)) + if CONF_RADON_LONG_TERM in config: + sens = await sensor.new_sensor(config[CONF_RADON_LONG_TERM]) + cg.add(var.set_radon_long_term(sens)) diff --git a/tests/test2.yaml b/tests/test2.yaml index 6d606a3143..2a122b971f 100644 --- a/tests/test2.yaml +++ b/tests/test2.yaml @@ -324,6 +324,13 @@ sensor: bus_voltage: name: "INA260 Voltage" update_interval: 60s + - platform: radon_eye_rd200 + ble_client_id: radon_eye_ble_id + update_interval: 10min + radon: + name: "RD200 Radon" + radon_long_term: + name: "RD200 Radon Long Term" time: - platform: homeassistant @@ -423,10 +430,14 @@ ble_client: id: airthings01 - mac_address: 01:02:03:04:05:06 id: airthingsmini01 + - mac_address: 01:02:03:04:05:06 + id: radon_eye_ble_id airthings_ble: +radon_eye_ble: + ruuvi_ble: xiaomi_ble: From 69856286e86dbb292fd1d8f4111ed9a1149ae0ba Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 8 Feb 2022 17:23:45 +1300 Subject: [PATCH 0191/1729] Text sensor schema generator similar to sensor (#3172) --- esphome/components/ble_scanner/text_sensor.py | 10 +-- esphome/components/bme680_bsec/text_sensor.py | 11 +-- esphome/components/ccs811/sensor.py | 11 +-- .../components/custom/text_sensor/__init__.py | 6 +- esphome/components/daly_bms/text_sensor.py | 12 +-- esphome/components/debug/text_sensor.py | 13 +-- esphome/components/demo/__init__.py | 13 +-- esphome/components/dsmr/text_sensor.py | 85 ++++--------------- .../homeassistant/text_sensor/__init__.py | 8 +- .../modbus_controller/text_sensor/__init__.py | 3 +- .../mqtt_subscribe/text_sensor/__init__.py | 26 +++--- .../nextion/text_sensor/__init__.py | 8 +- esphome/components/pipsolar/__init__.py | 2 +- .../pipsolar/text_sensor/__init__.py | 18 +--- .../text_sensor/pipsolar_textsensor.cpp | 13 --- .../text_sensor/pipsolar_textsensor.h | 20 ----- .../components/sun/text_sensor/__init__.py | 10 +-- esphome/components/teleinfo/__init__.py | 13 ++- .../components/teleinfo/sensor/__init__.py | 22 ++--- .../teleinfo/text_sensor/__init__.py | 13 +-- .../template/text_sensor/__init__.py | 19 +++-- esphome/components/text_sensor/__init__.py | 36 +++++++- .../components/tuya/text_sensor/__init__.py | 23 ++--- esphome/components/version/text_sensor.py | 29 +++---- esphome/components/wifi_info/text_sensor.py | 50 +++-------- 25 files changed, 176 insertions(+), 298 deletions(-) delete mode 100644 esphome/components/pipsolar/text_sensor/pipsolar_textsensor.cpp delete mode 100644 esphome/components/pipsolar/text_sensor/pipsolar_textsensor.h diff --git a/esphome/components/ble_scanner/text_sensor.py b/esphome/components/ble_scanner/text_sensor.py index 0e140aa701..31dccdf119 100644 --- a/esphome/components/ble_scanner/text_sensor.py +++ b/esphome/components/ble_scanner/text_sensor.py @@ -1,7 +1,6 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import text_sensor, esp32_ble_tracker -from esphome.const import CONF_ID DEPENDENCIES = ["esp32_ble_tracker"] @@ -14,18 +13,13 @@ BLEScanner = ble_scanner_ns.class_( ) CONFIG_SCHEMA = cv.All( - text_sensor.TEXT_SENSOR_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(BLEScanner), - } - ) + text_sensor.text_sensor_schema(klass=BLEScanner) .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) .extend(cv.COMPONENT_SCHEMA) ) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await text_sensor.new_text_sensor(config) await cg.register_component(var, config) await esp32_ble_tracker.register_ble_device(var, config) - await text_sensor.register_text_sensor(var, config) diff --git a/esphome/components/bme680_bsec/text_sensor.py b/esphome/components/bme680_bsec/text_sensor.py index 96020544e7..2d93c90818 100644 --- a/esphome/components/bme680_bsec/text_sensor.py +++ b/esphome/components/bme680_bsec/text_sensor.py @@ -1,7 +1,6 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import text_sensor -from esphome.const import CONF_ID, CONF_ICON from . import BME680BSECComponent, CONF_BME680_BSEC_ID DEPENDENCIES = ["bme680_bsec"] @@ -14,11 +13,8 @@ TYPES = [CONF_IAQ_ACCURACY] CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(CONF_BME680_BSEC_ID): cv.use_id(BME680BSECComponent), - cv.Optional(CONF_IAQ_ACCURACY): text_sensor.TEXT_SENSOR_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), - cv.Optional(CONF_ICON, default=ICON_ACCURACY): cv.icon, - } + cv.Optional(CONF_IAQ_ACCURACY): text_sensor.text_sensor_schema( + icon=ICON_ACCURACY ), } ) @@ -27,8 +23,7 @@ CONFIG_SCHEMA = cv.Schema( async def setup_conf(config, key, hub): if key in config: conf = config[key] - sens = cg.new_Pvariable(conf[CONF_ID]) - await text_sensor.register_text_sensor(sens, conf) + sens = await text_sensor.new_text_sensor(conf) cg.add(getattr(hub, f"set_{key}_text_sensor")(sens)) diff --git a/esphome/components/ccs811/sensor.py b/esphome/components/ccs811/sensor.py index bb8200273d..cb5c1108ba 100644 --- a/esphome/components/ccs811/sensor.py +++ b/esphome/components/ccs811/sensor.py @@ -2,7 +2,6 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor, text_sensor from esphome.const import ( - CONF_ICON, CONF_ID, ICON_RADIATOR, ICON_RESTART, @@ -47,11 +46,8 @@ CONFIG_SCHEMA = ( device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, state_class=STATE_CLASS_MEASUREMENT, ), - cv.Optional(CONF_VERSION): text_sensor.TEXT_SENSOR_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), - cv.Optional(CONF_ICON, default=ICON_RESTART): cv.icon, - } + cv.Optional(CONF_VERSION): text_sensor.text_sensor_schema( + icon=ICON_RESTART ), cv.Optional(CONF_BASELINE): cv.hex_uint16_t, cv.Optional(CONF_TEMPERATURE): cv.use_id(sensor.Sensor), @@ -74,8 +70,7 @@ async def to_code(config): cg.add(var.set_tvoc(sens)) if CONF_VERSION in config: - sens = cg.new_Pvariable(config[CONF_VERSION][CONF_ID]) - await text_sensor.register_text_sensor(sens, config[CONF_VERSION]) + sens = await text_sensor.new_text_sensor(config[CONF_VERSION]) cg.add(var.set_version(sens)) if CONF_BASELINE in config: diff --git a/esphome/components/custom/text_sensor/__init__.py b/esphome/components/custom/text_sensor/__init__.py index 5b6d416436..70728af604 100644 --- a/esphome/components/custom/text_sensor/__init__.py +++ b/esphome/components/custom/text_sensor/__init__.py @@ -11,11 +11,7 @@ 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.extend( - { - cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), - } - ) + text_sensor.text_sensor_schema() ), } ) diff --git a/esphome/components/daly_bms/text_sensor.py b/esphome/components/daly_bms/text_sensor.py index de49a0b4b9..9f23e5f373 100644 --- a/esphome/components/daly_bms/text_sensor.py +++ b/esphome/components/daly_bms/text_sensor.py @@ -1,7 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import text_sensor -from esphome.const import CONF_ICON, CONF_ID, CONF_STATUS +from esphome.const import CONF_STATUS from . import DalyBmsComponent, CONF_BMS_DALY_ID ICON_CAR_BATTERY = "mdi:car-battery" @@ -14,11 +14,8 @@ CONFIG_SCHEMA = cv.All( cv.Schema( { cv.GenerateID(CONF_BMS_DALY_ID): cv.use_id(DalyBmsComponent), - cv.Optional(CONF_STATUS): text_sensor.TEXT_SENSOR_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), - cv.Optional(CONF_ICON, default=ICON_CAR_BATTERY): cv.icon, - } + cv.Optional(CONF_STATUS): text_sensor.text_sensor_schema( + icon=ICON_CAR_BATTERY ), } ).extend(cv.COMPONENT_SCHEMA) @@ -28,8 +25,7 @@ CONFIG_SCHEMA = cv.All( async def setup_conf(config, key, hub): if key in config: conf = config[key] - sens = cg.new_Pvariable(conf[CONF_ID]) - await text_sensor.register_text_sensor(sens, conf) + sens = await text_sensor.new_text_sensor(conf) cg.add(getattr(hub, f"set_{key}_text_sensor")(sens)) diff --git a/esphome/components/debug/text_sensor.py b/esphome/components/debug/text_sensor.py index 98abc67245..f8d1016fbf 100644 --- a/esphome/components/debug/text_sensor.py +++ b/esphome/components/debug/text_sensor.py @@ -1,10 +1,8 @@ from esphome.components import text_sensor import esphome.config_validation as cv import esphome.codegen as cg -from esphome.const import ( - CONF_ID, - CONF_DEVICE, -) +from esphome.const import CONF_DEVICE + from . import CONF_DEBUG_ID, DebugComponent DEPENDENCIES = ["debug"] @@ -13,9 +11,7 @@ DEPENDENCIES = ["debug"] CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(CONF_DEBUG_ID): cv.use_id(DebugComponent), - cv.Optional(CONF_DEVICE): text_sensor.TEXT_SENSOR_SCHEMA.extend( - {cv.GenerateID(): cv.declare_id(text_sensor.TextSensor)} - ), + cv.Optional(CONF_DEVICE): text_sensor.text_sensor_schema(), } ) @@ -24,6 +20,5 @@ async def to_code(config): debug_component = await cg.get_variable(config[CONF_DEBUG_ID]) if CONF_DEVICE in config: - sens = cg.new_Pvariable(config[CONF_DEVICE][CONF_ID]) - await text_sensor.register_text_sensor(sens, config[CONF_DEVICE]) + sens = await text_sensor.new_text_sensor(config[CONF_DEVICE]) cg.add(debug_component.set_device_info_sensor(sens)) diff --git a/esphome/components/demo/__init__.py b/esphome/components/demo/__init__.py index 6b4a55aac9..f20a96ebd4 100644 --- a/esphome/components/demo/__init__.py +++ b/esphome/components/demo/__init__.py @@ -37,12 +37,10 @@ from esphome.const import ( DEVICE_CLASS_TEMPERATURE, ICON_BLUETOOTH, ICON_BLUR, - ICON_EMPTY, ICON_THERMOMETER, STATE_CLASS_MEASUREMENT, STATE_CLASS_TOTAL_INCREASING, UNIT_CELSIUS, - UNIT_EMPTY, UNIT_PERCENT, UNIT_WATT_HOURS, ) @@ -339,7 +337,7 @@ CONFIG_SCHEMA = cv.Schema( }, ], ): [ - sensor.sensor_schema(UNIT_EMPTY, ICON_EMPTY, 0) + sensor.sensor_schema(accuracy_decimals=0) .extend(cv.polling_component_schema("60s")) .extend( { @@ -378,12 +376,8 @@ CONFIG_SCHEMA = cv.Schema( }, ], ): [ - text_sensor.TEXT_SENSOR_SCHEMA.extend( + text_sensor.text_sensor_schema(klass=DemoTextSensor).extend( cv.polling_component_schema("60s") - ).extend( - { - cv.GenerateID(): cv.declare_id(DemoTextSensor), - } ) ], } @@ -443,6 +437,5 @@ async def to_code(config): await switch.register_switch(var, conf) for conf in config[CONF_TEXT_SENSORS]: - var = cg.new_Pvariable(conf[CONF_ID]) + var = await text_sensor.new_text_sensor(conf) await cg.register_component(var, conf) - await text_sensor.register_text_sensor(var, conf) diff --git a/esphome/components/dsmr/text_sensor.py b/esphome/components/dsmr/text_sensor.py index 339eea711f..202cc07020 100644 --- a/esphome/components/dsmr/text_sensor.py +++ b/esphome/components/dsmr/text_sensor.py @@ -1,9 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import text_sensor -from esphome.const import ( - CONF_ID, -) + from . import Dsmr, CONF_DSMR_ID AUTO_LOAD = ["dsmr"] @@ -11,71 +9,19 @@ AUTO_LOAD = ["dsmr"] CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(CONF_DSMR_ID): cv.use_id(Dsmr), - cv.Optional("identification"): text_sensor.TEXT_SENSOR_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), - } - ), - cv.Optional("p1_version"): text_sensor.TEXT_SENSOR_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), - } - ), - cv.Optional("p1_version_be"): text_sensor.TEXT_SENSOR_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), - } - ), - cv.Optional("timestamp"): text_sensor.TEXT_SENSOR_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), - } - ), - cv.Optional("electricity_tariff"): text_sensor.TEXT_SENSOR_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), - } - ), - cv.Optional("electricity_failure_log"): text_sensor.TEXT_SENSOR_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), - } - ), - cv.Optional("message_short"): text_sensor.TEXT_SENSOR_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), - } - ), - cv.Optional("message_long"): text_sensor.TEXT_SENSOR_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), - } - ), - cv.Optional("gas_equipment_id"): text_sensor.TEXT_SENSOR_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), - } - ), - cv.Optional("thermal_equipment_id"): text_sensor.TEXT_SENSOR_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), - } - ), - cv.Optional("water_equipment_id"): text_sensor.TEXT_SENSOR_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), - } - ), - cv.Optional("sub_equipment_id"): text_sensor.TEXT_SENSOR_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), - } - ), - cv.Optional("gas_delivered_text"): text_sensor.TEXT_SENSOR_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), - } - ), + cv.Optional("identification"): text_sensor.text_sensor_schema(), + cv.Optional("p1_version"): text_sensor.text_sensor_schema(), + cv.Optional("p1_version_be"): text_sensor.text_sensor_schema(), + cv.Optional("timestamp"): text_sensor.text_sensor_schema(), + cv.Optional("electricity_tariff"): text_sensor.text_sensor_schema(), + cv.Optional("electricity_failure_log"): text_sensor.text_sensor_schema(), + cv.Optional("message_short"): text_sensor.text_sensor_schema(), + cv.Optional("message_long"): text_sensor.text_sensor_schema(), + cv.Optional("gas_equipment_id"): text_sensor.text_sensor_schema(), + cv.Optional("thermal_equipment_id"): text_sensor.text_sensor_schema(), + cv.Optional("water_equipment_id"): text_sensor.text_sensor_schema(), + cv.Optional("sub_equipment_id"): text_sensor.text_sensor_schema(), + cv.Optional("gas_delivered_text"): text_sensor.text_sensor_schema(), } ).extend(cv.COMPONENT_SCHEMA) @@ -89,8 +35,7 @@ async def to_code(config): continue id = conf.get("id") if id and id.type == text_sensor.TextSensor: - var = cg.new_Pvariable(conf[CONF_ID]) - await text_sensor.register_text_sensor(var, conf) + var = await text_sensor.new_text_sensor(conf) cg.add(getattr(hub, f"set_{key}")(var)) text_sensors.append(f"F({key})") diff --git a/esphome/components/homeassistant/text_sensor/__init__.py b/esphome/components/homeassistant/text_sensor/__init__.py index b63d45b9ce..be59bab676 100644 --- a/esphome/components/homeassistant/text_sensor/__init__.py +++ b/esphome/components/homeassistant/text_sensor/__init__.py @@ -1,7 +1,8 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import text_sensor -from esphome.const import CONF_ATTRIBUTE, CONF_ENTITY_ID, CONF_ID +from esphome.const import CONF_ATTRIBUTE, CONF_ENTITY_ID + from .. import homeassistant_ns DEPENDENCIES = ["api"] @@ -10,7 +11,7 @@ HomeassistantTextSensor = homeassistant_ns.class_( "HomeassistantTextSensor", text_sensor.TextSensor, cg.Component ) -CONFIG_SCHEMA = text_sensor.TEXT_SENSOR_SCHEMA.extend( +CONFIG_SCHEMA = text_sensor.text_sensor_schema().extend( { cv.GenerateID(): cv.declare_id(HomeassistantTextSensor), cv.Required(CONF_ENTITY_ID): cv.entity_id, @@ -20,9 +21,8 @@ CONFIG_SCHEMA = text_sensor.TEXT_SENSOR_SCHEMA.extend( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await text_sensor.new_text_sensor(config) await cg.register_component(var, config) - await text_sensor.register_text_sensor(var, config) cg.add(var.set_entity_id(config[CONF_ENTITY_ID])) if CONF_ATTRIBUTE in config: diff --git a/esphome/components/modbus_controller/text_sensor/__init__.py b/esphome/components/modbus_controller/text_sensor/__init__.py index 5cc85af5bc..763336e104 100644 --- a/esphome/components/modbus_controller/text_sensor/__init__.py +++ b/esphome/components/modbus_controller/text_sensor/__init__.py @@ -40,7 +40,8 @@ RAW_ENCODING = { } CONFIG_SCHEMA = cv.All( - text_sensor.TEXT_SENSOR_SCHEMA.extend(cv.COMPONENT_SCHEMA) + text_sensor.text_sensor_schema() + .extend(cv.COMPONENT_SCHEMA) .extend(ModbusItemBaseSchema) .extend( { diff --git a/esphome/components/mqtt_subscribe/text_sensor/__init__.py b/esphome/components/mqtt_subscribe/text_sensor/__init__.py index 477e4dec45..5b5c0ae17f 100644 --- a/esphome/components/mqtt_subscribe/text_sensor/__init__.py +++ b/esphome/components/mqtt_subscribe/text_sensor/__init__.py @@ -1,7 +1,8 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import text_sensor, mqtt -from esphome.const import CONF_ID, CONF_QOS, CONF_TOPIC +from esphome.const import CONF_QOS, CONF_TOPIC + from .. import mqtt_subscribe_ns DEPENDENCIES = ["mqtt"] @@ -11,20 +12,23 @@ MQTTSubscribeTextSensor = mqtt_subscribe_ns.class_( "MQTTSubscribeTextSensor", text_sensor.TextSensor, cg.Component ) -CONFIG_SCHEMA = text_sensor.TEXT_SENSOR_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(MQTTSubscribeTextSensor), - cv.GenerateID(CONF_MQTT_PARENT_ID): cv.use_id(mqtt.MQTTClientComponent), - cv.Required(CONF_TOPIC): cv.subscribe_topic, - cv.Optional(CONF_QOS, default=0): cv.mqtt_qos, - } -).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = ( + text_sensor.text_sensor_schema() + .extend( + { + cv.GenerateID(): cv.declare_id(MQTTSubscribeTextSensor), + cv.GenerateID(CONF_MQTT_PARENT_ID): cv.use_id(mqtt.MQTTClientComponent), + cv.Required(CONF_TOPIC): cv.subscribe_topic, + cv.Optional(CONF_QOS, default=0): cv.mqtt_qos, + } + ) + .extend(cv.COMPONENT_SCHEMA) +) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await text_sensor.new_text_sensor(config) await cg.register_component(var, config) - await text_sensor.register_text_sensor(var, config) parent = await cg.get_variable(config[CONF_MQTT_PARENT_ID]) cg.add(var.set_parent(parent)) diff --git a/esphome/components/nextion/text_sensor/__init__.py b/esphome/components/nextion/text_sensor/__init__.py index 9c170dd807..9b8518d8c4 100644 --- a/esphome/components/nextion/text_sensor/__init__.py +++ b/esphome/components/nextion/text_sensor/__init__.py @@ -17,11 +17,7 @@ NextionTextSensor = nextion_ns.class_( ) CONFIG_SCHEMA = ( - text_sensor.TEXT_SENSOR_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(NextionTextSensor), - } - ) + text_sensor.text_sensor_schema(klass=NextionTextSensor) .extend(CONFIG_TEXT_COMPONENT_SCHEMA) .extend(cv.polling_component_schema("never")) ) @@ -30,8 +26,8 @@ CONFIG_SCHEMA = ( async def to_code(config): hub = await cg.get_variable(config[CONF_NEXTION_ID]) var = cg.new_Pvariable(config[CONF_ID], hub) - await cg.register_component(var, config) await text_sensor.register_text_sensor(var, config) + await cg.register_component(var, config) cg.add(hub.register_textsensor_component(var)) diff --git a/esphome/components/pipsolar/__init__.py b/esphome/components/pipsolar/__init__.py index 20e4672125..875de05713 100644 --- a/esphome/components/pipsolar/__init__.py +++ b/esphome/components/pipsolar/__init__.py @@ -13,7 +13,7 @@ CONF_PIPSOLAR_ID = "pipsolar_id" pipsolar_ns = cg.esphome_ns.namespace("pipsolar") PipsolarComponent = pipsolar_ns.class_("Pipsolar", cg.Component) -PIPSOLAR_COMPONENT_SCHEMA = cv.COMPONENT_SCHEMA.extend( +PIPSOLAR_COMPONENT_SCHEMA = cv.Schema( { cv.Required(CONF_PIPSOLAR_ID): cv.use_id(PipsolarComponent), } diff --git a/esphome/components/pipsolar/text_sensor/__init__.py b/esphome/components/pipsolar/text_sensor/__init__.py index fe6c4979f3..856352f8cb 100644 --- a/esphome/components/pipsolar/text_sensor/__init__.py +++ b/esphome/components/pipsolar/text_sensor/__init__.py @@ -1,8 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import text_sensor -from esphome.const import CONF_ID -from .. import CONF_PIPSOLAR_ID, PIPSOLAR_COMPONENT_SCHEMA, pipsolar_ns +from .. import CONF_PIPSOLAR_ID, PIPSOLAR_COMPONENT_SCHEMA DEPENDENCIES = ["uart"] @@ -15,10 +14,6 @@ CONF_LAST_QPIWS = "last_qpiws" CONF_LAST_QT = "last_qt" CONF_LAST_QMN = "last_qmn" -PipsolarTextSensor = pipsolar_ns.class_( - "PipsolarTextSensor", text_sensor.TextSensor, cg.Component -) - TYPES = [ CONF_DEVICE_MODE, CONF_LAST_QPIGS, @@ -31,12 +26,7 @@ TYPES = [ ] CONFIG_SCHEMA = PIPSOLAR_COMPONENT_SCHEMA.extend( - { - cv.Optional(type): text_sensor.TEXT_SENSOR_SCHEMA.extend( - {cv.GenerateID(): cv.declare_id(PipsolarTextSensor)} - ) - for type in TYPES - } + {cv.Optional(type): text_sensor.text_sensor_schema() for type in TYPES} ) @@ -46,7 +36,5 @@ async def to_code(config): for type in TYPES: if type in config: conf = config[type] - var = cg.new_Pvariable(conf[CONF_ID]) - await text_sensor.register_text_sensor(var, conf) - await cg.register_component(var, conf) + var = await text_sensor.new_text_sensor(conf) cg.add(getattr(paren, f"set_{type}")(var)) diff --git a/esphome/components/pipsolar/text_sensor/pipsolar_textsensor.cpp b/esphome/components/pipsolar/text_sensor/pipsolar_textsensor.cpp deleted file mode 100644 index ee1fe2d1d8..0000000000 --- a/esphome/components/pipsolar/text_sensor/pipsolar_textsensor.cpp +++ /dev/null @@ -1,13 +0,0 @@ -#include "pipsolar_textsensor.h" -#include "esphome/core/log.h" -#include "esphome/core/application.h" - -namespace esphome { -namespace pipsolar { - -static const char *const TAG = "pipsolar.text_sensor"; - -void PipsolarTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Pipsolar TextSensor", this); } - -} // namespace pipsolar -} // namespace esphome diff --git a/esphome/components/pipsolar/text_sensor/pipsolar_textsensor.h b/esphome/components/pipsolar/text_sensor/pipsolar_textsensor.h deleted file mode 100644 index 871f6d8dee..0000000000 --- a/esphome/components/pipsolar/text_sensor/pipsolar_textsensor.h +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once - -#include "../pipsolar.h" -#include "esphome/components/text_sensor/text_sensor.h" -#include "esphome/core/component.h" - -namespace esphome { -namespace pipsolar { -class Pipsolar; -class PipsolarTextSensor : public Component, public text_sensor::TextSensor { - public: - void set_parent(Pipsolar *parent) { this->parent_ = parent; }; - void dump_config() override; - - protected: - Pipsolar *parent_; -}; - -} // namespace pipsolar -} // namespace esphome diff --git a/esphome/components/sun/text_sensor/__init__.py b/esphome/components/sun/text_sensor/__init__.py index ac1ce223d1..80737bb9f2 100644 --- a/esphome/components/sun/text_sensor/__init__.py +++ b/esphome/components/sun/text_sensor/__init__.py @@ -6,7 +6,6 @@ from esphome.const import ( ICON_WEATHER_SUNSET_DOWN, ICON_WEATHER_SUNSET_UP, CONF_TYPE, - CONF_ID, CONF_FORMAT, ) from .. import sun_ns, CONF_SUN_ID, Sun, CONF_ELEVATION, elevation, DEFAULT_ELEVATION @@ -33,7 +32,8 @@ def validate_optional_icon(config): CONFIG_SCHEMA = cv.All( - text_sensor.TEXT_SENSOR_SCHEMA.extend( + text_sensor.text_sensor_schema() + .extend( { cv.GenerateID(): cv.declare_id(SunTextSensor), cv.GenerateID(CONF_SUN_ID): cv.use_id(Sun), @@ -41,15 +41,15 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_ELEVATION, default=DEFAULT_ELEVATION): elevation, cv.Optional(CONF_FORMAT, default="%X"): cv.string_strict, } - ).extend(cv.polling_component_schema("60s")), + ) + .extend(cv.polling_component_schema("60s")), validate_optional_icon, ) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await text_sensor.new_text_sensor(config) await cg.register_component(var, config) - await text_sensor.register_text_sensor(var, config) paren = await cg.get_variable(config[CONF_SUN_ID]) cg.add(var.set_parent(paren)) diff --git a/esphome/components/teleinfo/__init__.py b/esphome/components/teleinfo/__init__.py index 9a5712e10f..33b748a031 100644 --- a/esphome/components/teleinfo/__init__.py +++ b/esphome/components/teleinfo/__init__.py @@ -7,9 +7,20 @@ CODEOWNERS = ["@0hax"] teleinfo_ns = cg.esphome_ns.namespace("teleinfo") TeleInfo = teleinfo_ns.class_("TeleInfo", cg.PollingComponent, uart.UARTDevice) -CONF_TELEINFO_ID = "teleinfo_id" +CONF_TELEINFO_ID = "teleinfo_id" +CONF_TAG_NAME = "tag_name" CONF_HISTORICAL_MODE = "historical_mode" + + +TELEINFO_LISTENER_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_TELEINFO_ID): cv.use_id(TeleInfo), + cv.Required(CONF_TAG_NAME): cv.string, + } +) + + CONFIG_SCHEMA = ( cv.Schema( { diff --git a/esphome/components/teleinfo/sensor/__init__.py b/esphome/components/teleinfo/sensor/__init__.py index e7cc2fcb1b..aa875be157 100644 --- a/esphome/components/teleinfo/sensor/__init__.py +++ b/esphome/components/teleinfo/sensor/__init__.py @@ -3,20 +3,22 @@ import esphome.config_validation as cv from esphome.components import sensor from esphome.const import CONF_ID, ICON_FLASH, UNIT_WATT_HOURS -from .. import teleinfo_ns, TeleInfo, CONF_TELEINFO_ID +from .. import ( + CONF_TAG_NAME, + TELEINFO_LISTENER_SCHEMA, + teleinfo_ns, + CONF_TELEINFO_ID, +) -CONF_TAG_NAME = "tag_name" TeleInfoSensor = teleinfo_ns.class_("TeleInfoSensor", sensor.Sensor, cg.Component) -CONFIG_SCHEMA = sensor.sensor_schema( - unit_of_measurement=UNIT_WATT_HOURS, icon=ICON_FLASH, accuracy_decimals=0 -).extend( - { - cv.GenerateID(): cv.declare_id(TeleInfoSensor), - cv.GenerateID(CONF_TELEINFO_ID): cv.use_id(TeleInfo), - cv.Required(CONF_TAG_NAME): cv.string, - } +CONFIG_SCHEMA = ( + sensor.sensor_schema( + unit_of_measurement=UNIT_WATT_HOURS, icon=ICON_FLASH, accuracy_decimals=0 + ) + .extend({cv.GenerateID(): cv.declare_id(TeleInfoSensor)}) + .extend(TELEINFO_LISTENER_SCHEMA) ) diff --git a/esphome/components/teleinfo/text_sensor/__init__.py b/esphome/components/teleinfo/text_sensor/__init__.py index 3bd73ff272..848b08d742 100644 --- a/esphome/components/teleinfo/text_sensor/__init__.py +++ b/esphome/components/teleinfo/text_sensor/__init__.py @@ -1,22 +1,15 @@ import esphome.codegen as cg -import esphome.config_validation as cv from esphome.components import text_sensor from esphome.const import CONF_ID -from .. import teleinfo_ns, TeleInfo, CONF_TELEINFO_ID - -CONF_TAG_NAME = "tag_name" +from .. import CONF_TAG_NAME, TELEINFO_LISTENER_SCHEMA, teleinfo_ns, CONF_TELEINFO_ID TeleInfoTextSensor = teleinfo_ns.class_( "TeleInfoTextSensor", text_sensor.TextSensor, cg.Component ) -CONFIG_SCHEMA = text_sensor.TEXT_SENSOR_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(TeleInfoTextSensor), - cv.GenerateID(CONF_TELEINFO_ID): cv.use_id(TeleInfo), - cv.Required(CONF_TAG_NAME): cv.string, - } +CONFIG_SCHEMA = text_sensor.text_sensor_schema(klass=TeleInfoTextSensor).extend( + TELEINFO_LISTENER_SCHEMA ) diff --git a/esphome/components/template/text_sensor/__init__.py b/esphome/components/template/text_sensor/__init__.py index 2e098a77c2..9bd603bbca 100644 --- a/esphome/components/template/text_sensor/__init__.py +++ b/esphome/components/template/text_sensor/__init__.py @@ -10,18 +10,21 @@ TemplateTextSensor = template_ns.class_( "TemplateTextSensor", text_sensor.TextSensor, cg.PollingComponent ) -CONFIG_SCHEMA = text_sensor.TEXT_SENSOR_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(TemplateTextSensor), - cv.Optional(CONF_LAMBDA): cv.returning_lambda, - } -).extend(cv.polling_component_schema("60s")) +CONFIG_SCHEMA = ( + text_sensor.text_sensor_schema() + .extend( + { + cv.GenerateID(): cv.declare_id(TemplateTextSensor), + cv.Optional(CONF_LAMBDA): cv.returning_lambda, + } + ) + .extend(cv.polling_component_schema("60s")) +) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await text_sensor.new_text_sensor(config) await cg.register_component(var, config) - await text_sensor.register_text_sensor(var, config) if CONF_LAMBDA in config: template_ = await cg.process_lambda( diff --git a/esphome/components/text_sensor/__init__.py b/esphome/components/text_sensor/__init__.py index e0fc6af19c..de72579402 100644 --- a/esphome/components/text_sensor/__init__.py +++ b/esphome/components/text_sensor/__init__.py @@ -3,7 +3,9 @@ import esphome.config_validation as cv from esphome import automation from esphome.components import mqtt from esphome.const import ( + CONF_ENTITY_CATEGORY, CONF_FILTERS, + CONF_ICON, CONF_ID, CONF_ON_VALUE, CONF_ON_RAW_VALUE, @@ -14,6 +16,7 @@ from esphome.const import ( CONF_TO, ) from esphome.core import CORE, coroutine_with_priority +from esphome.cpp_generator import MockObjClass from esphome.cpp_helpers import setup_entity from esphome.util import Registry @@ -110,12 +113,10 @@ async def map_filter_to_code(config, filter_id): ) -icon = cv.icon - - TEXT_SENSOR_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).extend( { cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTTextSensor), + cv.GenerateID(): cv.declare_id(TextSensor), cv.Optional(CONF_FILTERS): validate_filters, cv.Optional(CONF_ON_VALUE): automation.validate_automation( { @@ -132,6 +133,29 @@ TEXT_SENSOR_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).exte } ) +_UNDEF = object() + + +def text_sensor_schema( + klass: MockObjClass = _UNDEF, + icon: str = _UNDEF, + entity_category: str = _UNDEF, +) -> cv.Schema: + schema = TEXT_SENSOR_SCHEMA + if klass is not _UNDEF: + schema = schema.extend({cv.GenerateID(): cv.declare_id(klass)}) + if icon is not _UNDEF: + schema = schema.extend({cv.Optional(CONF_ICON, default=icon): cv.icon}) + if entity_category is not _UNDEF: + schema = schema.extend( + { + cv.Optional( + CONF_ENTITY_CATEGORY, default=entity_category + ): cv.entity_category + } + ) + return schema + async def build_filters(config): return await cg.build_registry_list(FILTER_REGISTRY, config) @@ -164,6 +188,12 @@ async def register_text_sensor(var, config): await setup_text_sensor_core_(var, config) +async def new_text_sensor(config): + var = cg.new_Pvariable(config[CONF_ID]) + await register_text_sensor(var, config) + return var + + @coroutine_with_priority(100.0) async def to_code(config): cg.add_define("USE_TEXT_SENSOR") diff --git a/esphome/components/tuya/text_sensor/__init__.py b/esphome/components/tuya/text_sensor/__init__.py index 1989ca10e3..bc60369377 100644 --- a/esphome/components/tuya/text_sensor/__init__.py +++ b/esphome/components/tuya/text_sensor/__init__.py @@ -1,7 +1,7 @@ from esphome.components import text_sensor import esphome.config_validation as cv import esphome.codegen as cg -from esphome.const import CONF_ID, CONF_SENSOR_DATAPOINT +from esphome.const import CONF_SENSOR_DATAPOINT from .. import tuya_ns, CONF_TUYA_ID, Tuya DEPENDENCIES = ["tuya"] @@ -9,19 +9,22 @@ CODEOWNERS = ["@dentra"] TuyaTextSensor = tuya_ns.class_("TuyaTextSensor", text_sensor.TextSensor, cg.Component) -CONFIG_SCHEMA = text_sensor.TEXT_SENSOR_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(TuyaTextSensor), - cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya), - cv.Required(CONF_SENSOR_DATAPOINT): cv.uint8_t, - } -).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = ( + text_sensor.text_sensor_schema() + .extend( + { + cv.GenerateID(): cv.declare_id(TuyaTextSensor), + cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya), + cv.Required(CONF_SENSOR_DATAPOINT): cv.uint8_t, + } + ) + .extend(cv.COMPONENT_SCHEMA) +) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await text_sensor.new_text_sensor(config) await cg.register_component(var, config) - await text_sensor.register_text_sensor(var, config) paren = await cg.get_variable(config[CONF_TUYA_ID]) cg.add(var.set_tuya_parent(paren)) diff --git a/esphome/components/version/text_sensor.py b/esphome/components/version/text_sensor.py index 4835caf35b..c8774bb322 100644 --- a/esphome/components/version/text_sensor.py +++ b/esphome/components/version/text_sensor.py @@ -2,9 +2,6 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import text_sensor from esphome.const import ( - CONF_ENTITY_CATEGORY, - CONF_ID, - CONF_ICON, ENTITY_CATEGORY_DIAGNOSTIC, ICON_NEW_BOX, CONF_HIDE_TIMESTAMP, @@ -15,20 +12,22 @@ VersionTextSensor = version_ns.class_( "VersionTextSensor", text_sensor.TextSensor, cg.Component ) -CONFIG_SCHEMA = text_sensor.TEXT_SENSOR_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(VersionTextSensor), - cv.Optional(CONF_ICON, default=ICON_NEW_BOX): text_sensor.icon, - cv.Optional(CONF_HIDE_TIMESTAMP, default=False): cv.boolean, - cv.Optional( - CONF_ENTITY_CATEGORY, default=ENTITY_CATEGORY_DIAGNOSTIC - ): cv.entity_category, - } -).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = ( + text_sensor.text_sensor_schema( + icon=ICON_NEW_BOX, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ) + .extend( + { + cv.GenerateID(): cv.declare_id(VersionTextSensor), + cv.Optional(CONF_HIDE_TIMESTAMP, default=False): cv.boolean, + } + ) + .extend(cv.COMPONENT_SCHEMA) +) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await text_sensor.register_text_sensor(var, config) + var = await text_sensor.new_text_sensor(config) await cg.register_component(var, config) cg.add(var.set_hide_timestamp(config[CONF_HIDE_TIMESTAMP])) diff --git a/esphome/components/wifi_info/text_sensor.py b/esphome/components/wifi_info/text_sensor.py index 706a8967be..d7d652ac7b 100644 --- a/esphome/components/wifi_info/text_sensor.py +++ b/esphome/components/wifi_info/text_sensor.py @@ -3,8 +3,6 @@ import esphome.config_validation as cv from esphome.components import text_sensor from esphome.const import ( CONF_BSSID, - CONF_ENTITY_CATEGORY, - CONF_ID, CONF_IP_ADDRESS, CONF_SCAN_RESULTS, CONF_SSID, @@ -31,45 +29,20 @@ MacAddressWifiInfo = wifi_info_ns.class_( CONFIG_SCHEMA = cv.Schema( { - cv.Optional(CONF_IP_ADDRESS): text_sensor.TEXT_SENSOR_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(IPAddressWiFiInfo), - cv.Optional( - CONF_ENTITY_CATEGORY, default=ENTITY_CATEGORY_DIAGNOSTIC - ): cv.entity_category, - } + cv.Optional(CONF_IP_ADDRESS): text_sensor.text_sensor_schema( + klass=IPAddressWiFiInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC ), - cv.Optional(CONF_SCAN_RESULTS): text_sensor.TEXT_SENSOR_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(ScanResultsWiFiInfo), - cv.Optional( - CONF_ENTITY_CATEGORY, default=ENTITY_CATEGORY_DIAGNOSTIC - ): cv.entity_category, - } + cv.Optional(CONF_SCAN_RESULTS): text_sensor.text_sensor_schema( + klass=ScanResultsWiFiInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC ).extend(cv.polling_component_schema("60s")), - cv.Optional(CONF_SSID): text_sensor.TEXT_SENSOR_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(SSIDWiFiInfo), - cv.Optional( - CONF_ENTITY_CATEGORY, default=ENTITY_CATEGORY_DIAGNOSTIC - ): cv.entity_category, - } + cv.Optional(CONF_SSID): text_sensor.text_sensor_schema( + klass=SSIDWiFiInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC ), - cv.Optional(CONF_BSSID): text_sensor.TEXT_SENSOR_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(BSSIDWiFiInfo), - cv.Optional( - CONF_ENTITY_CATEGORY, default=ENTITY_CATEGORY_DIAGNOSTIC - ): cv.entity_category, - } + cv.Optional(CONF_BSSID): text_sensor.text_sensor_schema( + klass=BSSIDWiFiInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC ), - cv.Optional(CONF_MAC_ADDRESS): text_sensor.TEXT_SENSOR_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(MacAddressWifiInfo), - cv.Optional( - CONF_ENTITY_CATEGORY, default=ENTITY_CATEGORY_DIAGNOSTIC - ): cv.entity_category, - } + cv.Optional(CONF_MAC_ADDRESS): text_sensor.text_sensor_schema( + klass=MacAddressWifiInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC ), } ) @@ -78,9 +51,8 @@ CONFIG_SCHEMA = cv.Schema( async def setup_conf(config, key): if key in config: conf = config[key] - var = cg.new_Pvariable(conf[CONF_ID]) + var = await text_sensor.new_text_sensor(conf) await cg.register_component(var, conf) - await text_sensor.register_text_sensor(var, conf) async def to_code(config): From 7ca9245735ee04b22fe5fb00ea81d52449b0f7c8 Mon Sep 17 00:00:00 2001 From: Jonas Bergler Date: Tue, 8 Feb 2022 20:27:39 +1300 Subject: [PATCH 0192/1729] wifi_info, reduce polling interval (#3165) Co-authored-by: Jonas Bergler Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/wifi_info/text_sensor.py | 14 ++++++++------ .../components/wifi_info/wifi_info_text_sensor.h | 12 ++++++------ 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/esphome/components/wifi_info/text_sensor.py b/esphome/components/wifi_info/text_sensor.py index d7d652ac7b..58250c3759 100644 --- a/esphome/components/wifi_info/text_sensor.py +++ b/esphome/components/wifi_info/text_sensor.py @@ -14,14 +14,16 @@ DEPENDENCIES = ["wifi"] wifi_info_ns = cg.esphome_ns.namespace("wifi_info") IPAddressWiFiInfo = wifi_info_ns.class_( - "IPAddressWiFiInfo", text_sensor.TextSensor, cg.Component + "IPAddressWiFiInfo", text_sensor.TextSensor, cg.PollingComponent ) ScanResultsWiFiInfo = wifi_info_ns.class_( "ScanResultsWiFiInfo", text_sensor.TextSensor, cg.PollingComponent ) -SSIDWiFiInfo = wifi_info_ns.class_("SSIDWiFiInfo", text_sensor.TextSensor, cg.Component) +SSIDWiFiInfo = wifi_info_ns.class_( + "SSIDWiFiInfo", text_sensor.TextSensor, cg.PollingComponent +) BSSIDWiFiInfo = wifi_info_ns.class_( - "BSSIDWiFiInfo", text_sensor.TextSensor, cg.Component + "BSSIDWiFiInfo", text_sensor.TextSensor, cg.PollingComponent ) MacAddressWifiInfo = wifi_info_ns.class_( "MacAddressWifiInfo", text_sensor.TextSensor, cg.Component @@ -31,16 +33,16 @@ CONFIG_SCHEMA = cv.Schema( { cv.Optional(CONF_IP_ADDRESS): text_sensor.text_sensor_schema( klass=IPAddressWiFiInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC - ), + ).extend(cv.polling_component_schema("1s")), cv.Optional(CONF_SCAN_RESULTS): text_sensor.text_sensor_schema( klass=ScanResultsWiFiInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC ).extend(cv.polling_component_schema("60s")), cv.Optional(CONF_SSID): text_sensor.text_sensor_schema( klass=SSIDWiFiInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC - ), + ).extend(cv.polling_component_schema("1s")), cv.Optional(CONF_BSSID): text_sensor.text_sensor_schema( klass=BSSIDWiFiInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC - ), + ).extend(cv.polling_component_schema("1s")), cv.Optional(CONF_MAC_ADDRESS): text_sensor.text_sensor_schema( klass=MacAddressWifiInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC ), diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.h b/esphome/components/wifi_info/wifi_info_text_sensor.h index 5b54451ed0..e5b0fa3223 100644 --- a/esphome/components/wifi_info/wifi_info_text_sensor.h +++ b/esphome/components/wifi_info/wifi_info_text_sensor.h @@ -7,9 +7,9 @@ namespace esphome { namespace wifi_info { -class IPAddressWiFiInfo : public Component, public text_sensor::TextSensor { +class IPAddressWiFiInfo : public PollingComponent, public text_sensor::TextSensor { public: - void loop() override { + void update() override { auto ip = wifi::global_wifi_component->wifi_sta_ip(); if (ip != this->last_ip_) { this->last_ip_ = ip; @@ -53,9 +53,9 @@ class ScanResultsWiFiInfo : public PollingComponent, public text_sensor::TextSen std::string last_scan_results_; }; -class SSIDWiFiInfo : public Component, public text_sensor::TextSensor { +class SSIDWiFiInfo : public PollingComponent, public text_sensor::TextSensor { public: - void loop() override { + void update() override { std::string ssid = wifi::global_wifi_component->wifi_ssid(); if (this->last_ssid_ != ssid) { this->last_ssid_ = ssid; @@ -70,9 +70,9 @@ class SSIDWiFiInfo : public Component, public text_sensor::TextSensor { std::string last_ssid_; }; -class BSSIDWiFiInfo : public Component, public text_sensor::TextSensor { +class BSSIDWiFiInfo : public PollingComponent, public text_sensor::TextSensor { public: - void loop() override { + void update() override { wifi::bssid_t bssid = wifi::global_wifi_component->wifi_bssid(); if (memcmp(bssid.data(), last_bssid_.data(), 6) != 0) { std::copy(bssid.begin(), bssid.end(), last_bssid_.begin()); From 397ef72b16c595c74285820697387b206967cbf5 Mon Sep 17 00:00:00 2001 From: functionpointer Date: Tue, 8 Feb 2022 08:42:11 +0100 Subject: [PATCH 0193/1729] MLX90393 three-axis magnetometer (#2770) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/mlx90393/__init__.py | 1 + esphome/components/mlx90393/sensor.py | 135 ++++++++++++++++++ .../components/mlx90393/sensor_mlx90393.cpp | 91 ++++++++++++ esphome/components/mlx90393/sensor_mlx90393.h | 59 ++++++++ platformio.ini | 1 + tests/test3.yaml | 14 ++ 7 files changed, 302 insertions(+) create mode 100644 esphome/components/mlx90393/__init__.py create mode 100644 esphome/components/mlx90393/sensor.py create mode 100644 esphome/components/mlx90393/sensor_mlx90393.cpp create mode 100644 esphome/components/mlx90393/sensor_mlx90393.h diff --git a/CODEOWNERS b/CODEOWNERS index a353906da2..d786dc165e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -109,6 +109,7 @@ esphome/components/mdns/* @esphome/core esphome/components/midea/* @dudanov esphome/components/midea_ir/* @dudanov esphome/components/mitsubishi/* @RubyBailey +esphome/components/mlx90393/* @functionpointer esphome/components/modbus_controller/* @martgras esphome/components/modbus_controller/binary_sensor/* @martgras esphome/components/modbus_controller/number/* @martgras diff --git a/esphome/components/mlx90393/__init__.py b/esphome/components/mlx90393/__init__.py new file mode 100644 index 0000000000..fc92f02120 --- /dev/null +++ b/esphome/components/mlx90393/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@functionpointer"] diff --git a/esphome/components/mlx90393/sensor.py b/esphome/components/mlx90393/sensor.py new file mode 100644 index 0000000000..92ba30bea3 --- /dev/null +++ b/esphome/components/mlx90393/sensor.py @@ -0,0 +1,135 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_ID, + UNIT_MICROTESLA, + UNIT_CELSIUS, + STATE_CLASS_MEASUREMENT, + ICON_MAGNET, + ICON_THERMOMETER, + CONF_GAIN, + CONF_RESOLUTION, + CONF_OVERSAMPLING, + CONF_FILTER, + CONF_TEMPERATURE, +) +from esphome import pins + +CODEOWNERS = ["@functionpointer"] +DEPENDENCIES = ["i2c"] + +mlx90393_ns = cg.esphome_ns.namespace("mlx90393") + +MLX90393Component = mlx90393_ns.class_( + "MLX90393Cls", cg.PollingComponent, i2c.I2CDevice +) + +GAIN = { + "1X": 7, + "1_33X": 6, + "1_67X": 5, + "2X": 4, + "2_5X": 3, + "3X": 2, + "4X": 1, + "5X": 0, +} + +RESOLUTION = { + "16BIT": 0, + "17BIT": 1, + "18BIT": 2, + "19BIT": 3, +} + +CONF_X_AXIS = "x_axis" +CONF_Y_AXIS = "y_axis" +CONF_Z_AXIS = "z_axis" +CONF_DRDY_PIN = "drdy_pin" + + +def mlx90393_axis_schema(default_resolution: str): + return sensor.sensor_schema( + unit_of_measurement=UNIT_MICROTESLA, + accuracy_decimals=0, + icon=ICON_MAGNET, + state_class=STATE_CLASS_MEASUREMENT, + ).extend( + cv.Schema( + { + cv.Optional(CONF_RESOLUTION, default=default_resolution): cv.enum( + RESOLUTION, upper=True, space="_" + ) + } + ) + ) + + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(MLX90393Component), + cv.Optional(CONF_GAIN, default="2_5X"): cv.enum( + GAIN, upper=True, space="_" + ), + cv.Optional(CONF_DRDY_PIN): pins.gpio_input_pin_schema, + cv.Optional(CONF_OVERSAMPLING, default=2): cv.int_range(min=0, max=3), + cv.Optional(CONF_FILTER, default=6): cv.int_range(min=0, max=7), + cv.Optional(CONF_X_AXIS): mlx90393_axis_schema("19BIT"), + cv.Optional(CONF_Y_AXIS): mlx90393_axis_schema("19BIT"), + cv.Optional(CONF_Z_AXIS): mlx90393_axis_schema("16BIT"), + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + icon=ICON_THERMOMETER, + state_class=STATE_CLASS_MEASUREMENT, + ).extend( + cv.Schema( + { + cv.Optional(CONF_OVERSAMPLING, default=0): cv.int_range( + min=0, max=3 + ), + } + ) + ), + }, + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x0C)) +) + + +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) + + if CONF_DRDY_PIN in config: + pin = await cg.gpio_pin_expression(config[CONF_DRDY_PIN]) + cg.add(var.set_drdy_pin(pin)) + cg.add(var.set_gain(GAIN[config[CONF_GAIN]])) + cg.add(var.set_oversampling(config[CONF_OVERSAMPLING])) + cg.add(var.set_filter(config[CONF_FILTER])) + + if CONF_X_AXIS in config: + sens = await sensor.new_sensor(config[CONF_X_AXIS]) + cg.add(var.set_x_sensor(sens)) + cg.add(var.set_resolution(0, RESOLUTION[config[CONF_X_AXIS][CONF_RESOLUTION]])) + if CONF_Y_AXIS in config: + sens = await sensor.new_sensor(config[CONF_Y_AXIS]) + cg.add(var.set_y_sensor(sens)) + cg.add(var.set_resolution(1, RESOLUTION[config[CONF_Y_AXIS][CONF_RESOLUTION]])) + if CONF_Z_AXIS in config: + sens = await sensor.new_sensor(config[CONF_Z_AXIS]) + cg.add(var.set_z_sensor(sens)) + cg.add(var.set_resolution(2, RESOLUTION[config[CONF_Z_AXIS][CONF_RESOLUTION]])) + if CONF_TEMPERATURE in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) + cg.add(var.set_t_sensor(sens)) + cg.add(var.set_t_oversampling(config[CONF_TEMPERATURE][CONF_OVERSAMPLING])) + if CONF_DRDY_PIN in config: + pin = await cg.gpio_pin_expression(config[CONF_DRDY_PIN]) + cg.add(var.set_drdy_gpio(pin)) + + cg.add_library("functionpointer/arduino-MLX90393", "1.0.0") diff --git a/esphome/components/mlx90393/sensor_mlx90393.cpp b/esphome/components/mlx90393/sensor_mlx90393.cpp new file mode 100644 index 0000000000..d4431a7334 --- /dev/null +++ b/esphome/components/mlx90393/sensor_mlx90393.cpp @@ -0,0 +1,91 @@ +#include "sensor_mlx90393.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace mlx90393 { + +static const char *const TAG = "mlx90393"; + +bool MLX90393Cls::transceive(const uint8_t *request, size_t request_size, uint8_t *response, size_t response_size) { + i2c::ErrorCode e = this->write(request, request_size); + if (e != i2c::ErrorCode::ERROR_OK) { + return false; + } + e = this->read(response, response_size); + return e == i2c::ErrorCode::ERROR_OK; +} + +bool MLX90393Cls::has_drdy_pin() { return this->drdy_pin_ != nullptr; } + +bool MLX90393Cls::read_drdy_pin() { + if (this->drdy_pin_ == nullptr) { + return false; + } else { + return this->drdy_pin_->digital_read(); + } +} +void MLX90393Cls::sleep_millis(uint32_t millis) { delay(millis); } +void MLX90393Cls::sleep_micros(uint32_t micros) { delayMicroseconds(micros); } + +void MLX90393Cls::setup() { + ESP_LOGCONFIG(TAG, "Setting up MLX90393..."); + // note the two arguments A0 and A1 which are used to construct an i2c address + // we can hard-code these because we never actually use the constructed address + // see the transceive function above, which uses the address from I2CComponent + this->mlx_.begin_with_hal(this, 0, 0); + + this->mlx_.setGainSel(this->gain_); + + this->mlx_.setResolution(this->resolutions_[0], this->resolutions_[1], this->resolutions_[2]); + + this->mlx_.setOverSampling(this->oversampling_); + + this->mlx_.setDigitalFiltering(this->filter_); + + this->mlx_.setTemperatureOverSampling(this->temperature_oversampling_); +} + +void MLX90393Cls::dump_config() { + ESP_LOGCONFIG(TAG, "MLX90393:"); + LOG_I2C_DEVICE(this); + + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with MLX90393 failed!"); + return; + } + LOG_UPDATE_INTERVAL(this); + + LOG_SENSOR(" ", "X Axis", this->x_sensor_); + LOG_SENSOR(" ", "Y Axis", this->y_sensor_); + LOG_SENSOR(" ", "Z Axis", this->z_sensor_); + LOG_SENSOR(" ", "Temperature", this->t_sensor_); +} + +float MLX90393Cls::get_setup_priority() const { return setup_priority::DATA; } + +void MLX90393Cls::update() { + MLX90393::txyz data; + + if (this->mlx_.readData(data) == MLX90393::STATUS_OK) { + ESP_LOGD(TAG, "received %f %f %f", data.x, data.y, data.z); + if (this->x_sensor_ != nullptr) { + this->x_sensor_->publish_state(data.x); + } + if (this->y_sensor_ != nullptr) { + this->y_sensor_->publish_state(data.y); + } + if (this->z_sensor_ != nullptr) { + this->z_sensor_->publish_state(data.z); + } + if (this->t_sensor_ != nullptr) { + this->t_sensor_->publish_state(data.t); + } + this->status_clear_warning(); + } else { + ESP_LOGE(TAG, "failed to read data"); + this->status_set_warning(); + } +} + +} // namespace mlx90393 +} // namespace esphome diff --git a/esphome/components/mlx90393/sensor_mlx90393.h b/esphome/components/mlx90393/sensor_mlx90393.h new file mode 100644 index 0000000000..fc33ad1aa8 --- /dev/null +++ b/esphome/components/mlx90393/sensor_mlx90393.h @@ -0,0 +1,59 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" +#include "esphome/core/hal.h" +#include +#include + +namespace esphome { +namespace mlx90393 { + +class MLX90393Cls : public PollingComponent, public i2c::I2CDevice, public MLX90393Hal { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + void update() override; + + void set_drdy_gpio(GPIOPin *pin) { drdy_pin_ = pin; } + + void set_x_sensor(sensor::Sensor *x_sensor) { x_sensor_ = x_sensor; } + void set_y_sensor(sensor::Sensor *y_sensor) { y_sensor_ = y_sensor; } + void set_z_sensor(sensor::Sensor *z_sensor) { z_sensor_ = z_sensor; } + void set_t_sensor(sensor::Sensor *t_sensor) { t_sensor_ = t_sensor; } + + void set_oversampling(uint8_t osr) { oversampling_ = osr; } + void set_t_oversampling(uint8_t osr2) { temperature_oversampling_ = osr2; } + void set_resolution(uint8_t xyz, uint8_t res) { resolutions_[xyz] = res; } + void set_filter(uint8_t filter) { filter_ = filter; } + void set_gain(uint8_t gain_sel) { gain_ = gain_sel; } + + // overrides for MLX library + + // disable lint because it keeps suggesting const uint8_t *response. + // this->read() writes data into response, so it can't be const + bool transceive(const uint8_t *request, size_t request_size, uint8_t *response, + size_t response_size) override; // NOLINT + bool has_drdy_pin() override; + bool read_drdy_pin() override; + void sleep_millis(uint32_t millis) override; + void sleep_micros(uint32_t micros) override; + + protected: + MLX90393 mlx_; + sensor::Sensor *x_sensor_{nullptr}; + sensor::Sensor *y_sensor_{nullptr}; + sensor::Sensor *z_sensor_{nullptr}; + sensor::Sensor *t_sensor_{nullptr}; + uint8_t gain_; + uint8_t oversampling_; + uint8_t temperature_oversampling_ = 0; + uint8_t filter_; + uint8_t resolutions_[3] = {0}; + GPIOPin *drdy_pin_ = nullptr; +}; + +} // namespace mlx90393 +} // namespace esphome diff --git a/platformio.ini b/platformio.ini index 70cfb11bf2..ba232033a1 100644 --- a/platformio.ini +++ b/platformio.ini @@ -38,6 +38,7 @@ lib_deps = esphome/Improv@1.2.1 ; improv_serial / esp32_improv bblanchon/ArduinoJson@6.18.5 ; json wjtje/qr-code-generator-library@1.7.0 ; qr_code + functionpointer/arduino-MLX90393@1.0.0 ; mlx90393 build_flags = -DESPHOME_LOG_LEVEL=ESPHOME_LOG_LEVEL_VERY_VERBOSE src_filter = diff --git a/tests/test3.yaml b/tests/test3.yaml index 32a3b1be3d..853d7bd389 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -692,6 +692,20 @@ sensor: name: 'testwave' component_id: 2 wave_channel_id: 1 + - platform: mlx90393 + oversampling: 1 + filter: 0 + gain: "3X" + x_axis: + name: "mlxxaxis" + y_axis: + name: "mlxyaxis" + z_axis: + name: "mlxzaxis" + resolution: 17BIT + temperature: + name: "mlxtemp" + oversampling: 2 time: - platform: homeassistant From 434ca47ea086affd044b97780250e9689b0c3820 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Tue, 8 Feb 2022 09:21:52 +0100 Subject: [PATCH 0194/1729] Enable mDNS during OTA safe mode (#3146) --- esphome/components/mdns/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/mdns/__init__.py b/esphome/components/mdns/__init__.py index b95469d9da..b5be153d5a 100644 --- a/esphome/components/mdns/__init__.py +++ b/esphome/components/mdns/__init__.py @@ -1,7 +1,7 @@ from esphome.const import CONF_ID import esphome.codegen as cg import esphome.config_validation as cv -from esphome.core import CORE +from esphome.core import CORE, coroutine_with_priority CODEOWNERS = ["@esphome/core"] DEPENDENCIES = ["network"] @@ -29,6 +29,7 @@ CONFIG_SCHEMA = cv.All( ) +@coroutine_with_priority(55.0) async def to_code(config): if CORE.using_arduino: if CORE.is_esp32: From 1c0697b5d45be313a4dc2fba43309d4834d6747d Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 8 Feb 2022 21:28:12 +1300 Subject: [PATCH 0195/1729] Dont warn on nonnull comparisons (#3123) --- esphome/components/esp8266/__init__.py | 1 + platformio.ini | 1 + 2 files changed, 2 insertions(+) diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index 015caf92e6..3c83400c1d 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -166,6 +166,7 @@ async def to_code(config): cg.add_platformio_option("framework", "arduino") cg.add_build_flag("-DUSE_ARDUINO") cg.add_build_flag("-DUSE_ESP8266_FRAMEWORK_ARDUINO") + cg.add_build_flag("-Wno-nonnull-compare") cg.add_platformio_option("platform", conf[CONF_PLATFORM_VERSION]) cg.add_platformio_option( "platform_packages", diff --git a/platformio.ini b/platformio.ini index ba232033a1..8775b28156 100644 --- a/platformio.ini +++ b/platformio.ini @@ -94,6 +94,7 @@ lib_deps = DNSServer ; captive_portal (Arduino built-in) build_flags = ${common:arduino.build_flags} + -Wno-nonnull-compare -DUSE_ESP8266 -DUSE_ESP8266_FRAMEWORK_ARDUINO extra_scripts = post:esphome/components/esp8266/post_build.py.script From 116ddbdd017347abdf6ada854e8658ecd39dad7c Mon Sep 17 00:00:00 2001 From: Ashton Kemerling Date: Tue, 8 Feb 2022 01:30:31 -0700 Subject: [PATCH 0196/1729] Add require response option for BLE binary output (#3091) --- esphome/components/ble_client/ble_client.cpp | 8 ++++++-- esphome/components/ble_client/ble_client.h | 1 + esphome/components/ble_client/output/__init__.py | 8 +++++--- .../components/ble_client/output/ble_binary_output.cpp | 6 +++++- esphome/components/ble_client/output/ble_binary_output.h | 2 ++ 5 files changed, 19 insertions(+), 6 deletions(-) diff --git a/esphome/components/ble_client/ble_client.cpp b/esphome/components/ble_client/ble_client.cpp index 89981e8174..5b2701d77a 100644 --- a/esphome/components/ble_client/ble_client.cpp +++ b/esphome/components/ble_client/ble_client.cpp @@ -395,15 +395,19 @@ BLEDescriptor *BLECharacteristic::get_descriptor(uint16_t uuid) { return this->get_descriptor(espbt::ESPBTUUID::from_uint16(uuid)); } -void BLECharacteristic::write_value(uint8_t *new_val, int16_t new_val_size) { +void BLECharacteristic::write_value(uint8_t *new_val, int16_t new_val_size, esp_gatt_write_type_t write_type) { auto *client = this->service->client; auto status = esp_ble_gattc_write_char(client->gattc_if, client->conn_id, this->handle, new_val_size, new_val, - ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); + write_type, ESP_GATT_AUTH_REQ_NONE); if (status) { ESP_LOGW(TAG, "Error sending write value to BLE gattc server, status=%d", status); } } +void BLECharacteristic::write_value(uint8_t *new_val, int16_t new_val_size) { + write_value(new_val, new_val_size, ESP_GATT_WRITE_TYPE_NO_RSP); +} + } // namespace ble_client } // namespace esphome diff --git a/esphome/components/ble_client/ble_client.h b/esphome/components/ble_client/ble_client.h index c173f7a995..e0a1bf61b9 100644 --- a/esphome/components/ble_client/ble_client.h +++ b/esphome/components/ble_client/ble_client.h @@ -60,6 +60,7 @@ class BLECharacteristic { BLEDescriptor *get_descriptor(espbt::ESPBTUUID uuid); BLEDescriptor *get_descriptor(uint16_t uuid); void write_value(uint8_t *new_val, int16_t new_val_size); + void write_value(uint8_t *new_val, int16_t new_val_size, esp_gatt_write_type_t write_type); BLEService *service; }; diff --git a/esphome/components/ble_client/output/__init__.py b/esphome/components/ble_client/output/__init__.py index fe5835ca82..e28421d1a7 100644 --- a/esphome/components/ble_client/output/__init__.py +++ b/esphome/components/ble_client/output/__init__.py @@ -1,13 +1,14 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import output, ble_client, esp32_ble_tracker +from esphome.components import ble_client, esp32_ble_tracker, output from esphome.const import CONF_ID, CONF_SERVICE_UUID -from .. import ble_client_ns +from .. import ble_client_ns DEPENDENCIES = ["ble_client"] CONF_CHARACTERISTIC_UUID = "characteristic_uuid" +CONF_REQUIRE_RESPONSE = "require_response" BLEBinaryOutput = ble_client_ns.class_( "BLEBinaryOutput", output.BinaryOutput, ble_client.BLEClientNode, cg.Component @@ -19,6 +20,7 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_ID): cv.declare_id(BLEBinaryOutput), cv.Required(CONF_SERVICE_UUID): esp32_ble_tracker.bt_uuid, cv.Required(CONF_CHARACTERISTIC_UUID): esp32_ble_tracker.bt_uuid, + cv.Optional(CONF_REQUIRE_RESPONSE, default=False): cv.boolean, } ) .extend(cv.COMPONENT_SCHEMA) @@ -61,7 +63,7 @@ def to_code(config): config[CONF_CHARACTERISTIC_UUID] ) cg.add(var.set_char_uuid128(uuid128)) - + cg.add(var.set_require_response(config[CONF_REQUIRE_RESPONSE])) yield output.register_output(var, config) yield ble_client.register_ble_node(var, config) yield cg.register_component(var, config) diff --git a/esphome/components/ble_client/output/ble_binary_output.cpp b/esphome/components/ble_client/output/ble_binary_output.cpp index 7bf5ea4ead..6709803936 100644 --- a/esphome/components/ble_client/output/ble_binary_output.cpp +++ b/esphome/components/ble_client/output/ble_binary_output.cpp @@ -63,7 +63,11 @@ void BLEBinaryOutput::write_state(bool state) { uint8_t state_as_uint = (uint8_t) state; ESP_LOGV(TAG, "[%s] Write State: %d", this->char_uuid_.to_string().c_str(), state_as_uint); - chr->write_value(&state_as_uint, sizeof(state_as_uint)); + if (this->require_response_) { + chr->write_value(&state_as_uint, sizeof(state_as_uint), ESP_GATT_WRITE_TYPE_RSP); + } else { + chr->write_value(&state_as_uint, sizeof(state_as_uint), ESP_GATT_WRITE_TYPE_NO_RSP); + } } } // namespace ble_client diff --git a/esphome/components/ble_client/output/ble_binary_output.h b/esphome/components/ble_client/output/ble_binary_output.h index e1d62a267b..83eabcf5f2 100644 --- a/esphome/components/ble_client/output/ble_binary_output.h +++ b/esphome/components/ble_client/output/ble_binary_output.h @@ -25,9 +25,11 @@ class BLEBinaryOutput : public output::BinaryOutput, public BLEClientNode, publi void set_char_uuid128(uint8_t *uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override; + void set_require_response(bool response) { this->require_response_ = response; } protected: void write_state(bool state) override; + bool require_response_; espbt::ESPBTUUID service_uuid_; espbt::ESPBTUUID char_uuid_; espbt::ClientState client_state_; From 94f944dc9c37074a46970f6efe9da916698bf88d Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 8 Feb 2022 21:50:25 +1300 Subject: [PATCH 0197/1729] Add Lilygo t5 4.7 Touchscreen (#3084) --- CODEOWNERS | 1 + esphome/codegen.py | 1 + esphome/components/lilygo_t5_47/__init__.py | 3 + .../lilygo_t5_47/touchscreen/__init__.py | 45 ++++++ .../touchscreen/lilygo_t5_47_touchscreen.cpp | 141 ++++++++++++++++++ .../touchscreen/lilygo_t5_47_touchscreen.h | 35 +++++ .../touchscreen/binary_sensor/__init__.py | 13 +- .../binary_sensor/touchscreen_binary_sensor.h | 11 +- esphome/cpp_types.py | 1 + tests/test4.yaml | 10 ++ 10 files changed, 255 insertions(+), 6 deletions(-) create mode 100644 esphome/components/lilygo_t5_47/__init__.py create mode 100644 esphome/components/lilygo_t5_47/touchscreen/__init__.py create mode 100644 esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.cpp create mode 100644 esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.h diff --git a/CODEOWNERS b/CODEOWNERS index d786dc165e..931e699566 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -89,6 +89,7 @@ esphome/components/json/* @OttoWinter esphome/components/kalman_combinator/* @Cat-Ion esphome/components/ledc/* @OttoWinter esphome/components/light/* @esphome/core +esphome/components/lilygo_t5_47/touchscreen/* @jesserockz esphome/components/lock/* @esphome/core esphome/components/logger/* @esphome/core esphome/components/ltr390/* @sjtrny diff --git a/esphome/codegen.py b/esphome/codegen.py index 3ea3df8706..52191e05e2 100644 --- a/esphome/codegen.py +++ b/esphome/codegen.py @@ -81,4 +81,5 @@ from esphome.cpp_types import ( # noqa InternalGPIOPin, gpio_Flags, EntityCategory, + Parented, ) diff --git a/esphome/components/lilygo_t5_47/__init__.py b/esphome/components/lilygo_t5_47/__init__.py new file mode 100644 index 0000000000..5499d096a9 --- /dev/null +++ b/esphome/components/lilygo_t5_47/__init__.py @@ -0,0 +1,3 @@ +import esphome.codegen as cg + +lilygo_t5_47_ns = cg.esphome_ns.namespace("lilygo_t5_47") diff --git a/esphome/components/lilygo_t5_47/touchscreen/__init__.py b/esphome/components/lilygo_t5_47/touchscreen/__init__.py new file mode 100644 index 0000000000..9ec6c925ee --- /dev/null +++ b/esphome/components/lilygo_t5_47/touchscreen/__init__.py @@ -0,0 +1,45 @@ +import esphome.codegen as cg +import esphome.config_validation as cv + +from esphome import pins +from esphome.components import i2c, touchscreen +from esphome.const import CONF_ID + +from .. import lilygo_t5_47_ns + +CODEOWNERS = ["@jesserockz"] +DEPENDENCIES = ["i2c"] + +LilygoT547Touchscreen = lilygo_t5_47_ns.class_( + "LilygoT547Touchscreen", + touchscreen.Touchscreen, + cg.Component, + i2c.I2CDevice, +) + +CONF_LILYGO_T5_47_TOUCHSCREEN_ID = "lilygo_t5_47_touchscreen_id" +CONF_INTERRUPT_PIN = "interrupt_pin" + + +CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(LilygoT547Touchscreen), + cv.Required(CONF_INTERRUPT_PIN): cv.All( + pins.internal_gpio_input_pin_schema + ), + } + ) + .extend(i2c.i2c_device_schema(0x5A)) + .extend(cv.COMPONENT_SCHEMA) +) + + +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) + await touchscreen.register_touchscreen(var, config) + + interrupt_pin = await cg.gpio_pin_expression(config[CONF_INTERRUPT_PIN]) + cg.add(var.set_interrupt_pin(interrupt_pin)) diff --git a/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.cpp b/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.cpp new file mode 100644 index 0000000000..b5cf63980b --- /dev/null +++ b/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.cpp @@ -0,0 +1,141 @@ +#include "lilygo_t5_47_touchscreen.h" + +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace lilygo_t5_47 { + +static const char *const TAG = "lilygo_t5_47.touchscreen"; + +static const uint8_t POWER_REGISTER = 0xD6; +static const uint8_t TOUCH_REGISTER = 0xD0; + +static const uint8_t WAKEUP_CMD[1] = {0x06}; +static const uint8_t READ_FLAGS[1] = {0x00}; +static const uint8_t CLEAR_FLAGS[2] = {0x00, 0xAB}; +static const uint8_t READ_TOUCH[1] = {0x07}; + +#define ERROR_CHECK(err) \ + if ((err) != i2c::ERROR_OK) { \ + ESP_LOGE(TAG, "Failed to communicate!"); \ + this->status_set_warning(); \ + return; \ + } + +void Store::gpio_intr(Store *store) { store->touch = true; } + +void LilygoT547Touchscreen::setup() { + ESP_LOGCONFIG(TAG, "Setting up Lilygo T5 4.7 Touchscreen..."); + this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); + this->interrupt_pin_->setup(); + + this->store_.pin = this->interrupt_pin_->to_isr(); + this->interrupt_pin_->attach_interrupt(Store::gpio_intr, &this->store_, gpio::INTERRUPT_FALLING_EDGE); + + if (this->write(nullptr, 0) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Failed to communicate!"); + this->interrupt_pin_->detach_interrupt(); + this->mark_failed(); + return; + } + + this->write_register(POWER_REGISTER, WAKEUP_CMD, 1); +} + +void LilygoT547Touchscreen::loop() { + if (!this->store_.touch) { + for (auto *listener : this->touch_listeners_) + listener->release(); + return; + } + this->store_.touch = false; + + uint8_t point = 0; + uint8_t buffer[40] = {0}; + uint32_t sum_l = 0, sum_h = 0; + + i2c::ErrorCode err; + err = this->write_register(TOUCH_REGISTER, READ_FLAGS, 1); + ERROR_CHECK(err); + + err = this->read(buffer, 7); + ERROR_CHECK(err); + + if (buffer[0] == 0xAB) { + this->write_register(TOUCH_REGISTER, CLEAR_FLAGS, 2); + return; + } + + point = buffer[5] & 0xF; + + if (point == 0) { + for (auto *listener : this->touch_listeners_) + listener->release(); + return; + } else if (point == 1) { + err = this->write_register(TOUCH_REGISTER, READ_TOUCH, 1); + ERROR_CHECK(err); + err = this->read(&buffer[5], 2); + ERROR_CHECK(err); + + sum_l = buffer[5] << 8 | buffer[6]; + } else if (point > 1) { + err = this->write_register(TOUCH_REGISTER, READ_TOUCH, 1); + ERROR_CHECK(err); + err = this->read(&buffer[5], 5 * (point - 1) + 3); + ERROR_CHECK(err); + + sum_l = buffer[5 * point + 1] << 8 | buffer[5 * point + 2]; + } + + this->write_register(TOUCH_REGISTER, CLEAR_FLAGS, 2); + + for (int i = 0; i < 5 * point; i++) + sum_h += buffer[i]; + + if (sum_l != sum_h) + point = 0; + + if (point) { + uint8_t offset; + for (int i = 0; i < point; i++) { + if (i == 0) { + offset = 0; + } else { + offset = 4; + } + + TouchPoint tp; + + tp.id = (buffer[i * 5 + offset] >> 4) & 0x0F; + tp.state = buffer[i * 5 + offset] & 0x0F; + if (tp.state == 0x06) + tp.state = 0x07; + + tp.y = (uint16_t)((buffer[i * 5 + 1 + offset] << 4) | ((buffer[i * 5 + 3 + offset] >> 4) & 0x0F)); + tp.x = (uint16_t)((buffer[i * 5 + 2 + offset] << 4) | (buffer[i * 5 + 3 + offset] & 0x0F)); + + this->defer([this, tp]() { this->send_touch_(tp); }); + } + } else { + TouchPoint tp; + tp.id = (buffer[0] >> 4) & 0x0F; + tp.state = 0x06; + tp.y = (uint16_t)((buffer[0 * 5 + 1] << 4) | ((buffer[0 * 5 + 3] >> 4) & 0x0F)); + tp.x = (uint16_t)((buffer[0 * 5 + 2] << 4) | (buffer[0 * 5 + 3] & 0x0F)); + + this->defer([this, tp]() { this->send_touch_(tp); }); + } + + this->status_clear_warning(); +} + +void LilygoT547Touchscreen::dump_config() { + ESP_LOGCONFIG(TAG, "Lilygo T5 47 Touchscreen:"); + LOG_I2C_DEVICE(this); + LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_); +} + +} // namespace lilygo_t5_47 +} // namespace esphome diff --git a/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.h b/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.h new file mode 100644 index 0000000000..3d00e0b117 --- /dev/null +++ b/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.h @@ -0,0 +1,35 @@ +#pragma once + +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/touchscreen/touchscreen.h" +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace lilygo_t5_47 { + +struct Store { + volatile bool touch; + ISRInternalGPIOPin pin; + + static void gpio_intr(Store *store); +}; + +using namespace touchscreen; + +class LilygoT547Touchscreen : public Touchscreen, public Component, public i2c::I2CDevice { + public: + void setup() override; + void loop() override; + void dump_config() override; + + void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } + + protected: + InternalGPIOPin *interrupt_pin_; + Store store_; +}; + +} // namespace lilygo_t5_47 +} // namespace esphome diff --git a/esphome/components/touchscreen/binary_sensor/__init__.py b/esphome/components/touchscreen/binary_sensor/__init__.py index cbd03c0a32..9dba821d4d 100644 --- a/esphome/components/touchscreen/binary_sensor/__init__.py +++ b/esphome/components/touchscreen/binary_sensor/__init__.py @@ -9,7 +9,11 @@ from .. import touchscreen_ns, CONF_TOUCHSCREEN_ID, Touchscreen, TouchListener DEPENDENCIES = ["touchscreen"] TouchscreenBinarySensor = touchscreen_ns.class_( - "TouchscreenBinarySensor", binary_sensor.BinarySensor, TouchListener + "TouchscreenBinarySensor", + binary_sensor.BinarySensor, + cg.Component, + TouchListener, + cg.Parented.template(Touchscreen), ) CONF_X_MIN = "x_min" @@ -39,7 +43,7 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_Y_MIN): cv.int_range(min=0, max=2000), cv.Required(CONF_Y_MAX): cv.int_range(min=0, max=2000), } - ), + ).extend(cv.COMPONENT_SCHEMA), validate_coords, ) @@ -47,7 +51,9 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await binary_sensor.register_binary_sensor(var, config) - hub = await cg.get_variable(config[CONF_TOUCHSCREEN_ID]) + await cg.register_component(var, config) + await cg.register_parented(var, config[CONF_TOUCHSCREEN_ID]) + cg.add( var.set_area( config[CONF_X_MIN], @@ -56,4 +62,3 @@ async def to_code(config): config[CONF_Y_MAX], ) ) - cg.add(hub.register_listener(var)) diff --git a/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.h b/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.h index 8fb7749766..7b8cac5c4c 100644 --- a/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.h +++ b/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.h @@ -1,13 +1,20 @@ #pragma once -#include "esphome/components/touchscreen/touchscreen.h" #include "esphome/components/binary_sensor/binary_sensor.h" +#include "esphome/components/touchscreen/touchscreen.h" +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" namespace esphome { namespace touchscreen { -class TouchscreenBinarySensor : public binary_sensor::BinarySensor, public TouchListener { +class TouchscreenBinarySensor : public binary_sensor::BinarySensor, + public Component, + public TouchListener, + public Parented { public: + void setup() override { this->parent_->register_listener(this); } + /// Set the touch screen area where the button will detect the touch. void set_area(int16_t x_min, int16_t x_max, int16_t y_min, int16_t y_max) { this->x_min_ = x_min; diff --git a/esphome/cpp_types.py b/esphome/cpp_types.py index 806a2d832c..110641d6c9 100644 --- a/esphome/cpp_types.py +++ b/esphome/cpp_types.py @@ -34,3 +34,4 @@ InternalGPIOPin = esphome_ns.class_("InternalGPIOPin", GPIOPin) gpio_ns = esphome_ns.namespace("gpio") gpio_Flags = gpio_ns.enum("Flags", is_class=True) EntityCategory = esphome_ns.enum("EntityCategory") +Parented = esphome_ns.class_("Parented") diff --git a/tests/test4.yaml b/tests/test4.yaml index de641d92ff..998db8ed2d 100644 --- a/tests/test4.yaml +++ b/tests/test4.yaml @@ -328,6 +328,7 @@ binary_sensor: number: 3 - platform: touchscreen + touchscreen_id: lilygo_touchscreen id: touch_key1 x_min: 0 x_max: 100 @@ -561,3 +562,12 @@ touchscreen: - logger.log: format: Touch at (%d, %d) args: ["touch.x", "touch.y"] + + - platform: lilygo_t5_47 + id: lilygo_touchscreen + interrupt_pin: GPIO36 + display: inkplate_display + on_touch: + - logger.log: + format: Touch at (%d, %d) + args: ["touch.x", "touch.y"] From 58fa63ad88b2c441d5d765f6865f0a10dd08ebed Mon Sep 17 00:00:00 2001 From: stegm Date: Tue, 8 Feb 2022 10:27:22 +0100 Subject: [PATCH 0198/1729] Add Select for modbus (#3032) Co-authored-by: Oxan van Leeuwen --- CODEOWNERS | 1 + esphome/codegen.py | 1 + .../modbus_controller/modbus_controller.cpp | 104 +++++-------- .../modbus_controller/modbus_controller.h | 54 +++++-- .../modbus_controller/select/__init__.py | 142 ++++++++++++++++++ .../select/modbus_select.cpp | 86 +++++++++++ .../modbus_controller/select/modbus_select.h | 51 +++++++ esphome/cpp_generator.py | 4 +- esphome/cpp_types.py | 1 + 9 files changed, 361 insertions(+), 83 deletions(-) create mode 100644 esphome/components/modbus_controller/select/__init__.py create mode 100644 esphome/components/modbus_controller/select/modbus_select.cpp create mode 100644 esphome/components/modbus_controller/select/modbus_select.h diff --git a/CODEOWNERS b/CODEOWNERS index 931e699566..3326b7d90c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -115,6 +115,7 @@ esphome/components/modbus_controller/* @martgras esphome/components/modbus_controller/binary_sensor/* @martgras esphome/components/modbus_controller/number/* @martgras esphome/components/modbus_controller/output/* @martgras +esphome/components/modbus_controller/select/* @martgras @stegm esphome/components/modbus_controller/sensor/* @martgras esphome/components/modbus_controller/switch/* @martgras esphome/components/modbus_controller/text_sensor/* @martgras diff --git a/esphome/codegen.py b/esphome/codegen.py index 52191e05e2..b862a8ce86 100644 --- a/esphome/codegen.py +++ b/esphome/codegen.py @@ -63,6 +63,7 @@ from esphome.cpp_types import ( # noqa uint32, uint64, int32, + int64, const_char_ptr, NAN, esphome_ns, diff --git a/esphome/components/modbus_controller/modbus_controller.cpp b/esphome/components/modbus_controller/modbus_controller.cpp index 8b6482b1dc..64046b9578 100644 --- a/esphome/components/modbus_controller/modbus_controller.cpp +++ b/esphome/components/modbus_controller/modbus_controller.cpp @@ -110,7 +110,8 @@ void ModbusController::queue_command(const ModbusCommandItem &command) { for (auto &item : command_queue_) { if (item->register_address == command.register_address && item->register_count == command.register_count && item->register_type == command.register_type && item->function_code == command.function_code) { - ESP_LOGW(TAG, "Duplicate modbus command found"); + ESP_LOGW(TAG, "Duplicate modbus command found: type=0x%x address=%u count=%u", + static_cast(command.register_type), command.register_address, command.register_count); // update the payload of the queued command // replaces a previous command item->payload = command.payload; @@ -360,8 +361,9 @@ ModbusCommandItem ModbusCommandItem::create_write_multiple_command(ModbusControl modbusdevice->on_write_register_response(cmd.register_type, start_address, data); }; for (auto v : values) { - cmd.payload.push_back((v / 256) & 0xFF); - cmd.payload.push_back(v & 0xFF); + auto decoded_value = decode_value(v); + cmd.payload.push_back(decoded_value[0]); + cmd.payload.push_back(decoded_value[1]); } return cmd; } @@ -416,7 +418,7 @@ ModbusCommandItem ModbusCommandItem::create_write_multiple_coils(ModbusControlle } ModbusCommandItem ModbusCommandItem::create_write_single_command(ModbusController *modbusdevice, uint16_t start_address, - int16_t value) { + uint16_t value) { ModbusCommandItem cmd; cmd.modbusdevice = modbusdevice; cmd.register_type = ModbusRegisterType::HOLDING; @@ -427,8 +429,10 @@ ModbusCommandItem ModbusCommandItem::create_write_single_command(ModbusControlle const std::vector &data) { modbusdevice->on_write_register_response(cmd.register_type, start_address, data); }; - cmd.payload.push_back((value / 256) & 0xFF); - cmd.payload.push_back((value % 256) & 0xFF); + + auto decoded_value = decode_value(value); + cmd.payload.push_back(decoded_value[0]); + cmd.payload.push_back(decoded_value[1]); return cmd; } @@ -463,84 +467,70 @@ bool ModbusCommandItem::send() { return true; } -std::vector float_to_payload(float value, SensorValueType value_type) { - union { - float float_value; - uint32_t raw; - } raw_to_float; - - std::vector data; - int32_t val; - +void number_to_payload(std::vector &data, int64_t value, SensorValueType value_type) { switch (value_type) { case SensorValueType::U_WORD: case SensorValueType::S_WORD: - // cast truncates the float do some rounding here - data.push_back(lroundf(value) & 0xFFFF); + data.push_back(value & 0xFFFF); break; case SensorValueType::U_DWORD: case SensorValueType::S_DWORD: - val = lroundf(value); - data.push_back((val & 0xFFFF0000) >> 16); - data.push_back(val & 0xFFFF); + case SensorValueType::FP32: + case SensorValueType::FP32_R: + data.push_back((value & 0xFFFF0000) >> 16); + data.push_back(value & 0xFFFF); break; case SensorValueType::U_DWORD_R: case SensorValueType::S_DWORD_R: - val = lroundf(value); - data.push_back(val & 0xFFFF); - data.push_back((val & 0xFFFF0000) >> 16); + data.push_back(value & 0xFFFF); + data.push_back((value & 0xFFFF0000) >> 16); break; - case SensorValueType::FP32: - raw_to_float.float_value = value; - data.push_back((raw_to_float.raw & 0xFFFF0000) >> 16); - data.push_back(raw_to_float.raw & 0xFFFF); + case SensorValueType::U_QWORD: + case SensorValueType::S_QWORD: + data.push_back((value & 0xFFFF000000000000) >> 48); + data.push_back((value & 0xFFFF00000000) >> 32); + data.push_back((value & 0xFFFF0000) >> 16); + data.push_back(value & 0xFFFF); break; - case SensorValueType::FP32_R: - raw_to_float.float_value = value; - data.push_back(raw_to_float.raw & 0xFFFF); - data.push_back((raw_to_float.raw & 0xFFFF0000) >> 16); + case SensorValueType::U_QWORD_R: + case SensorValueType::S_QWORD_R: + data.push_back(value & 0xFFFF); + data.push_back((value & 0xFFFF0000) >> 16); + data.push_back((value & 0xFFFF00000000) >> 32); + data.push_back((value & 0xFFFF000000000000) >> 48); break; default: - ESP_LOGE(TAG, "Invalid data type for modbus float to payload conversation"); + ESP_LOGE(TAG, "Invalid data type for modbus number to payload conversation: %d", + static_cast(value_type)); break; } - return data; } -float payload_to_float(const std::vector &data, SensorValueType sensor_value_type, uint8_t offset, - uint32_t bitmask) { - union { - float float_value; - uint32_t raw; - } raw_to_float; - +int64_t payload_to_number(const std::vector &data, SensorValueType sensor_value_type, uint8_t offset, + uint32_t bitmask) { int64_t value = 0; // int64_t because it can hold signed and unsigned 32 bits - float result = NAN; switch (sensor_value_type) { case SensorValueType::U_WORD: value = mask_and_shift_by_rightbit(get_data(data, offset), bitmask); // default is 0xFFFF ; - result = static_cast(value); break; case SensorValueType::U_DWORD: + case SensorValueType::FP32: value = get_data(data, offset); value = mask_and_shift_by_rightbit((uint32_t) value, bitmask); - result = static_cast(value); break; case SensorValueType::U_DWORD_R: + case SensorValueType::FP32_R: value = get_data(data, offset); value = static_cast(value & 0xFFFF) << 16 | (value & 0xFFFF0000) >> 16; value = mask_and_shift_by_rightbit((uint32_t) value, bitmask); - result = static_cast(value); break; case SensorValueType::S_WORD: value = mask_and_shift_by_rightbit(get_data(data, offset), bitmask); // default is 0xFFFF ; - result = static_cast(value); break; case SensorValueType::S_DWORD: value = mask_and_shift_by_rightbit(get_data(data, offset), bitmask); - result = static_cast(value); break; case SensorValueType::S_DWORD_R: { value = get_data(data, offset); @@ -549,18 +539,14 @@ float payload_to_float(const std::vector &data, SensorValueType sensor_ uint32_t sign_bit = (value & 0x8000) << 16; value = mask_and_shift_by_rightbit( static_cast(((value & 0x7FFF) << 16 | (value & 0xFFFF0000) >> 16) | sign_bit), bitmask); - result = static_cast(value); } break; case SensorValueType::U_QWORD: // Ignore bitmask for U_QWORD value = get_data(data, offset); - result = static_cast(value); break; - case SensorValueType::S_QWORD: // Ignore bitmask for S_QWORD value = get_data(data, offset); - result = static_cast(value); break; case SensorValueType::U_QWORD_R: // Ignore bitmask for U_QWORD @@ -568,32 +554,16 @@ float payload_to_float(const std::vector &data, SensorValueType sensor_ value = static_cast(value & 0xFFFF) << 48 | (value & 0xFFFF000000000000) >> 48 | static_cast(value & 0xFFFF0000) << 32 | (value & 0x0000FFFF00000000) >> 32 | static_cast(value & 0xFFFF00000000) << 16 | (value & 0x00000000FFFF0000) >> 16; - result = static_cast(value); break; - case SensorValueType::S_QWORD_R: // Ignore bitmask for S_QWORD value = get_data(data, offset); - result = static_cast(value); break; - case SensorValueType::FP32: - raw_to_float.raw = get_data(data, offset); - ESP_LOGD(TAG, "FP32 = 0x%08X => %f", raw_to_float.raw, raw_to_float.float_value); - result = raw_to_float.float_value; - break; - case SensorValueType::FP32_R: { - auto tmp = get_data(data, offset); - raw_to_float.raw = static_cast(tmp & 0xFFFF) << 16 | (tmp & 0xFFFF0000) >> 16; - ESP_LOGD(TAG, "FP32_R = 0x%08X => %f", raw_to_float.raw, raw_to_float.float_value); - result = raw_to_float.float_value; - } break; case SensorValueType::RAW: - result = NAN; - break; default: break; } - return result; + return value; } } // namespace modbus_controller diff --git a/esphome/components/modbus_controller/modbus_controller.h b/esphome/components/modbus_controller/modbus_controller.h index 88e2bba9ad..09395f29b3 100644 --- a/esphome/components/modbus_controller/modbus_controller.h +++ b/esphome/components/modbus_controller/modbus_controller.h @@ -195,7 +195,7 @@ inline bool coil_from_vector(int coil, const std::vector &data) { */ template N mask_and_shift_by_rightbit(N data, uint32_t mask) { auto result = (mask & data); - if (result == 0) { + if (result == 0 || mask == 0xFFFFFFFF) { return result; } for (size_t pos = 0; pos < sizeof(N) << 3; pos++) { @@ -205,22 +205,23 @@ template N mask_and_shift_by_rightbit(N data, uint32_t mask) { return 0; } -/** convert float value to vector suitable for sending - * @param value float value to cconvert +/** Convert float value to vector suitable for sending + * @param data target for payload + * @param value float value to convert * @param value_type defines if 16/32 or FP32 is used * @return vector containing the modbus register words in correct order */ -std::vector float_to_payload(float value, SensorValueType value_type); +void number_to_payload(std::vector &data, int64_t value, SensorValueType value_type); -/** convert vector response payload to float - * @param value float value to cconvert +/** Convert vector response payload to number. + * @param data payload with the data to convert * @param sensor_value_type defines if 16/32/64 bits or FP32 is used * @param offset offset to the data in data * @param bitmask bitmask used for masking and shifting - * @return float version of the input + * @return 64-bit number of the payload */ -float payload_to_float(const std::vector &data, SensorValueType sensor_value_type, uint8_t offset, - uint32_t bitmask); +int64_t payload_to_number(const std::vector &data, SensorValueType sensor_value_type, uint8_t offset, + uint32_t bitmask); class ModbusController; @@ -348,11 +349,11 @@ class ModbusCommandItem { * @param modbusdevice pointer to the device to execute the command * @param start_address modbus address of the first register to read * @param register_count number of registers to read - * @param values uint16_t array to be written to the registers + * @param value uint16_t single register value to write * @return ModbusCommandItem with the prepared command */ static ModbusCommandItem create_write_single_command(ModbusController *modbusdevice, uint16_t start_address, - int16_t value); + uint16_t value); /** Create modbus write single registers command * Function 05 (05hex) Write Single Coil * @param modbusdevice pointer to the device to execute the command @@ -446,13 +447,36 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice { uint16_t command_throttle_; }; -/** convert vector response payload to float - * @param value float value to cconvert +/** Convert vector response payload to float. + * @param data payload with data * @param item SensorItem object - * @return float version of the input + * @return float value of data */ inline float payload_to_float(const std::vector &data, const SensorItem &item) { - return payload_to_float(data, item.sensor_value_type, item.offset, item.bitmask); + int64_t number = payload_to_number(data, item.sensor_value_type, item.offset, item.bitmask); + + float float_value; + if (item.sensor_value_type == SensorValueType::FP32 || item.sensor_value_type == SensorValueType::FP32_R) { + float_value = bit_cast(static_cast(number)); + } else { + float_value = static_cast(number); + } + + return float_value; +} + +inline std::vector float_to_payload(float value, SensorValueType value_type) { + int64_t val; + + if (value_type == SensorValueType::FP32 || value_type == SensorValueType::FP32_R) { + val = bit_cast(value); + } else { + val = llroundf(value); + } + + std::vector data; + number_to_payload(data, val, value_type); + return data; } } // namespace modbus_controller diff --git a/esphome/components/modbus_controller/select/__init__.py b/esphome/components/modbus_controller/select/__init__.py new file mode 100644 index 0000000000..9651b5fedb --- /dev/null +++ b/esphome/components/modbus_controller/select/__init__.py @@ -0,0 +1,142 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import select +from esphome.const import CONF_ADDRESS, CONF_ID, CONF_LAMBDA +from esphome.jsonschema import jschema_composite + +from .. import ( + SENSOR_VALUE_TYPE, + TYPE_REGISTER_MAP, + ModbusController, + SensorItem, + modbus_controller_ns, +) +from ..const import ( + CONF_FORCE_NEW_RANGE, + CONF_MODBUS_CONTROLLER_ID, + CONF_REGISTER_COUNT, + CONF_SKIP_UPDATES, + CONF_USE_WRITE_MULTIPLE, + CONF_VALUE_TYPE, + CONF_WRITE_LAMBDA, +) + +DEPENDENCIES = ["modbus_controller"] +CODEOWNERS = ["@martgras", "@stegm"] +CONF_OPTIONSMAP = "optionsmap" + +ModbusSelect = modbus_controller_ns.class_( + "ModbusSelect", cg.Component, select.Select, SensorItem +) + + +@jschema_composite +def ensure_option_map(): + def validator(value): + cv.check_not_templatable(value) + option = cv.All(cv.string_strict) + mapping = cv.All(cv.int_range(-(2 ** 63), 2 ** 63 - 1)) + options_map_schema = cv.Schema({option: mapping}) + value = options_map_schema(value) + + all_values = list(value.values()) + unique_values = set(value.values()) + if len(all_values) != len(unique_values): + raise cv.Invalid("Mapping values must be unique.") + + return value + + return validator + + +def register_count_value_type_min(value): + reg_count = value.get(CONF_REGISTER_COUNT) + if reg_count is not None: + value_type = value[CONF_VALUE_TYPE] + min_register_count = TYPE_REGISTER_MAP[value_type] + if min_register_count > reg_count: + raise cv.Invalid( + f"Value type {value_type} needs at least {min_register_count} registers" + ) + return value + + +INTEGER_SENSOR_VALUE_TYPE = { + key: value for key, value in SENSOR_VALUE_TYPE.items() if not key.startswith("FP") +} + +CONFIG_SCHEMA = cv.All( + select.SELECT_SCHEMA.extend(cv.COMPONENT_SCHEMA).extend( + { + cv.GenerateID(): cv.declare_id(ModbusSelect), + cv.GenerateID(CONF_MODBUS_CONTROLLER_ID): cv.use_id(ModbusController), + cv.Required(CONF_ADDRESS): cv.positive_int, + cv.Optional(CONF_VALUE_TYPE, default="U_WORD"): cv.enum( + INTEGER_SENSOR_VALUE_TYPE + ), + cv.Optional(CONF_REGISTER_COUNT): cv.positive_int, + cv.Optional(CONF_SKIP_UPDATES, default=0): cv.positive_int, + cv.Optional(CONF_FORCE_NEW_RANGE, default=False): cv.boolean, + cv.Required(CONF_OPTIONSMAP): ensure_option_map(), + cv.Optional(CONF_USE_WRITE_MULTIPLE, default=False): cv.boolean, + cv.Optional(CONF_LAMBDA): cv.returning_lambda, + cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda, + }, + ), + register_count_value_type_min, +) + + +async def to_code(config): + value_type = config[CONF_VALUE_TYPE] + reg_count = config.get(CONF_REGISTER_COUNT) + if reg_count is None: + reg_count = TYPE_REGISTER_MAP[value_type] + + options_map = config[CONF_OPTIONSMAP] + + var = cg.new_Pvariable( + config[CONF_ID], + value_type, + config[CONF_ADDRESS], + reg_count, + config[CONF_SKIP_UPDATES], + config[CONF_FORCE_NEW_RANGE], + list(options_map.values()), + ) + + await cg.register_component(var, config) + await select.register_select(var, config, options=list(options_map.keys())) + + parent = await cg.get_variable(config[CONF_MODBUS_CONTROLLER_ID]) + cg.add(parent.add_sensor_item(var)) + cg.add(var.set_parent(parent)) + cg.add(var.set_use_write_mutiple(config[CONF_USE_WRITE_MULTIPLE])) + + if CONF_LAMBDA in config: + template_ = await cg.process_lambda( + config[CONF_LAMBDA], + [ + (ModbusSelect.operator("const_ptr"), "item"), + (cg.int64, "x"), + ( + cg.std_vector.template(cg.uint8).operator("const").operator("ref"), + "data", + ), + ], + return_type=cg.optional.template(cg.std_string), + ) + cg.add(var.set_template(template_)) + + if CONF_WRITE_LAMBDA in config: + template_ = await cg.process_lambda( + config[CONF_WRITE_LAMBDA], + [ + (ModbusSelect.operator("const_ptr"), "item"), + (cg.std_string.operator("const").operator("ref"), "x"), + (cg.int64, "value"), + (cg.std_vector.template(cg.uint16).operator("ref"), "payload"), + ], + return_type=cg.optional.template(cg.int64), + ) + cg.add(var.set_write_template(template_)) diff --git a/esphome/components/modbus_controller/select/modbus_select.cpp b/esphome/components/modbus_controller/select/modbus_select.cpp new file mode 100644 index 0000000000..2c6b32f545 --- /dev/null +++ b/esphome/components/modbus_controller/select/modbus_select.cpp @@ -0,0 +1,86 @@ +#include "modbus_select.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace modbus_controller { + +static const char *const TAG = "modbus_controller.select"; + +void ModbusSelect::dump_config() { LOG_SELECT(TAG, "Modbus Controller Select", this); } + +void ModbusSelect::parse_and_publish(const std::vector &data) { + int64_t value = payload_to_number(data, this->sensor_value_type, this->offset, this->bitmask); + + ESP_LOGD(TAG, "New select value %lld from payload", value); + + optional new_state; + + if (this->transform_func_.has_value()) { + auto val = (*this->transform_func_)(this, value, data); + if (val.has_value()) { + new_state = *val; + ESP_LOGV(TAG, "lambda returned option %s", new_state->c_str()); + } + } + + if (!new_state.has_value()) { + auto map_it = std::find(this->mapping_.cbegin(), this->mapping_.cend(), value); + + if (map_it != this->mapping_.cend()) { + size_t idx = std::distance(this->mapping_.cbegin(), map_it); + new_state = this->traits.get_options()[idx]; + ESP_LOGV(TAG, "Found option %s for value %lld", new_state->c_str(), value); + } else { + ESP_LOGE(TAG, "No option found for mapping %lld", value); + } + } + + if (new_state.has_value()) { + this->publish_state(new_state.value()); + } +} + +void ModbusSelect::control(const std::string &value) { + auto options = this->traits.get_options(); + auto opt_it = std::find(options.cbegin(), options.cend(), value); + size_t idx = std::distance(options.cbegin(), opt_it); + optional mapval = this->mapping_[idx]; + ESP_LOGD(TAG, "Found value %lld for option '%s'", *mapval, value.c_str()); + + std::vector data; + + if (this->write_transform_func_.has_value()) { + auto val = (*this->write_transform_func_)(this, value, *mapval, data); + if (val.has_value()) { + mapval = *val; + ESP_LOGV(TAG, "write_lambda returned mapping value %lld", *mapval); + } else { + ESP_LOGD(TAG, "Communication handled by write_lambda - exiting control"); + return; + } + } + + if (data.empty()) { + number_to_payload(data, *mapval, this->sensor_value_type); + } else { + ESP_LOGV(TAG, "Using payload from write lambda"); + } + + if (data.empty()) { + ESP_LOGW(TAG, "No payload was created for updating select"); + return; + } + + const uint16_t write_address = this->start_address + this->offset / 2; + ModbusCommandItem write_cmd; + if ((this->register_count == 1) && (!this->use_write_multiple_)) { + write_cmd = ModbusCommandItem::create_write_single_command(parent_, write_address, data[0]); + } else { + write_cmd = ModbusCommandItem::create_write_multiple_command(parent_, write_address, this->register_count, data); + } + + parent_->queue_command(write_cmd); +} + +} // namespace modbus_controller +} // namespace esphome diff --git a/esphome/components/modbus_controller/select/modbus_select.h b/esphome/components/modbus_controller/select/modbus_select.h new file mode 100644 index 0000000000..0875194768 --- /dev/null +++ b/esphome/components/modbus_controller/select/modbus_select.h @@ -0,0 +1,51 @@ +#pragma once + +#include + +#include "esphome/components/modbus_controller/modbus_controller.h" +#include "esphome/components/select/select.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace modbus_controller { + +class ModbusSelect : public Component, public select::Select, public SensorItem { + public: + ModbusSelect(SensorValueType sensor_value_type, uint16_t start_address, uint8_t register_count, uint8_t skip_updates, + bool force_new_range, std::vector mapping) { + this->register_type = ModbusRegisterType::HOLDING; // not configurable + this->sensor_value_type = sensor_value_type; + this->start_address = start_address; + this->offset = 0; // not configurable + this->bitmask = 0xFFFFFFFF; // not configurable + this->register_count = register_count; + this->response_bytes = 0; // not configurable + this->skip_updates = skip_updates; + this->force_new_range = force_new_range; + this->mapping_ = std::move(mapping); + } + + using transform_func_t = + std::function(ModbusSelect *const, int64_t, const std::vector &)>; + using write_transform_func_t = + std::function(ModbusSelect *const, const std::string &, int64_t, std::vector &)>; + + void set_parent(ModbusController *const parent) { this->parent_ = parent; } + void set_use_write_mutiple(bool use_write_multiple) { this->use_write_multiple_ = use_write_multiple; } + void set_template(transform_func_t &&f) { this->transform_func_ = f; } + void set_write_template(write_transform_func_t &&f) { this->write_transform_func_ = f; } + + void dump_config() override; + void parse_and_publish(const std::vector &data) override; + void control(const std::string &value) override; + + protected: + std::vector mapping_; + ModbusController *parent_; + bool use_write_multiple_{false}; + optional transform_func_; + optional write_transform_func_; +}; + +} // namespace modbus_controller +} // namespace esphome diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index 937b6cceb4..82deec70ec 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -773,9 +773,11 @@ class MockObj(Expression): return MockObj(f"{self.base} &", "") if name == "ptr": return MockObj(f"{self.base} *", "") + if name == "const_ptr": + return MockObj(f"{self.base} *const", "") if name == "const": return MockObj(f"const {self.base}", "") - raise ValueError("Expected one of ref, ptr, const.") + raise ValueError("Expected one of ref, ptr, const_ptr, const.") @property def using(self) -> "MockObj": diff --git a/esphome/cpp_types.py b/esphome/cpp_types.py index 110641d6c9..2323b2578f 100644 --- a/esphome/cpp_types.py +++ b/esphome/cpp_types.py @@ -15,6 +15,7 @@ uint16 = global_ns.namespace("uint16_t") uint32 = global_ns.namespace("uint32_t") uint64 = global_ns.namespace("uint64_t") int32 = global_ns.namespace("int32_t") +int64 = global_ns.namespace("int64_t") const_char_ptr = global_ns.namespace("const char *") NAN = global_ns.namespace("NAN") esphome_ns = global_ns # using namespace esphome; From 4aeacfd16e171f1914c9e60e881b9b760a8b1dea Mon Sep 17 00:00:00 2001 From: mckaymatthew Date: Tue, 8 Feb 2022 03:56:40 -0600 Subject: [PATCH 0199/1729] Add max9611 High Side Current Shunt ADC (#2705) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/max9611/__init__.py | 1 + esphome/components/max9611/max9611.cpp | 93 ++++++++++++++++++++++++++ esphome/components/max9611/max9611.h | 62 +++++++++++++++++ esphome/components/max9611/sensor.py | 92 +++++++++++++++++++++++++ tests/test1.yaml | 16 ++++- 6 files changed, 264 insertions(+), 1 deletion(-) create mode 100644 esphome/components/max9611/__init__.py create mode 100644 esphome/components/max9611/max9611.cpp create mode 100644 esphome/components/max9611/max9611.h create mode 100644 esphome/components/max9611/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 3326b7d90c..165ce52485 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -94,6 +94,7 @@ esphome/components/lock/* @esphome/core esphome/components/logger/* @esphome/core esphome/components/ltr390/* @sjtrny esphome/components/max7219digit/* @rspaargaren +esphome/components/max9611/* @mckaymatthew esphome/components/mcp23008/* @jesserockz esphome/components/mcp23017/* @jesserockz esphome/components/mcp23s08/* @SenexCrenshaw @jesserockz diff --git a/esphome/components/max9611/__init__.py b/esphome/components/max9611/__init__.py new file mode 100644 index 0000000000..9b0ea14b57 --- /dev/null +++ b/esphome/components/max9611/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@mckaymatthew"] diff --git a/esphome/components/max9611/max9611.cpp b/esphome/components/max9611/max9611.cpp new file mode 100644 index 0000000000..418a577948 --- /dev/null +++ b/esphome/components/max9611/max9611.cpp @@ -0,0 +1,93 @@ +#include "max9611.h" +#include "esphome/core/log.h" +#include "esphome/components/i2c/i2c_bus.h" +namespace esphome { +namespace max9611 { +using namespace esphome::i2c; +// Sign extend +// http://graphics.stanford.edu/~seander/bithacks.html#FixedSignExtend +template inline T signextend(const T x) { + struct { + T x : B; + } s; + return s.x = x; +} +// Map the gain register to in uV/LSB +const float gain_to_lsb(MAX9611Multiplexer gain) { + float lsb = 0.0; + if (gain == MAX9611_MULTIPLEXER_CSA_GAIN1) { + lsb = 107.50; + } else if (gain == MAX9611_MULTIPLEXER_CSA_GAIN4) { + lsb = 26.88; + } else if (gain == MAX9611_MULTIPLEXER_CSA_GAIN8) { + lsb = 13.44; + } + return lsb; +} +static const char *const TAG = "max9611"; +static const uint8_t SETUP_DELAY = 4; // Wait 2 integration periods. +static const float VOUT_LSB = 14.0 / 1000.0; // 14mV/LSB +static const float TEMP_LSB = 0.48; // 0.48C/LSB +static const float MICRO_VOLTS_PER_VOLT = 1000000.0; +void MAX9611Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up max9611..."); + // Perform dummy-read + uint8_t value; + this->read(&value, 1); + // Configuration Stage. + // First send an integration request with the specified gain + const uint8_t setup_dat[] = {CONTROL_REGISTER_1_ADRR, static_cast(gain_)}; + // Then send a request that samples all channels as fast as possible, using the last provided gain + const uint8_t fast_mode_dat[] = {CONTROL_REGISTER_1_ADRR, MAX9611Multiplexer::MAX9611_MULTIPLEXER_FAST_MODE}; + + if (this->write(reinterpret_cast(&setup_dat), sizeof(setup_dat)) != ErrorCode::ERROR_OK) { + ESP_LOGE(TAG, "Failed to setup Max9611 during GAIN SET"); + return; + } + delay(SETUP_DELAY); + if (this->write(reinterpret_cast(&fast_mode_dat), sizeof(fast_mode_dat)) != ErrorCode::ERROR_OK) { + ESP_LOGE(TAG, "Failed to setup Max9611 during FAST MODE SET"); + return; + } +} +void MAX9611Component::dump_config() { + ESP_LOGCONFIG(TAG, "Dump Config max9611..."); + ESP_LOGCONFIG(TAG, " CSA Gain Register: %x", gain_); + LOG_I2C_DEVICE(this); +} +void MAX9611Component::update() { + // Setup read from 0x0 register base + const uint8_t reg_base = 0x0; + const ErrorCode write_result = this->write(®_base, 1); + // Just read the entire register map in a bulk read, faster than individually querying register. + const ErrorCode read_result = this->read(register_map_, sizeof(register_map_)); + if (write_result != ErrorCode::ERROR_OK || read_result != ErrorCode::ERROR_OK) { + ESP_LOGW(TAG, "MAX9611 Update FAILED!"); + return; + } + uint16_t csa_register = ((register_map_[CSA_DATA_BYTE_MSB_ADRR] << 8) | (register_map_[CSA_DATA_BYTE_LSB_ADRR])) >> 4; + uint16_t rs_register = ((register_map_[RS_DATA_BYTE_MSB_ADRR] << 8) | (register_map_[RS_DATA_BYTE_LSB_ADRR])) >> 4; + uint16_t t_register = ((register_map_[TEMP_DATA_BYTE_MSB_ADRR] << 8) | (register_map_[TEMP_DATA_BYTE_LSB_ADRR])) >> 7; + float voltage = rs_register * VOUT_LSB; + float shunt_voltage = (csa_register * gain_to_lsb(gain_)) / MICRO_VOLTS_PER_VOLT; + float temp = signextend(t_register) * TEMP_LSB; + float amps = shunt_voltage / current_resistor_; + float watts = amps * voltage; + + if (voltage_sensor_ != nullptr) { + voltage_sensor_->publish_state(voltage); + } + if (current_sensor_ != nullptr) { + current_sensor_->publish_state(amps); + } + if (watt_sensor_ != nullptr) { + watt_sensor_->publish_state(watts); + } + if (temperature_sensor_ != nullptr) { + temperature_sensor_->publish_state(temp); + } + + ESP_LOGD(TAG, "V: %f, A: %f, W: %f, Deg C: %f", voltage, amps, watts, temp); +} +} // namespace max9611 +} // namespace esphome diff --git a/esphome/components/max9611/max9611.h b/esphome/components/max9611/max9611.h new file mode 100644 index 0000000000..017f56b1a7 --- /dev/null +++ b/esphome/components/max9611/max9611.h @@ -0,0 +1,62 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace max9611 { + +enum MAX9611Multiplexer { + MAX9611_MULTIPLEXER_CSA_GAIN1 = 0b000, + MAX9611_MULTIPLEXER_CSA_GAIN4 = 0b001, + MAX9611_MULTIPLEXER_CSA_GAIN8 = 0b010, + MAX9611_MULTIPLEXER_RS = 0b011, + MAX9611_MULTIPLEXER_OUT = 0b100, + MAX9611_MULTIPLEXER_SET = 0b101, + MAX9611_MULTIPLEXER_TEMP = 0b110, + MAX9611_MULTIPLEXER_FAST_MODE = 0b111, +}; + +enum MAX9611RegisterMap { + CSA_DATA_BYTE_MSB_ADRR = 0x00, + CSA_DATA_BYTE_LSB_ADRR = 0x01, + RS_DATA_BYTE_MSB_ADRR = 0x02, + RS_DATA_BYTE_LSB_ADRR = 0x03, + OUT_DATA_BYTE_MSB_ADRR = 0x04, // Unused Op-Amp + OUT_DATA_BYTE_LSB_ADRR = 0x05, // Unused Op-Amp + SET_DATA_BYTE_MSB_ADRR = 0x06, // Unused Op-Amp + SET_DATA_BYTE_LSB_ADRR = 0x07, // Unused Op-Amp + TEMP_DATA_BYTE_MSB_ADRR = 0x08, + TEMP_DATA_BYTE_LSB_ADRR = 0x09, + CONTROL_REGISTER_1_ADRR = 0x0A, + CONTROL_REGISTER_2_ADRR = 0x0B, +}; + +class MAX9611Component : public PollingComponent, public i2c::I2CDevice { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void update() override; + void set_voltage_sensor(sensor::Sensor *vs) { voltage_sensor_ = vs; } + void set_current_sensor(sensor::Sensor *cs) { current_sensor_ = cs; } + void set_watt_sensor(sensor::Sensor *ws) { watt_sensor_ = ws; } + void set_temp_sensor(sensor::Sensor *ts) { temperature_sensor_ = ts; } + + void set_current_resistor(float r) { current_resistor_ = r; } + void set_gain(MAX9611Multiplexer g) { gain_ = g; } + + protected: + sensor::Sensor *voltage_sensor_{nullptr}; + sensor::Sensor *current_sensor_{nullptr}; + sensor::Sensor *watt_sensor_{nullptr}; + sensor::Sensor *temperature_sensor_{nullptr}; + float current_resistor_; + uint8_t register_map_[0x0C]; + MAX9611Multiplexer gain_; +}; + +} // namespace max9611 +} // namespace esphome diff --git a/esphome/components/max9611/sensor.py b/esphome/components/max9611/sensor.py new file mode 100644 index 0000000000..246d332a86 --- /dev/null +++ b/esphome/components/max9611/sensor.py @@ -0,0 +1,92 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_ID, + CONF_SHUNT_RESISTANCE, + CONF_GAIN, + CONF_VOLTAGE, + CONF_CURRENT, + CONF_POWER, + CONF_TEMPERATURE, + UNIT_VOLT, + UNIT_AMPERE, + UNIT_WATT, + UNIT_CELSIUS, + DEVICE_CLASS_VOLTAGE, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_POWER, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, +) + +DEPENDENCIES = ["i2c"] +max9611_ns = cg.esphome_ns.namespace("max9611") +max9611Gain = max9611_ns.enum("MAX9611Multiplexer") +MAX9611_GAIN = { + "8X": max9611Gain.MAX9611_MULTIPLEXER_CSA_GAIN8, + "4X": max9611Gain.MAX9611_MULTIPLEXER_CSA_GAIN4, + "1X": max9611Gain.MAX9611_MULTIPLEXER_CSA_GAIN1, +} +MAX9611Component = max9611_ns.class_( + "MAX9611Component", cg.PollingComponent, i2c.I2CDevice +) +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(MAX9611Component), + cv.Required(CONF_SHUNT_RESISTANCE): cv.resistance, + cv.Required(CONF_GAIN): cv.enum(MAX9611_GAIN, upper=True), + cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_CURRENT): sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_POWER): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x70)) +) + + +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_current_resistor(config[CONF_SHUNT_RESISTANCE])) + cg.add(var.set_gain(config[CONF_GAIN])) + if CONF_VOLTAGE in config: + conf = config[CONF_VOLTAGE] + sens = await sensor.new_sensor(conf) + cg.add(var.set_voltage_sensor(sens)) + if CONF_CURRENT in config: + conf = config[CONF_CURRENT] + sens = await sensor.new_sensor(conf) + cg.add(var.set_current_sensor(sens)) + if CONF_POWER in config: + conf = config[CONF_POWER] + sens = await sensor.new_sensor(conf) + cg.add(var.set_watt_sensor(sens)) + if CONF_TEMPERATURE in config: + conf = config[CONF_TEMPERATURE] + sens = await sensor.new_sensor(conf) + cg.add(var.set_temp_sensor(sens)) diff --git a/tests/test1.yaml b/tests/test1.yaml index e97b3aed73..d8fea223dc 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1077,7 +1077,21 @@ sensor: cs_pin: mcp23xxx: mcp23017_hub number: 14 - + - platform: max9611 + i2c_id: i2c_bus + shunt_resistance: 0.2 ohm + gain: '1X' + voltage: + name: Max9611 Voltage + current: + name: Max9611 Current + power: + name: Max9611 Watts + temperature: + name: Max9611 Temp + update_interval: 1s + + esp32_touch: setup_mode: False iir_filter: 10ms From c66d0550e85518cbe87604de557bbc43129adb92 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 8 Feb 2022 22:56:56 +1300 Subject: [PATCH 0200/1729] Inkplate 6 PLUS (#3013) --- esphome/components/inkplate6/display.py | 1 + esphome/components/inkplate6/inkplate.cpp | 434 +++++++++++++--------- esphome/components/inkplate6/inkplate.h | 54 +-- 3 files changed, 281 insertions(+), 208 deletions(-) diff --git a/esphome/components/inkplate6/display.py b/esphome/components/inkplate6/display.py index dca764c6ed..a17f37c920 100644 --- a/esphome/components/inkplate6/display.py +++ b/esphome/components/inkplate6/display.py @@ -47,6 +47,7 @@ InkplateModel = inkplate6_ns.enum("InkplateModel") MODELS = { "inkplate_6": InkplateModel.INKPLATE_6, "inkplate_10": InkplateModel.INKPLATE_10, + "inkplate_6_plus": InkplateModel.INKPLATE_6_PLUS, } CONFIG_SCHEMA = cv.All( diff --git a/esphome/components/inkplate6/inkplate.cpp b/esphome/components/inkplate6/inkplate.cpp index 8b7890feb7..e6fb9b773c 100644 --- a/esphome/components/inkplate6/inkplate.cpp +++ b/esphome/components/inkplate6/inkplate.cpp @@ -13,6 +13,11 @@ namespace inkplate6 { static const char *const TAG = "inkplate"; void Inkplate6::setup() { + for (uint32_t i = 0; i < 256; i++) { + this->pin_lut_[i] = ((i & 0b00000011) << 4) | (((i & 0b00001100) >> 2) << 18) | (((i & 0b00010000) >> 4) << 23) | + (((i & 0b11100000) >> 5) << 25); + } + this->initialize_(); this->vcom_pin_->setup(); @@ -38,11 +43,21 @@ void Inkplate6::setup() { this->display_data_6_pin_->setup(); this->display_data_7_pin_->setup(); - this->clean(); - this->display(); + this->wakeup_pin_->digital_write(true); + delay(1); + this->write_bytes(0x09, { + 0b00011011, // Power up seq. + 0b00000000, // Power up delay (3mS per rail) + 0b00011011, // Power down seq. + 0b00000000, // Power down delay (6mS per rail) + }); + delay(1); + this->wakeup_pin_->digital_write(false); } + void Inkplate6::initialize_() { ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + ExternalRAMAllocator allocator32(ExternalRAMAllocator::ALLOW_FAILURE); uint32_t buffer_size = this->get_buffer_length_(); if (buffer_size == 0) return; @@ -53,6 +68,10 @@ void Inkplate6::initialize_() { allocator.deallocate(this->partial_buffer_2_, buffer_size * 2); if (this->buffer_ != nullptr) allocator.deallocate(this->buffer_, buffer_size); + if (this->glut_ != nullptr) + allocator32.deallocate(this->glut_, 256 * (this->model_ == INKPLATE_6_PLUS ? 9 : 8)); + if (this->glut2_ != nullptr) + allocator32.deallocate(this->glut2_, 256 * (this->model_ == INKPLATE_6_PLUS ? 9 : 8)); this->buffer_ = allocator.allocate(buffer_size); if (this->buffer_ == nullptr) { @@ -60,7 +79,34 @@ void Inkplate6::initialize_() { this->mark_failed(); return; } - if (!this->greyscale_) { + if (this->greyscale_) { + uint8_t glut_size = (this->model_ == INKPLATE_6_PLUS ? 9 : 8); + + this->glut_ = allocator32.allocate(256 * glut_size); + if (this->glut_ == nullptr) { + ESP_LOGE(TAG, "Could not allocate glut!"); + this->mark_failed(); + return; + } + this->glut2_ = allocator32.allocate(256 * glut_size); + if (this->glut2_ == nullptr) { + ESP_LOGE(TAG, "Could not allocate glut2!"); + this->mark_failed(); + return; + } + + for (int i = 0; i < glut_size; i++) { + for (uint32_t j = 0; j < 256; j++) { + uint8_t z = (waveform3Bit[j & 0x07][i] << 2) | (waveform3Bit[(j >> 4) & 0x07][i]); + this->glut_[i * 256 + j] = ((z & 0b00000011) << 4) | (((z & 0b00001100) >> 2) << 18) | + (((z & 0b00010000) >> 4) << 23) | (((z & 0b11100000) >> 5) << 25); + z = ((waveform3Bit[j & 0x07][i] << 2) | (waveform3Bit[(j >> 4) & 0x07][i])) << 4; + this->glut2_[i * 256 + j] = ((z & 0b00000011) << 4) | (((z & 0b00001100) >> 2) << 18) | + (((z & 0b00010000) >> 4) << 23) | (((z & 0b11100000) >> 5) << 25); + } + } + + } else { this->partial_buffer_ = allocator.allocate(buffer_size); if (this->partial_buffer_ == nullptr) { ESP_LOGE(TAG, "Could not allocate partial buffer for display!"); @@ -73,13 +119,16 @@ void Inkplate6::initialize_() { this->mark_failed(); return; } + memset(this->partial_buffer_, 0, buffer_size); memset(this->partial_buffer_2_, 0, buffer_size * 2); } memset(this->buffer_, 0, buffer_size); } + float Inkplate6::get_setup_priority() const { return setup_priority::PROCESSOR; } + size_t Inkplate6::get_buffer_length_() { if (this->greyscale_) { return size_t(this->get_width_internal()) * size_t(this->get_height_internal()) / 2u; @@ -87,6 +136,7 @@ size_t Inkplate6::get_buffer_length_() { return size_t(this->get_width_internal()) * size_t(this->get_height_internal()) / 8u; } } + void Inkplate6::update() { this->do_update_(); @@ -96,6 +146,7 @@ void Inkplate6::update() { this->display(); } + void HOT Inkplate6::draw_absolute_pixel_internal(int x, int y, Color color) { if (x >= this->get_width_internal() || y >= this->get_height_internal() || x < 0 || y < 0) return; @@ -121,6 +172,7 @@ void HOT Inkplate6::draw_absolute_pixel_internal(int x, int y, Color color) { this->partial_buffer_[pos] = (~pixelMaskLUT[x_sub] & current) | (color.is_on() ? 0 : pixelMaskLUT[x_sub]); } } + void Inkplate6::dump_config() { LOG_DISPLAY("", "Inkplate", this); ESP_LOGCONFIG(TAG, " Greyscale: %s", YESNO(this->greyscale_)); @@ -150,44 +202,51 @@ void Inkplate6::dump_config() { LOG_UPDATE_INTERVAL(this); } + void Inkplate6::eink_off_() { ESP_LOGV(TAG, "Eink off called"); - if (panel_on_ == 0) + if (!panel_on_) return; - panel_on_ = 0; - this->gmod_pin_->digital_write(false); + panel_on_ = false; + this->oe_pin_->digital_write(false); + this->gmod_pin_->digital_write(false); - GPIO.out &= ~(get_data_pin_mask_() | (1 << this->cl_pin_->get_pin()) | (1 << this->le_pin_->get_pin())); - + GPIO.out &= ~(this->get_data_pin_mask_() | (1 << this->cl_pin_->get_pin()) | (1 << this->le_pin_->get_pin())); + this->ckv_pin_->digital_write(false); this->sph_pin_->digital_write(false); this->spv_pin_->digital_write(false); - this->powerup_pin_->digital_write(false); - this->wakeup_pin_->digital_write(false); this->vcom_pin_->digital_write(false); + this->write_byte(0x01, 0x6F); // Put TPS65186 into standby mode + + delay(100); // NOLINT + + this->write_byte(0x01, 0x4f); // Disable 3V3 to the panel + + if (this->model_ != INKPLATE_6_PLUS) + this->wakeup_pin_->digital_write(false); + pins_z_state_(); } + void Inkplate6::eink_on_() { ESP_LOGV(TAG, "Eink on called"); - if (panel_on_ == 1) + if (panel_on_) return; - panel_on_ = 1; - pins_as_outputs_(); + this->panel_on_ = true; + + this->pins_as_outputs_(); this->wakeup_pin_->digital_write(true); - this->powerup_pin_->digital_write(true); this->vcom_pin_->digital_write(true); - - this->write_byte(0x01, 0x3F); - - delay(40); - - this->write_byte(0x0D, 0x80); - delay(2); - this->read_register(0x00, nullptr, 0); + this->write_byte(0x01, 0b00101111); // Enable all rails + + delay(1); + + this->write_byte(0x01, 0b10101111); // Switch TPS65186 into active mode this->le_pin_->digital_write(false); this->oe_pin_->digital_write(false); @@ -196,8 +255,33 @@ void Inkplate6::eink_on_() { this->gmod_pin_->digital_write(true); this->spv_pin_->digital_write(true); this->ckv_pin_->digital_write(false); + this->oe_pin_->digital_write(false); + + uint32_t timer = millis(); + do { + delay(1); + } while (!this->read_power_status_() && ((millis() - timer) < 250)); + if ((millis() - timer) >= 250) { + ESP_LOGW(TAG, "Power supply not detected"); + this->wakeup_pin_->digital_write(false); + this->vcom_pin_->digital_write(false); + this->powerup_pin_->digital_write(false); + this->panel_on_ = false; + return; + } + this->oe_pin_->digital_write(true); } + +bool Inkplate6::read_power_status_() { + uint8_t data; + auto err = this->read_register(0x0F, &data, 1); + if (err == i2c::ERROR_OK) { + return data == 0b11111010; + } + return false; +} + void Inkplate6::fill(Color color) { ESP_LOGV(TAG, "Fill called"); uint32_t start_time = millis(); @@ -212,6 +296,7 @@ void Inkplate6::fill(Color color) { ESP_LOGV(TAG, "Fill finished (%ums)", millis() - start_time); } + void Inkplate6::display() { ESP_LOGV(TAG, "Display called"); uint32_t start_time = millis(); @@ -227,201 +312,185 @@ void Inkplate6::display() { } ESP_LOGV(TAG, "Display finished (full) (%ums)", millis() - start_time); } + void Inkplate6::display1b_() { ESP_LOGV(TAG, "Display1b called"); uint32_t start_time = millis(); memcpy(this->buffer_, this->partial_buffer_, this->get_buffer_length_()); - uint32_t send; uint8_t data; uint8_t buffer_value; const uint8_t *buffer_ptr; eink_on_(); - clean_fast_(0, 1); - clean_fast_(1, 5); - clean_fast_(2, 1); - clean_fast_(0, 5); - clean_fast_(2, 1); - clean_fast_(1, 12); - clean_fast_(2, 1); - clean_fast_(0, 11); + if (this->model_ == INKPLATE_6_PLUS) { + clean_fast_(0, 1); + clean_fast_(1, 15); + clean_fast_(2, 1); + clean_fast_(0, 5); + clean_fast_(2, 1); + clean_fast_(1, 15); + } else { + clean_fast_(0, 1); + clean_fast_(1, 21); + clean_fast_(2, 1); + clean_fast_(0, 12); + clean_fast_(2, 1); + clean_fast_(1, 21); + clean_fast_(2, 1); + clean_fast_(0, 12); + } uint32_t clock = (1 << this->cl_pin_->get_pin()); + uint32_t data_mask = this->get_data_pin_mask_(); ESP_LOGV(TAG, "Display1b start loops (%ums)", millis() - start_time); - for (int k = 0; k < 3; k++) { + + for (int k = 0; k < 4; k++) { buffer_ptr = &this->buffer_[this->get_buffer_length_() - 1]; vscan_start_(); - for (int i = 0; i < this->get_height_internal(); i++) { + for (int i = 0, im = this->get_height_internal(); i < im; i++) { buffer_value = *(buffer_ptr--); - data = LUTB[(buffer_value >> 4) & 0x0F]; - send = ((data & 0b00000011) << 4) | (((data & 0b00001100) >> 2) << 18) | (((data & 0b00010000) >> 4) << 23) | - (((data & 0b11100000) >> 5) << 25); - hscan_start_(send); - data = LUTB[buffer_value & 0x0F]; - send = ((data & 0b00000011) << 4) | (((data & 0b00001100) >> 2) << 18) | (((data & 0b00010000) >> 4) << 23) | - (((data & 0b11100000) >> 5) << 25) | clock; - GPIO.out_w1ts = send; - GPIO.out_w1tc = send; + data = this->model_ == INKPLATE_6_PLUS ? LUTW[(~buffer_value >> 4) & 0x0F] : LUTB[(buffer_value >> 4) & 0x0F]; + hscan_start_(this->pin_lut_[data]); + data = this->model_ == INKPLATE_6_PLUS ? LUTW[(~buffer_value) & 0x0F] : LUTB[buffer_value & 0x0F]; + GPIO.out_w1ts = this->pin_lut_[data] | clock; + GPIO.out_w1tc = data_mask | clock; for (int j = 0, jm = (this->get_width_internal() / 8) - 1; j < jm; j++) { buffer_value = *(buffer_ptr--); - data = LUTB[(buffer_value >> 4) & 0x0F]; - send = ((data & 0b00000011) << 4) | (((data & 0b00001100) >> 2) << 18) | (((data & 0b00010000) >> 4) << 23) | - (((data & 0b11100000) >> 5) << 25) | clock; - GPIO.out_w1ts = send; - GPIO.out_w1tc = send; - data = LUTB[buffer_value & 0x0F]; - send = ((data & 0b00000011) << 4) | (((data & 0b00001100) >> 2) << 18) | (((data & 0b00010000) >> 4) << 23) | - (((data & 0b11100000) >> 5) << 25) | clock; - GPIO.out_w1ts = send; - GPIO.out_w1tc = send; + data = this->model_ == INKPLATE_6_PLUS ? LUTW[(~buffer_value >> 4) & 0x0F] : LUTB[(buffer_value >> 4) & 0x0F]; + GPIO.out_w1ts = this->pin_lut_[data] | clock; + GPIO.out_w1tc = data_mask | clock; + data = this->model_ == INKPLATE_6_PLUS ? LUTW[(~buffer_value) & 0x0F] : LUTB[buffer_value & 0x0F]; + GPIO.out_w1ts = this->pin_lut_[data] | clock; + GPIO.out_w1tc = data_mask | clock; } - GPIO.out_w1ts = send; - GPIO.out_w1tc = get_data_pin_mask_() | clock; + GPIO.out_w1ts = clock; + GPIO.out_w1tc = data_mask | clock; vscan_end_(); } delayMicroseconds(230); } - ESP_LOGV(TAG, "Display1b first loop x %d (%ums)", 3, millis() - start_time); + ESP_LOGV(TAG, "Display1b first loop x %d (%ums)", 4, millis() - start_time); buffer_ptr = &this->buffer_[this->get_buffer_length_() - 1]; vscan_start_(); - for (int i = 0; i < this->get_height_internal(); i++) { + for (int i = 0, im = this->get_height_internal(); i < im; i++) { buffer_value = *(buffer_ptr--); - data = LUT2[(buffer_value >> 4) & 0x0F]; - send = ((data & 0b00000011) << 4) | (((data & 0b00001100) >> 2) << 18) | (((data & 0b00010000) >> 4) << 23) | - (((data & 0b11100000) >> 5) << 25); - hscan_start_(send); - data = LUT2[buffer_value & 0x0F]; - send = ((data & 0b00000011) << 4) | (((data & 0b00001100) >> 2) << 18) | (((data & 0b00010000) >> 4) << 23) | - (((data & 0b11100000) >> 5) << 25) | clock; - GPIO.out_w1ts = send; - GPIO.out_w1tc = send; + data = this->model_ == INKPLATE_6_PLUS ? LUTB[(buffer_value >> 4) & 0x0F] : LUT2[(buffer_value >> 4) & 0x0F]; + hscan_start_(this->pin_lut_[data] | clock); + data = this->model_ == INKPLATE_6_PLUS ? LUTB[buffer_value & 0x0F] : LUT2[buffer_value & 0x0F]; + GPIO.out_w1ts = this->pin_lut_[data] | clock; + GPIO.out_w1tc = data_mask | clock; + for (int j = 0, jm = (this->get_width_internal() / 8) - 1; j < jm; j++) { buffer_value = *(buffer_ptr--); - data = LUT2[(buffer_value >> 4) & 0x0F]; - send = ((data & 0b00000011) << 4) | (((data & 0b00001100) >> 2) << 18) | (((data & 0b00010000) >> 4) << 23) | - (((data & 0b11100000) >> 5) << 25) | clock; - GPIO.out_w1ts = send; - GPIO.out_w1tc = send; - data = LUT2[buffer_value & 0x0F]; - send = ((data & 0b00000011) << 4) | (((data & 0b00001100) >> 2) << 18) | (((data & 0b00010000) >> 4) << 23) | - (((data & 0b11100000) >> 5) << 25) | clock; - GPIO.out_w1ts = send; - GPIO.out_w1tc = send; + data = this->model_ == INKPLATE_6_PLUS ? LUTB[(buffer_value >> 4) & 0x0F] : LUT2[(buffer_value >> 4) & 0x0F]; + GPIO.out_w1ts = this->pin_lut_[data] | clock; + GPIO.out_w1tc = data_mask | clock; + data = this->model_ == INKPLATE_6_PLUS ? LUTB[buffer_value & 0x0F] : LUT2[buffer_value & 0x0F]; + GPIO.out_w1ts = this->pin_lut_[data] | clock; + GPIO.out_w1tc = data_mask | clock; } - GPIO.out_w1ts = send; - GPIO.out_w1tc = get_data_pin_mask_() | clock; + GPIO.out_w1ts = clock; + GPIO.out_w1tc = data_mask | clock; vscan_end_(); } delayMicroseconds(230); ESP_LOGV(TAG, "Display1b second loop (%ums)", millis() - start_time); - vscan_start_(); - for (int i = 0; i < this->get_height_internal(); i++) { - data = 0b00000000; - send = ((data & 0b00000011) << 4) | (((data & 0b00001100) >> 2) << 18) | (((data & 0b00010000) >> 4) << 23) | - (((data & 0b11100000) >> 5) << 25); - hscan_start_(send); - send |= clock; - GPIO.out_w1ts = send; - GPIO.out_w1tc = send; - for (int j = 0; j < (this->get_width_internal() / 8) - 1; j++) { - GPIO.out_w1ts = send; - GPIO.out_w1tc = send; - GPIO.out_w1ts = send; - GPIO.out_w1tc = send; + if (this->model_ == INKPLATE_6_PLUS) { + clean_fast_(2, 2); + clean_fast_(3, 1); + } else { + uint32_t send = this->pin_lut_[0]; + vscan_start_(); + for (int i = 0, im = this->get_height_internal(); i < im; i++) { + hscan_start_(send); + GPIO.out_w1ts = send | clock; + GPIO.out_w1tc = data_mask | clock; + for (int j = 0, jm = (this->get_width_internal() / 8) - 1; j < jm; j++) { + GPIO.out_w1ts = send | clock; + GPIO.out_w1tc = data_mask | clock; + GPIO.out_w1ts = send | clock; + GPIO.out_w1tc = data_mask | clock; + } + GPIO.out_w1ts = send | clock; + GPIO.out_w1tc = data_mask | clock; + vscan_end_(); } - GPIO.out_w1ts = clock; - GPIO.out_w1tc = get_data_pin_mask_() | clock; - vscan_end_(); + delayMicroseconds(230); + ESP_LOGV(TAG, "Display1b third loop (%ums)", millis() - start_time); } - delayMicroseconds(230); - ESP_LOGV(TAG, "Display1b third loop (%ums)", millis() - start_time); - vscan_start_(); eink_off_(); this->block_partial_ = false; this->partial_updates_ = 0; ESP_LOGV(TAG, "Display1b finished (%ums)", millis() - start_time); } + void Inkplate6::display3b_() { ESP_LOGV(TAG, "Display3b called"); uint32_t start_time = millis(); eink_on_(); - clean_fast_(0, 1); - clean_fast_(1, 12); - clean_fast_(2, 1); - clean_fast_(0, 11); - clean_fast_(2, 1); - clean_fast_(1, 12); - clean_fast_(2, 1); - clean_fast_(0, 11); + if (this->model_ == INKPLATE_6_PLUS) { + clean_fast_(0, 1); + clean_fast_(1, 15); + clean_fast_(2, 1); + clean_fast_(0, 5); + clean_fast_(2, 1); + clean_fast_(1, 15); + } else { + clean_fast_(0, 1); + clean_fast_(1, 21); + clean_fast_(2, 1); + clean_fast_(0, 12); + clean_fast_(2, 1); + clean_fast_(1, 21); + clean_fast_(2, 1); + clean_fast_(0, 12); + } uint32_t clock = (1 << this->cl_pin_->get_pin()); - for (int k = 0; k < 8; k++) { - const uint8_t *buffer_ptr = &this->buffer_[this->get_buffer_length_() - 1]; - uint32_t send; - uint8_t pix1; - uint8_t pix2; - uint8_t pix3; - uint8_t pix4; - uint8_t pixel; - uint8_t pixel2; - + uint32_t data_mask = this->get_data_pin_mask_(); + uint32_t pos; + uint32_t data; + uint8_t glut_size = this->model_ == INKPLATE_6_PLUS ? 9 : 8; + for (int k = 0; k < glut_size; k++) { + pos = this->get_buffer_length_(); vscan_start_(); for (int i = 0; i < this->get_height_internal(); i++) { - pix1 = (*buffer_ptr--); - pix2 = (*buffer_ptr--); - pix3 = (*buffer_ptr--); - pix4 = (*buffer_ptr--); - pixel = (waveform3Bit[pix1 & 0x07][k] << 6) | (waveform3Bit[(pix1 >> 4) & 0x07][k] << 4) | - (waveform3Bit[pix2 & 0x07][k] << 2) | (waveform3Bit[(pix2 >> 4) & 0x07][k] << 0); - pixel2 = (waveform3Bit[pix3 & 0x07][k] << 6) | (waveform3Bit[(pix3 >> 4) & 0x07][k] << 4) | - (waveform3Bit[pix4 & 0x07][k] << 2) | (waveform3Bit[(pix4 >> 4) & 0x07][k] << 0); + data = this->glut2_[k * 256 + this->buffer_[--pos]]; + data |= this->glut_[k * 256 + this->buffer_[--pos]]; + hscan_start_(data); + data = this->glut2_[k * 256 + this->buffer_[--pos]]; + data |= this->glut_[k * 256 + this->buffer_[--pos]]; + GPIO.out_w1ts = data | clock; + GPIO.out_w1tc = data_mask | clock; - send = ((pixel & 0b00000011) << 4) | (((pixel & 0b00001100) >> 2) << 18) | (((pixel & 0b00010000) >> 4) << 23) | - (((pixel & 0b11100000) >> 5) << 25); - hscan_start_(send); - send = ((pixel2 & 0b00000011) << 4) | (((pixel2 & 0b00001100) >> 2) << 18) | - (((pixel2 & 0b00010000) >> 4) << 23) | (((pixel2 & 0b11100000) >> 5) << 25) | clock; - GPIO.out_w1ts = send; - GPIO.out_w1tc = send; - - for (int j = 0, jm = (this->get_width_internal() / 8) - 1; j < jm; j++) { - pix1 = (*buffer_ptr--); - pix2 = (*buffer_ptr--); - pix3 = (*buffer_ptr--); - pix4 = (*buffer_ptr--); - pixel = (waveform3Bit[pix1 & 0x07][k] << 6) | (waveform3Bit[(pix1 >> 4) & 0x07][k] << 4) | - (waveform3Bit[pix2 & 0x07][k] << 2) | (waveform3Bit[(pix2 >> 4) & 0x07][k] << 0); - pixel2 = (waveform3Bit[pix3 & 0x07][k] << 6) | (waveform3Bit[(pix3 >> 4) & 0x07][k] << 4) | - (waveform3Bit[pix4 & 0x07][k] << 2) | (waveform3Bit[(pix4 >> 4) & 0x07][k] << 0); - - send = ((pixel & 0b00000011) << 4) | (((pixel & 0b00001100) >> 2) << 18) | (((pixel & 0b00010000) >> 4) << 23) | - (((pixel & 0b11100000) >> 5) << 25) | clock; - GPIO.out_w1ts = send; - GPIO.out_w1tc = send; - - send = ((pixel2 & 0b00000011) << 4) | (((pixel2 & 0b00001100) >> 2) << 18) | - (((pixel2 & 0b00010000) >> 4) << 23) | (((pixel2 & 0b11100000) >> 5) << 25) | clock; - GPIO.out_w1ts = send; - GPIO.out_w1tc = send; + for (int j = 0; j < (this->get_width_internal() / 8) - 1; j++) { + data = this->glut2_[k * 256 + this->buffer_[--pos]]; + data |= this->glut_[k * 256 + this->buffer_[--pos]]; + GPIO.out_w1ts = data | clock; + GPIO.out_w1tc = data_mask | clock; + data = this->glut2_[k * 256 + this->buffer_[--pos]]; + data |= this->glut_[k * 256 + this->buffer_[--pos]]; + GPIO.out_w1ts = data | clock; + GPIO.out_w1tc = data_mask | clock; } - GPIO.out_w1ts = send; - GPIO.out_w1tc = get_data_pin_mask_() | clock; + GPIO.out_w1ts = clock; + GPIO.out_w1tc = data_mask | clock; vscan_end_(); } delayMicroseconds(230); } - clean_fast_(2, 1); clean_fast_(3, 1); vscan_start_(); eink_off_(); ESP_LOGV(TAG, "Display3b finished (%ums)", millis() - start_time); } + bool Inkplate6::partial_update_() { ESP_LOGV(TAG, "Partial update called"); uint32_t start_time = millis(); @@ -432,16 +501,15 @@ bool Inkplate6::partial_update_() { this->partial_updates_++; - uint16_t pos = this->get_buffer_length_() - 1; - uint32_t send; + uint32_t pos = this->get_buffer_length_() - 1; uint8_t data; uint8_t diffw, diffb; uint32_t n = (this->get_buffer_length_() * 2) - 1; for (int i = 0, im = this->get_height_internal(); i < im; i++) { for (int j = 0, jm = (this->get_width_internal() / 8); j < jm; j++) { - diffw = (this->buffer_[pos] ^ this->partial_buffer_[pos]) & ~(this->partial_buffer_[pos]); - diffb = (this->buffer_[pos] ^ this->partial_buffer_[pos]) & this->partial_buffer_[pos]; + diffw = this->buffer_[pos] & ~(this->partial_buffer_[pos]); + diffb = ~(this->buffer_[pos]) & this->partial_buffer_[pos]; pos--; this->partial_buffer_2_[n--] = LUTW[diffw >> 4] & LUTB[diffb >> 4]; this->partial_buffer_2_[n--] = LUTW[diffw & 0x0F] & LUTB[diffb & 0x0F]; @@ -451,23 +519,20 @@ bool Inkplate6::partial_update_() { eink_on_(); uint32_t clock = (1 << this->cl_pin_->get_pin()); + uint32_t data_mask = this->get_data_pin_mask_(); for (int k = 0; k < 5; k++) { vscan_start_(); const uint8_t *data_ptr = &this->partial_buffer_2_[(this->get_buffer_length_() * 2) - 1]; for (int i = 0; i < this->get_height_internal(); i++) { data = *(data_ptr--); - send = ((data & 0b00000011) << 4) | (((data & 0b00001100) >> 2) << 18) | (((data & 0b00010000) >> 4) << 23) | - (((data & 0b11100000) >> 5) << 25); - hscan_start_(send); + hscan_start_(this->pin_lut_[data]); for (int j = 0, jm = (this->get_width_internal() / 4) - 1; j < jm; j++) { data = *(data_ptr--); - send = ((data & 0b00000011) << 4) | (((data & 0b00001100) >> 2) << 18) | (((data & 0b00010000) >> 4) << 23) | - (((data & 0b11100000) >> 5) << 25) | clock; - GPIO.out_w1ts = send; - GPIO.out_w1tc = send; + GPIO.out_w1ts = this->pin_lut_[data] | clock; + GPIO.out_w1tc = data_mask | clock; } - GPIO.out_w1ts = send; - GPIO.out_w1tc = get_data_pin_mask_() | clock; + GPIO.out_w1ts = clock; + GPIO.out_w1tc = data_mask | clock; vscan_end_(); } delayMicroseconds(230); @@ -482,6 +547,7 @@ bool Inkplate6::partial_update_() { ESP_LOGV(TAG, "Partial update finished (%ums)", millis() - start_time); return true; } + void Inkplate6::vscan_start_() { this->ckv_pin_->digital_write(true); delayMicroseconds(7); @@ -505,30 +571,23 @@ void Inkplate6::vscan_start_() { delayMicroseconds(0); this->ckv_pin_->digital_write(true); } -void Inkplate6::vscan_write_() { - this->ckv_pin_->digital_write(false); - this->le_pin_->digital_write(true); - this->le_pin_->digital_write(false); - delayMicroseconds(0); + +void Inkplate6::hscan_start_(uint32_t d) { + uint8_t clock = (1 << this->cl_pin_->get_pin()); this->sph_pin_->digital_write(false); - this->cl_pin_->digital_write(true); - this->cl_pin_->digital_write(false); + GPIO.out_w1ts = d | clock; + GPIO.out_w1tc = this->get_data_pin_mask_() | clock; this->sph_pin_->digital_write(true); this->ckv_pin_->digital_write(true); } -void Inkplate6::hscan_start_(uint32_t d) { - this->sph_pin_->digital_write(false); - GPIO.out_w1ts = (d) | (1 << this->cl_pin_->get_pin()); - GPIO.out_w1tc = get_data_pin_mask_() | (1 << this->cl_pin_->get_pin()); - this->sph_pin_->digital_write(true); -} + void Inkplate6::vscan_end_() { this->ckv_pin_->digital_write(false); this->le_pin_->digital_write(true); this->le_pin_->digital_write(false); - delayMicroseconds(1); - this->ckv_pin_->digital_write(true); + delayMicroseconds(0); } + void Inkplate6::clean() { ESP_LOGV(TAG, "Clean called"); uint32_t start_time = millis(); @@ -542,6 +601,7 @@ void Inkplate6::clean() { clean_fast_(1, 10); // White to White ESP_LOGV(TAG, "Clean finished (%ums)", millis() - start_time); } + void Inkplate6::clean_fast_(uint8_t c, uint8_t rep) { ESP_LOGV(TAG, "Clean fast called with: (%d, %d)", c, rep); uint32_t start_time = millis(); @@ -568,14 +628,14 @@ void Inkplate6::clean_fast_(uint8_t c, uint8_t rep) { hscan_start_(send); GPIO.out_w1ts = send | clock; GPIO.out_w1tc = clock; - for (int j = 0, jm = this->get_width_internal() / 8; j < jm; j++) { + for (int j = 0; j < (this->get_width_internal() / 8) - 1; j++) { GPIO.out_w1ts = clock; GPIO.out_w1tc = clock; GPIO.out_w1ts = clock; GPIO.out_w1tc = clock; } - GPIO.out_w1ts = clock; - GPIO.out_w1tc = get_data_pin_mask_() | clock; + GPIO.out_w1ts = send | clock; + GPIO.out_w1tc = clock; vscan_end_(); } delayMicroseconds(230); @@ -583,7 +643,10 @@ void Inkplate6::clean_fast_(uint8_t c, uint8_t rep) { } ESP_LOGV(TAG, "Clean fast finished (%ums)", millis() - start_time); } + void Inkplate6::pins_z_state_() { + this->cl_pin_->pin_mode(gpio::FLAG_INPUT); + this->le_pin_->pin_mode(gpio::FLAG_INPUT); this->ckv_pin_->pin_mode(gpio::FLAG_INPUT); this->sph_pin_->pin_mode(gpio::FLAG_INPUT); @@ -600,7 +663,10 @@ void Inkplate6::pins_z_state_() { this->display_data_6_pin_->pin_mode(gpio::FLAG_INPUT); this->display_data_7_pin_->pin_mode(gpio::FLAG_INPUT); } + void Inkplate6::pins_as_outputs_() { + this->cl_pin_->pin_mode(gpio::FLAG_OUTPUT); + this->le_pin_->pin_mode(gpio::FLAG_OUTPUT); this->ckv_pin_->pin_mode(gpio::FLAG_OUTPUT); this->sph_pin_->pin_mode(gpio::FLAG_OUTPUT); diff --git a/esphome/components/inkplate6/inkplate.h b/esphome/components/inkplate6/inkplate.h index 6cbb24805d..9355154c5a 100644 --- a/esphome/components/inkplate6/inkplate.h +++ b/esphome/components/inkplate6/inkplate.h @@ -13,32 +13,28 @@ namespace inkplate6 { enum InkplateModel : uint8_t { INKPLATE_6 = 0, INKPLATE_10 = 1, + INKPLATE_6_PLUS = 2, }; class Inkplate6 : public PollingComponent, public display::DisplayBuffer, public i2c::I2CDevice { public: - const uint8_t LUT2[16] = {0b10101010, 0b10101001, 0b10100110, 0b10100101, 0b10011010, 0b10011001, - 0b10010110, 0b10010101, 0b01101010, 0b01101001, 0b01100110, 0b01100101, - 0b01011010, 0b01011001, 0b01010110, 0b01010101}; - const uint8_t LUTW[16] = {0b11111111, 0b11111110, 0b11111011, 0b11111010, 0b11101111, 0b11101110, - 0b11101011, 0b11101010, 0b10111111, 0b10111110, 0b10111011, 0b10111010, - 0b10101111, 0b10101110, 0b10101011, 0b10101010}; - const uint8_t LUTB[16] = {0b11111111, 0b11111101, 0b11110111, 0b11110101, 0b11011111, 0b11011101, - 0b11010111, 0b11010101, 0b01111111, 0b01111101, 0b01110111, 0b01110101, - 0b01011111, 0b01011101, 0b01010111, 0b01010101}; - const uint8_t pixelMaskLUT[8] = {0b00000001, 0b00000010, 0b00000100, 0b00001000, - 0b00010000, 0b00100000, 0b01000000, 0b10000000}; - const uint8_t pixelMaskGLUT[2] = {0b00001111, 0b11110000}; - const uint8_t waveform3Bit[8][8] = {{0, 0, 0, 0, 1, 1, 1, 0}, {1, 2, 2, 2, 1, 1, 1, 0}, {0, 1, 2, 1, 1, 2, 1, 0}, - {0, 2, 1, 2, 1, 2, 1, 0}, {0, 0, 0, 1, 1, 1, 2, 0}, {2, 1, 1, 1, 2, 1, 2, 0}, + const uint8_t LUT2[16] = {0xAA, 0xA9, 0xA6, 0xA5, 0x9A, 0x99, 0x96, 0x95, + 0x6A, 0x69, 0x66, 0x65, 0x5A, 0x59, 0x56, 0x55}; + const uint8_t LUTW[16] = {0xFF, 0xFE, 0xFB, 0xFA, 0xEF, 0xEE, 0xEB, 0xEA, + 0xBF, 0xBE, 0xBB, 0xBA, 0xAF, 0xAE, 0xAB, 0xAA}; + const uint8_t LUTB[16] = {0xFF, 0xFD, 0xF7, 0xF5, 0xDF, 0xDD, 0xD7, 0xD5, + 0x7F, 0x7D, 0x77, 0x75, 0x5F, 0x5D, 0x57, 0x55}; + + const uint8_t pixelMaskLUT[8] = {0x1, 0x2, 0x4, 0x8, 0x10, 0x20, 0x40, 0x80}; + const uint8_t pixelMaskGLUT[2] = {0x0F, 0xF0}; + + const uint8_t waveform3Bit[8][8] = {{0, 1, 1, 0, 0, 1, 1, 0}, {0, 1, 2, 1, 1, 2, 1, 0}, {1, 1, 1, 2, 2, 1, 0, 0}, + {0, 0, 0, 1, 1, 1, 2, 0}, {2, 1, 1, 1, 2, 1, 2, 0}, {2, 2, 1, 1, 2, 1, 2, 0}, {1, 1, 1, 2, 1, 2, 2, 0}, {0, 0, 0, 0, 0, 0, 2, 0}}; - const uint32_t waveform[50] = { - 0x00000008, 0x00000008, 0x00200408, 0x80281888, 0x60a81898, 0x60a8a8a8, 0x60a8a8a8, 0x6068a868, 0x6868a868, - 0x6868a868, 0x68686868, 0x6a686868, 0x5a686868, 0x5a686868, 0x5a586a68, 0x5a5a6a68, 0x5a5a6a68, 0x55566a68, - 0x55565a64, 0x55555654, 0x55555556, 0x55555556, 0x55555556, 0x55555516, 0x55555596, 0x15555595, 0x95955595, - 0x95959595, 0x95949495, 0x94949495, 0x94949495, 0xa4949494, 0x9494a4a4, 0x84a49494, 0x84948484, 0x84848484, - 0x84848484, 0x84848484, 0xa5a48484, 0xa9a4a4a8, 0xa9a8a8a8, 0xa5a9a9a4, 0xa5a5a5a4, 0xa1a5a5a1, 0xa9a9a9a9, - 0xa9a9a9a9, 0xa9a9a9a9, 0xa9a9a9a9, 0x15151515, 0x11111111}; + const uint8_t waveform3Bit6Plus[8][9] = {{0, 0, 0, 0, 0, 2, 1, 1, 0}, {0, 0, 2, 1, 1, 1, 2, 1, 0}, + {0, 2, 2, 2, 1, 1, 2, 1, 0}, {0, 0, 2, 2, 2, 1, 2, 1, 0}, + {0, 0, 0, 0, 2, 2, 2, 1, 0}, {0, 0, 2, 1, 2, 1, 1, 2, 0}, + {0, 0, 2, 2, 2, 1, 1, 2, 0}, {0, 0, 0, 0, 2, 2, 2, 2, 0}}; void set_greyscale(bool greyscale) { this->greyscale_ = greyscale; @@ -88,6 +84,8 @@ class Inkplate6 : public PollingComponent, public display::DisplayBuffer, public bool get_partial_updating() { return this->partial_updating_; } uint8_t get_temperature() { return this->temperature_; } + void block_partial() { this->block_partial_ = true; } + protected: void draw_absolute_pixel_internal(int x, int y, Color color) override; void display1b_(); @@ -99,10 +97,10 @@ class Inkplate6 : public PollingComponent, public display::DisplayBuffer, public void hscan_start_(uint32_t d); void vscan_end_(); void vscan_start_(); - void vscan_write_(); void eink_off_(); void eink_on_(); + bool read_power_status_(); void setup_pins_(); void pins_z_state_(); @@ -113,6 +111,8 @@ class Inkplate6 : public PollingComponent, public display::DisplayBuffer, public return 800; } else if (this->model_ == INKPLATE_10) { return 1200; + } else if (this->model_ == INKPLATE_6_PLUS) { + return 1024; } return 0; } @@ -122,6 +122,8 @@ class Inkplate6 : public PollingComponent, public display::DisplayBuffer, public return 600; } else if (this->model_ == INKPLATE_10) { return 825; + } else if (this->model_ == INKPLATE_6_PLUS) { + return 758; } return 0; } @@ -141,16 +143,20 @@ class Inkplate6 : public PollingComponent, public display::DisplayBuffer, public return data; } - uint8_t panel_on_ = 0; + bool panel_on_{false}; uint8_t temperature_; uint8_t *partial_buffer_{nullptr}; uint8_t *partial_buffer_2_{nullptr}; + uint32_t *glut_{nullptr}; + uint32_t *glut2_{nullptr}; + uint32_t pin_lut_[256]; + uint32_t full_update_every_; uint32_t partial_updates_{0}; - bool block_partial_; + bool block_partial_{true}; bool greyscale_; bool partial_updating_; From 9826726a72624f78bfb77fec7656e1474ef2158b Mon Sep 17 00:00:00 2001 From: Andrej Komelj Date: Tue, 8 Feb 2022 10:58:38 +0100 Subject: [PATCH 0201/1729] Implement MQTT discovery object_id generator (#3114) --- esphome/components/mqtt/__init__.py | 22 ++++++++++++++++++++-- esphome/components/mqtt/mqtt_client.cpp | 4 +++- esphome/components/mqtt/mqtt_client.h | 12 ++++++++++-- esphome/components/mqtt/mqtt_component.cpp | 7 +++++-- esphome/components/mqtt/mqtt_const.h | 2 ++ esphome/const.py | 1 + 6 files changed, 41 insertions(+), 7 deletions(-) diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index 88e5d43509..901b77474d 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -16,6 +16,7 @@ from esphome.const import ( CONF_DISCOVERY_PREFIX, CONF_DISCOVERY_RETAIN, CONF_DISCOVERY_UNIQUE_ID_GENERATOR, + CONF_DISCOVERY_OBJECT_ID_GENERATOR, CONF_ID, CONF_KEEPALIVE, CONF_LEVEL, @@ -105,6 +106,12 @@ MQTT_DISCOVERY_UNIQUE_ID_GENERATOR_OPTIONS = { "mac": MQTTDiscoveryUniqueIdGenerator.MQTT_MAC_ADDRESS_UNIQUE_ID_GENERATOR, } +MQTTDiscoveryObjectIdGenerator = mqtt_ns.enum("MQTTDiscoveryObjectIdGenerator") +MQTT_DISCOVERY_OBJECT_ID_GENERATOR_OPTIONS = { + "none": MQTTDiscoveryObjectIdGenerator.MQTT_NONE_OBJECT_ID_GENERATOR, + "device_name": MQTTDiscoveryObjectIdGenerator.MQTT_DEVICE_NAME_OBJECT_ID_GENERATOR, +} + def validate_config(value): # Populate default fields @@ -166,6 +173,9 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_DISCOVERY_UNIQUE_ID_GENERATOR, default="legacy"): cv.enum( MQTT_DISCOVERY_UNIQUE_ID_GENERATOR_OPTIONS ), + cv.Optional(CONF_DISCOVERY_OBJECT_ID_GENERATOR, default="none"): cv.enum( + MQTT_DISCOVERY_OBJECT_ID_GENERATOR_OPTIONS + ), cv.Optional(CONF_USE_ABBREVIATIONS, default=True): cv.boolean, cv.Optional(CONF_BIRTH_MESSAGE): MQTT_MESSAGE_SCHEMA, cv.Optional(CONF_WILL_MESSAGE): MQTT_MESSAGE_SCHEMA, @@ -245,19 +255,27 @@ async def to_code(config): discovery_retain = config[CONF_DISCOVERY_RETAIN] discovery_prefix = config[CONF_DISCOVERY_PREFIX] discovery_unique_id_generator = config[CONF_DISCOVERY_UNIQUE_ID_GENERATOR] + discovery_object_id_generator = config[CONF_DISCOVERY_OBJECT_ID_GENERATOR] if not discovery: cg.add(var.disable_discovery()) elif discovery == "CLEAN": cg.add( var.set_discovery_info( - discovery_prefix, discovery_unique_id_generator, discovery_retain, True + discovery_prefix, + discovery_unique_id_generator, + discovery_object_id_generator, + discovery_retain, + True, ) ) elif CONF_DISCOVERY_RETAIN in config or CONF_DISCOVERY_PREFIX in config: cg.add( var.set_discovery_info( - discovery_prefix, discovery_unique_id_generator, discovery_retain + discovery_prefix, + discovery_unique_id_generator, + discovery_object_id_generator, + discovery_retain, ) ) diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index 67063d4c72..148316672a 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -538,9 +538,11 @@ void MQTTClientComponent::set_birth_message(MQTTMessage &&message) { void MQTTClientComponent::set_shutdown_message(MQTTMessage &&message) { this->shutdown_message_ = std::move(message); } void MQTTClientComponent::set_discovery_info(std::string &&prefix, MQTTDiscoveryUniqueIdGenerator unique_id_generator, - bool retain, bool clean) { + MQTTDiscoveryObjectIdGenerator object_id_generator, bool retain, + bool clean) { this->discovery_info_.prefix = std::move(prefix); this->discovery_info_.unique_id_generator = unique_id_generator; + this->discovery_info_.object_id_generator = object_id_generator; this->discovery_info_.retain = retain; this->discovery_info_.clean = clean; } diff --git a/esphome/components/mqtt/mqtt_client.h b/esphome/components/mqtt/mqtt_client.h index a6a7025c6f..58a4fbe166 100644 --- a/esphome/components/mqtt/mqtt_client.h +++ b/esphome/components/mqtt/mqtt_client.h @@ -61,6 +61,12 @@ enum MQTTDiscoveryUniqueIdGenerator { MQTT_MAC_ADDRESS_UNIQUE_ID_GENERATOR, }; +/// available discovery object_id generators +enum MQTTDiscoveryObjectIdGenerator { + MQTT_NONE_OBJECT_ID_GENERATOR = 0, + MQTT_DEVICE_NAME_OBJECT_ID_GENERATOR, +}; + /** Internal struct for MQTT Home Assistant discovery * * See MQTT Discovery. @@ -70,6 +76,7 @@ struct MQTTDiscoveryInfo { bool retain; ///< Whether to retain discovery messages. bool clean; MQTTDiscoveryUniqueIdGenerator unique_id_generator; + MQTTDiscoveryObjectIdGenerator object_id_generator; }; enum MQTTClientState { @@ -106,10 +113,11 @@ class MQTTClientComponent : public Component { * See MQTT Discovery. * @param prefix The Home Assistant discovery prefix. * @param unique_id_generator Controls how UniqueId is generated. + * @param object_id_generator Controls how ObjectId is generated. * @param retain Whether to retain discovery messages. */ - void set_discovery_info(std::string &&prefix, MQTTDiscoveryUniqueIdGenerator unique_id_generator, bool retain, - bool clean = false); + void set_discovery_info(std::string &&prefix, MQTTDiscoveryUniqueIdGenerator unique_id_generator, + MQTTDiscoveryObjectIdGenerator object_id_generator, bool retain, bool clean = false); /// Get Home Assistant discovery info. const MQTTDiscoveryInfo &get_discovery_info() const; /// Globally disable Home Assistant discovery. diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index 341ac50e37..cf228efd1b 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -111,12 +111,11 @@ bool MQTTComponent::send_discovery_() { root[MQTT_PAYLOAD_NOT_AVAILABLE] = this->availability_->payload_not_available; } - const std::string &node_name = App.get_name(); std::string unique_id = this->unique_id(); + const MQTTDiscoveryInfo &discovery_info = global_mqtt_client->get_discovery_info(); if (!unique_id.empty()) { root[MQTT_UNIQUE_ID] = unique_id; } else { - const MQTTDiscoveryInfo &discovery_info = global_mqtt_client->get_discovery_info(); if (discovery_info.unique_id_generator == MQTT_MAC_ADDRESS_UNIQUE_ID_GENERATOR) { char friendly_name_hash[9]; sprintf(friendly_name_hash, "%08x", fnv1_hash(this->friendly_name())); @@ -129,6 +128,10 @@ bool MQTTComponent::send_discovery_() { } } + const std::string &node_name = App.get_name(); + if (discovery_info.object_id_generator == MQTT_DEVICE_NAME_OBJECT_ID_GENERATOR) + root[MQTT_OBJECT_ID] = node_name + "_" + this->get_default_object_id_(); + JsonObject device_info = root.createNestedObject(MQTT_DEVICE); device_info[MQTT_DEVICE_IDENTIFIERS] = get_mac_address(); device_info[MQTT_DEVICE_NAME] = node_name; diff --git a/esphome/components/mqtt/mqtt_const.h b/esphome/components/mqtt/mqtt_const.h index 52ca0ed7c0..7f74197ab4 100644 --- a/esphome/components/mqtt/mqtt_const.h +++ b/esphome/components/mqtt/mqtt_const.h @@ -107,6 +107,7 @@ constexpr const char *const MQTT_MODE_STATE_TOPIC = "mode_stat_t"; constexpr const char *const MQTT_MODE_STATE_TEMPLATE = "mode_stat_tpl"; constexpr const char *const MQTT_MODES = "modes"; constexpr const char *const MQTT_NAME = "name"; +constexpr const char *const MQTT_OBJECT_ID = "obj_id"; constexpr const char *const MQTT_OFF_DELAY = "off_dly"; constexpr const char *const MQTT_ON_COMMAND_TYPE = "on_cmd_type"; constexpr const char *const MQTT_OPTIONS = "ops"; @@ -360,6 +361,7 @@ constexpr const char *const MQTT_MODE_STATE_TOPIC = "mode_state_topic"; constexpr const char *const MQTT_MODE_STATE_TEMPLATE = "mode_state_template"; constexpr const char *const MQTT_MODES = "modes"; constexpr const char *const MQTT_NAME = "name"; +constexpr const char *const MQTT_OBJECT_ID = "object_id"; constexpr const char *const MQTT_OFF_DELAY = "off_delay"; constexpr const char *const MQTT_ON_COMMAND_TYPE = "on_command_type"; constexpr const char *const MQTT_OPTIONS = "options"; diff --git a/esphome/const.py b/esphome/const.py index 61b152654a..dcd6fea31f 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -168,6 +168,7 @@ CONF_DIRECTION = "direction" CONF_DIRECTION_OUTPUT = "direction_output" CONF_DISABLED_BY_DEFAULT = "disabled_by_default" CONF_DISCOVERY = "discovery" +CONF_DISCOVERY_OBJECT_ID_GENERATOR = "discovery_object_id_generator" CONF_DISCOVERY_PREFIX = "discovery_prefix" CONF_DISCOVERY_RETAIN = "discovery_retain" CONF_DISCOVERY_UNIQUE_ID_GENERATOR = "discovery_unique_id_generator" From 88d72f8c9a8694213b8206bd1fed316c3f481709 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 9 Feb 2022 08:04:44 +1300 Subject: [PATCH 0202/1729] Fix files CI after merging (#3175) --- esphome/components/max9611/max9611.cpp | 2 +- esphome/components/modbus_controller/select/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/max9611/max9611.cpp b/esphome/components/max9611/max9611.cpp index 418a577948..70a94c4ad9 100644 --- a/esphome/components/max9611/max9611.cpp +++ b/esphome/components/max9611/max9611.cpp @@ -13,7 +13,7 @@ template inline T signextend(const T x) { return s.x = x; } // Map the gain register to in uV/LSB -const float gain_to_lsb(MAX9611Multiplexer gain) { +float gain_to_lsb(MAX9611Multiplexer gain) { float lsb = 0.0; if (gain == MAX9611_MULTIPLEXER_CSA_GAIN1) { lsb = 107.50; diff --git a/esphome/components/modbus_controller/select/__init__.py b/esphome/components/modbus_controller/select/__init__.py index 9651b5fedb..7d03064fa5 100644 --- a/esphome/components/modbus_controller/select/__init__.py +++ b/esphome/components/modbus_controller/select/__init__.py @@ -35,7 +35,7 @@ def ensure_option_map(): def validator(value): cv.check_not_templatable(value) option = cv.All(cv.string_strict) - mapping = cv.All(cv.int_range(-(2 ** 63), 2 ** 63 - 1)) + mapping = cv.All(cv.int_range(-(2**63), 2**63 - 1)) options_map_schema = cv.Schema({option: mapping}) value = options_map_schema(value) From f43e04e15aeec45d1e98e72bf1a57367d0e77403 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 9 Feb 2022 11:42:00 +1300 Subject: [PATCH 0203/1729] Try fix canbus config validation (#3173) --- esphome/components/canbus/__init__.py | 32 +++++++++++++-------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/esphome/components/canbus/__init__.py b/esphome/components/canbus/__init__.py index 3a3cece579..808b31d1d2 100644 --- a/esphome/components/canbus/__init__.py +++ b/esphome/components/canbus/__init__.py @@ -14,10 +14,14 @@ CONF_BIT_RATE = "bit_rate" CONF_ON_FRAME = "on_frame" -def validate_id(id_value, id_ext): - if not id_ext: - if id_value > 0x7FF: - raise cv.Invalid("Standard IDs must be 11 Bit (0x000-0x7ff / 0-2047)") +def validate_id(config): + if CONF_CAN_ID in config: + id_value = config[CONF_CAN_ID] + id_ext = config[CONF_USE_EXTENDED_ID] + if not id_ext: + if id_value > 0x7FF: + raise cv.Invalid("Standard IDs must be 11 Bit (0x000-0x7ff / 0-2047)") + return config def validate_raw_data(value): @@ -67,23 +71,18 @@ CANBUS_SCHEMA = cv.Schema( cv.Optional(CONF_ON_FRAME): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CanbusTrigger), - cv.GenerateID(CONF_CAN_ID): cv.int_range(min=0, max=0x1FFFFFFF), + cv.Required(CONF_CAN_ID): cv.int_range(min=0, max=0x1FFFFFFF), cv.Optional(CONF_USE_EXTENDED_ID, default=False): cv.boolean, - cv.Optional(CONF_ON_FRAME): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CanbusTrigger), - cv.GenerateID(CONF_CAN_ID): cv.int_range(min=0, max=0x1FFFFFFF), - cv.Optional(CONF_USE_EXTENDED_ID, default=False): cv.boolean, - } - ), - } + }, + validate_id, ), - } + }, ).extend(cv.COMPONENT_SCHEMA) +CANBUS_SCHEMA.add_extra(validate_id) + async def setup_canbus_core_(var, config): - validate_id(config[CONF_CAN_ID], config[CONF_USE_EXTENDED_ID]) await cg.register_component(var, config) cg.add(var.set_can_id([config[CONF_CAN_ID]])) cg.add(var.set_use_extended_id([config[CONF_USE_EXTENDED_ID]])) @@ -92,7 +91,6 @@ async def setup_canbus_core_(var, config): for conf in config.get(CONF_ON_FRAME, []): can_id = conf[CONF_CAN_ID] ext_id = conf[CONF_USE_EXTENDED_ID] - validate_id(can_id, ext_id) trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var, can_id, ext_id) await cg.register_component(trigger, conf) await automation.build_automation( @@ -117,11 +115,11 @@ async def register_canbus(var, config): cv.Optional(CONF_USE_EXTENDED_ID, default=False): cv.boolean, cv.Required(CONF_DATA): cv.templatable(validate_raw_data), }, + validate_id, key=CONF_DATA, ), ) async def canbus_action_to_code(config, action_id, template_arg, args): - validate_id(config[CONF_CAN_ID], config[CONF_USE_EXTENDED_ID]) var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_CANBUS_ID]) From 64f798d4b2e92a8f22b35606ea1c9cca10e13c76 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Feb 2022 11:42:44 +1300 Subject: [PATCH 0204/1729] Bump pytest from 6.2.5 to 7.0.0 (#3163) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 45d8c02099..62d1ff31af 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -4,7 +4,7 @@ black==22.1.0 pre-commit # Unit tests -pytest==6.2.5 +pytest==7.0.0 pytest-cov==3.0.0 pytest-mock==3.7.0 pytest-asyncio==0.17.2 From 9ad84150aa35673fbc63c0168d0601d4967476ff Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Tue, 8 Feb 2022 09:21:52 +0100 Subject: [PATCH 0205/1729] Enable mDNS during OTA safe mode (#3146) --- esphome/components/mdns/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/mdns/__init__.py b/esphome/components/mdns/__init__.py index b95469d9da..b5be153d5a 100644 --- a/esphome/components/mdns/__init__.py +++ b/esphome/components/mdns/__init__.py @@ -1,7 +1,7 @@ from esphome.const import CONF_ID import esphome.codegen as cg import esphome.config_validation as cv -from esphome.core import CORE +from esphome.core import CORE, coroutine_with_priority CODEOWNERS = ["@esphome/core"] DEPENDENCIES = ["network"] @@ -29,6 +29,7 @@ CONFIG_SCHEMA = cv.All( ) +@coroutine_with_priority(55.0) async def to_code(config): if CORE.using_arduino: if CORE.is_esp32: From 2b5dce52327094cda8d209e7b0cc0ec0ce799df3 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 9 Feb 2022 11:42:00 +1300 Subject: [PATCH 0206/1729] Try fix canbus config validation (#3173) --- esphome/components/canbus/__init__.py | 32 +++++++++++++-------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/esphome/components/canbus/__init__.py b/esphome/components/canbus/__init__.py index 3a3cece579..808b31d1d2 100644 --- a/esphome/components/canbus/__init__.py +++ b/esphome/components/canbus/__init__.py @@ -14,10 +14,14 @@ CONF_BIT_RATE = "bit_rate" CONF_ON_FRAME = "on_frame" -def validate_id(id_value, id_ext): - if not id_ext: - if id_value > 0x7FF: - raise cv.Invalid("Standard IDs must be 11 Bit (0x000-0x7ff / 0-2047)") +def validate_id(config): + if CONF_CAN_ID in config: + id_value = config[CONF_CAN_ID] + id_ext = config[CONF_USE_EXTENDED_ID] + if not id_ext: + if id_value > 0x7FF: + raise cv.Invalid("Standard IDs must be 11 Bit (0x000-0x7ff / 0-2047)") + return config def validate_raw_data(value): @@ -67,23 +71,18 @@ CANBUS_SCHEMA = cv.Schema( cv.Optional(CONF_ON_FRAME): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CanbusTrigger), - cv.GenerateID(CONF_CAN_ID): cv.int_range(min=0, max=0x1FFFFFFF), + cv.Required(CONF_CAN_ID): cv.int_range(min=0, max=0x1FFFFFFF), cv.Optional(CONF_USE_EXTENDED_ID, default=False): cv.boolean, - cv.Optional(CONF_ON_FRAME): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CanbusTrigger), - cv.GenerateID(CONF_CAN_ID): cv.int_range(min=0, max=0x1FFFFFFF), - cv.Optional(CONF_USE_EXTENDED_ID, default=False): cv.boolean, - } - ), - } + }, + validate_id, ), - } + }, ).extend(cv.COMPONENT_SCHEMA) +CANBUS_SCHEMA.add_extra(validate_id) + async def setup_canbus_core_(var, config): - validate_id(config[CONF_CAN_ID], config[CONF_USE_EXTENDED_ID]) await cg.register_component(var, config) cg.add(var.set_can_id([config[CONF_CAN_ID]])) cg.add(var.set_use_extended_id([config[CONF_USE_EXTENDED_ID]])) @@ -92,7 +91,6 @@ async def setup_canbus_core_(var, config): for conf in config.get(CONF_ON_FRAME, []): can_id = conf[CONF_CAN_ID] ext_id = conf[CONF_USE_EXTENDED_ID] - validate_id(can_id, ext_id) trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var, can_id, ext_id) await cg.register_component(trigger, conf) await automation.build_automation( @@ -117,11 +115,11 @@ async def register_canbus(var, config): cv.Optional(CONF_USE_EXTENDED_ID, default=False): cv.boolean, cv.Required(CONF_DATA): cv.templatable(validate_raw_data), }, + validate_id, key=CONF_DATA, ), ) async def canbus_action_to_code(config, action_id, template_arg, args): - validate_id(config[CONF_CAN_ID], config[CONF_USE_EXTENDED_ID]) var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_CANBUS_ID]) From 53d3718028c9f3257f12a95d8a154221bda3b183 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 9 Feb 2022 11:54:41 +1300 Subject: [PATCH 0207/1729] Bump version to 2022.1.4 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 243b3d84b1..566f79a390 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2022.1.3" +__version__ = "2022.1.4" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From 3bf042dce943e6c6549f62d8d77636ff94e8fbe1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Feb 2022 12:57:12 +1300 Subject: [PATCH 0208/1729] Bump pytest-asyncio from 0.17.2 to 0.18.0 (#3168) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 62d1ff31af..1e5a5c2ebc 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -7,6 +7,6 @@ pre-commit pytest==7.0.0 pytest-cov==3.0.0 pytest-mock==3.7.0 -pytest-asyncio==0.17.2 +pytest-asyncio==0.18.0 asyncmock==0.4.2 hypothesis==5.49.0 From e7dd6c52ac9ef348cce2fa1e6362fb6c7535cfd3 Mon Sep 17 00:00:00 2001 From: Borys Pierov Date: Wed, 9 Feb 2022 05:29:32 -0500 Subject: [PATCH 0209/1729] Allow to set manufacturer data for BLEAdvertising (#3179) --- esphome/components/esp32_ble/ble_advertising.cpp | 5 +++++ esphome/components/esp32_ble/ble_advertising.h | 1 + 2 files changed, 6 insertions(+) diff --git a/esphome/components/esp32_ble/ble_advertising.cpp b/esphome/components/esp32_ble/ble_advertising.cpp index 31b1f4c383..2083bf5f08 100644 --- a/esphome/components/esp32_ble/ble_advertising.cpp +++ b/esphome/components/esp32_ble/ble_advertising.cpp @@ -42,6 +42,11 @@ void BLEAdvertising::remove_service_uuid(ESPBTUUID uuid) { this->advertising_uuids_.end()); } +void BLEAdvertising::set_manufacturer_data(uint8_t *data, uint16_t size) { + this->advertising_data_.p_manufacturer_data = data; + this->advertising_data_.manufacturer_len = size; +} + void BLEAdvertising::start() { int num_services = this->advertising_uuids_.size(); if (num_services == 0) { diff --git a/esphome/components/esp32_ble/ble_advertising.h b/esphome/components/esp32_ble/ble_advertising.h index 01e2ba1295..079bd6c14c 100644 --- a/esphome/components/esp32_ble/ble_advertising.h +++ b/esphome/components/esp32_ble/ble_advertising.h @@ -20,6 +20,7 @@ class BLEAdvertising { void remove_service_uuid(ESPBTUUID uuid); void set_scan_response(bool scan_response) { this->scan_response_ = scan_response; } void set_min_preferred_interval(uint16_t interval) { this->advertising_data_.min_interval = interval; } + void set_manufacturer_data(uint8_t *data, uint16_t size); void start(); void stop(); From 5c220651355ca63e4040d80c0aef196093945c4c Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 9 Feb 2022 23:46:20 +1300 Subject: [PATCH 0210/1729] Change most references from hassio to ha-addon (#3178) --- .github/workflows/release.yml | 4 +-- docker/Dockerfile | 2 +- .../etc/cont-init.d/10-requirements.sh | 0 .../etc/cont-init.d/20-nginx.sh | 0 .../etc/cont-init.d/30-dirs.sh | 0 .../etc/nginx/includes/mime.types | 0 .../etc/nginx/includes/proxy_params.conf | 0 .../etc/nginx/includes/server_params.conf | 0 .../etc/nginx/includes/ssl_params.conf | 0 .../etc/nginx/nginx.conf | 0 .../etc/nginx/servers/direct-ssl.disabled | 2 +- .../etc/nginx/servers/direct.disabled | 2 +- .../etc/nginx/servers/ingress.conf | 4 +-- .../etc/services.d/esphome/finish | 0 .../etc/services.d/esphome/run | 4 +-- .../etc/services.d/nginx/finish | 0 .../etc/services.d/nginx/run | 0 esphome/__main__.py | 2 +- esphome/core/__init__.py | 6 ++-- esphome/dashboard/dashboard.py | 30 +++++++++---------- esphome/helpers.py | 4 +-- requirements.txt | 2 +- script/ci-custom.py | 8 ++++- tests/unit_tests/test_helpers.py | 8 ++--- 24 files changed, 42 insertions(+), 36 deletions(-) rename docker/{hassio-rootfs => ha-addon-rootfs}/etc/cont-init.d/10-requirements.sh (100%) rename docker/{hassio-rootfs => ha-addon-rootfs}/etc/cont-init.d/20-nginx.sh (100%) rename docker/{hassio-rootfs => ha-addon-rootfs}/etc/cont-init.d/30-dirs.sh (100%) rename docker/{hassio-rootfs => ha-addon-rootfs}/etc/nginx/includes/mime.types (100%) rename docker/{hassio-rootfs => ha-addon-rootfs}/etc/nginx/includes/proxy_params.conf (100%) rename docker/{hassio-rootfs => ha-addon-rootfs}/etc/nginx/includes/server_params.conf (100%) rename docker/{hassio-rootfs => ha-addon-rootfs}/etc/nginx/includes/ssl_params.conf (100%) rename docker/{hassio-rootfs => ha-addon-rootfs}/etc/nginx/nginx.conf (100%) rename docker/{hassio-rootfs => ha-addon-rootfs}/etc/nginx/servers/direct-ssl.disabled (93%) rename docker/{hassio-rootfs => ha-addon-rootfs}/etc/nginx/servers/direct.disabled (85%) rename docker/{hassio-rootfs => ha-addon-rootfs}/etc/nginx/servers/ingress.conf (79%) rename docker/{hassio-rootfs => ha-addon-rootfs}/etc/services.d/esphome/finish (100%) rename docker/{hassio-rootfs => ha-addon-rootfs}/etc/services.d/esphome/run (96%) rename docker/{hassio-rootfs => ha-addon-rootfs}/etc/services.d/nginx/finish (100%) rename docker/{hassio-rootfs => ha-addon-rootfs}/etc/services.d/nginx/run (100%) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d6895becc0..02a55494e9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -137,7 +137,7 @@ jobs: --build-type "${{ matrix.build_type }}" \ manifest - deploy-hassio-repo: + deploy-ha-addon-repo: if: github.repository == 'esphome/esphome' && github.event_name == 'release' runs-on: ubuntu-latest needs: [deploy-docker] @@ -150,5 +150,5 @@ jobs: -u ":$TOKEN" \ -X POST \ -H "Accept: application/vnd.github.v3+json" \ - https://api.github.com/repos/esphome/hassio/actions/workflows/bump-version.yml/dispatches \ + https://api.github.com/repos/esphome/home-assistant-addon/actions/workflows/bump-version.yml/dispatches \ -d "{\"ref\":\"main\",\"inputs\":{\"version\":\"$TAG\"}}" diff --git a/docker/Dockerfile b/docker/Dockerfile index 0eebbe827e..5d9decbf1b 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -102,7 +102,7 @@ RUN \ ARG BUILD_VERSION=dev # Copy root filesystem -COPY docker/hassio-rootfs/ / +COPY docker/ha-addon-rootfs/ / # First install requirements to leverage caching when requirements don't change COPY requirements.txt requirements_optional.txt docker/platformio_install_deps.py platformio.ini / diff --git a/docker/hassio-rootfs/etc/cont-init.d/10-requirements.sh b/docker/ha-addon-rootfs/etc/cont-init.d/10-requirements.sh similarity index 100% rename from docker/hassio-rootfs/etc/cont-init.d/10-requirements.sh rename to docker/ha-addon-rootfs/etc/cont-init.d/10-requirements.sh diff --git a/docker/hassio-rootfs/etc/cont-init.d/20-nginx.sh b/docker/ha-addon-rootfs/etc/cont-init.d/20-nginx.sh similarity index 100% rename from docker/hassio-rootfs/etc/cont-init.d/20-nginx.sh rename to docker/ha-addon-rootfs/etc/cont-init.d/20-nginx.sh diff --git a/docker/hassio-rootfs/etc/cont-init.d/30-dirs.sh b/docker/ha-addon-rootfs/etc/cont-init.d/30-dirs.sh similarity index 100% rename from docker/hassio-rootfs/etc/cont-init.d/30-dirs.sh rename to docker/ha-addon-rootfs/etc/cont-init.d/30-dirs.sh diff --git a/docker/hassio-rootfs/etc/nginx/includes/mime.types b/docker/ha-addon-rootfs/etc/nginx/includes/mime.types similarity index 100% rename from docker/hassio-rootfs/etc/nginx/includes/mime.types rename to docker/ha-addon-rootfs/etc/nginx/includes/mime.types diff --git a/docker/hassio-rootfs/etc/nginx/includes/proxy_params.conf b/docker/ha-addon-rootfs/etc/nginx/includes/proxy_params.conf similarity index 100% rename from docker/hassio-rootfs/etc/nginx/includes/proxy_params.conf rename to docker/ha-addon-rootfs/etc/nginx/includes/proxy_params.conf diff --git a/docker/hassio-rootfs/etc/nginx/includes/server_params.conf b/docker/ha-addon-rootfs/etc/nginx/includes/server_params.conf similarity index 100% rename from docker/hassio-rootfs/etc/nginx/includes/server_params.conf rename to docker/ha-addon-rootfs/etc/nginx/includes/server_params.conf diff --git a/docker/hassio-rootfs/etc/nginx/includes/ssl_params.conf b/docker/ha-addon-rootfs/etc/nginx/includes/ssl_params.conf similarity index 100% rename from docker/hassio-rootfs/etc/nginx/includes/ssl_params.conf rename to docker/ha-addon-rootfs/etc/nginx/includes/ssl_params.conf diff --git a/docker/hassio-rootfs/etc/nginx/nginx.conf b/docker/ha-addon-rootfs/etc/nginx/nginx.conf similarity index 100% rename from docker/hassio-rootfs/etc/nginx/nginx.conf rename to docker/ha-addon-rootfs/etc/nginx/nginx.conf diff --git a/docker/hassio-rootfs/etc/nginx/servers/direct-ssl.disabled b/docker/ha-addon-rootfs/etc/nginx/servers/direct-ssl.disabled similarity index 93% rename from docker/hassio-rootfs/etc/nginx/servers/direct-ssl.disabled rename to docker/ha-addon-rootfs/etc/nginx/servers/direct-ssl.disabled index c2b8d6567d..4ebc435dbb 100644 --- a/docker/hassio-rootfs/etc/nginx/servers/direct-ssl.disabled +++ b/docker/ha-addon-rootfs/etc/nginx/servers/direct-ssl.disabled @@ -10,7 +10,7 @@ server { ssl_certificate_key /ssl/%%keyfile%%; # Clear Hass.io Ingress header - proxy_set_header X-Hassio-Ingress ""; + proxy_set_header X-HA-Ingress ""; # Redirect http requests to https on the same port. # https://rageagainstshell.com/2016/11/redirect-http-to-https-on-the-same-port-in-nginx/ diff --git a/docker/hassio-rootfs/etc/nginx/servers/direct.disabled b/docker/ha-addon-rootfs/etc/nginx/servers/direct.disabled similarity index 85% rename from docker/hassio-rootfs/etc/nginx/servers/direct.disabled rename to docker/ha-addon-rootfs/etc/nginx/servers/direct.disabled index 51f57cab88..80300fc6aa 100644 --- a/docker/hassio-rootfs/etc/nginx/servers/direct.disabled +++ b/docker/ha-addon-rootfs/etc/nginx/servers/direct.disabled @@ -4,7 +4,7 @@ server { include /etc/nginx/includes/server_params.conf; include /etc/nginx/includes/proxy_params.conf; # Clear Hass.io Ingress header - proxy_set_header X-Hassio-Ingress ""; + proxy_set_header X-HA-Ingress ""; location / { proxy_pass http://esphome; diff --git a/docker/hassio-rootfs/etc/nginx/servers/ingress.conf b/docker/ha-addon-rootfs/etc/nginx/servers/ingress.conf similarity index 79% rename from docker/hassio-rootfs/etc/nginx/servers/ingress.conf rename to docker/ha-addon-rootfs/etc/nginx/servers/ingress.conf index 3a800d97e7..9d0d2d3e66 100644 --- a/docker/hassio-rootfs/etc/nginx/servers/ingress.conf +++ b/docker/ha-addon-rootfs/etc/nginx/servers/ingress.conf @@ -3,8 +3,8 @@ server { include /etc/nginx/includes/server_params.conf; include /etc/nginx/includes/proxy_params.conf; - # Set Hass.io Ingress header - proxy_set_header X-Hassio-Ingress "YES"; + # Set Home Assistant Ingress header + proxy_set_header X-HA-Ingress "YES"; location / { # Only allow from Hass.io supervisor diff --git a/docker/hassio-rootfs/etc/services.d/esphome/finish b/docker/ha-addon-rootfs/etc/services.d/esphome/finish similarity index 100% rename from docker/hassio-rootfs/etc/services.d/esphome/finish rename to docker/ha-addon-rootfs/etc/services.d/esphome/finish diff --git a/docker/hassio-rootfs/etc/services.d/esphome/run b/docker/ha-addon-rootfs/etc/services.d/esphome/run similarity index 96% rename from docker/hassio-rootfs/etc/services.d/esphome/run rename to docker/ha-addon-rootfs/etc/services.d/esphome/run index a0f20d63d6..2c821bf4ee 100755 --- a/docker/hassio-rootfs/etc/services.d/esphome/run +++ b/docker/ha-addon-rootfs/etc/services.d/esphome/run @@ -4,7 +4,7 @@ # Runs the ESPHome dashboard # ============================================================================== -export ESPHOME_IS_HASSIO=true +export ESPHOME_IS_HA_ADDON=true if bashio::config.true 'leave_front_door_open'; then export DISABLE_HA_AUTHENTICATION=true @@ -32,4 +32,4 @@ export PLATFORMIO_CACHE_DIR="${pio_cache_base}/cache" export PLATFORMIO_GLOBALLIB_DIR=/piolibs bashio::log.info "Starting ESPHome dashboard..." -exec esphome dashboard /config/esphome --socket /var/run/esphome.sock --hassio +exec esphome dashboard /config/esphome --socket /var/run/esphome.sock --ha-addon diff --git a/docker/hassio-rootfs/etc/services.d/nginx/finish b/docker/ha-addon-rootfs/etc/services.d/nginx/finish similarity index 100% rename from docker/hassio-rootfs/etc/services.d/nginx/finish rename to docker/ha-addon-rootfs/etc/services.d/nginx/finish diff --git a/docker/hassio-rootfs/etc/services.d/nginx/run b/docker/ha-addon-rootfs/etc/services.d/nginx/run similarity index 100% rename from docker/hassio-rootfs/etc/services.d/nginx/run rename to docker/ha-addon-rootfs/etc/services.d/nginx/run diff --git a/esphome/__main__.py b/esphome/__main__.py index 6f57791480..a64f096d54 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -661,7 +661,7 @@ def parse_args(argv): "--open-ui", help="Open the dashboard UI in a browser.", action="store_true" ) parser_dashboard.add_argument( - "--hassio", help=argparse.SUPPRESS, action="store_true" + "--ha-addon", help=argparse.SUPPRESS, action="store_true" ) parser_dashboard.add_argument( "--socket", help="Make the dashboard serve under a unix socket", type=str diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index addecf1326..a4867915bb 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -20,7 +20,7 @@ from esphome.coroutine import FakeEventLoop as _FakeEventLoop # pylint: disable=unused-import from esphome.coroutine import coroutine, coroutine_with_priority # noqa -from esphome.helpers import ensure_unique_string, is_hassio +from esphome.helpers import ensure_unique_string, is_ha_addon from esphome.util import OrderedDict if TYPE_CHECKING: @@ -568,12 +568,12 @@ class EsphomeCore: return self.relative_build_path("src", *path) def relative_pioenvs_path(self, *path): - if is_hassio(): + if is_ha_addon(): return os.path.join("/data", self.name, ".pioenvs", *path) return self.relative_build_path(".pioenvs", *path) def relative_piolibdeps_path(self, *path): - if is_hassio(): + if is_ha_addon(): return os.path.join("/data", self.name, ".piolibdeps", *path) return self.relative_build_path(".piolibdeps", *path) diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index c68d037fe6..0ace97f10e 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -55,13 +55,13 @@ class DashboardSettings: self.password_hash = "" self.username = "" self.using_password = False - self.on_hassio = False + self.on_ha_addon = False self.cookie_secret = None def parse_args(self, args): - self.on_hassio = args.hassio + self.on_ha_addon = args.ha_addon password = args.password or os.getenv("PASSWORD", "") - if not self.on_hassio: + if not self.on_ha_addon: self.username = args.username or os.getenv("USERNAME", "") self.using_password = bool(password) if self.using_password: @@ -77,14 +77,14 @@ class DashboardSettings: return get_bool_env("ESPHOME_DASHBOARD_USE_PING") @property - def using_hassio_auth(self): - if not self.on_hassio: + def using_ha_addon_auth(self): + if not self.on_ha_addon: return False return not get_bool_env("DISABLE_HA_AUTHENTICATION") @property def using_auth(self): - return self.using_password or self.using_hassio_auth + return self.using_password or self.using_ha_addon_auth def check_password(self, username, password): if not self.using_auth: @@ -138,10 +138,10 @@ def authenticated(func): def is_authenticated(request_handler): - if settings.on_hassio: + if settings.on_ha_addon: # Handle ingress - disable auth on ingress port - # X-Hassio-Ingress is automatically stripped on the non-ingress server in nginx - header = request_handler.request.headers.get("X-Hassio-Ingress", "NO") + # X-HA-Ingress is automatically stripped on the non-ingress server in nginx + header = request_handler.request.headers.get("X-HA-Ingress", "NO") if str(header) == "YES": return True if settings.using_auth: @@ -792,23 +792,23 @@ class LoginHandler(BaseHandler): self.render( "login.template.html", error=error, - hassio=settings.using_hassio_auth, + ha_addon=settings.using_ha_addon_auth, has_username=bool(settings.username), **template_args(), ) - def post_hassio_login(self): + def post_ha_addon_login(self): import requests headers = { - "X-HASSIO-KEY": os.getenv("HASSIO_TOKEN"), + "Authentication": f"Bearer {os.getenv('SUPERVISOR_TOKEN')}", } data = { "username": self.get_argument("username", ""), "password": self.get_argument("password", ""), } try: - req = requests.post("http://hassio/auth", headers=headers, data=data) + req = requests.post("http://supervisor/auth", headers=headers, data=data) if req.status_code == 200: self.set_secure_cookie("authenticated", cookie_authenticated_yes) self.redirect("/") @@ -835,8 +835,8 @@ class LoginHandler(BaseHandler): self.render_login_page(error=error_str) def post(self): - if settings.using_hassio_auth: - self.post_hassio_login() + if settings.using_ha_addon_auth: + self.post_ha_addon_login() else: self.post_native_login() diff --git a/esphome/helpers.py b/esphome/helpers.py index 289abe5459..76158a1bfd 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -144,8 +144,8 @@ def get_bool_env(var, default=False): return bool(os.getenv(var, default)) -def is_hassio(): - return get_bool_env("ESPHOME_IS_HASSIO") +def is_ha_addon(): + return get_bool_env("ESPHOME_IS_HA_ADDON") def walk_files(path): diff --git a/requirements.txt b/requirements.txt index 2cbebb11f5..a33461b79b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ pyserial==3.5 platformio==5.2.4 # When updating platformio, also update Dockerfile esptool==3.2 click==8.0.3 -esphome-dashboard==20220116.0 +esphome-dashboard==20220209.0 aioesphomeapi==10.8.1 zeroconf==0.37.0 diff --git a/script/ci-custom.py b/script/ci-custom.py index 7bbaaf1c79..d1efa22d85 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -222,7 +222,13 @@ def lint_ext_check(fname): @lint_file_check( - exclude=["**.sh", "docker/hassio-rootfs/**", "docker/*.py", "script/*", "setup.py"] + exclude=[ + "**.sh", + "docker/ha-addon-rootfs/**", + "docker/*.py", + "script/*", + "setup.py", + ] ) def lint_executable_bit(fname): ex = EXECUTABLE_BIT[fname] diff --git a/tests/unit_tests/test_helpers.py b/tests/unit_tests/test_helpers.py index 00a6b08133..f883b8b44f 100644 --- a/tests/unit_tests/test_helpers.py +++ b/tests/unit_tests/test_helpers.py @@ -124,13 +124,13 @@ def test_get_bool_env(monkeypatch, var, value, default, expected): @pytest.mark.parametrize("value, expected", ((None, False), ("Yes", True))) -def test_is_hassio(monkeypatch, value, expected): +def test_is_ha_addon(monkeypatch, value, expected): if value is None: - monkeypatch.delenv("ESPHOME_IS_HASSIO", raising=False) + monkeypatch.delenv("ESPHOME_IS_HA_ADDON", raising=False) else: - monkeypatch.setenv("ESPHOME_IS_HASSIO", value) + monkeypatch.setenv("ESPHOME_IS_HA_ADDON", value) - actual = helpers.is_hassio() + actual = helpers.is_ha_addon() assert actual == expected From 71a438e2cb00e2f6b9fac735800c29b2012010b2 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 9 Feb 2022 23:47:36 +1300 Subject: [PATCH 0211/1729] Bump version to 2022.2.0b1 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index dcd6fea31f..d3b78ca44a 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2022.2.0-dev" +__version__ = "2022.2.0b1" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From b48490badce8c14d14dbc3f0664c463c8b01974c Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 9 Feb 2022 23:47:36 +1300 Subject: [PATCH 0212/1729] Bump version to 2022.3.0-dev --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index dcd6fea31f..1494920385 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2022.2.0-dev" +__version__ = "2022.3.0-dev" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From 35e6a13cd1230519464aa80f42e2ac951335f3cb Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Wed, 9 Feb 2022 14:56:42 +0100 Subject: [PATCH 0213/1729] Remove unused obj attribute from AssignmentExpression (#3145) --- esphome/cpp_generator.py | 23 +++++++++++------------ tests/unit_tests/test_cpp_generator.py | 8 ++++---- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index 82deec70ec..4ff16ba703 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -62,14 +62,13 @@ class RawExpression(Expression): class AssignmentExpression(Expression): - __slots__ = ("type", "modifier", "name", "rhs", "obj") + __slots__ = ("type", "modifier", "name", "rhs") - def __init__(self, type_, modifier, name, rhs, obj): + def __init__(self, type_, modifier, name, rhs): self.type = type_ self.modifier = modifier self.name = name self.rhs = safe_exp(rhs) - self.obj = obj def __str__(self): if self.type is None: @@ -427,8 +426,8 @@ class LineComment(Statement): class ProgmemAssignmentExpression(AssignmentExpression): __slots__ = () - def __init__(self, type_, name, rhs, obj): - super().__init__(type_, "", name, rhs, obj) + def __init__(self, type_, name, rhs): + super().__init__(type_, "", name, rhs) def __str__(self): return f"static const {self.type} {self.name}[] PROGMEM = {self.rhs}" @@ -437,8 +436,8 @@ class ProgmemAssignmentExpression(AssignmentExpression): class StaticConstAssignmentExpression(AssignmentExpression): __slots__ = () - def __init__(self, type_, name, rhs, obj): - super().__init__(type_, "", name, rhs, obj) + def __init__(self, type_, name, rhs): + super().__init__(type_, "", name, rhs) def __str__(self): return f"static const {self.type} {self.name}[] = {self.rhs}" @@ -447,7 +446,7 @@ class StaticConstAssignmentExpression(AssignmentExpression): def progmem_array(id_, rhs) -> "MockObj": rhs = safe_exp(rhs) obj = MockObj(id_, ".") - assignment = ProgmemAssignmentExpression(id_.type, id_, rhs, obj) + assignment = ProgmemAssignmentExpression(id_.type, id_, rhs) CORE.add(assignment) CORE.register_variable(id_, obj) return obj @@ -456,7 +455,7 @@ def progmem_array(id_, rhs) -> "MockObj": def static_const_array(id_, rhs) -> "MockObj": rhs = safe_exp(rhs) obj = MockObj(id_, ".") - assignment = StaticConstAssignmentExpression(id_.type, id_, rhs, obj) + assignment = StaticConstAssignmentExpression(id_.type, id_, rhs) CORE.add(assignment) CORE.register_variable(id_, obj) return obj @@ -484,7 +483,7 @@ def variable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj": obj = MockObj(id_, ".") if type_ is not None: id_.type = type_ - assignment = AssignmentExpression(id_.type, "", id_, rhs, obj) + assignment = AssignmentExpression(id_.type, "", id_, rhs) CORE.add(assignment) CORE.register_variable(id_, obj) return obj @@ -507,7 +506,7 @@ def new_variable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj id_.type = type_ decl = VariableDeclarationExpression(id_.type, "", id_) CORE.add_global(decl) - assignment = AssignmentExpression(None, "", id_, rhs, obj) + assignment = AssignmentExpression(None, "", id_, rhs) CORE.add(assignment) CORE.register_variable(id_, obj) return obj @@ -529,7 +528,7 @@ def Pvariable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj": id_.type = type_ decl = VariableDeclarationExpression(id_.type, "*", id_) CORE.add_global(decl) - assignment = AssignmentExpression(None, None, id_, rhs, obj) + assignment = AssignmentExpression(None, None, id_, rhs) CORE.add(assignment) CORE.register_variable(id_, obj) return obj diff --git a/tests/unit_tests/test_cpp_generator.py b/tests/unit_tests/test_cpp_generator.py index 5a8087ffa9..331c500c04 100644 --- a/tests/unit_tests/test_cpp_generator.py +++ b/tests/unit_tests/test_cpp_generator.py @@ -13,9 +13,9 @@ class TestExpressions: "target, expected", ( (cg.RawExpression("foo && bar"), "foo && bar"), - (cg.AssignmentExpression(None, None, "foo", "bar", None), 'foo = "bar"'), - (cg.AssignmentExpression(ct.float_, "*", "foo", 1, None), "float *foo = 1"), - (cg.AssignmentExpression(ct.float_, "", "foo", 1, None), "float foo = 1"), + (cg.AssignmentExpression(None, None, "foo", "bar"), 'foo = "bar"'), + (cg.AssignmentExpression(ct.float_, "*", "foo", 1), "float *foo = 1"), + (cg.AssignmentExpression(ct.float_, "", "foo", 1), "float foo = 1"), (cg.VariableDeclarationExpression(ct.int32, "*", "foo"), "int32_t *foo"), (cg.VariableDeclarationExpression(ct.int32, "", "foo"), "int32_t foo"), (cg.ParameterExpression(ct.std_string, "foo"), "std::string foo"), @@ -274,7 +274,7 @@ class TestStatements: "// Help help\n// I'm being repressed", ), ( - cg.ProgmemAssignmentExpression(ct.uint16, "foo", "bar", None), + cg.ProgmemAssignmentExpression(ct.uint16, "foo", "bar"), 'static const uint16_t foo[] PROGMEM = "bar"', ), ), From 2622e59b0b6004f754e8ec7d0ce6129ef4b0b80c Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Wed, 9 Feb 2022 14:57:00 +0100 Subject: [PATCH 0214/1729] Remove spurious Zeroconf instance from api client (#3143) --- esphome/components/api/client.py | 1 - 1 file changed, 1 deletion(-) diff --git a/esphome/components/api/client.py b/esphome/components/api/client.py index b2920f239b..c777c3be9d 100644 --- a/esphome/components/api/client.py +++ b/esphome/components/api/client.py @@ -21,7 +21,6 @@ async def async_run_logs(config, address): if CONF_ENCRYPTION in conf: noise_psk = conf[CONF_ENCRYPTION][CONF_KEY] _LOGGER.info("Starting log output from %s using esphome API", address) - zc = zeroconf.Zeroconf() cli = APIClient( address, port, From 335512e2329c915563332a3b3bf83deca73d8a28 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Feb 2022 20:13:02 +0100 Subject: [PATCH 0215/1729] Bump aioesphomeapi from 10.8.1 to 10.8.2 (#3182) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a33461b79b..0c408f90b5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ platformio==5.2.4 # When updating platformio, also update Dockerfile esptool==3.2 click==8.0.3 esphome-dashboard==20220209.0 -aioesphomeapi==10.8.1 +aioesphomeapi==10.8.2 zeroconf==0.37.0 # esp-idf requires this, but doesn't bundle it by default From ad6c5ff11d8bdcb8b180554b20c914838dce2799 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 10 Feb 2022 11:12:05 +1300 Subject: [PATCH 0216/1729] Clamp rotary_encoder restored value to min and max (#3184) --- esphome/components/rotary_encoder/rotary_encoder.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/rotary_encoder/rotary_encoder.cpp b/esphome/components/rotary_encoder/rotary_encoder.cpp index c5227b41a7..c5e9cec596 100644 --- a/esphome/components/rotary_encoder/rotary_encoder.cpp +++ b/esphome/components/rotary_encoder/rotary_encoder.cpp @@ -138,6 +138,8 @@ void RotaryEncoderSensor::setup() { initial_value = 0; break; } + initial_value = clamp(initial_value, this->store_.min_value, this->store_.max_value); + this->store_.counter = initial_value; this->store_.last_read = initial_value; From 40e06c9819f17409615d4f4eec5cfe4dc9a3776d Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 10 Feb 2022 21:55:11 +1300 Subject: [PATCH 0217/1729] Raise minimum python version to 3.8 (#3176) --- .github/workflows/ci.yml | 2 +- .gitignore | 1 + .pre-commit-config.yaml | 5 + esphome/__main__.py | 6 +- esphome/components/lcd_gpio/display.py | 2 +- esphome/components/remote_base/__init__.py | 36 ++-- esphome/components/tuya/climate/__init__.py | 32 +-- esphome/components/web_server/__init__.py | 4 +- esphome/components/wifi/__init__.py | 4 +- esphome/dashboard/dashboard.py | 2 +- requirements_test.txt | 1 + script/build_jsonschema.py | 2 +- script/ci-custom.py | 4 +- script/clang-format | 61 +++--- script/clang-tidy | 217 ++++++++++++-------- script/helpers.py | 11 +- script/lint-python | 33 ++- setup.py | 10 +- 18 files changed, 253 insertions(+), 180 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9473dc87dc..c2ad769156 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -80,7 +80,7 @@ jobs: uses: actions/setup-python@v2 id: python with: - python-version: '3.7' + python-version: '3.8' - name: Cache virtualenv uses: actions/cache@v2 diff --git a/.gitignore b/.gitignore index 57b8478bd7..110437c368 100644 --- a/.gitignore +++ b/.gitignore @@ -77,6 +77,7 @@ venv/ ENV/ env.bak/ venv.bak/ +venv-*/ # mypy .mypy_cache/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e38717fe5b..9549d5cedc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,3 +25,8 @@ repos: - --branch=dev - --branch=release - --branch=beta + - repo: https://github.com/asottile/pyupgrade + rev: v2.31.0 + hooks: + - id: pyupgrade + args: [--py38-plus] diff --git a/esphome/__main__.py b/esphome/__main__.py index a64f096d54..85cf4ede85 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -778,10 +778,10 @@ def run_esphome(argv): _LOGGER.warning("Please instead use:") _LOGGER.warning(" esphome %s", " ".join(args.deprecated_argv_suggestion)) - if sys.version_info < (3, 7, 0): + if sys.version_info < (3, 8, 0): _LOGGER.error( - "You're running ESPHome with Python <3.7. ESPHome is no longer compatible " - "with this Python version. Please reinstall ESPHome with Python 3.7+" + "You're running ESPHome with Python <3.8. ESPHome is no longer compatible " + "with this Python version. Please reinstall ESPHome with Python 3.8+" ) return 1 diff --git a/esphome/components/lcd_gpio/display.py b/esphome/components/lcd_gpio/display.py index 9fb635eafa..bfef402058 100644 --- a/esphome/components/lcd_gpio/display.py +++ b/esphome/components/lcd_gpio/display.py @@ -43,7 +43,7 @@ async def to_code(config): await lcd_base.setup_lcd_display(var, config) pins_ = [] for conf in config[CONF_DATA_PINS]: - pins_.append((await cg.gpio_pin_expression(conf))) + pins_.append(await cg.gpio_pin_expression(conf)) cg.add(var.set_data_pins(*pins_)) enable = await cg.gpio_pin_expression(config[CONF_ENABLE_PIN]) cg.add(var.set_enable_pin(enable)) diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index 72a91a99dd..9bf03aaf28 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -847,7 +847,7 @@ async def rc_switch_raw_action(var, config, args): config[CONF_PROTOCOL], args, RCSwitchBase, to_exp=build_rc_switch_protocol ) cg.add(var.set_protocol(proto)) - cg.add(var.set_code((await cg.templatable(config[CONF_CODE], args, cg.std_string)))) + cg.add(var.set_code(await cg.templatable(config[CONF_CODE], args, cg.std_string))) @register_binary_sensor( @@ -868,13 +868,11 @@ async def rc_switch_type_a_action(var, config, args): config[CONF_PROTOCOL], args, RCSwitchBase, to_exp=build_rc_switch_protocol ) cg.add(var.set_protocol(proto)) + cg.add(var.set_group(await cg.templatable(config[CONF_GROUP], args, cg.std_string))) cg.add( - var.set_group((await cg.templatable(config[CONF_GROUP], args, cg.std_string))) + var.set_device(await cg.templatable(config[CONF_DEVICE], args, cg.std_string)) ) - cg.add( - var.set_device((await cg.templatable(config[CONF_DEVICE], args, cg.std_string))) - ) - cg.add(var.set_state((await cg.templatable(config[CONF_STATE], args, bool)))) + cg.add(var.set_state(await cg.templatable(config[CONF_STATE], args, bool))) @register_binary_sensor( @@ -897,13 +895,9 @@ async def rc_switch_type_b_action(var, config, args): config[CONF_PROTOCOL], args, RCSwitchBase, to_exp=build_rc_switch_protocol ) cg.add(var.set_protocol(proto)) - cg.add( - var.set_address((await cg.templatable(config[CONF_ADDRESS], args, cg.uint8))) - ) - cg.add( - var.set_channel((await cg.templatable(config[CONF_CHANNEL], args, cg.uint8))) - ) - cg.add(var.set_state((await cg.templatable(config[CONF_STATE], args, bool)))) + cg.add(var.set_address(await cg.templatable(config[CONF_ADDRESS], args, cg.uint8))) + cg.add(var.set_channel(await cg.templatable(config[CONF_CHANNEL], args, cg.uint8))) + cg.add(var.set_state(await cg.templatable(config[CONF_STATE], args, bool))) @register_binary_sensor( @@ -932,11 +926,11 @@ async def rc_switch_type_c_action(var, config, args): ) cg.add(var.set_protocol(proto)) cg.add( - var.set_family((await cg.templatable(config[CONF_FAMILY], args, cg.std_string))) + var.set_family(await cg.templatable(config[CONF_FAMILY], args, cg.std_string)) ) - cg.add(var.set_group((await cg.templatable(config[CONF_GROUP], args, cg.uint8)))) - cg.add(var.set_device((await cg.templatable(config[CONF_DEVICE], args, cg.uint8)))) - cg.add(var.set_state((await cg.templatable(config[CONF_STATE], args, bool)))) + cg.add(var.set_group(await cg.templatable(config[CONF_GROUP], args, cg.uint8))) + cg.add(var.set_device(await cg.templatable(config[CONF_DEVICE], args, cg.uint8))) + cg.add(var.set_state(await cg.templatable(config[CONF_STATE], args, bool))) @register_binary_sensor( @@ -959,11 +953,9 @@ async def rc_switch_type_d_action(var, config, args): config[CONF_PROTOCOL], args, RCSwitchBase, to_exp=build_rc_switch_protocol ) cg.add(var.set_protocol(proto)) - cg.add( - var.set_group((await cg.templatable(config[CONF_GROUP], args, cg.std_string))) - ) - cg.add(var.set_device((await cg.templatable(config[CONF_DEVICE], args, cg.uint8)))) - cg.add(var.set_state((await cg.templatable(config[CONF_STATE], args, bool)))) + cg.add(var.set_group(await cg.templatable(config[CONF_GROUP], args, cg.std_string))) + cg.add(var.set_device(await cg.templatable(config[CONF_DEVICE], args, cg.uint8))) + cg.add(var.set_state(await cg.templatable(config[CONF_STATE], args, bool))) @register_trigger("rc_switch", RCSwitchTrigger, RCSwitchData) diff --git a/esphome/components/tuya/climate/__init__.py b/esphome/components/tuya/climate/__init__.py index 275a87edd3..7d4b37ad22 100644 --- a/esphome/components/tuya/climate/__init__.py +++ b/esphome/components/tuya/climate/__init__.py @@ -36,31 +36,25 @@ def validate_temperature_multipliers(value): or CONF_TARGET_TEMPERATURE_MULTIPLIER in value ): raise cv.Invalid( - ( - f"Cannot have {CONF_TEMPERATURE_MULTIPLIER} at the same time as " - f"{CONF_CURRENT_TEMPERATURE_MULTIPLIER} and " - f"{CONF_TARGET_TEMPERATURE_MULTIPLIER}" - ) + f"Cannot have {CONF_TEMPERATURE_MULTIPLIER} at the same time as " + f"{CONF_CURRENT_TEMPERATURE_MULTIPLIER} and " + f"{CONF_TARGET_TEMPERATURE_MULTIPLIER}" ) if ( CONF_CURRENT_TEMPERATURE_MULTIPLIER in value and CONF_TARGET_TEMPERATURE_MULTIPLIER not in value ): raise cv.Invalid( - ( - f"{CONF_TARGET_TEMPERATURE_MULTIPLIER} required if using " - f"{CONF_CURRENT_TEMPERATURE_MULTIPLIER}" - ) + f"{CONF_TARGET_TEMPERATURE_MULTIPLIER} required if using " + f"{CONF_CURRENT_TEMPERATURE_MULTIPLIER}" ) if ( CONF_TARGET_TEMPERATURE_MULTIPLIER in value and CONF_CURRENT_TEMPERATURE_MULTIPLIER not in value ): raise cv.Invalid( - ( - f"{CONF_CURRENT_TEMPERATURE_MULTIPLIER} required if using " - f"{CONF_TARGET_TEMPERATURE_MULTIPLIER}" - ) + f"{CONF_CURRENT_TEMPERATURE_MULTIPLIER} required if using " + f"{CONF_TARGET_TEMPERATURE_MULTIPLIER}" ) keys = ( CONF_TEMPERATURE_MULTIPLIER, @@ -76,18 +70,14 @@ def validate_active_state_values(value): if CONF_ACTIVE_STATE_DATAPOINT not in value: if CONF_ACTIVE_STATE_COOLING_VALUE in value: raise cv.Invalid( - ( - f"{CONF_ACTIVE_STATE_DATAPOINT} required if using " - f"{CONF_ACTIVE_STATE_COOLING_VALUE}" - ) + f"{CONF_ACTIVE_STATE_DATAPOINT} required if using " + f"{CONF_ACTIVE_STATE_COOLING_VALUE}" ) else: if value[CONF_SUPPORTS_COOL] and CONF_ACTIVE_STATE_COOLING_VALUE not in value: raise cv.Invalid( - ( - f"{CONF_ACTIVE_STATE_COOLING_VALUE} required if using " - f"{CONF_ACTIVE_STATE_DATAPOINT} and device supports cooling" - ) + f"{CONF_ACTIVE_STATE_COOLING_VALUE} required if using " + f"{CONF_ACTIVE_STATE_DATAPOINT} and device supports cooling" ) return value diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index 1e5f341717..eaa20ccbb4 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -77,11 +77,11 @@ async def to_code(config): if CONF_CSS_INCLUDE in config: cg.add_define("WEBSERVER_CSS_INCLUDE") path = CORE.relative_config_path(config[CONF_CSS_INCLUDE]) - with open(file=path, mode="r", encoding="utf-8") as myfile: + with open(file=path, encoding="utf-8") as myfile: cg.add(var.set_css_include(myfile.read())) if CONF_JS_INCLUDE in config: cg.add_define("WEBSERVER_JS_INCLUDE") path = CORE.relative_config_path(config[CONF_JS_INCLUDE]) - with open(file=path, mode="r", encoding="utf-8") as myfile: + with open(file=path, encoding="utf-8") as myfile: cg.add(var.set_js_include(myfile.read())) cg.add(var.set_include_internal(config[CONF_INCLUDE_INTERNAL])) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index a24791b458..20f43cb450 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -225,11 +225,11 @@ def _validate(config): if CONF_MANUAL_IP in config: use_address = str(config[CONF_MANUAL_IP][CONF_STATIC_IP]) elif CONF_NETWORKS in config: - ips = set( + ips = { str(net[CONF_MANUAL_IP][CONF_STATIC_IP]) for net in config[CONF_NETWORKS] if CONF_MANUAL_IP in net - ) + } if len(ips) > 1: raise cv.Invalid( "Must specify use_address when using multiple static IP addresses." diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index 0ace97f10e..af68f2ae08 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -733,7 +733,7 @@ class EditRequestHandler(BaseHandler): content = "" if os.path.isfile(filename): # pylint: disable=no-value-for-parameter - with open(file=filename, mode="r", encoding="utf-8") as f: + with open(file=filename, encoding="utf-8") as f: content = f.read() self.write(content) diff --git a/requirements_test.txt b/requirements_test.txt index 1e5a5c2ebc..46713adbb9 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,7 @@ pylint==2.12.2 flake8==4.0.1 black==22.1.0 +pyupgrade==2.31.0 pre-commit # Unit tests diff --git a/script/build_jsonschema.py b/script/build_jsonschema.py index 7a3257411c..7673519916 100644 --- a/script/build_jsonschema.py +++ b/script/build_jsonschema.py @@ -170,7 +170,7 @@ def get_logger_tags(): ] for x in os.walk(CORE_COMPONENTS_PATH): for y in glob.glob(os.path.join(x[0], "*.cpp")): - with open(y, "r") as file: + with open(y) as file: data = file.read() match = pattern.search(data) if match: diff --git a/script/ci-custom.py b/script/ci-custom.py index d1efa22d85..2703e7d311 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -281,9 +281,7 @@ def highlight(s): ], ) def lint_no_defines(fname, match): - s = highlight( - "static const uint8_t {} = {};".format(match.group(1), match.group(2)) - ) + s = highlight(f"static const uint8_t {match.group(1)} = {match.group(2)};") return ( "#define macros for integer constants are not allowed, please use " "{} style instead (replace uint8_t with the appropriate " diff --git a/script/clang-format b/script/clang-format index 515df4c027..ae807262f1 100755 --- a/script/clang-format +++ b/script/clang-format @@ -17,14 +17,14 @@ def run_format(args, queue, lock, failed_files): """Takes filenames out of queue and runs clang-format on them.""" while True: path = queue.get() - invocation = ['clang-format-11'] + invocation = ["clang-format-11"] if args.inplace: - invocation.append('-i') + invocation.append("-i") else: - invocation.extend(['--dry-run', '-Werror']) + invocation.extend(["--dry-run", "-Werror"]) invocation.append(path) - proc = subprocess.run(invocation, capture_output=True, encoding='utf-8') + proc = subprocess.run(invocation, capture_output=True, encoding="utf-8") if proc.returncode != 0: with lock: print_error_for_file(path, proc.stderr) @@ -33,28 +33,36 @@ def run_format(args, queue, lock, failed_files): def progress_bar_show(value): - return value if value is not None else '' + return value if value is not None else "" def main(): colorama.init() parser = argparse.ArgumentParser() - parser.add_argument('-j', '--jobs', type=int, - default=multiprocessing.cpu_count(), - help='number of format instances to be run in parallel.') - parser.add_argument('files', nargs='*', default=[], - help='files to be processed (regex on path)') - parser.add_argument('-i', '--inplace', action='store_true', - help='reformat files in-place') - parser.add_argument('-c', '--changed', action='store_true', - help='only run on changed files') + parser.add_argument( + "-j", + "--jobs", + type=int, + default=multiprocessing.cpu_count(), + help="number of format instances to be run in parallel.", + ) + parser.add_argument( + "files", nargs="*", default=[], help="files to be processed (regex on path)" + ) + parser.add_argument( + "-i", "--inplace", action="store_true", help="reformat files in-place" + ) + parser.add_argument( + "-c", "--changed", action="store_true", help="only run on changed files" + ) args = parser.parse_args() try: - get_output('clang-format-11', '-version') + get_output("clang-format-11", "-version") except: - print(""" + print( + """ Oops. It looks like clang-format is not installed. Please check you can run "clang-format-11 -version" in your terminal and install @@ -62,16 +70,17 @@ def main(): Note you can also upload your code as a pull request on GitHub and see the CI check output to apply clang-format. - """) + """ + ) return 1 files = [] - for path in git_ls_files(['*.cpp', '*.h', '*.tcc']): + for path in git_ls_files(["*.cpp", "*.h", "*.tcc"]): files.append(os.path.relpath(path, os.getcwd())) if args.files: # Match against files specified on command-line - file_name_re = re.compile('|'.join(args.files)) + file_name_re = re.compile("|".join(args.files)) files = [p for p in files if file_name_re.search(p)] if args.changed: @@ -84,14 +93,16 @@ def main(): task_queue = queue.Queue(args.jobs) lock = threading.Lock() for _ in range(args.jobs): - t = threading.Thread(target=run_format, - args=(args, task_queue, lock, failed_files)) + t = threading.Thread( + target=run_format, args=(args, task_queue, lock, failed_files) + ) t.daemon = True t.start() # Fill the queue with files. - with click.progressbar(files, width=30, file=sys.stderr, - item_show_func=progress_bar_show) as bar: + with click.progressbar( + files, width=30, file=sys.stderr, item_show_func=progress_bar_show + ) as bar: for name in bar: task_queue.put(name) @@ -100,11 +111,11 @@ def main(): except KeyboardInterrupt: print() - print('Ctrl-C detected, goodbye.') + print("Ctrl-C detected, goodbye.") os.kill(0, 9) sys.exit(len(failed_files)) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/script/clang-tidy b/script/clang-tidy index 8a7d229887..327b593008 100755 --- a/script/clang-tidy +++ b/script/clang-tidy @@ -1,7 +1,17 @@ #!/usr/bin/env python3 -from helpers import print_error_for_file, get_output, filter_grep, \ - build_all_include, temp_header_file, git_ls_files, filter_changed, load_idedata, root_path, basepath +from helpers import ( + print_error_for_file, + get_output, + filter_grep, + build_all_include, + temp_header_file, + git_ls_files, + filter_changed, + load_idedata, + root_path, + basepath, +) import argparse import click import colorama @@ -20,67 +30,81 @@ def clang_options(idedata): cmd = [] # extract target architecture from triplet in g++ filename - triplet = os.path.basename(idedata['cxx_path'])[:-4] + triplet = os.path.basename(idedata["cxx_path"])[:-4] if triplet.startswith("xtensa-"): # clang doesn't support Xtensa (yet?), so compile in 32-bit mode and pretend we're the Xtensa compiler - cmd.append('-m32') - cmd.append('-D__XTENSA__') + cmd.append("-m32") + cmd.append("-D__XTENSA__") else: - cmd.append(f'--target={triplet}') + cmd.append(f"--target={triplet}") # set flags - cmd.extend([ - # disable built-in include directories from the host - '-nostdinc', - '-nostdinc++', - # replace pgmspace.h, as it uses GNU extensions clang doesn't support - # https://github.com/earlephilhower/newlib-xtensa/pull/18 - '-D_PGMSPACE_H_', - '-Dpgm_read_byte(s)=(*(const uint8_t *)(s))', - '-Dpgm_read_byte_near(s)=(*(const uint8_t *)(s))', - '-Dpgm_read_word(s)=(*(const uint16_t *)(s))', - '-Dpgm_read_dword(s)=(*(const uint32_t *)(s))', - '-DPROGMEM=', - '-DPGM_P=const char *', - '-DPSTR(s)=(s)', - # this next one is also needed with upstream pgmspace.h - # suppress warning about identifier naming in expansion of this macro - '-DPSTRN(s, n)=(s)', - # suppress warning about attribute cannot be applied to type - # https://github.com/esp8266/Arduino/pull/8258 - '-Ddeprecated(x)=', - # allow to condition code on the presence of clang-tidy - '-DCLANG_TIDY', - # (esp-idf) Disable this header because they use asm with registers clang-tidy doesn't know - '-D__XTENSA_API_H__', - # (esp-idf) Fix __once_callable in some libstdc++ headers - '-D_GLIBCXX_HAVE_TLS', - ]) + cmd.extend( + [ + # disable built-in include directories from the host + "-nostdinc", + "-nostdinc++", + # replace pgmspace.h, as it uses GNU extensions clang doesn't support + # https://github.com/earlephilhower/newlib-xtensa/pull/18 + "-D_PGMSPACE_H_", + "-Dpgm_read_byte(s)=(*(const uint8_t *)(s))", + "-Dpgm_read_byte_near(s)=(*(const uint8_t *)(s))", + "-Dpgm_read_word(s)=(*(const uint16_t *)(s))", + "-Dpgm_read_dword(s)=(*(const uint32_t *)(s))", + "-DPROGMEM=", + "-DPGM_P=const char *", + "-DPSTR(s)=(s)", + # this next one is also needed with upstream pgmspace.h + # suppress warning about identifier naming in expansion of this macro + "-DPSTRN(s, n)=(s)", + # suppress warning about attribute cannot be applied to type + # https://github.com/esp8266/Arduino/pull/8258 + "-Ddeprecated(x)=", + # allow to condition code on the presence of clang-tidy + "-DCLANG_TIDY", + # (esp-idf) Disable this header because they use asm with registers clang-tidy doesn't know + "-D__XTENSA_API_H__", + # (esp-idf) Fix __once_callable in some libstdc++ headers + "-D_GLIBCXX_HAVE_TLS", + ] + ) # copy compiler flags, except those clang doesn't understand. - cmd.extend(flag for flag in idedata['cxx_flags'].split(' ') - if flag not in ('-free', '-fipa-pta', '-fstrict-volatile-bitfields', - '-mlongcalls', '-mtext-section-literals', - '-mfix-esp32-psram-cache-issue', '-mfix-esp32-psram-cache-strategy=memw')) + cmd.extend( + flag + for flag in idedata["cxx_flags"].split(" ") + if flag + not in ( + "-free", + "-fipa-pta", + "-fstrict-volatile-bitfields", + "-mlongcalls", + "-mtext-section-literals", + "-mfix-esp32-psram-cache-issue", + "-mfix-esp32-psram-cache-strategy=memw", + ) + ) # defines - cmd.extend(f'-D{define}' for define in idedata['defines']) + cmd.extend(f"-D{define}" for define in idedata["defines"]) # add toolchain include directories using -isystem to suppress their errors # idedata contains include directories for all toolchains of this platform, only use those from the one in use toolchain_dir = os.path.normpath(f"{idedata['cxx_path']}/../../") - for directory in idedata['includes']['toolchain']: + for directory in idedata["includes"]["toolchain"]: if directory.startswith(toolchain_dir): - cmd.extend(['-isystem', directory]) + cmd.extend(["-isystem", directory]) # add library include directories using -isystem to suppress their errors - for directory in sorted(set(idedata['includes']['build'])): + for directory in sorted(set(idedata["includes"]["build"])): # skip our own directories, we add those later - if not directory.startswith(f"{root_path}/") or directory.startswith(f"{root_path}/.pio/"): - cmd.extend(['-isystem', directory]) + if not directory.startswith(f"{root_path}/") or directory.startswith( + f"{root_path}/.pio/" + ): + cmd.extend(["-isystem", directory]) # add the esphome include directory using -I - cmd.extend(['-I', root_path]) + cmd.extend(["-I", root_path]) return cmd @@ -88,28 +112,28 @@ def clang_options(idedata): def run_tidy(args, options, tmpdir, queue, lock, failed_files): while True: path = queue.get() - invocation = ['clang-tidy-11'] + invocation = ["clang-tidy-11"] if tmpdir is not None: - invocation.append('--export-fixes') + invocation.append("--export-fixes") # Get a temporary file. We immediately close the handle so clang-tidy can # overwrite it. - (handle, name) = tempfile.mkstemp(suffix='.yaml', dir=tmpdir) + (handle, name) = tempfile.mkstemp(suffix=".yaml", dir=tmpdir) os.close(handle) invocation.append(name) if args.quiet: - invocation.append('--quiet') + invocation.append("--quiet") if sys.stdout.isatty(): - invocation.append('--use-color') + invocation.append("--use-color") invocation.append(f"--header-filter={os.path.abspath(basepath)}/.*") invocation.append(os.path.abspath(path)) - invocation.append('--') + invocation.append("--") invocation.extend(options) - proc = subprocess.run(invocation, capture_output=True, encoding='utf-8') + proc = subprocess.run(invocation, capture_output=True, encoding="utf-8") if proc.returncode != 0: with lock: print_error_for_file(path, proc.stdout) @@ -119,43 +143,60 @@ def run_tidy(args, options, tmpdir, queue, lock, failed_files): def progress_bar_show(value): if value is None: - return '' + return "" def split_list(a, n): k, m = divmod(len(a), n) - return [a[i * k + min(i, m):(i + 1) * k + min(i + 1, m)] for i in range(n)] + return [a[i * k + min(i, m) : (i + 1) * k + min(i + 1, m)] for i in range(n)] def main(): colorama.init() parser = argparse.ArgumentParser() - parser.add_argument('-j', '--jobs', type=int, - default=multiprocessing.cpu_count(), - help='number of tidy instances to be run in parallel.') - parser.add_argument('-e', '--environment', default='esp32-arduino-tidy', - help='the PlatformIO environment to use (as defined in platformio.ini)') - parser.add_argument('files', nargs='*', default=[], - help='files to be processed (regex on path)') - parser.add_argument('--fix', action='store_true', help='apply fix-its') - parser.add_argument('-q', '--quiet', action='store_false', - help='run clang-tidy in quiet mode') - parser.add_argument('-c', '--changed', action='store_true', - help='only run on changed files') - parser.add_argument('-g', '--grep', help='only run on files containing value') - parser.add_argument('--split-num', type=int, help='split the files into X jobs.', - default=None) - parser.add_argument('--split-at', type=int, help='which split is this? starts at 1', - default=None) - parser.add_argument('--all-headers', action='store_true', - help='create a dummy file that checks all headers') + parser.add_argument( + "-j", + "--jobs", + type=int, + default=multiprocessing.cpu_count(), + help="number of tidy instances to be run in parallel.", + ) + parser.add_argument( + "-e", + "--environment", + default="esp32-arduino-tidy", + help="the PlatformIO environment to use (as defined in platformio.ini)", + ) + parser.add_argument( + "files", nargs="*", default=[], help="files to be processed (regex on path)" + ) + parser.add_argument("--fix", action="store_true", help="apply fix-its") + parser.add_argument( + "-q", "--quiet", action="store_false", help="run clang-tidy in quiet mode" + ) + parser.add_argument( + "-c", "--changed", action="store_true", help="only run on changed files" + ) + parser.add_argument("-g", "--grep", help="only run on files containing value") + parser.add_argument( + "--split-num", type=int, help="split the files into X jobs.", default=None + ) + parser.add_argument( + "--split-at", type=int, help="which split is this? starts at 1", default=None + ) + parser.add_argument( + "--all-headers", + action="store_true", + help="create a dummy file that checks all headers", + ) args = parser.parse_args() try: - get_output('clang-tidy-11', '-version') + get_output("clang-tidy-11", "-version") except: - print(""" + print( + """ Oops. It looks like clang-tidy-11 is not installed. Please check you can run "clang-tidy-11 -version" in your terminal and install @@ -163,19 +204,20 @@ def main(): Note you can also upload your code as a pull request on GitHub and see the CI check output to apply clang-tidy. - """) + """ + ) return 1 idedata = load_idedata(args.environment) options = clang_options(idedata) files = [] - for path in git_ls_files(['*.cpp']): + for path in git_ls_files(["*.cpp"]): files.append(os.path.relpath(path, os.getcwd())) if args.files: # Match against files specified on command-line - file_name_re = re.compile('|'.join(args.files)) + file_name_re = re.compile("|".join(args.files)) files = [p for p in files if file_name_re.search(p)] if args.changed: @@ -202,14 +244,17 @@ def main(): task_queue = queue.Queue(args.jobs) lock = threading.Lock() for _ in range(args.jobs): - t = threading.Thread(target=run_tidy, - args=(args, options, tmpdir, task_queue, lock, failed_files)) + t = threading.Thread( + target=run_tidy, + args=(args, options, tmpdir, task_queue, lock, failed_files), + ) t.daemon = True t.start() # Fill the queue with files. - with click.progressbar(files, width=30, file=sys.stderr, - item_show_func=progress_bar_show) as bar: + with click.progressbar( + files, width=30, file=sys.stderr, item_show_func=progress_bar_show + ) as bar: for name in bar: task_queue.put(name) @@ -218,21 +263,21 @@ def main(): except KeyboardInterrupt: print() - print('Ctrl-C detected, goodbye.') + print("Ctrl-C detected, goodbye.") if tmpdir: shutil.rmtree(tmpdir) os.kill(0, 9) if args.fix and failed_files: - print('Applying fixes ...') + print("Applying fixes ...") try: - subprocess.call(['clang-apply-replacements-11', tmpdir]) + subprocess.call(["clang-apply-replacements-11", tmpdir]) except: - print('Error applying fixes.\n', file=sys.stderr) + print("Error applying fixes.\n", file=sys.stderr) raise sys.exit(len(failed_files)) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/script/helpers.py b/script/helpers.py index abf970b8a2..c042362aeb 100644 --- a/script/helpers.py +++ b/script/helpers.py @@ -12,13 +12,16 @@ temp_header_file = os.path.join(temp_folder, "all-include.cpp") def styled(color, msg, reset=True): - prefix = ''.join(color) if isinstance(color, tuple) else color - suffix = colorama.Style.RESET_ALL if reset else '' + prefix = "".join(color) if isinstance(color, tuple) else color + suffix = colorama.Style.RESET_ALL if reset else "" return prefix + msg + suffix def print_error_for_file(file, body): - print(styled(colorama.Fore.GREEN, "### File ") + styled((colorama.Fore.GREEN, colorama.Style.BRIGHT), file)) + print( + styled(colorama.Fore.GREEN, "### File ") + + styled((colorama.Fore.GREEN, colorama.Style.BRIGHT), file) + ) print() if body is not None: print(body) @@ -100,7 +103,7 @@ def filter_changed(files): def filter_grep(files, value): matched = [] for file in files: - with open(file, "r") as handle: + with open(file) as handle: contents = handle.read() if value in contents: matched.append(file) diff --git a/script/lint-python b/script/lint-python index 8ee038a661..90b5dcd59f 100755 --- a/script/lint-python +++ b/script/lint-python @@ -1,7 +1,13 @@ #!/usr/bin/env python3 -from __future__ import print_function -from helpers import styled, print_error_for_file, get_output, get_err, git_ls_files, filter_changed +from helpers import ( + styled, + print_error_for_file, + get_output, + get_err, + git_ls_files, + filter_changed, +) import argparse import colorama import os @@ -34,6 +40,12 @@ def main(): parser.add_argument( "-c", "--changed", action="store_true", help="Only run on changed files" ) + parser.add_argument( + "-a", + "--apply", + action="store_true", + help="Apply changes to files where possible", + ) args = parser.parse_args() files = [] @@ -56,7 +68,7 @@ def main(): errors = 0 - cmd = ["black", "--verbose", "--check"] + files + cmd = ["black", "--verbose"] + ([] if args.apply else ["--check"]) + files print("Running black...") print() log = get_err(*cmd) @@ -97,6 +109,21 @@ def main(): print_error(file_, linno, msg) errors += 1 + PYUPGRADE_TARGET = "--py38-plus" + cmd = ["pyupgrade", PYUPGRADE_TARGET] + files + print() + print("Running pyupgrade...") + print() + log = get_err(*cmd) + for line in log.splitlines(): + REWRITING = "Rewriting" + if line.startswith(REWRITING): + file_ = line[len(REWRITING) + 1 :] + print_error( + file_, None, f"Please run pyupgrade {PYUPGRADE_TARGET} on this file" + ) + errors += 1 + sys.exit(errors) diff --git a/setup.py b/setup.py index 967eadd70f..941c8089ec 100755 --- a/setup.py +++ b/setup.py @@ -17,11 +17,11 @@ PROJECT_EMAIL = "esphome@nabucasa.com" PROJECT_GITHUB_USERNAME = "esphome" PROJECT_GITHUB_REPOSITORY = "esphome" -PYPI_URL = "https://pypi.python.org/pypi/{}".format(PROJECT_PACKAGE_NAME) -GITHUB_PATH = "{}/{}".format(PROJECT_GITHUB_USERNAME, PROJECT_GITHUB_REPOSITORY) -GITHUB_URL = "https://github.com/{}".format(GITHUB_PATH) +PYPI_URL = f"https://pypi.python.org/pypi/{PROJECT_PACKAGE_NAME}" +GITHUB_PATH = f"{PROJECT_GITHUB_USERNAME}/{PROJECT_GITHUB_REPOSITORY}" +GITHUB_URL = f"https://github.com/{GITHUB_PATH}" -DOWNLOAD_URL = "{}/archive/{}.zip".format(GITHUB_URL, const.__version__) +DOWNLOAD_URL = f"{GITHUB_URL}/archive/{const.__version__}.zip" here = os.path.abspath(os.path.dirname(__file__)) @@ -74,7 +74,7 @@ setup( zip_safe=False, platforms="any", test_suite="tests", - python_requires=">=3.7,<4.0", + python_requires=">=3.8,<4.0", install_requires=REQUIRES, keywords=["home", "automation"], entry_points={"console_scripts": ["esphome = esphome.__main__:main"]}, From 72e716cdf154f062e646feae514bd05e09134773 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Fri, 11 Feb 2022 09:06:00 +0100 Subject: [PATCH 0218/1729] Make generating combined binary output verbose (#3127) --- esphome/components/esp32/post_build.py.script | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/esphome/components/esp32/post_build.py.script b/esphome/components/esp32/post_build.py.script index 7feaf9e8e5..2bb1a6c3d6 100644 --- a/esphome/components/esp32/post_build.py.script +++ b/esphome/components/esp32/post_build.py.script @@ -1,13 +1,16 @@ # Source https://github.com/letscontrolit/ESPEasy/pull/3845#issuecomment-1005864664 import esptool +from SCons.Script import ARGUMENTS # pylint: disable=E0602 Import("env") # noqa def esp32_create_combined_bin(source, target, env): - print("Generating combined binary for serial flashing") + verbose = bool(int(ARGUMENTS.get("PIOVERBOSE", "0"))) + if verbose: + print("Generating combined binary for serial flashing") app_offset = 0x10000 new_file_name = env.subst("$BUILD_DIR/${PROGNAME}-factory.bin") @@ -24,18 +27,21 @@ def esp32_create_combined_bin(source, target, env): "--flash_size", flash_size, ] - print(" Offset | File") + if verbose: + print(" Offset | File") for section in sections: sect_adr, sect_file = section.split(" ", 1) - print(f" - {sect_adr} | {sect_file}") + if verbose: + print(f" - {sect_adr} | {sect_file}") cmd += [sect_adr, sect_file] - print(f" - {hex(app_offset)} | {firmware_name}") cmd += [hex(app_offset), firmware_name] - print() - print(f"Using esptool.py arguments: {' '.join(cmd)}") - print() + if verbose: + print(f" - {hex(app_offset)} | {firmware_name}") + print() + print(f"Using esptool.py arguments: {' '.join(cmd)}") + print() esptool.main(cmd) From 3a678844516b8b8ffbf98514c1b794bb2f88b1b1 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Fri, 11 Feb 2022 09:06:06 +0100 Subject: [PATCH 0219/1729] Improve dallas timing (#3181) * Improve dallas timing * Format --- .../components/dallas/dallas_component.cpp | 66 ++++--- esphome/components/dallas/esp_one_wire.cpp | 177 ++++++++++-------- esphome/core/helpers.cpp | 2 + 3 files changed, 149 insertions(+), 96 deletions(-) diff --git a/esphome/components/dallas/dallas_component.cpp b/esphome/components/dallas/dallas_component.cpp index 1eed2ebf78..56526e98bb 100644 --- a/esphome/components/dallas/dallas_component.cpp +++ b/esphome/components/dallas/dallas_component.cpp @@ -32,6 +32,11 @@ void DallasComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up DallasComponent..."); pin_->setup(); + + // clear bus with 480µs high, otherwise initial reset in search_vec() fails + pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); + delayMicroseconds(480); + one_wire_ = new ESPOneWire(pin_); // NOLINT(cppcoreguidelines-owning-memory) std::vector raw_sensors; @@ -99,20 +104,22 @@ void DallasComponent::update() { this->status_clear_warning(); bool result; - if (!this->one_wire_->reset()) { - result = false; - } else { - result = true; - this->one_wire_->skip(); - this->one_wire_->write8(DALLAS_COMMAND_START_CONVERSION); + { + InterruptLock lock; + result = this->one_wire_->reset(); } - if (!result) { ESP_LOGE(TAG, "Requesting conversion failed"); this->status_set_warning(); return; } + { + InterruptLock lock; + this->one_wire_->skip(); + this->one_wire_->write8(DALLAS_COMMAND_START_CONVERSION); + } + for (auto *sensor : this->sensors_) { this->set_timeout(sensor->get_address_name(), sensor->millis_to_wait_for_conversion(), [this, sensor] { bool res = sensor->read_scratch_pad(); @@ -152,16 +159,26 @@ const std::string &DallasTemperatureSensor::get_address_name() { } bool IRAM_ATTR DallasTemperatureSensor::read_scratch_pad() { auto *wire = this->parent_->one_wire_; - if (!wire->reset()) { - return false; + + { + InterruptLock lock; + + if (!wire->reset()) { + return false; + } } - wire->select(this->address_); - wire->write8(DALLAS_COMMAND_READ_SCRATCH_PAD); + { + InterruptLock lock; - for (unsigned char &i : this->scratch_pad_) { - i = wire->read8(); + wire->select(this->address_); + wire->write8(DALLAS_COMMAND_READ_SCRATCH_PAD); + + for (unsigned char &i : this->scratch_pad_) { + i = wire->read8(); + } } + return true; } bool DallasTemperatureSensor::setup_sensor() { @@ -200,17 +217,20 @@ bool DallasTemperatureSensor::setup_sensor() { } auto *wire = this->parent_->one_wire_; - if (wire->reset()) { - wire->select(this->address_); - wire->write8(DALLAS_COMMAND_WRITE_SCRATCH_PAD); - wire->write8(this->scratch_pad_[2]); // high alarm temp - wire->write8(this->scratch_pad_[3]); // low alarm temp - wire->write8(this->scratch_pad_[4]); // resolution - wire->reset(); + { + InterruptLock lock; + if (wire->reset()) { + wire->select(this->address_); + wire->write8(DALLAS_COMMAND_WRITE_SCRATCH_PAD); + wire->write8(this->scratch_pad_[2]); // high alarm temp + wire->write8(this->scratch_pad_[3]); // low alarm temp + wire->write8(this->scratch_pad_[4]); // resolution + wire->reset(); - // write value to EEPROM - wire->select(this->address_); - wire->write8(0x48); + // write value to EEPROM + wire->select(this->address_); + wire->write8(0x48); + } } delay(20); // allow it to finish operation diff --git a/esphome/components/dallas/esp_one_wire.cpp b/esphome/components/dallas/esp_one_wire.cpp index 6dc085a0bf..885846e5e5 100644 --- a/esphome/components/dallas/esp_one_wire.cpp +++ b/esphome/components/dallas/esp_one_wire.cpp @@ -15,8 +15,6 @@ ESPOneWire::ESPOneWire(InternalGPIOPin *pin) { pin_ = pin->to_isr(); } bool HOT IRAM_ATTR ESPOneWire::reset() { // See reset here: // https://www.maximintegrated.com/en/design/technical-documents/app-notes/1/126.html - InterruptLock lock; - // Wait for communication to clear (delay G) pin_.pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); uint8_t retries = 125; @@ -43,16 +41,18 @@ bool HOT IRAM_ATTR ESPOneWire::reset() { } void HOT IRAM_ATTR ESPOneWire::write_bit(bool bit) { - // See write 1/0 bit here: - // https://www.maximintegrated.com/en/design/technical-documents/app-notes/1/126.html - InterruptLock lock; - // drive bus low pin_.pin_mode(gpio::FLAG_OUTPUT); pin_.digital_write(false); - uint32_t delay0 = bit ? 10 : 65; - uint32_t delay1 = bit ? 55 : 5; + // from datasheet: + // write 0 low time: t_low0: min=60µs, max=120µs + // write 1 low time: t_low1: min=1µs, max=15µs + // time slot: t_slot: min=60µs, max=120µs + // recovery time: t_rec: min=1µs + // ds18b20 appears to read the bus after roughly 14µs + uint32_t delay0 = bit ? 6 : 60; + uint32_t delay1 = bit ? 54 : 5; // delay A/C delayMicroseconds(delay0); @@ -63,72 +63,100 @@ void HOT IRAM_ATTR ESPOneWire::write_bit(bool bit) { } bool HOT IRAM_ATTR ESPOneWire::read_bit() { - // See read bit here: - // https://www.maximintegrated.com/en/design/technical-documents/app-notes/1/126.html - InterruptLock lock; - - // drive bus low, delay A + // drive bus low pin_.pin_mode(gpio::FLAG_OUTPUT); pin_.digital_write(false); + + // note: for reading we'll need very accurate timing, as the + // timing for the digital_read() is tight; according to the datasheet, + // we should read at the end of 16µs starting from the bus low + // typically, the ds18b20 pulls the line high after 11µs for a logical 1 + // and 29µs for a logical 0 + + uint32_t start = micros(); + // datasheet says >1µs delayMicroseconds(3); // release bus, delay E pin_.pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); - delayMicroseconds(10); + + // Unfortunately some frameworks have different characteristics than others + // esp32 arduino appears to pull the bus low only after the digital_write(false), + // whereas on esp-idf it already happens during the pin_mode(OUTPUT) + // manually correct for this with these constants. + +#ifdef USE_ESP32_FRAMEWORK_ARDUINO + uint32_t timing_constant = 14; +#elif defined(USE_ESP32_FRAMEWORK_ESP_IDF) + uint32_t timing_constant = 12; +#else + uint32_t timing_constant = 14; +#endif + + // measure from start value directly, to get best accurate timing no matter + // how long pin_mode/delayMicroseconds took + while (micros() - start < timing_constant) + ; // sample bus to read bit from peer bool r = pin_.digital_read(); - // delay F - delayMicroseconds(53); + // read slot is at least 60µs; get as close to 60µs to spend less time with interrupts locked + uint32_t now = micros(); + if (now - start < 60) + delayMicroseconds(60 - (now - start)); + return r; } -void ESPOneWire::write8(uint8_t val) { +void IRAM_ATTR ESPOneWire::write8(uint8_t val) { for (uint8_t i = 0; i < 8; i++) { this->write_bit(bool((1u << i) & val)); } } -void ESPOneWire::write64(uint64_t val) { +void IRAM_ATTR ESPOneWire::write64(uint64_t val) { for (uint8_t i = 0; i < 64; i++) { this->write_bit(bool((1ULL << i) & val)); } } -uint8_t ESPOneWire::read8() { +uint8_t IRAM_ATTR ESPOneWire::read8() { uint8_t ret = 0; for (uint8_t i = 0; i < 8; i++) { ret |= (uint8_t(this->read_bit()) << i); } return ret; } -uint64_t ESPOneWire::read64() { +uint64_t IRAM_ATTR ESPOneWire::read64() { uint64_t ret = 0; for (uint8_t i = 0; i < 8; i++) { ret |= (uint64_t(this->read_bit()) << i); } return ret; } -void ESPOneWire::select(uint64_t address) { +void IRAM_ATTR ESPOneWire::select(uint64_t address) { this->write8(ONE_WIRE_ROM_SELECT); this->write64(address); } -void ESPOneWire::reset_search() { +void IRAM_ATTR ESPOneWire::reset_search() { this->last_discrepancy_ = 0; this->last_device_flag_ = false; this->last_family_discrepancy_ = 0; this->rom_number_ = 0; } -uint64_t ESPOneWire::search() { +uint64_t IRAM_ATTR ESPOneWire::search() { if (this->last_device_flag_) { return 0u; } - if (!this->reset()) { - // Reset failed or no devices present - this->reset_search(); - return 0u; + { + InterruptLock lock; + if (!this->reset()) { + // Reset failed or no devices present + this->reset_search(); + return 0u; + } } uint8_t id_bit_number = 1; @@ -137,58 +165,61 @@ uint64_t ESPOneWire::search() { bool search_result = false; uint8_t rom_byte_mask = 1; - // Initiate search - this->write8(ONE_WIRE_ROM_SEARCH); - do { - // read bit - bool id_bit = this->read_bit(); - // read its complement - bool cmp_id_bit = this->read_bit(); + { + InterruptLock lock; + // Initiate search + this->write8(ONE_WIRE_ROM_SEARCH); + do { + // read bit + bool id_bit = this->read_bit(); + // read its complement + bool cmp_id_bit = this->read_bit(); - if (id_bit && cmp_id_bit) { - // No devices participating in search - break; - } - - bool branch; - - if (id_bit != cmp_id_bit) { - // only chose one branch, the other one doesn't have any devices. - branch = id_bit; - } else { - // there are devices with both 0s and 1s at this bit - if (id_bit_number < this->last_discrepancy_) { - branch = (this->rom_number8_()[rom_byte_number] & rom_byte_mask) > 0; - } else { - branch = id_bit_number == this->last_discrepancy_; + if (id_bit && cmp_id_bit) { + // No devices participating in search + break; } - if (!branch) { - last_zero = id_bit_number; - if (last_zero < 9) { - this->last_discrepancy_ = last_zero; + bool branch; + + if (id_bit != cmp_id_bit) { + // only chose one branch, the other one doesn't have any devices. + branch = id_bit; + } else { + // there are devices with both 0s and 1s at this bit + if (id_bit_number < this->last_discrepancy_) { + branch = (this->rom_number8_()[rom_byte_number] & rom_byte_mask) > 0; + } else { + branch = id_bit_number == this->last_discrepancy_; + } + + if (!branch) { + last_zero = id_bit_number; + if (last_zero < 9) { + this->last_discrepancy_ = last_zero; + } } } - } - if (branch) { - // set bit - this->rom_number8_()[rom_byte_number] |= rom_byte_mask; - } else { - // clear bit - this->rom_number8_()[rom_byte_number] &= ~rom_byte_mask; - } + if (branch) { + // set bit + this->rom_number8_()[rom_byte_number] |= rom_byte_mask; + } else { + // clear bit + this->rom_number8_()[rom_byte_number] &= ~rom_byte_mask; + } - // choose/announce branch - this->write_bit(branch); - id_bit_number++; - rom_byte_mask <<= 1; - if (rom_byte_mask == 0u) { - // go to next byte - rom_byte_number++; - rom_byte_mask = 1; - } - } while (rom_byte_number < 8); // loop through all bytes + // choose/announce branch + this->write_bit(branch); + id_bit_number++; + rom_byte_mask <<= 1; + if (rom_byte_mask == 0u) { + // go to next byte + rom_byte_number++; + rom_byte_mask = 1; + } + } while (rom_byte_number < 8); // loop through all bytes + } if (id_bit_number >= 65) { this->last_discrepancy_ = last_zero; @@ -217,7 +248,7 @@ std::vector ESPOneWire::search_vec() { return res; } -void ESPOneWire::skip() { +void IRAM_ATTR ESPOneWire::skip() { this->write8(0xCC); // skip ROM } diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 6d399c4064..a346cd7e0b 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -328,6 +328,8 @@ void hsv_to_rgb(int hue, float saturation, float value, float &red, float &green IRAM_ATTR InterruptLock::InterruptLock() { xt_state_ = xt_rsil(15); } IRAM_ATTR InterruptLock::~InterruptLock() { xt_wsr_ps(xt_state_); } #elif defined(USE_ESP32) +// only affects the executing core +// so should not be used as a mutex lock, only to get accurate timing IRAM_ATTR InterruptLock::InterruptLock() { portDISABLE_INTERRUPTS(); } IRAM_ATTR InterruptLock::~InterruptLock() { portENABLE_INTERRUPTS(); } #endif From cc0c1c08b94b47a5872a78185ab7f17ee41d9719 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 11 Feb 2022 13:57:46 +0100 Subject: [PATCH 0220/1729] Bump platformio from 5.2.4 to 5.2.5 (#3188) * Bump platformio from 5.2.4 to 5.2.5 Bumps [platformio](https://github.com/platformio/platformio) from 5.2.4 to 5.2.5. - [Release notes](https://github.com/platformio/platformio/releases) - [Changelog](https://github.com/platformio/platformio-core/blob/develop/HISTORY.rst) - [Commits](https://github.com/platformio/platformio/commits) --- updated-dependencies: - dependency-name: platformio dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * Update requirements.txt Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Otto Winter --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 0c408f90b5..acbf1d9984 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,12 +6,12 @@ tornado==6.1 tzlocal==4.1 # from time tzdata>=2021.1 # from time pyserial==3.5 -platformio==5.2.4 # When updating platformio, also update Dockerfile +platformio==5.2.5 # When updating platformio, also update Dockerfile esptool==3.2 click==8.0.3 esphome-dashboard==20220209.0 aioesphomeapi==10.8.2 -zeroconf==0.37.0 +zeroconf==0.38.3 # esp-idf requires this, but doesn't bundle it by default # https://github.com/espressif/esp-idf/blob/220590d599e134d7a5e7f1e683cc4550349ffbf8/requirements.txt#L24 From b1cefb7e3e9538c3e7f5fdb13cba55a4cb5664fd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 11 Feb 2022 13:58:10 +0100 Subject: [PATCH 0221/1729] Bump pytest-asyncio from 0.18.0 to 0.18.1 (#3187) Bumps [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio) from 0.18.0 to 0.18.1. - [Release notes](https://github.com/pytest-dev/pytest-asyncio/releases) - [Commits](https://github.com/pytest-dev/pytest-asyncio/compare/v0.18.0...v0.18.1) --- updated-dependencies: - dependency-name: pytest-asyncio dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 46713adbb9..71b2e434cb 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -8,6 +8,6 @@ pre-commit pytest==7.0.0 pytest-cov==3.0.0 pytest-mock==3.7.0 -pytest-asyncio==0.18.0 +pytest-asyncio==0.18.1 asyncmock==0.4.2 hypothesis==5.49.0 From 0ec84be5dacb1a9d0c3cc5c5da5de9a3e9b14df9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 11 Feb 2022 22:23:06 +0100 Subject: [PATCH 0222/1729] Bump pytest from 7.0.0 to 7.0.1 (#3189) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 71b2e434cb..afc4fd9d2a 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -5,7 +5,7 @@ pyupgrade==2.31.0 pre-commit # Unit tests -pytest==7.0.0 +pytest==7.0.1 pytest-cov==3.0.0 pytest-mock==3.7.0 pytest-asyncio==0.18.1 From a13a1225b7201c71d0db5b4dc921e00c5dcc07e5 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 15 Feb 2022 11:57:47 +1300 Subject: [PATCH 0223/1729] Allow framework version validator to be maximum version (#3197) --- esphome/components/fastled_clockless/light.py | 7 +++- esphome/components/fastled_spi/light.py | 7 +++- esphome/config_validation.py | 35 ++++++++++++++----- 3 files changed, 39 insertions(+), 10 deletions(-) diff --git a/esphome/components/fastled_clockless/light.py b/esphome/components/fastled_clockless/light.py index acf9488ae3..dc456d4959 100644 --- a/esphome/components/fastled_clockless/light.py +++ b/esphome/components/fastled_clockless/light.py @@ -49,7 +49,12 @@ CONFIG_SCHEMA = cv.All( } ), _validate, - cv.only_with_arduino, + cv.require_framework_version( + esp8266_arduino=cv.Version(2, 7, 4), + esp32_arduino=cv.Version(99, 0, 0), + max_version=True, + extra_message="Please see note on documentation for FastLED", + ), ) diff --git a/esphome/components/fastled_spi/light.py b/esphome/components/fastled_spi/light.py index a729fc015a..b3ce1722ee 100644 --- a/esphome/components/fastled_spi/light.py +++ b/esphome/components/fastled_spi/light.py @@ -33,7 +33,12 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_DATA_RATE): cv.frequency, } ), - cv.only_with_arduino, + cv.require_framework_version( + esp8266_arduino=cv.Version(2, 7, 4), + esp32_arduino=cv.Version(99, 0, 0), + max_version=True, + extra_message="Please see note on documentation for FastLED", + ), ) diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 2e7a4e5677..8e1c63a54e 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -1713,30 +1713,49 @@ def require_framework_version( esp_idf=None, esp32_arduino=None, esp8266_arduino=None, + max_version=False, + extra_message=None, ): def validator(value): core_data = CORE.data[KEY_CORE] framework = core_data[KEY_TARGET_FRAMEWORK] if framework == "esp-idf": if esp_idf is None: - raise Invalid("This feature is incompatible with esp-idf") + msg = "This feature is incompatible with esp-idf" + if extra_message: + msg += f". {extra_message}" + raise Invalid(msg) required = esp_idf elif CORE.is_esp32 and framework == "arduino": if esp32_arduino is None: - raise Invalid( - "This feature is incompatible with ESP32 using arduino framework" - ) + msg = "This feature is incompatible with ESP32 using arduino framework" + if extra_message: + msg += f". {extra_message}" + raise Invalid(msg) required = esp32_arduino elif CORE.is_esp8266 and framework == "arduino": if esp8266_arduino is None: - raise Invalid("This feature is incompatible with ESP8266") + msg = "This feature is incompatible with ESP8266" + if extra_message: + msg += f". {extra_message}" + raise Invalid(msg) required = esp8266_arduino else: raise NotImplementedError + + if max_version: + if core_data[KEY_FRAMEWORK_VERSION] > required: + msg = f"This feature requires framework version {required} or lower" + if extra_message: + msg += f". {extra_message}" + raise Invalid(msg) + return value + if core_data[KEY_FRAMEWORK_VERSION] < required: - raise Invalid( - f"This feature requires at least framework version {required}" - ) + msg = f"This feature requires at least framework version {required}" + if extra_message: + msg += f". {extra_message}" + raise Invalid(msg) return value return validator From 8dcc9d6b66ac90ec9945dd083d5eab8d5ba63148 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Feb 2022 20:13:02 +0100 Subject: [PATCH 0224/1729] Bump aioesphomeapi from 10.8.1 to 10.8.2 (#3182) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a33461b79b..0c408f90b5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ platformio==5.2.4 # When updating platformio, also update Dockerfile esptool==3.2 click==8.0.3 esphome-dashboard==20220209.0 -aioesphomeapi==10.8.1 +aioesphomeapi==10.8.2 zeroconf==0.37.0 # esp-idf requires this, but doesn't bundle it by default From f376a39e55c724c5dbda7eb2cc0b8380249678cf Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 10 Feb 2022 11:12:05 +1300 Subject: [PATCH 0225/1729] Clamp rotary_encoder restored value to min and max (#3184) --- esphome/components/rotary_encoder/rotary_encoder.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/rotary_encoder/rotary_encoder.cpp b/esphome/components/rotary_encoder/rotary_encoder.cpp index c5227b41a7..c5e9cec596 100644 --- a/esphome/components/rotary_encoder/rotary_encoder.cpp +++ b/esphome/components/rotary_encoder/rotary_encoder.cpp @@ -138,6 +138,8 @@ void RotaryEncoderSensor::setup() { initial_value = 0; break; } + initial_value = clamp(initial_value, this->store_.min_value, this->store_.max_value); + this->store_.counter = initial_value; this->store_.last_read = initial_value; From dd554bcdf4fe0041ea144d44ce4e2313c229f6a2 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Fri, 11 Feb 2022 09:06:00 +0100 Subject: [PATCH 0226/1729] Make generating combined binary output verbose (#3127) --- esphome/components/esp32/post_build.py.script | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/esphome/components/esp32/post_build.py.script b/esphome/components/esp32/post_build.py.script index 7feaf9e8e5..2bb1a6c3d6 100644 --- a/esphome/components/esp32/post_build.py.script +++ b/esphome/components/esp32/post_build.py.script @@ -1,13 +1,16 @@ # Source https://github.com/letscontrolit/ESPEasy/pull/3845#issuecomment-1005864664 import esptool +from SCons.Script import ARGUMENTS # pylint: disable=E0602 Import("env") # noqa def esp32_create_combined_bin(source, target, env): - print("Generating combined binary for serial flashing") + verbose = bool(int(ARGUMENTS.get("PIOVERBOSE", "0"))) + if verbose: + print("Generating combined binary for serial flashing") app_offset = 0x10000 new_file_name = env.subst("$BUILD_DIR/${PROGNAME}-factory.bin") @@ -24,18 +27,21 @@ def esp32_create_combined_bin(source, target, env): "--flash_size", flash_size, ] - print(" Offset | File") + if verbose: + print(" Offset | File") for section in sections: sect_adr, sect_file = section.split(" ", 1) - print(f" - {sect_adr} | {sect_file}") + if verbose: + print(f" - {sect_adr} | {sect_file}") cmd += [sect_adr, sect_file] - print(f" - {hex(app_offset)} | {firmware_name}") cmd += [hex(app_offset), firmware_name] - print() - print(f"Using esptool.py arguments: {' '.join(cmd)}") - print() + if verbose: + print(f" - {hex(app_offset)} | {firmware_name}") + print() + print(f"Using esptool.py arguments: {' '.join(cmd)}") + print() esptool.main(cmd) From dcc80f9032c6d30bdf6b22dfbd76ee7b27b33d7f Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 15 Feb 2022 11:57:47 +1300 Subject: [PATCH 0227/1729] Allow framework version validator to be maximum version (#3197) --- esphome/components/fastled_clockless/light.py | 7 +++- esphome/components/fastled_spi/light.py | 7 +++- esphome/config_validation.py | 35 ++++++++++++++----- 3 files changed, 39 insertions(+), 10 deletions(-) diff --git a/esphome/components/fastled_clockless/light.py b/esphome/components/fastled_clockless/light.py index acf9488ae3..dc456d4959 100644 --- a/esphome/components/fastled_clockless/light.py +++ b/esphome/components/fastled_clockless/light.py @@ -49,7 +49,12 @@ CONFIG_SCHEMA = cv.All( } ), _validate, - cv.only_with_arduino, + cv.require_framework_version( + esp8266_arduino=cv.Version(2, 7, 4), + esp32_arduino=cv.Version(99, 0, 0), + max_version=True, + extra_message="Please see note on documentation for FastLED", + ), ) diff --git a/esphome/components/fastled_spi/light.py b/esphome/components/fastled_spi/light.py index a729fc015a..b3ce1722ee 100644 --- a/esphome/components/fastled_spi/light.py +++ b/esphome/components/fastled_spi/light.py @@ -33,7 +33,12 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_DATA_RATE): cv.frequency, } ), - cv.only_with_arduino, + cv.require_framework_version( + esp8266_arduino=cv.Version(2, 7, 4), + esp32_arduino=cv.Version(99, 0, 0), + max_version=True, + extra_message="Please see note on documentation for FastLED", + ), ) diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 2e7a4e5677..8e1c63a54e 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -1713,30 +1713,49 @@ def require_framework_version( esp_idf=None, esp32_arduino=None, esp8266_arduino=None, + max_version=False, + extra_message=None, ): def validator(value): core_data = CORE.data[KEY_CORE] framework = core_data[KEY_TARGET_FRAMEWORK] if framework == "esp-idf": if esp_idf is None: - raise Invalid("This feature is incompatible with esp-idf") + msg = "This feature is incompatible with esp-idf" + if extra_message: + msg += f". {extra_message}" + raise Invalid(msg) required = esp_idf elif CORE.is_esp32 and framework == "arduino": if esp32_arduino is None: - raise Invalid( - "This feature is incompatible with ESP32 using arduino framework" - ) + msg = "This feature is incompatible with ESP32 using arduino framework" + if extra_message: + msg += f". {extra_message}" + raise Invalid(msg) required = esp32_arduino elif CORE.is_esp8266 and framework == "arduino": if esp8266_arduino is None: - raise Invalid("This feature is incompatible with ESP8266") + msg = "This feature is incompatible with ESP8266" + if extra_message: + msg += f". {extra_message}" + raise Invalid(msg) required = esp8266_arduino else: raise NotImplementedError + + if max_version: + if core_data[KEY_FRAMEWORK_VERSION] > required: + msg = f"This feature requires framework version {required} or lower" + if extra_message: + msg += f". {extra_message}" + raise Invalid(msg) + return value + if core_data[KEY_FRAMEWORK_VERSION] < required: - raise Invalid( - f"This feature requires at least framework version {required}" - ) + msg = f"This feature requires at least framework version {required}" + if extra_message: + msg += f". {extra_message}" + raise Invalid(msg) return value return validator From bb6b77bd98a71e983166dcceae042873f5e51456 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 15 Feb 2022 12:00:12 +1300 Subject: [PATCH 0228/1729] Bump version to 2022.2.0b2 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index d3b78ca44a..580826c954 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2022.2.0b1" +__version__ = "2022.2.0b2" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From 113232ebb6d7c0109f483d5cb0992739a8c416fd Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Tue, 15 Feb 2022 01:01:50 -0300 Subject: [PATCH 0229/1729] add sim800l diagnostics (#3136) --- esphome/components/sim800l/__init__.py | 6 +++- esphome/components/sim800l/binary_sensor.py | 36 +++++++++++++++++++++ esphome/components/sim800l/sensor.py | 33 +++++++++++++++++++ esphome/components/sim800l/sim800l.cpp | 34 +++++++++++++++---- esphome/components/sim800l/sim800l.h | 24 +++++++++++++- 5 files changed, 125 insertions(+), 8 deletions(-) create mode 100644 esphome/components/sim800l/binary_sensor.py create mode 100644 esphome/components/sim800l/sensor.py diff --git a/esphome/components/sim800l/__init__.py b/esphome/components/sim800l/__init__.py index 4143627084..564b685b37 100644 --- a/esphome/components/sim800l/__init__.py +++ b/esphome/components/sim800l/__init__.py @@ -1,7 +1,10 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation -from esphome.const import CONF_ID, CONF_TRIGGER_ID +from esphome.const import ( + CONF_ID, + CONF_TRIGGER_ID, +) from esphome.components import uart DEPENDENCIES = ["uart"] @@ -20,6 +23,7 @@ Sim800LReceivedMessageTrigger = sim800l_ns.class_( Sim800LSendSmsAction = sim800l_ns.class_("Sim800LSendSmsAction", automation.Action) Sim800LDialAction = sim800l_ns.class_("Sim800LDialAction", automation.Action) +CONF_SIM800L_ID = "sim800l_id" CONF_ON_SMS_RECEIVED = "on_sms_received" CONF_RECIPIENT = "recipient" CONF_MESSAGE = "message" diff --git a/esphome/components/sim800l/binary_sensor.py b/esphome/components/sim800l/binary_sensor.py new file mode 100644 index 0000000000..7cee04374b --- /dev/null +++ b/esphome/components/sim800l/binary_sensor.py @@ -0,0 +1,36 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import binary_sensor +from esphome.const import ( + CONF_DEVICE_CLASS, + CONF_ENTITY_CATEGORY, + DEVICE_CLASS_CONNECTIVITY, + ENTITY_CATEGORY_DIAGNOSTIC, +) +from . import CONF_SIM800L_ID, Sim800LComponent + +DEPENDENCIES = ["sim800l"] + +CONF_REGISTERED = "registered" + +CONFIG_SCHEMA = { + cv.GenerateID(CONF_SIM800L_ID): cv.use_id(Sim800LComponent), + cv.Optional(CONF_REGISTERED): binary_sensor.BINARY_SENSOR_SCHEMA.extend( + { + cv.Optional( + CONF_DEVICE_CLASS, default=DEVICE_CLASS_CONNECTIVITY + ): binary_sensor.device_class, + cv.Optional( + CONF_ENTITY_CATEGORY, default=ENTITY_CATEGORY_DIAGNOSTIC + ): cv.entity_category, + } + ), +} + + +async def to_code(config): + sim800l_component = await cg.get_variable(config[CONF_SIM800L_ID]) + + if CONF_REGISTERED in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_REGISTERED]) + cg.add(sim800l_component.set_registered_binary_sensor(sens)) diff --git a/esphome/components/sim800l/sensor.py b/esphome/components/sim800l/sensor.py new file mode 100644 index 0000000000..156bd6a040 --- /dev/null +++ b/esphome/components/sim800l/sensor.py @@ -0,0 +1,33 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import ( + DEVICE_CLASS_SIGNAL_STRENGTH, + ENTITY_CATEGORY_DIAGNOSTIC, + STATE_CLASS_MEASUREMENT, + UNIT_DECIBEL_MILLIWATT, +) +from . import CONF_SIM800L_ID, Sim800LComponent + +DEPENDENCIES = ["sim800l"] + +CONF_RSSI = "rssi" + +CONFIG_SCHEMA = { + cv.GenerateID(CONF_SIM800L_ID): cv.use_id(Sim800LComponent), + cv.Optional(CONF_RSSI): sensor.sensor_schema( + unit_of_measurement=UNIT_DECIBEL_MILLIWATT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), +} + + +async def to_code(config): + sim800l_component = await cg.get_variable(config[CONF_SIM800L_ID]) + + if CONF_RSSI in config: + sens = await sensor.new_sensor(config[CONF_RSSI]) + cg.add(sim800l_component.set_rssi_sensor(sens)) diff --git a/esphome/components/sim800l/sim800l.cpp b/esphome/components/sim800l/sim800l.cpp index eb6d62ca33..709e241491 100644 --- a/esphome/components/sim800l/sim800l.cpp +++ b/esphome/components/sim800l/sim800l.cpp @@ -117,7 +117,7 @@ void Sim800LComponent::parse_cmd_(std::string message) { this->state_ = STATE_CREG; } } - this->registered_ = registered; + set_registered_(registered); break; } case STATE_CSQ: @@ -128,8 +128,17 @@ void Sim800LComponent::parse_cmd_(std::string message) { if (message.compare(0, 5, "+CSQ:") == 0) { size_t comma = message.find(',', 6); if (comma != 6) { - this->rssi_ = parse_number(message.substr(6, comma - 6)).value_or(0); - ESP_LOGD(TAG, "RSSI: %d", this->rssi_); + int rssi = parse_number(message.substr(6, comma - 6)).value_or(0); + +#ifdef USE_SENSOR + if (this->rssi_sensor_ != nullptr) { + this->rssi_sensor_->publish_state(rssi); + } else { + ESP_LOGD(TAG, "RSSI: %d", rssi); + } +#else + ESP_LOGD(TAG, "RSSI: %d", rssi); +#endif } } this->expect_ack_ = true; @@ -201,7 +210,7 @@ void Sim800LComponent::parse_cmd_(std::string message) { this->write(26); this->state_ = STATE_SENDINGSMS3; } else { - this->registered_ = false; + set_registered_(false); this->state_ = STATE_INIT; this->send_cmd_("AT+CMEE=2"); this->write(26); @@ -226,7 +235,7 @@ void Sim800LComponent::parse_cmd_(std::string message) { this->state_ = STATE_INIT; this->dial_pending_ = false; } else { - this->registered_ = false; + this->set_registered_(false); this->state_ = STATE_INIT; this->send_cmd_("AT+CMEE=2"); this->write(26); @@ -277,7 +286,12 @@ void Sim800LComponent::send_sms(const std::string &recipient, const std::string } void Sim800LComponent::dump_config() { ESP_LOGCONFIG(TAG, "SIM800L:"); - ESP_LOGCONFIG(TAG, " RSSI: %d dB", this->rssi_); +#ifdef USE_BINARY_SENSOR + LOG_BINARY_SENSOR(" ", "Registered", this->registered_binary_sensor_); +#endif +#ifdef USE_SENSOR + LOG_SENSOR(" ", "Rssi", this->rssi_sensor_); +#endif } void Sim800LComponent::dial(const std::string &recipient) { ESP_LOGD(TAG, "Dialing %s", recipient.c_str()); @@ -286,5 +300,13 @@ void Sim800LComponent::dial(const std::string &recipient) { this->update(); } +void Sim800LComponent::set_registered_(bool registered) { + this->registered_ = registered; +#ifdef USE_BINARY_SENSOR + if (this->registered_binary_sensor_ != nullptr) + this->registered_binary_sensor_->publish_state(registered); +#endif +} + } // namespace sim800l } // namespace esphome diff --git a/esphome/components/sim800l/sim800l.h b/esphome/components/sim800l/sim800l.h index 4f738b0a8c..3535b96283 100644 --- a/esphome/components/sim800l/sim800l.h +++ b/esphome/components/sim800l/sim800l.h @@ -2,7 +2,14 @@ #include +#include "esphome/core/defines.h" #include "esphome/core/component.h" +#ifdef USE_BINARY_SENSOR +#include "esphome/components/binary_sensor/binary_sensor.h" +#endif +#ifdef USE_SENSOR +#include "esphome/components/sensor/sensor.h" +#endif #include "esphome/components/uart/uart.h" #include "esphome/core/automation.h" @@ -42,6 +49,14 @@ class Sim800LComponent : public uart::UARTDevice, public PollingComponent { void update() override; void loop() override; void dump_config() override; +#ifdef USE_BINARY_SENSOR + void set_registered_binary_sensor(binary_sensor::BinarySensor *registered_binary_sensor) { + registered_binary_sensor_ = registered_binary_sensor; + } +#endif +#ifdef USE_SENSOR + void set_rssi_sensor(sensor::Sensor *rssi_sensor) { rssi_sensor_ = rssi_sensor; } +#endif void add_on_sms_received_callback(std::function callback) { this->callback_.add(std::move(callback)); } @@ -51,7 +66,15 @@ class Sim800LComponent : public uart::UARTDevice, public PollingComponent { protected: void send_cmd_(const std::string &message); void parse_cmd_(std::string message); + void set_registered_(bool registered); +#ifdef USE_BINARY_SENSOR + binary_sensor::BinarySensor *registered_binary_sensor_{nullptr}; +#endif + +#ifdef USE_SENSOR + sensor::Sensor *rssi_sensor_{nullptr}; +#endif std::string sender_; char read_buffer_[SIM800L_READ_BUFFER_LENGTH]; size_t read_pos_{0}; @@ -60,7 +83,6 @@ class Sim800LComponent : public uart::UARTDevice, public PollingComponent { bool expect_ack_{false}; sim800l::State state_{STATE_IDLE}; bool registered_{false}; - int rssi_{0}; std::string recipient_; std::string outgoing_message_; From ce073a704be3631c0a2966f97d5445ec037578d9 Mon Sep 17 00:00:00 2001 From: Maurice Makaay Date: Tue, 15 Feb 2022 20:54:21 +0100 Subject: [PATCH 0230/1729] Fix strlcpy() uses to make long SSIDs and passwords work (#3199) Co-authored-by: Maurice Makaay --- esphome/components/wifi/wifi_component_esp32_arduino.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/wifi/wifi_component_esp32_arduino.cpp b/esphome/components/wifi/wifi_component_esp32_arduino.cpp index e1332e3181..83381f3424 100644 --- a/esphome/components/wifi/wifi_component_esp32_arduino.cpp +++ b/esphome/components/wifi/wifi_component_esp32_arduino.cpp @@ -161,8 +161,8 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { // https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/network/esp_wifi.html#_CPPv417wifi_sta_config_t wifi_config_t conf; memset(&conf, 0, sizeof(conf)); - strlcpy(reinterpret_cast(conf.sta.ssid), ap.get_ssid().c_str(), sizeof(conf.sta.ssid)); - strlcpy(reinterpret_cast(conf.sta.password), ap.get_password().c_str(), sizeof(conf.sta.password)); + strncpy(reinterpret_cast(conf.sta.ssid), ap.get_ssid().c_str(), sizeof(conf.sta.ssid)); + strncpy(reinterpret_cast(conf.sta.password), ap.get_password().c_str(), sizeof(conf.sta.password)); // The weakest authmode to accept in the fast scan mode if (ap.get_password().empty()) { @@ -709,7 +709,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { wifi_config_t conf; memset(&conf, 0, sizeof(conf)); - strlcpy(reinterpret_cast(conf.ap.ssid), ap.get_ssid().c_str(), sizeof(conf.ap.ssid)); + strncpy(reinterpret_cast(conf.ap.ssid), ap.get_ssid().c_str(), sizeof(conf.ap.ssid)); conf.ap.channel = ap.get_channel().value_or(1); conf.ap.ssid_hidden = ap.get_ssid().size(); conf.ap.max_connection = 5; @@ -720,7 +720,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { *conf.ap.password = 0; } else { conf.ap.authmode = WIFI_AUTH_WPA2_PSK; - strlcpy(reinterpret_cast(conf.ap.password), ap.get_password().c_str(), sizeof(conf.ap.ssid)); + strncpy(reinterpret_cast(conf.ap.password), ap.get_password().c_str(), sizeof(conf.ap.ssid)); } #if ESP_IDF_VERSION_MAJOR >= 4 From 41f84447cc31adcf7f0a651ac8bf5962b7c863cb Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 16 Feb 2022 09:11:46 +1300 Subject: [PATCH 0231/1729] Update HA addon token (#3200) --- .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 02a55494e9..f5281c2fb7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -143,7 +143,7 @@ jobs: needs: [deploy-docker] steps: - env: - TOKEN: ${{ secrets.DEPLOY_HASSIO_TOKEN }} + TOKEN: ${{ secrets.DEPLOY_HA_ADDON_REPO_TOKEN }} run: | TAG="${GITHUB_REF#refs/tags/}" curl \ From 16dc7762f9935a4714925de39a3323c2dc880b82 Mon Sep 17 00:00:00 2001 From: Maurice Makaay Date: Tue, 15 Feb 2022 20:54:21 +0100 Subject: [PATCH 0232/1729] Fix strlcpy() uses to make long SSIDs and passwords work (#3199) Co-authored-by: Maurice Makaay --- esphome/components/wifi/wifi_component_esp32_arduino.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/wifi/wifi_component_esp32_arduino.cpp b/esphome/components/wifi/wifi_component_esp32_arduino.cpp index e1332e3181..83381f3424 100644 --- a/esphome/components/wifi/wifi_component_esp32_arduino.cpp +++ b/esphome/components/wifi/wifi_component_esp32_arduino.cpp @@ -161,8 +161,8 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { // https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/network/esp_wifi.html#_CPPv417wifi_sta_config_t wifi_config_t conf; memset(&conf, 0, sizeof(conf)); - strlcpy(reinterpret_cast(conf.sta.ssid), ap.get_ssid().c_str(), sizeof(conf.sta.ssid)); - strlcpy(reinterpret_cast(conf.sta.password), ap.get_password().c_str(), sizeof(conf.sta.password)); + strncpy(reinterpret_cast(conf.sta.ssid), ap.get_ssid().c_str(), sizeof(conf.sta.ssid)); + strncpy(reinterpret_cast(conf.sta.password), ap.get_password().c_str(), sizeof(conf.sta.password)); // The weakest authmode to accept in the fast scan mode if (ap.get_password().empty()) { @@ -709,7 +709,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { wifi_config_t conf; memset(&conf, 0, sizeof(conf)); - strlcpy(reinterpret_cast(conf.ap.ssid), ap.get_ssid().c_str(), sizeof(conf.ap.ssid)); + strncpy(reinterpret_cast(conf.ap.ssid), ap.get_ssid().c_str(), sizeof(conf.ap.ssid)); conf.ap.channel = ap.get_channel().value_or(1); conf.ap.ssid_hidden = ap.get_ssid().size(); conf.ap.max_connection = 5; @@ -720,7 +720,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { *conf.ap.password = 0; } else { conf.ap.authmode = WIFI_AUTH_WPA2_PSK; - strlcpy(reinterpret_cast(conf.ap.password), ap.get_password().c_str(), sizeof(conf.ap.ssid)); + strncpy(reinterpret_cast(conf.ap.password), ap.get_password().c_str(), sizeof(conf.ap.ssid)); } #if ESP_IDF_VERSION_MAJOR >= 4 From 6ddad6b299db67d937f315f1cfcf2b4d9c40f570 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 16 Feb 2022 09:11:46 +1300 Subject: [PATCH 0233/1729] Update HA addon token (#3200) --- .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 02a55494e9..f5281c2fb7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -143,7 +143,7 @@ jobs: needs: [deploy-docker] steps: - env: - TOKEN: ${{ secrets.DEPLOY_HASSIO_TOKEN }} + TOKEN: ${{ secrets.DEPLOY_HA_ADDON_REPO_TOKEN }} run: | TAG="${GITHUB_REF#refs/tags/}" curl \ From ec7a79049a37af82b0c01f20e0cac0ec2c94f930 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 16 Feb 2022 09:45:05 +1300 Subject: [PATCH 0234/1729] Bump version to 2022.2.0b3 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 580826c954..8bb99752aa 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2022.2.0b2" +__version__ = "2022.2.0b3" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From 69002fb1e6cdd3a6d79f2be389d0ec6fe78bfe55 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 16 Feb 2022 20:13:14 +1300 Subject: [PATCH 0235/1729] Bump version to 2022.2.0 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 8bb99752aa..70e27374ae 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2022.2.0b3" +__version__ = "2022.2.0" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From 51cb5da7f025ba10a23bff76d96756744ceca75b Mon Sep 17 00:00:00 2001 From: Stewart Date: Wed, 16 Feb 2022 15:50:10 +0000 Subject: [PATCH 0236/1729] Fix missed ARDUINO_VERSION_CODE to USE_ARDUINO_VERSION_CODE changes (#3206) Co-authored-by: Stewart Morgan --- esphome/components/debug/debug_component.cpp | 6 +++--- esphome/components/debug/debug_component.h | 4 ++-- esphome/components/ota/ota_backend_arduino_esp8266.h | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/esphome/components/debug/debug_component.cpp b/esphome/components/debug/debug_component.cpp index a2697084bd..97d5aeb8a8 100644 --- a/esphome/components/debug/debug_component.cpp +++ b/esphome/components/debug/debug_component.cpp @@ -53,9 +53,9 @@ void DebugComponent::dump_config() { #ifdef USE_SENSOR LOG_SENSOR(" ", "Free space on heap", this->free_sensor_); LOG_SENSOR(" ", "Largest free heap block", this->block_sensor_); -#if defined(USE_ESP8266) && ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2) +#if defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2) LOG_SENSOR(" ", "Heap fragmentation", this->fragmentation_sensor_); -#endif // defined(USE_ESP8266) && ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2) +#endif // defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2) #endif // USE_SENSOR ESP_LOGD(TAG, "ESPHome version %s", ESPHOME_VERSION); @@ -316,7 +316,7 @@ void DebugComponent::update() { #endif } -#if defined(USE_ESP8266) && ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2) +#if defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2) if (this->fragmentation_sensor_ != nullptr) { // NOLINTNEXTLINE(readability-static-accessed-through-instance) this->fragmentation_sensor_->publish_state(ESP.getHeapFragmentation()); diff --git a/esphome/components/debug/debug_component.h b/esphome/components/debug/debug_component.h index f966b4fafc..4dc1659616 100644 --- a/esphome/components/debug/debug_component.h +++ b/esphome/components/debug/debug_component.h @@ -28,7 +28,7 @@ class DebugComponent : public PollingComponent { #ifdef USE_SENSOR void set_free_sensor(sensor::Sensor *free_sensor) { free_sensor_ = free_sensor; } void set_block_sensor(sensor::Sensor *block_sensor) { block_sensor_ = block_sensor; } -#if defined(USE_ESP8266) && ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2) +#if defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2) void set_fragmentation_sensor(sensor::Sensor *fragmentation_sensor) { fragmentation_sensor_ = fragmentation_sensor; } #endif void set_loop_time_sensor(sensor::Sensor *loop_time_sensor) { loop_time_sensor_ = loop_time_sensor; } @@ -42,7 +42,7 @@ class DebugComponent : public PollingComponent { sensor::Sensor *free_sensor_{nullptr}; sensor::Sensor *block_sensor_{nullptr}; -#if defined(USE_ESP8266) && ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2) +#if defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2) sensor::Sensor *fragmentation_sensor_{nullptr}; #endif sensor::Sensor *loop_time_sensor_{nullptr}; diff --git a/esphome/components/ota/ota_backend_arduino_esp8266.h b/esphome/components/ota/ota_backend_arduino_esp8266.h index 329f2cf0f2..7937c665b0 100644 --- a/esphome/components/ota/ota_backend_arduino_esp8266.h +++ b/esphome/components/ota/ota_backend_arduino_esp8266.h @@ -17,7 +17,7 @@ class ArduinoESP8266OTABackend : public OTABackend { OTAResponseTypes write(uint8_t *data, size_t len) override; OTAResponseTypes end() override; void abort() override; -#if ARDUINO_VERSION_CODE >= VERSION_CODE(2, 7, 0) +#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 7, 0) bool supports_compression() override { return true; } #else bool supports_compression() override { return false; } From 1fb214165bb8297b1661d693e5ac2841fd8f8eb5 Mon Sep 17 00:00:00 2001 From: Stewart Date: Wed, 16 Feb 2022 15:50:10 +0000 Subject: [PATCH 0237/1729] Fix missed ARDUINO_VERSION_CODE to USE_ARDUINO_VERSION_CODE changes (#3206) Co-authored-by: Stewart Morgan --- esphome/components/debug/debug_component.cpp | 6 +++--- esphome/components/debug/debug_component.h | 4 ++-- esphome/components/ota/ota_backend_arduino_esp8266.h | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/esphome/components/debug/debug_component.cpp b/esphome/components/debug/debug_component.cpp index a2697084bd..97d5aeb8a8 100644 --- a/esphome/components/debug/debug_component.cpp +++ b/esphome/components/debug/debug_component.cpp @@ -53,9 +53,9 @@ void DebugComponent::dump_config() { #ifdef USE_SENSOR LOG_SENSOR(" ", "Free space on heap", this->free_sensor_); LOG_SENSOR(" ", "Largest free heap block", this->block_sensor_); -#if defined(USE_ESP8266) && ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2) +#if defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2) LOG_SENSOR(" ", "Heap fragmentation", this->fragmentation_sensor_); -#endif // defined(USE_ESP8266) && ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2) +#endif // defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2) #endif // USE_SENSOR ESP_LOGD(TAG, "ESPHome version %s", ESPHOME_VERSION); @@ -316,7 +316,7 @@ void DebugComponent::update() { #endif } -#if defined(USE_ESP8266) && ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2) +#if defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2) if (this->fragmentation_sensor_ != nullptr) { // NOLINTNEXTLINE(readability-static-accessed-through-instance) this->fragmentation_sensor_->publish_state(ESP.getHeapFragmentation()); diff --git a/esphome/components/debug/debug_component.h b/esphome/components/debug/debug_component.h index f966b4fafc..4dc1659616 100644 --- a/esphome/components/debug/debug_component.h +++ b/esphome/components/debug/debug_component.h @@ -28,7 +28,7 @@ class DebugComponent : public PollingComponent { #ifdef USE_SENSOR void set_free_sensor(sensor::Sensor *free_sensor) { free_sensor_ = free_sensor; } void set_block_sensor(sensor::Sensor *block_sensor) { block_sensor_ = block_sensor; } -#if defined(USE_ESP8266) && ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2) +#if defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2) void set_fragmentation_sensor(sensor::Sensor *fragmentation_sensor) { fragmentation_sensor_ = fragmentation_sensor; } #endif void set_loop_time_sensor(sensor::Sensor *loop_time_sensor) { loop_time_sensor_ = loop_time_sensor; } @@ -42,7 +42,7 @@ class DebugComponent : public PollingComponent { sensor::Sensor *free_sensor_{nullptr}; sensor::Sensor *block_sensor_{nullptr}; -#if defined(USE_ESP8266) && ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2) +#if defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2) sensor::Sensor *fragmentation_sensor_{nullptr}; #endif sensor::Sensor *loop_time_sensor_{nullptr}; diff --git a/esphome/components/ota/ota_backend_arduino_esp8266.h b/esphome/components/ota/ota_backend_arduino_esp8266.h index 329f2cf0f2..7937c665b0 100644 --- a/esphome/components/ota/ota_backend_arduino_esp8266.h +++ b/esphome/components/ota/ota_backend_arduino_esp8266.h @@ -17,7 +17,7 @@ class ArduinoESP8266OTABackend : public OTABackend { OTAResponseTypes write(uint8_t *data, size_t len) override; OTAResponseTypes end() override; void abort() override; -#if ARDUINO_VERSION_CODE >= VERSION_CODE(2, 7, 0) +#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 7, 0) bool supports_compression() override { return true; } #else bool supports_compression() override { return false; } From dc54b177785f2b49f852e4cbd9a0b13ee128fcfc Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 17 Feb 2022 07:36:14 +1300 Subject: [PATCH 0238/1729] Bump version to 2022.2.1 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 70e27374ae..7d4fc234db 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2022.2.0" +__version__ = "2022.2.1" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From 4e24551b90958bb71ab8172fde34de869b8b6d6b Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Wed, 16 Feb 2022 22:25:04 +0100 Subject: [PATCH 0239/1729] Docker move deps install into base (#3207) --- docker/Dockerfile | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 5d9decbf1b..d97eed65f7 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -5,9 +5,11 @@ # One of "docker", "hassio" ARG BASEIMGTYPE=docker +# https://github.com/hassio-addons/addon-debian-base/releases FROM ghcr.io/hassio-addons/debian-base/amd64:5.2.3 AS base-hassio-amd64 FROM ghcr.io/hassio-addons/debian-base/aarch64:5.2.3 AS base-hassio-arm64 FROM ghcr.io/hassio-addons/debian-base/armv7:5.2.3 AS base-hassio-armv7 +# https://hub.docker.com/_/debian?tab=tags&page=1&name=bullseye FROM debian:bullseye-20220125-slim AS base-docker-amd64 FROM debian:bullseye-20220125-slim AS base-docker-arm64 FROM debian:bullseye-20220125-slim AS base-docker-armv7 @@ -52,16 +54,16 @@ RUN \ && mkdir -p /piolibs - -# ======================= docker-type image ======================= -FROM base AS docker - # First install requirements to leverage caching when requirements don't change COPY requirements.txt requirements_optional.txt docker/platformio_install_deps.py platformio.ini / RUN \ pip3 install --no-cache-dir -r /requirements.txt -r /requirements_optional.txt \ && /platformio_install_deps.py /platformio.ini + +# ======================= docker-type image ======================= +FROM base AS docker + # Copy esphome and install COPY . /esphome RUN pip3 install --no-cache-dir --no-use-pep517 -e /esphome @@ -104,12 +106,6 @@ ARG BUILD_VERSION=dev # Copy root filesystem COPY docker/ha-addon-rootfs/ / -# First install requirements to leverage caching when requirements don't change -COPY requirements.txt requirements_optional.txt docker/platformio_install_deps.py platformio.ini / -RUN \ - pip3 install --no-cache-dir -r /requirements.txt -r /requirements_optional.txt \ - && /platformio_install_deps.py /platformio.ini - # Copy esphome and install COPY . /esphome RUN pip3 install --no-cache-dir --no-use-pep517 -e /esphome @@ -147,10 +143,8 @@ RUN \ /var/{cache,log}/* \ /var/lib/apt/lists/* -COPY requirements.txt requirements_optional.txt requirements_test.txt docker/platformio_install_deps.py platformio.ini / -RUN \ - pip3 install --no-cache-dir -r /requirements.txt -r /requirements_optional.txt -r /requirements_test.txt \ - && /platformio_install_deps.py /platformio.ini +COPY requirements_test.txt / +RUN pip3 install --no-cache-dir -r /requirements_test.txt VOLUME ["/esphome"] WORKDIR /esphome From c123804294b5aa81c15cbd2fd0ca1b02a7316e99 Mon Sep 17 00:00:00 2001 From: Stewart Date: Thu, 17 Feb 2022 00:53:26 +0000 Subject: [PATCH 0240/1729] Set entity-category to diagnostic for debug component (#3209) Co-authored-by: Stewart Morgan Co-authored-by: root --- esphome/components/debug/sensor.py | 29 +++++++++++++++++++++---- esphome/components/debug/text_sensor.py | 6 +++-- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/esphome/components/debug/sensor.py b/esphome/components/debug/sensor.py index deea6fd5ed..f7ea07d138 100644 --- a/esphome/components/debug/sensor.py +++ b/esphome/components/debug/sensor.py @@ -6,6 +6,7 @@ from esphome.const import ( CONF_FRAGMENTATION, CONF_BLOCK, CONF_LOOP_TIME, + ENTITY_CATEGORY_DIAGNOSTIC, UNIT_MILLISECOND, UNIT_PERCENT, UNIT_BYTES, @@ -18,14 +19,34 @@ DEPENDENCIES = ["debug"] CONFIG_SCHEMA = { cv.GenerateID(CONF_DEBUG_ID): cv.use_id(DebugComponent), - cv.Optional(CONF_FREE): sensor.sensor_schema(UNIT_BYTES, ICON_COUNTER, 0), - cv.Optional(CONF_BLOCK): sensor.sensor_schema(UNIT_BYTES, ICON_COUNTER, 0), + cv.Optional(CONF_FREE): sensor.sensor_schema( + unit_of_measurement=UNIT_BYTES, + icon=ICON_COUNTER, + accuracy_decimals=0, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_BLOCK): sensor.sensor_schema( + unit_of_measurement=UNIT_BYTES, + icon=ICON_COUNTER, + accuracy_decimals=0, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), cv.Optional(CONF_FRAGMENTATION): cv.All( cv.only_on_esp8266, cv.require_framework_version(esp8266_arduino=cv.Version(2, 5, 2)), - sensor.sensor_schema(UNIT_PERCENT, ICON_COUNTER, 1), + sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + icon=ICON_COUNTER, + accuracy_decimals=1, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + ), + cv.Optional(CONF_LOOP_TIME): sensor.sensor_schema( + unit_of_measurement=UNIT_MILLISECOND, + icon=ICON_TIMER, + accuracy_decimals=0, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), - cv.Optional(CONF_LOOP_TIME): sensor.sensor_schema(UNIT_MILLISECOND, ICON_TIMER, 0), } diff --git a/esphome/components/debug/text_sensor.py b/esphome/components/debug/text_sensor.py index f8d1016fbf..11e6354f57 100644 --- a/esphome/components/debug/text_sensor.py +++ b/esphome/components/debug/text_sensor.py @@ -1,7 +1,7 @@ from esphome.components import text_sensor import esphome.config_validation as cv import esphome.codegen as cg -from esphome.const import CONF_DEVICE +from esphome.const import CONF_DEVICE, ENTITY_CATEGORY_DIAGNOSTIC from . import CONF_DEBUG_ID, DebugComponent @@ -11,7 +11,9 @@ DEPENDENCIES = ["debug"] CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(CONF_DEBUG_ID): cv.use_id(DebugComponent), - cv.Optional(CONF_DEVICE): text_sensor.text_sensor_schema(), + cv.Optional(CONF_DEVICE): text_sensor.text_sensor_schema( + entity_category=ENTITY_CATEGORY_DIAGNOSTIC + ), } ) From ffa19426d79b92f2e16f4c5a71eb25481d0d4a61 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 17 Feb 2022 16:56:44 +1300 Subject: [PATCH 0241/1729] Remove redundant name from binary_sensor constructor (#3213) --- esphome/components/binary_sensor/__init__.py | 3 +-- esphome/components/binary_sensor/binary_sensor.cpp | 3 +-- esphome/components/binary_sensor/binary_sensor.h | 5 ----- 3 files changed, 2 insertions(+), 9 deletions(-) diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index 1eab76d54e..c6065ddae4 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -22,7 +22,6 @@ from esphome.const import ( CONF_STATE, CONF_TIMING, CONF_TRIGGER_ID, - CONF_NAME, CONF_MQTT_ID, DEVICE_CLASS_EMPTY, DEVICE_CLASS_BATTERY, @@ -443,7 +442,7 @@ async def register_binary_sensor(var, config): async def new_binary_sensor(config): - var = cg.new_Pvariable(config[CONF_ID], config[CONF_NAME]) + var = cg.new_Pvariable(config[CONF_ID]) await register_binary_sensor(var, config) return var diff --git a/esphome/components/binary_sensor/binary_sensor.cpp b/esphome/components/binary_sensor/binary_sensor.cpp index 71422609d7..02735feaae 100644 --- a/esphome/components/binary_sensor/binary_sensor.cpp +++ b/esphome/components/binary_sensor/binary_sensor.cpp @@ -42,8 +42,7 @@ void BinarySensor::send_state_internal(bool state, bool is_initial) { } } std::string BinarySensor::device_class() { return ""; } -BinarySensor::BinarySensor(const std::string &name) : EntityBase(name), state(false) {} -BinarySensor::BinarySensor() : BinarySensor("") {} +BinarySensor::BinarySensor() : state(false) {} void BinarySensor::set_device_class(const std::string &device_class) { this->device_class_ = device_class; } std::string BinarySensor::get_device_class() { if (this->device_class_.has_value()) diff --git a/esphome/components/binary_sensor/binary_sensor.h b/esphome/components/binary_sensor/binary_sensor.h index 591f444387..b5d1244bce 100644 --- a/esphome/components/binary_sensor/binary_sensor.h +++ b/esphome/components/binary_sensor/binary_sensor.h @@ -26,11 +26,6 @@ namespace binary_sensor { class BinarySensor : public EntityBase { public: explicit BinarySensor(); - /** Construct a binary sensor with the specified name - * - * @param name Name of this binary sensor. - */ - explicit BinarySensor(const std::string &name); /** Add a callback to be notified of state changes. * From 5a0b8328d81264e38ca9d486f857b36a7cbdadb2 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Thu, 17 Feb 2022 04:59:46 +0100 Subject: [PATCH 0242/1729] ESP8266 early init for pins (#3144) --- esphome/components/esp8266/__init__.py | 16 ++++++-- esphome/components/esp8266/const.py | 1 + esphome/components/esp8266/core.cpp | 10 +++++ esphome/components/esp8266/core.h | 14 +++++++ esphome/components/esp8266/gpio.py | 54 ++++++++++++++++++++++++-- 5 files changed, 89 insertions(+), 6 deletions(-) create mode 100644 esphome/components/esp8266/core.h diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index 3c83400c1d..7b1be32e38 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -17,11 +17,16 @@ import esphome.config_validation as cv import esphome.codegen as cg from esphome.helpers import copy_file_if_changed -from .const import CONF_RESTORE_FROM_FLASH, KEY_BOARD, KEY_ESP8266, esp8266_ns +from .const import ( + CONF_RESTORE_FROM_FLASH, + KEY_BOARD, + KEY_ESP8266, + KEY_PIN_INITIAL_STATES, + esp8266_ns, +) from .boards import ESP8266_FLASH_SIZES, ESP8266_LD_SCRIPTS -# force import gpio to register pin schema -from .gpio import esp8266_pin_to_code # noqa +from .gpio import PinInitialState, add_pin_initial_states_array CODEOWNERS = ["@esphome/core"] @@ -37,6 +42,9 @@ def set_core_data(config): config[CONF_FRAMEWORK][CONF_VERSION] ) CORE.data[KEY_ESP8266][KEY_BOARD] = config[CONF_BOARD] + CORE.data[KEY_ESP8266][KEY_PIN_INITIAL_STATES] = [ + PinInitialState() for _ in range(16) + ] return config @@ -221,6 +229,8 @@ async def to_code(config): if ld_script is not None: cg.add_platformio_option("board_build.ldscript", ld_script) + CORE.add_job(add_pin_initial_states_array) + # Called by writer.py def copy_files(): diff --git a/esphome/components/esp8266/const.py b/esphome/components/esp8266/const.py index 16a050360c..70429297e0 100644 --- a/esphome/components/esp8266/const.py +++ b/esphome/components/esp8266/const.py @@ -2,6 +2,7 @@ import esphome.codegen as cg KEY_ESP8266 = "esp8266" KEY_BOARD = "board" +KEY_PIN_INITIAL_STATES = "pin_initial_states" CONF_RESTORE_FROM_FLASH = "restore_from_flash" # esp8266 namespace is already defined by arduino, manually prefix esphome diff --git a/esphome/components/esp8266/core.cpp b/esphome/components/esp8266/core.cpp index 828d71a3bd..a9460f51f2 100644 --- a/esphome/components/esp8266/core.cpp +++ b/esphome/components/esp8266/core.cpp @@ -1,5 +1,6 @@ #ifdef USE_ESP8266 +#include "core.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "preferences.h" @@ -53,6 +54,15 @@ extern "C" void resetPins() { // NOLINT // however, not strictly needed as we set up the pins properly // ourselves and this causes pins to toggle during reboot. force_link_symbols(); + + for (int i = 0; i < 16; i++) { + uint8_t mode = ESPHOME_ESP8266_GPIO_INITIAL_MODE[i]; + uint8_t level = ESPHOME_ESP8266_GPIO_INITIAL_LEVEL[i]; + if (mode != 255) + pinMode(i, mode); // NOLINT + if (level != 255) + digitalWrite(i, level); // NOLINT + } } } // namespace esphome diff --git a/esphome/components/esp8266/core.h b/esphome/components/esp8266/core.h new file mode 100644 index 0000000000..ac33305669 --- /dev/null +++ b/esphome/components/esp8266/core.h @@ -0,0 +1,14 @@ +#pragma once + +#ifdef USE_ESP8266 + +#include + +extern const uint8_t ESPHOME_ESP8266_GPIO_INITIAL_MODE[16]; +extern const uint8_t ESPHOME_ESP8266_GPIO_INITIAL_LEVEL[16]; + +namespace esphome { +namespace esp8266 {} // namespace esp8266 +} // namespace esphome + +#endif // USE_ESP8266 diff --git a/esphome/components/esp8266/gpio.py b/esphome/components/esp8266/gpio.py index fa5c94dff5..cf33ec126b 100644 --- a/esphome/components/esp8266/gpio.py +++ b/esphome/components/esp8266/gpio.py @@ -1,4 +1,6 @@ import logging +from dataclasses import dataclass +from typing import List from esphome.const import ( CONF_ID, @@ -12,12 +14,12 @@ from esphome.const import ( CONF_PULLUP, ) from esphome import pins -from esphome.core import CORE +from esphome.core import CORE, coroutine_with_priority import esphome.config_validation as cv import esphome.codegen as cg from . import boards -from .const import KEY_BOARD, KEY_ESP8266, esp8266_ns +from .const import KEY_BOARD, KEY_ESP8266, KEY_PIN_INITIAL_STATES, esp8266_ns _LOGGER = logging.getLogger(__name__) @@ -160,11 +162,57 @@ ESP8266_PIN_SCHEMA = cv.All( ) +@dataclass +class PinInitialState: + mode = 255 + level: int = 255 + + @pins.PIN_SCHEMA_REGISTRY.register("esp8266", ESP8266_PIN_SCHEMA) async def esp8266_pin_to_code(config): var = cg.new_Pvariable(config[CONF_ID]) num = config[CONF_NUMBER] + mode = config[CONF_MODE] cg.add(var.set_pin(num)) cg.add(var.set_inverted(config[CONF_INVERTED])) - cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) + cg.add(var.set_flags(pins.gpio_flags_expr(mode))) + if num < 16: + initial_state: PinInitialState = CORE.data[KEY_ESP8266][KEY_PIN_INITIAL_STATES][ + num + ] + if mode[CONF_INPUT]: + if mode[CONF_PULLDOWN]: + initial_state.mode = cg.global_ns.INPUT_PULLDOWN_16 + elif mode[CONF_PULLUP]: + initial_state.mode = cg.global_ns.INPUT_PULLUP + else: + initial_state.mode = cg.global_ns.INPUT + elif mode[CONF_OUTPUT]: + if mode[CONF_OPEN_DRAIN]: + initial_state.mode = cg.global_ns.OUTPUT_OPEN_DRAIN + else: + initial_state.mode = cg.global_ns.OUTPUT + initial_state.level = int(config[CONF_INVERTED]) + return var + + +@coroutine_with_priority(-999.0) +async def add_pin_initial_states_array(): + # Add includes at the very end, so that they override everything + initial_states: List[PinInitialState] = CORE.data[KEY_ESP8266][ + KEY_PIN_INITIAL_STATES + ] + initial_modes_s = ", ".join(str(x.mode) for x in initial_states) + initial_levels_s = ", ".join(str(x.level) for x in initial_states) + + cg.add_global( + cg.RawExpression( + f"const uint8_t ESPHOME_ESP8266_GPIO_INITIAL_MODE[16] = {{{initial_modes_s}}}" + ) + ) + cg.add_global( + cg.RawExpression( + f"const uint8_t ESPHOME_ESP8266_GPIO_INITIAL_LEVEL[16] = {{{initial_levels_s}}}" + ) + ) From c054fb8a2c5a60da3961728222d66ebbadfb50c3 Mon Sep 17 00:00:00 2001 From: Felix Storm Date: Thu, 17 Feb 2022 05:00:14 +0100 Subject: [PATCH 0243/1729] CAN bus: read all queued messages (#3194) --- esphome/components/canbus/canbus.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/esphome/components/canbus/canbus.cpp b/esphome/components/canbus/canbus.cpp index 731682c277..d9f6ded85d 100644 --- a/esphome/components/canbus/canbus.cpp +++ b/esphome/components/canbus/canbus.cpp @@ -56,13 +56,15 @@ void Canbus::add_trigger(CanbusTrigger *trigger) { void Canbus::loop() { struct CanFrame can_message; - // readmessage - if (this->read_message(&can_message) == canbus::ERROR_OK) { + // read all messages until queue is empty + int message_counter = 0; + while (this->read_message(&can_message) == canbus::ERROR_OK) { + message_counter++; if (can_message.use_extended_id) { - ESP_LOGD(TAG, "received can message extended can_id=0x%x size=%d", can_message.can_id, + ESP_LOGD(TAG, "received can message (#%d) extended can_id=0x%x size=%d", message_counter, can_message.can_id, can_message.can_data_length_code); } else { - ESP_LOGD(TAG, "received can message std can_id=0x%x size=%d", can_message.can_id, + ESP_LOGD(TAG, "received can message (#%d) std can_id=0x%x size=%d", message_counter, can_message.can_id, can_message.can_data_length_code); } From 38259c96c9c5e49154be9f25a89e68741034071a Mon Sep 17 00:00:00 2001 From: Felix Storm Date: Thu, 17 Feb 2022 05:00:31 +0100 Subject: [PATCH 0244/1729] CAN bus: support bit mask for on_frame can_id (#3196) --- esphome/components/canbus/__init__.py | 16 ++++++++--- esphome/components/canbus/canbus.cpp | 5 ++-- esphome/components/canbus/canbus.h | 8 +++--- tests/test1.yaml | 38 +++++++++++++++++++++++++++ 4 files changed, 58 insertions(+), 9 deletions(-) diff --git a/esphome/components/canbus/__init__.py b/esphome/components/canbus/__init__.py index 808b31d1d2..5f614eb0a4 100644 --- a/esphome/components/canbus/__init__.py +++ b/esphome/components/canbus/__init__.py @@ -8,6 +8,7 @@ CODEOWNERS = ["@mvturnho", "@danielschramm"] IS_PLATFORM_COMPONENT = True CONF_CAN_ID = "can_id" +CONF_CAN_ID_MASK = "can_id_mask" CONF_USE_EXTENDED_ID = "use_extended_id" CONF_CANBUS_ID = "canbus_id" CONF_BIT_RATE = "bit_rate" @@ -38,7 +39,7 @@ canbus_ns = cg.esphome_ns.namespace("canbus") CanbusComponent = canbus_ns.class_("CanbusComponent", cg.Component) CanbusTrigger = canbus_ns.class_( "CanbusTrigger", - automation.Trigger.template(cg.std_vector.template(cg.uint8)), + automation.Trigger.template(cg.std_vector.template(cg.uint8), cg.uint32), cg.Component, ) CanSpeed = canbus_ns.enum("CAN_SPEED") @@ -72,6 +73,9 @@ CANBUS_SCHEMA = cv.Schema( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CanbusTrigger), cv.Required(CONF_CAN_ID): cv.int_range(min=0, max=0x1FFFFFFF), + cv.Optional(CONF_CAN_ID_MASK, default=0x1FFFFFFF): cv.int_range( + min=0, max=0x1FFFFFFF + ), cv.Optional(CONF_USE_EXTENDED_ID, default=False): cv.boolean, }, validate_id, @@ -90,11 +94,16 @@ async def setup_canbus_core_(var, config): for conf in config.get(CONF_ON_FRAME, []): can_id = conf[CONF_CAN_ID] + can_id_mask = conf[CONF_CAN_ID_MASK] ext_id = conf[CONF_USE_EXTENDED_ID] - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var, can_id, ext_id) + trigger = cg.new_Pvariable( + conf[CONF_TRIGGER_ID], var, can_id, can_id_mask, ext_id + ) await cg.register_component(trigger, conf) await automation.build_automation( - trigger, [(cg.std_vector.template(cg.uint8), "x")], conf + trigger, + [(cg.std_vector.template(cg.uint8), "x"), (cg.uint32, "can_id")], + conf, ) @@ -126,7 +135,6 @@ async def canbus_action_to_code(config, action_id, template_arg, args): if CONF_CAN_ID in config: can_id = await cg.templatable(config[CONF_CAN_ID], args, cg.uint32) cg.add(var.set_can_id(can_id)) - use_extended_id = await cg.templatable( config[CONF_USE_EXTENDED_ID], args, cg.uint32 ) diff --git a/esphome/components/canbus/canbus.cpp b/esphome/components/canbus/canbus.cpp index d9f6ded85d..14dc1544cf 100644 --- a/esphome/components/canbus/canbus.cpp +++ b/esphome/components/canbus/canbus.cpp @@ -78,8 +78,9 @@ void Canbus::loop() { // fire all triggers for (auto *trigger : this->triggers_) { - if ((trigger->can_id_ == can_message.can_id) && (trigger->use_extended_id_ == can_message.use_extended_id)) { - trigger->trigger(data); + if ((trigger->can_id_ == (can_message.can_id & trigger->can_id_mask_)) && + (trigger->use_extended_id_ == can_message.use_extended_id)) { + trigger->trigger(data, can_message.can_id); } } } diff --git a/esphome/components/canbus/canbus.h b/esphome/components/canbus/canbus.h index 37adf0bc9c..0491e8d3c1 100644 --- a/esphome/components/canbus/canbus.h +++ b/esphome/components/canbus/canbus.h @@ -116,17 +116,19 @@ template class CanbusSendAction : public Action, public P std::vector data_static_{}; }; -class CanbusTrigger : public Trigger>, public Component { +class CanbusTrigger : public Trigger, uint32_t>, public Component { friend class Canbus; public: - explicit CanbusTrigger(Canbus *parent, const std::uint32_t can_id, const bool use_extended_id) - : parent_(parent), can_id_(can_id), use_extended_id_(use_extended_id){}; + explicit CanbusTrigger(Canbus *parent, const std::uint32_t can_id, const std::uint32_t can_id_mask, + const bool use_extended_id) + : parent_(parent), can_id_(can_id), can_id_mask_(can_id_mask), use_extended_id_(use_extended_id){}; void setup() override { this->parent_->add_trigger(this); } protected: Canbus *parent_; uint32_t can_id_; + uint32_t can_id_mask_; bool use_extended_id_; }; diff --git a/tests/test1.yaml b/tests/test1.yaml index d8fea223dc..54343f59c7 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -2551,6 +2551,25 @@ canbus: lambda: "return x[0] == 0x11;" then: light.toggle: ${roomname}_lights + - can_id: 0b00000000000000000000001000000 + can_id_mask: 0b11111000000000011111111000000 + use_extended_id: true + then: + - lambda: |- + auto pdo_id = can_id >> 14; + switch (pdo_id) + { + case 117: + ESP_LOGD("canbus", "exhaust_fan_duty"); + break; + case 118: + ESP_LOGD("canbus", "supply_fan_duty"); + break; + case 119: + ESP_LOGD("canbus", "supply_fan_flow"); + break; + // to be continued... + } - platform: esp32_can id: esp32_internal_can rx_pin: GPIO04 @@ -2570,6 +2589,25 @@ canbus: lambda: "return x[0] == 0x11;" then: light.toggle: ${roomname}_lights + - can_id: 0b00000000000000000000001000000 + can_id_mask: 0b11111000000000011111111000000 + use_extended_id: true + then: + - lambda: |- + auto pdo_id = can_id >> 14; + switch (pdo_id) + { + case 117: + ESP_LOGD("canbus", "exhaust_fan_duty"); + break; + case 118: + ESP_LOGD("canbus", "supply_fan_duty"); + break; + case 119: + ESP_LOGD("canbus", "supply_fan_flow"); + break; + // to be continued... + } teleinfo: id: myteleinfo From 36ddd9dd69bfed26cf67c67aad88aeb4572665df Mon Sep 17 00:00:00 2001 From: wilberforce Date: Thu, 17 Feb 2022 17:02:10 +1300 Subject: [PATCH 0245/1729] Simplify captive portal to compressed single page (#2872) --- .../components/captive_portal/captive_index.h | 107 ++++++++++++++++++ .../captive_portal/captive_portal.cpp | 90 +++------------ .../captive_portal/captive_portal.h | 2 +- esphome/components/captive_portal/index.html | 55 --------- esphome/components/captive_portal/lock.svg | 1 - .../components/captive_portal/stylesheet.css | 58 ---------- .../captive_portal/wifi-strength-1.svg | 1 - .../captive_portal/wifi-strength-2.svg | 1 - .../captive_portal/wifi-strength-3.svg | 1 - .../captive_portal/wifi-strength-4.svg | 1 - 10 files changed, 125 insertions(+), 192 deletions(-) create mode 100644 esphome/components/captive_portal/captive_index.h delete mode 100644 esphome/components/captive_portal/index.html delete mode 100644 esphome/components/captive_portal/lock.svg delete mode 100644 esphome/components/captive_portal/stylesheet.css delete mode 100644 esphome/components/captive_portal/wifi-strength-1.svg delete mode 100644 esphome/components/captive_portal/wifi-strength-2.svg delete mode 100644 esphome/components/captive_portal/wifi-strength-3.svg delete mode 100644 esphome/components/captive_portal/wifi-strength-4.svg diff --git a/esphome/components/captive_portal/captive_index.h b/esphome/components/captive_portal/captive_index.h new file mode 100644 index 0000000000..bf2e6e6e8b --- /dev/null +++ b/esphome/components/captive_portal/captive_index.h @@ -0,0 +1,107 @@ +#pragma once +// Generated from https://github.com/esphome/esphome-webserver +#include "esphome/core/hal.h" +namespace esphome { + +namespace captive_portal { + +const uint8_t INDEX_GZ[] PROGMEM = { + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0xdd, 0x58, 0x09, 0x6f, 0xdc, 0x36, 0x16, 0xfe, 0x2b, + 0xac, 0x92, 0x74, 0x34, 0x8d, 0xc5, 0xd1, 0x31, 0x97, 0x35, 0xd2, 0x14, 0x89, 0x37, 0x45, 0x0b, 0x24, 0x69, 0x00, + 0xbb, 0x5d, 0x14, 0x69, 0x00, 0x73, 0x24, 0x6a, 0xc4, 0x58, 0xa2, 0x54, 0x91, 0x9a, 0x23, 0x83, 0xd9, 0xdf, 0xde, + 0x47, 0x52, 0x73, 0x38, 0x6b, 0x2f, 0x90, 0x62, 0x8b, 0xa2, 0x4d, 0x6c, 0x9a, 0xc7, 0x3b, 0x3f, 0xf2, 0xf1, 0x3d, + 0x2a, 0xfa, 0x2a, 0xad, 0x12, 0xb9, 0xad, 0x29, 0xca, 0x65, 0x59, 0xcc, 0x23, 0xd5, 0xa2, 0x82, 0xf0, 0x65, 0x4c, + 0x39, 0x8c, 0x28, 0x49, 0xe7, 0x51, 0x49, 0x25, 0x41, 0x49, 0x4e, 0x1a, 0x41, 0x65, 0xfc, 0xd3, 0xcd, 0x77, 0xce, + 0x14, 0x0d, 0xe6, 0x51, 0xc1, 0xf8, 0x1d, 0x6a, 0x68, 0x11, 0xb3, 0xa4, 0xe2, 0x28, 0x6f, 0x68, 0x16, 0xa7, 0x44, + 0x92, 0x90, 0x95, 0x64, 0x49, 0x15, 0x81, 0x66, 0xe3, 0xa4, 0xa4, 0xf1, 0x8a, 0xd1, 0x75, 0x5d, 0x35, 0x12, 0x01, + 0xa5, 0xa4, 0x5c, 0xc6, 0xd6, 0x9a, 0xa5, 0x32, 0x8f, 0x53, 0xba, 0x62, 0x09, 0x75, 0xf4, 0xe0, 0x82, 0x71, 0x26, + 0x19, 0x29, 0x1c, 0x91, 0x90, 0x82, 0xc6, 0xde, 0x45, 0x2b, 0x68, 0xa3, 0x07, 0x64, 0x01, 0x63, 0x5e, 0x59, 0x20, + 0x52, 0x24, 0x0d, 0xab, 0x25, 0x52, 0xf6, 0xc6, 0x65, 0x95, 0xb6, 0x05, 0x9d, 0x67, 0x2d, 0x4f, 0x24, 0x03, 0x0b, + 0x84, 0xcd, 0xfb, 0xbb, 0x82, 0x4a, 0x44, 0xe3, 0x37, 0x44, 0xe6, 0xb8, 0x24, 0x1b, 0xdb, 0x74, 0x18, 0xb7, 0xfd, + 0x6f, 0x6c, 0xfe, 0xdc, 0x73, 0xdd, 0xfe, 0x85, 0x6e, 0xdc, 0xfe, 0x00, 0xfe, 0xce, 0x1a, 0x2a, 0xdb, 0x86, 0x23, + 0x62, 0xdf, 0x46, 0x35, 0x50, 0xa2, 0x34, 0xb6, 0x4a, 0xcf, 0xc7, 0xae, 0x3b, 0x45, 0xde, 0x25, 0xf6, 0x47, 0x8e, + 0xe7, 0xe1, 0xc0, 0xf1, 0x46, 0xc9, 0xc4, 0x19, 0x21, 0x6f, 0x08, 0x8d, 0xef, 0xe3, 0x11, 0x72, 0x3f, 0x59, 0x28, + 0x63, 0x45, 0x11, 0x5b, 0xbc, 0xe2, 0xd4, 0x42, 0x42, 0x36, 0xd5, 0x1d, 0x8d, 0xad, 0xa4, 0x6d, 0x1a, 0xf0, 0xee, + 0xaa, 0x2a, 0xaa, 0x06, 0xac, 0xfd, 0x95, 0xa3, 0x7b, 0xff, 0xbe, 0x58, 0x87, 0x6c, 0x08, 0x17, 0x59, 0xd5, 0x94, + 0xb1, 0xa5, 0x41, 0xb1, 0x9f, 0xee, 0xe8, 0x1e, 0xa9, 0xa6, 0x7f, 0xb6, 0xe8, 0x54, 0x0d, 0x5b, 0x32, 0x1e, 0x5b, + 0x9e, 0x8f, 0xbc, 0x29, 0xe8, 0xbd, 0xed, 0xef, 0x8f, 0xa0, 0x10, 0x05, 0x4a, 0xe7, 0x66, 0x65, 0xbf, 0xbf, 0x8d, + 0xc4, 0x6a, 0x89, 0x36, 0x65, 0xc1, 0x45, 0x6c, 0xe5, 0x52, 0xd6, 0xe1, 0x60, 0xb0, 0x5e, 0xaf, 0xf1, 0x3a, 0xc0, + 0x55, 0xb3, 0x1c, 0xf8, 0xae, 0xeb, 0x0e, 0x80, 0xc2, 0x42, 0x66, 0x7f, 0x2c, 0x7f, 0x68, 0xa1, 0x9c, 0xb2, 0x65, + 0x2e, 0x75, 0x7f, 0xfe, 0x74, 0xc7, 0xf7, 0x91, 0xa2, 0x98, 0xdf, 0x7e, 0x38, 0xd3, 0xd2, 0x9c, 0x69, 0xe1, 0xdf, + 0x12, 0xdb, 0x3a, 0xb8, 0xda, 0x7b, 0xa3, 0x8c, 0x9a, 0x10, 0x1f, 0xf9, 0xc8, 0xd5, 0xff, 0x7d, 0x47, 0xf5, 0xbb, + 0x91, 0xf3, 0xd9, 0x08, 0x9d, 0x8d, 0xe0, 0xaf, 0x02, 0xd0, 0x2f, 0xc7, 0xce, 0xe5, 0x91, 0xdf, 0x53, 0xeb, 0x2b, + 0xcf, 0x3d, 0x4d, 0x28, 0xa6, 0xef, 0xc7, 0xe7, 0x63, 0xc7, 0xff, 0x59, 0x11, 0x68, 0xf4, 0x8f, 0x5c, 0x8e, 0x9f, + 0x7b, 0x3f, 0x8f, 0xc9, 0x08, 0x8d, 0xba, 0x99, 0x91, 0xa3, 0xfa, 0xc7, 0x91, 0xd6, 0x85, 0x46, 0x2b, 0x20, 0x2b, + 0x9d, 0xb1, 0x33, 0x22, 0x01, 0x0a, 0x3a, 0xab, 0xa0, 0x07, 0xd3, 0x63, 0xe0, 0x3e, 0x9b, 0x73, 0x82, 0x4f, 0xbd, + 0xc1, 0xdc, 0xea, 0x87, 0x96, 0x75, 0x82, 0xa1, 0x3a, 0x87, 0x01, 0x7f, 0xac, 0xe0, 0xdc, 0x59, 0x56, 0x7f, 0x6f, + 0x7d, 0x2b, 0xc8, 0x8a, 0x5a, 0x71, 0x1c, 0x43, 0xa8, 0xb5, 0x25, 0x9c, 0x10, 0x5c, 0x54, 0x09, 0x51, 0x2c, 0x58, + 0x50, 0xd2, 0x24, 0xf9, 0xd7, 0x5f, 0xdb, 0xc7, 0xa5, 0x25, 0x95, 0xaf, 0x0a, 0xaa, 0xba, 0xe2, 0xe5, 0xf6, 0x86, + 0x2c, 0xdf, 0x42, 0x00, 0xd9, 0x16, 0x11, 0x2c, 0xa5, 0x56, 0xff, 0xbd, 0xfb, 0x01, 0x0b, 0xb9, 0x2d, 0x28, 0x4e, + 0x99, 0xa8, 0x0b, 0xb2, 0x8d, 0xad, 0x05, 0xc8, 0xba, 0xb3, 0xfa, 0x17, 0x19, 0x95, 0x49, 0x6e, 0x5b, 0x03, 0x08, + 0xb1, 0x8c, 0x2d, 0xf1, 0x47, 0x51, 0x71, 0xab, 0x8f, 0x65, 0x4e, 0xb9, 0x6d, 0x1f, 0x2c, 0x54, 0xf6, 0x71, 0xbd, + 0x64, 0x3f, 0xb4, 0x74, 0xb4, 0x41, 0x32, 0xa9, 0x42, 0x0e, 0xab, 0xe0, 0xbd, 0x38, 0xce, 0x2e, 0xaa, 0x74, 0xfb, + 0x88, 0x79, 0xb9, 0x67, 0x6c, 0x63, 0x9c, 0xd3, 0xe6, 0x86, 0x6e, 0xe0, 0xb8, 0xfc, 0x9b, 0x7d, 0xc7, 0xd0, 0x5b, + 0x2a, 0xd7, 0x55, 0x73, 0x27, 0x42, 0x64, 0x3d, 0x37, 0xe2, 0x66, 0x26, 0x42, 0x39, 0x26, 0xb5, 0xc0, 0xa2, 0x80, + 0xf0, 0xb7, 0xbd, 0x3e, 0xc4, 0x6a, 0x7d, 0xdf, 0x14, 0x83, 0xe2, 0x6d, 0x94, 0xb2, 0x15, 0x4a, 0x0a, 0x22, 0xe0, + 0xb8, 0x72, 0x23, 0xcb, 0x42, 0x87, 0xb8, 0xaa, 0x78, 0x02, 0xfc, 0x77, 0xb1, 0xf5, 0x00, 0x76, 0x2f, 0xb7, 0x3f, + 0xa4, 0x76, 0x4f, 0x00, 0x6a, 0xbd, 0x3e, 0x5e, 0x91, 0xa2, 0xa5, 0x28, 0x46, 0x32, 0x67, 0xe2, 0x64, 0xe2, 0xec, + 0x51, 0xb6, 0x5a, 0xdc, 0x01, 0x57, 0x06, 0xcb, 0xc2, 0xee, 0x5b, 0xc7, 0x38, 0x8e, 0x88, 0xb9, 0xe5, 0xac, 0x27, + 0xd6, 0x67, 0x36, 0x39, 0x05, 0xcd, 0xa4, 0x75, 0x16, 0xf0, 0x4f, 0x77, 0x70, 0x1b, 0xe1, 0x06, 0xf4, 0xf7, 0xf7, + 0xa7, 0xd9, 0x48, 0xd4, 0x84, 0x7f, 0xce, 0xaa, 0x6c, 0xd4, 0x81, 0x85, 0x55, 0x4f, 0x45, 0x17, 0x10, 0x9d, 0x74, + 0x0e, 0xc8, 0xb1, 0xff, 0x74, 0x07, 0x71, 0xa6, 0x8e, 0xce, 0xdd, 0x49, 0x68, 0x34, 0x00, 0x84, 0xe6, 0xb7, 0xfb, + 0x7e, 0xff, 0xe4, 0xce, 0x6f, 0x2d, 0x6d, 0xb6, 0xd7, 0xb4, 0xa0, 0x89, 0xac, 0x1a, 0xdb, 0x7a, 0x02, 0x9a, 0xe0, + 0x24, 0x68, 0xbf, 0xbf, 0xbf, 0x79, 0xf3, 0x3a, 0xae, 0x6c, 0xda, 0xbf, 0x78, 0x8c, 0x5a, 0xdd, 0xea, 0xef, 0xe1, + 0x56, 0xff, 0x4f, 0xdc, 0x53, 0xf7, 0x7a, 0xef, 0x03, 0xb0, 0x1a, 0xaf, 0x4f, 0x97, 0xbb, 0xba, 0x00, 0x9e, 0xc3, + 0x25, 0x72, 0x61, 0x3d, 0x17, 0xb6, 0x33, 0x1e, 0xf5, 0x41, 0x3d, 0xfc, 0x80, 0xe9, 0xfa, 0x7a, 0x86, 0x6b, 0x5a, + 0x1d, 0xd1, 0xf9, 0x37, 0xbb, 0x45, 0xb5, 0x71, 0x04, 0xfb, 0xc4, 0xf8, 0x32, 0x64, 0x3c, 0xa7, 0x0d, 0x93, 0x7b, + 0x30, 0x17, 0x6e, 0xfa, 0xba, 0x95, 0xbb, 0x9a, 0xa4, 0xa9, 0x5a, 0x19, 0xd5, 0x9b, 0x59, 0x06, 0x79, 0x41, 0x51, + 0xd2, 0xd0, 0xa3, 0xe5, 0xde, 0xac, 0xeb, 0x2b, 0x28, 0xbc, 0x1c, 0x3d, 0xdb, 0xab, 0x83, 0xb7, 0x93, 0xb0, 0x65, + 0x0e, 0x29, 0xd8, 0x92, 0x87, 0x09, 0xd8, 0x4d, 0x1b, 0xc3, 0x94, 0x91, 0x92, 0x15, 0xdb, 0x50, 0xc0, 0x65, 0xe8, + 0x40, 0xc2, 0x60, 0xd9, 0x7e, 0xd1, 0x4a, 0x59, 0x71, 0xd0, 0xdd, 0xa4, 0xb4, 0x09, 0xdd, 0x99, 0xe9, 0x38, 0x0d, + 0x49, 0x59, 0x2b, 0x42, 0x1c, 0x34, 0xb4, 0x9c, 0x2d, 0x48, 0x72, 0xb7, 0x6c, 0xaa, 0x96, 0xa7, 0x4e, 0xa2, 0x6e, + 0xeb, 0xf0, 0x89, 0x97, 0x91, 0x80, 0x26, 0xb3, 0x6e, 0x94, 0x65, 0xd9, 0x0c, 0x90, 0xa0, 0x8e, 0xb9, 0xfc, 0x42, + 0x1f, 0x0f, 0x15, 0xdb, 0x99, 0x99, 0xd8, 0x57, 0x13, 0xc6, 0x46, 0x48, 0x25, 0xcf, 0x66, 0x07, 0x77, 0xdc, 0x19, + 0xa4, 0x01, 0x01, 0x42, 0x6a, 0x88, 0x7f, 0x30, 0x73, 0x5f, 0x12, 0xc6, 0xcf, 0xad, 0x57, 0x67, 0x65, 0xd6, 0x85, + 0x2f, 0xc0, 0xa2, 0xd5, 0xe8, 0x20, 0x9e, 0x41, 0xa2, 0x32, 0xb9, 0x30, 0xf4, 0xc7, 0x6e, 0xbd, 0xd9, 0xe3, 0xee, + 0x8c, 0xec, 0x0e, 0xd4, 0x59, 0x41, 0x37, 0xb3, 0x8f, 0xad, 0x90, 0x2c, 0xdb, 0x3a, 0x5d, 0x2e, 0x0d, 0xe1, 0xbc, + 0x40, 0x0e, 0x5d, 0x00, 0x29, 0xa5, 0x7c, 0xa6, 0x75, 0x38, 0x4c, 0xd2, 0x52, 0x74, 0x38, 0x1d, 0xc5, 0xe8, 0x53, + 0x7a, 0x5f, 0xd6, 0xff, 0xa2, 0x56, 0xc7, 0x71, 0x57, 0x92, 0x06, 0x72, 0x8b, 0xb3, 0xa8, 0x00, 0xd3, 0x32, 0x74, + 0x26, 0xb0, 0x57, 0xdd, 0x94, 0x12, 0x06, 0x9e, 0x83, 0x99, 0xfa, 0x6e, 0x3a, 0xe0, 0xed, 0xd5, 0x1b, 0x24, 0xaa, + 0x82, 0xa5, 0x1d, 0x9d, 0x26, 0x41, 0xee, 0x11, 0x1e, 0x0f, 0xb6, 0x1b, 0xa9, 0xb9, 0x03, 0xd4, 0xc3, 0x6c, 0x4a, + 0x3c, 0xf7, 0x81, 0x1d, 0x49, 0xb3, 0xcc, 0x5f, 0x64, 0x47, 0xa4, 0x54, 0xaa, 0xdd, 0xb3, 0xee, 0x54, 0xf8, 0x43, + 0x10, 0x70, 0xd8, 0x1b, 0xe8, 0xef, 0x99, 0x8e, 0x8b, 0xdd, 0x99, 0x14, 0x7d, 0x52, 0xc3, 0xb6, 0x29, 0xec, 0x87, + 0x4e, 0xee, 0xb3, 0xe0, 0xea, 0x94, 0x09, 0x7b, 0x8f, 0x67, 0xc2, 0x1e, 0x52, 0xb5, 0xcb, 0xcb, 0x6a, 0x13, 0xf7, + 0x74, 0x4e, 0x1a, 0xc2, 0x4f, 0xef, 0x59, 0xf0, 0x0a, 0xf8, 0xff, 0x2f, 0x29, 0xee, 0x0f, 0xa7, 0xb7, 0x2f, 0x48, + 0x6d, 0x5f, 0x98, 0xd5, 0x8c, 0x77, 0xca, 0x79, 0xe8, 0x41, 0xfa, 0x62, 0x58, 0xb0, 0xa5, 0xf7, 0x67, 0x40, 0xfb, + 0xdf, 0x38, 0x06, 0x2f, 0xbc, 0x29, 0xbe, 0x44, 0xba, 0x31, 0x10, 0xe1, 0x60, 0x8a, 0x26, 0x57, 0x43, 0x3c, 0xf4, + 0x90, 0xaa, 0x9a, 0xc6, 0x68, 0x82, 0xa7, 0x40, 0x30, 0xc6, 0xc1, 0x04, 0x26, 0x90, 0xef, 0xe1, 0xd1, 0x6b, 0x3f, + 0xc0, 0xe3, 0x11, 0x50, 0xf9, 0x2e, 0x0e, 0x7c, 0x64, 0x68, 0xc7, 0xd8, 0x07, 0x71, 0x8a, 0x24, 0x28, 0x01, 0xe8, + 0x24, 0xc0, 0xee, 0x04, 0xc4, 0x8d, 0xb1, 0x7b, 0x89, 0xa7, 0x63, 0x34, 0xc5, 0x13, 0x80, 0x0e, 0x0f, 0x47, 0x85, + 0x33, 0xc2, 0x1e, 0x4c, 0x07, 0x63, 0x32, 0xc5, 0xc3, 0x00, 0xe9, 0xc6, 0xc0, 0x31, 0x01, 0x11, 0x0e, 0x76, 0xbd, + 0xd7, 0x01, 0xf6, 0x27, 0xa0, 0x77, 0x38, 0x7c, 0x01, 0x62, 0x2f, 0x87, 0xc8, 0xb4, 0x06, 0x5e, 0x50, 0x30, 0x7a, + 0x0c, 0x34, 0xff, 0x9f, 0x0b, 0x1a, 0x40, 0xe2, 0xa1, 0x00, 0x5f, 0x42, 0xec, 0x7a, 0x8a, 0xdf, 0xb4, 0x06, 0x37, + 0xcf, 0x43, 0xee, 0x1f, 0xc6, 0x2c, 0xf8, 0xe7, 0x62, 0xe6, 0x29, 0x04, 0xa0, 0x0b, 0xba, 0x41, 0x0e, 0xd2, 0x8d, + 0xd1, 0x0d, 0xcc, 0xd3, 0xab, 0x4b, 0x34, 0x05, 0xae, 0xf1, 0x14, 0x5d, 0xa2, 0x91, 0x42, 0x17, 0xd8, 0x87, 0x86, + 0xc9, 0x01, 0xa6, 0x2f, 0x84, 0x71, 0xf8, 0x37, 0x86, 0xf1, 0x31, 0x9f, 0xfe, 0xc6, 0x2e, 0xfd, 0x15, 0x57, 0x10, + 0x94, 0x63, 0xba, 0x0c, 0x8b, 0x06, 0xe6, 0x15, 0xaf, 0xaa, 0x28, 0x78, 0x94, 0x43, 0x35, 0x02, 0xef, 0x7a, 0x0f, + 0xb1, 0x34, 0xce, 0xbd, 0xf9, 0xbd, 0x2a, 0x1d, 0x28, 0xbd, 0x79, 0xa4, 0xd3, 0xf9, 0xfc, 0x26, 0xa7, 0xe8, 0xd5, + 0xf5, 0x3b, 0x78, 0x08, 0x16, 0x05, 0xe2, 0xd5, 0x1a, 0xde, 0x9b, 0x5b, 0x24, 0x2b, 0xf5, 0x82, 0xe7, 0x50, 0x2a, + 0xaa, 0x2e, 0x3c, 0x20, 0x50, 0x57, 0x2c, 0x60, 0x8c, 0xa3, 0x45, 0x33, 0x7f, 0x57, 0x50, 0x22, 0x28, 0x5a, 0xb2, + 0x15, 0x45, 0x4c, 0x42, 0x1d, 0x50, 0x52, 0x24, 0x99, 0x6a, 0x8e, 0x8c, 0x9a, 0xee, 0x6d, 0x25, 0x69, 0x88, 0xae, + 0xaa, 0x7a, 0xab, 0x85, 0x24, 0x39, 0xe1, 0x4b, 0x9a, 0x1e, 0x84, 0x29, 0xea, 0x6d, 0xd5, 0x36, 0xe8, 0x97, 0x17, + 0x6f, 0x5e, 0xab, 0x87, 0x36, 0x45, 0x4e, 0xa7, 0x6c, 0x23, 0xd1, 0x8f, 0x37, 0x2f, 0x50, 0x5b, 0xc3, 0xa6, 0x53, + 0x63, 0x5b, 0xb5, 0xa2, 0xcd, 0x1a, 0x2a, 0x4b, 0xaa, 0x48, 0x40, 0xb9, 0xa0, 0x52, 0x42, 0xa1, 0x21, 0x30, 0x94, + 0xce, 0xda, 0x13, 0x53, 0x75, 0x83, 0xbb, 0x20, 0x7e, 0xde, 0x95, 0xd7, 0x51, 0x1e, 0x18, 0xd7, 0xaf, 0x3b, 0x6a, + 0x70, 0x3d, 0x98, 0x47, 0xea, 0x39, 0x8d, 0x88, 0x7e, 0x84, 0xc4, 0x83, 0x35, 0xcb, 0x98, 0x7a, 0xb8, 0xcd, 0x23, + 0x5d, 0x8f, 0x2a, 0x09, 0xaa, 0x24, 0x32, 0x5f, 0x34, 0x74, 0xaf, 0xa0, 0x7c, 0x09, 0xaf, 0x64, 0xd8, 0x70, 0xa8, + 0x50, 0x12, 0x9a, 0x57, 0x05, 0x54, 0x40, 0xf1, 0xf5, 0xf5, 0x0f, 0xff, 0x52, 0x9f, 0x3f, 0xc0, 0xcf, 0x13, 0x27, + 0x3c, 0x29, 0x0c, 0xa3, 0xea, 0x74, 0x7c, 0xe3, 0xa1, 0xf9, 0x90, 0x51, 0xc3, 0x7b, 0x00, 0xfc, 0x4e, 0xef, 0x49, + 0x79, 0x77, 0x98, 0xec, 0x24, 0xe9, 0x5f, 0x5d, 0xd9, 0x1a, 0x26, 0xd1, 0x2e, 0x4a, 0x26, 0xe7, 0xd7, 0x60, 0x60, + 0x34, 0x30, 0x0b, 0xe0, 0x9c, 0x72, 0xc0, 0xd0, 0xe6, 0x1d, 0x0f, 0xec, 0xa8, 0x42, 0xec, 0x27, 0x8d, 0x98, 0xd9, + 0x60, 0xed, 0x65, 0x49, 0x65, 0x5e, 0xa5, 0xf1, 0xbb, 0x1f, 0xaf, 0x6f, 0x8e, 0x1e, 0x77, 0xb0, 0x52, 0x9e, 0x98, + 0x0f, 0x2c, 0x6d, 0x21, 0x59, 0x4d, 0x1a, 0xa9, 0xc5, 0x3a, 0x2a, 0xce, 0x0e, 0x1e, 0xe9, 0x75, 0xbd, 0x33, 0xda, + 0xa9, 0x8e, 0x71, 0x30, 0x47, 0x0f, 0xd9, 0x78, 0xd0, 0xfd, 0x99, 0x95, 0x03, 0x73, 0x14, 0x07, 0xe6, 0x5c, 0x0e, + 0xf4, 0xe7, 0xa7, 0xdf, 0x01, 0xf1, 0x69, 0xfc, 0xac, 0x8e, 0x12, 0x00, 0x00}; + +} // namespace captive_portal +} // namespace esphome diff --git a/esphome/components/captive_portal/captive_portal.cpp b/esphome/components/captive_portal/captive_portal.cpp index d4e37f62f2..3bfdea0ab5 100644 --- a/esphome/components/captive_portal/captive_portal.cpp +++ b/esphome/components/captive_portal/captive_portal.cpp @@ -4,60 +4,27 @@ #include "esphome/core/log.h" #include "esphome/core/application.h" #include "esphome/components/wifi/wifi_component.h" +#include "captive_index.h" namespace esphome { namespace captive_portal { static const char *const TAG = "captive_portal"; -void CaptivePortal::handle_index(AsyncWebServerRequest *request) { - AsyncResponseStream *stream = request->beginResponseStream("text/html"); - stream->print(F("")); - stream->print(App.get_name().c_str()); - stream->print(F("")); - stream->print(F("")); - stream->print(F("")); - stream->print(F("

WiFi Networks

")); - - if (request->hasArg("save")) { - stream->print(F("
The ESP will now try to connect to the network...
Please give it some " - "time to connect.
Note: Copy the changed network to your YAML file - the next OTA update will " - "overwrite these settings.
")); - } +void CaptivePortal::handle_config(AsyncWebServerRequest *request) { + AsyncResponseStream *stream = request->beginResponseStream("application/json"); + stream->addHeader("cache-control", "public, max-age=0, must-revalidate"); + stream->printf(R"({"name":"%s","aps":[{})", App.get_name().c_str()); for (auto &scan : wifi::global_wifi_component->get_scan_result()) { if (scan.get_is_hidden()) continue; - stream->print(F("")); + // Assumes no " in ssid, possible unicode isses? + stream->printf(R"(,{"ssid":"%s","rssi":%d,"lock":%d})", scan.get_ssid().c_str(), scan.get_rssi(), + scan.get_with_auth()); } - - stream->print(F("

WiFi Settings







")); - stream->print(F("

OTA Update

")); - stream->print(F("
")); + stream->print(F("]}")); request->send(stream); } void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) { @@ -68,7 +35,7 @@ void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) { ESP_LOGI(TAG, " Password=" LOG_SECRET("'%s'"), psk.c_str()); wifi::global_wifi_component->save_wifi_sta(ssid, psk); wifi::global_wifi_component->start_scanning(); - request->redirect("/?save=true"); + request->redirect("/?save"); } void CaptivePortal::setup() {} @@ -98,44 +65,21 @@ void CaptivePortal::start() { this->active_ = true; } -const char STYLESHEET_CSS[] PROGMEM = - R"(*{box-sizing:inherit}div,input{padding:5px;font-size:1em}input{width:95%}body{text-align:center;font-family:sans-serif}button{border:0;border-radius:.3rem;background-color:#1fa3ec;color:#fff;line-height:2.4rem;font-size:1.2rem;width:100%;padding:0}.main{text-align:left;display:inline-block;min-width:260px}.network{display:flex;justify-content:space-between;align-items:center}.network-left{display:flex;align-items:center}.network-ssid{margin-bottom:-7px;margin-left:10px}.info{border:1px solid;margin:10px 0;padding:15px 10px;color:#4f8a10;background-color:#dff2bf})"; -const char LOCK_SVG[] PROGMEM = - R"()"; - void CaptivePortal::handleRequest(AsyncWebServerRequest *req) { if (req->url() == "/") { - this->handle_index(req); + AsyncWebServerResponse *response = req->beginResponse_P(200, "text/html", INDEX_GZ, sizeof(INDEX_GZ)); + response->addHeader("Content-Encoding", "gzip"); + req->send(response); + return; + } else if (req->url() == "/config.json") { + this->handle_config(req); return; } else if (req->url() == "/wifisave") { this->handle_wifisave(req); return; - } else if (req->url() == "/stylesheet.css") { - req->send_P(200, "text/css", STYLESHEET_CSS); - return; - } else if (req->url() == "/lock.svg") { - req->send_P(200, "image/svg+xml", LOCK_SVG); - return; } - - AsyncResponseStream *stream = req->beginResponseStream("image/svg+xml"); - stream->print(F("url() == "/wifi-strength-4.svg") { - stream->print(F("3z")); - } else { - if (req->url() == "/wifi-strength-1.svg") { - stream->print(F("3m0 2c3.07 0 6.09.86 8.71 2.45l-5.1 6.36a8.43 8.43 0 0 0-7.22-.01L3.27 7.4")); - } else if (req->url() == "/wifi-strength-2.svg") { - stream->print(F("3m0 2c3.07 0 6.09.86 8.71 2.45l-3.21 3.98a11.32 11.32 0 0 0-11 0L3.27 7.4")); - } else if (req->url() == "/wifi-strength-3.svg") { - stream->print(F("3m0 2c3.07 0 6.09.86 8.71 2.45l-1.94 2.43A13.6 13.6 0 0 0 12 8C9 8 6.68 9 5.21 9.84l-1.94-2.")); - } - stream->print(F("4A16.94 16.94 0 0 1 12 5z")); - } - stream->print(F("\"/>")); - req->send(stream); } + CaptivePortal::CaptivePortal(web_server_base::WebServerBase *base) : base_(base) { global_captive_portal = this; } float CaptivePortal::get_setup_priority() const { // Before WiFi diff --git a/esphome/components/captive_portal/captive_portal.h b/esphome/components/captive_portal/captive_portal.h index b308de42b7..0e68bc9cef 100644 --- a/esphome/components/captive_portal/captive_portal.h +++ b/esphome/components/captive_portal/captive_portal.h @@ -58,7 +58,7 @@ class CaptivePortal : public AsyncWebHandler, public Component { return false; } - void handle_index(AsyncWebServerRequest *request); + void handle_config(AsyncWebServerRequest *request); void handle_wifisave(AsyncWebServerRequest *request); diff --git a/esphome/components/captive_portal/index.html b/esphome/components/captive_portal/index.html deleted file mode 100644 index 627bf81215..0000000000 --- a/esphome/components/captive_portal/index.html +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - {{ App.get_name() }} - - - - -
-

WiFi Networks

-
- The ESP will now try to connect to the network...
- Please give it some time to connect.
- Note: Copy the changed network to your YAML file - the next OTA update will overwrite these settings. -
- - - -

WiFi Settings

-
-
-
-
- -
-

-
- -

OTA Update

-
- - -
-
- - diff --git a/esphome/components/captive_portal/lock.svg b/esphome/components/captive_portal/lock.svg deleted file mode 100644 index 743a1cc55a..0000000000 --- a/esphome/components/captive_portal/lock.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/esphome/components/captive_portal/stylesheet.css b/esphome/components/captive_portal/stylesheet.css deleted file mode 100644 index 73f82f05f1..0000000000 --- a/esphome/components/captive_portal/stylesheet.css +++ /dev/null @@ -1,58 +0,0 @@ -* { - box-sizing: inherit; -} - -div, input { - padding: 5px; - font-size: 1em; -} - -input { - width: 95%; -} - -body { - text-align: center; - font-family: sans-serif; -} - -button { - border: 0; - border-radius: 0.3rem; - background-color: #1fa3ec; - color: #fff; - line-height: 2.4rem; - font-size: 1.2rem; - width: 100%; - padding: 0; -} - -.main { - text-align: left; - display: inline-block; - min-width: 260px; -} - -.network { - display: flex; - justify-content: space-between; - align-items: center; -} - -.network-left { - display: flex; - align-items: center; -} - -.network-ssid { - margin-bottom: -7px; - margin-left: 10px; -} - -.info { - border: 1px solid; - margin: 10px 0px; - padding: 15px 10px; - color: #4f8a10; - background-color: #dff2bf; -} diff --git a/esphome/components/captive_portal/wifi-strength-1.svg b/esphome/components/captive_portal/wifi-strength-1.svg deleted file mode 100644 index 189a38193c..0000000000 --- a/esphome/components/captive_portal/wifi-strength-1.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/esphome/components/captive_portal/wifi-strength-2.svg b/esphome/components/captive_portal/wifi-strength-2.svg deleted file mode 100644 index 9b4b2d2396..0000000000 --- a/esphome/components/captive_portal/wifi-strength-2.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/esphome/components/captive_portal/wifi-strength-3.svg b/esphome/components/captive_portal/wifi-strength-3.svg deleted file mode 100644 index 44b7532bb7..0000000000 --- a/esphome/components/captive_portal/wifi-strength-3.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/esphome/components/captive_portal/wifi-strength-4.svg b/esphome/components/captive_portal/wifi-strength-4.svg deleted file mode 100644 index a22b0b8281..0000000000 --- a/esphome/components/captive_portal/wifi-strength-4.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file From 958ad0d750378e6ca47bd45bbd9a44cf391de5cc Mon Sep 17 00:00:00 2001 From: Roi Tagar Date: Thu, 17 Feb 2022 06:03:54 +0200 Subject: [PATCH 0246/1729] HttpRequestComponent::get_string - avoid copy (#2988) --- esphome/components/http_request/http_request.cpp | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/esphome/components/http_request/http_request.cpp b/esphome/components/http_request/http_request.cpp index 64f3f97de9..4e1cfe94b3 100644 --- a/esphome/components/http_request/http_request.cpp +++ b/esphome/components/http_request/http_request.cpp @@ -120,10 +120,16 @@ void HttpRequestComponent::close() { } const char *HttpRequestComponent::get_string() { - // The static variable is here because HTTPClient::getString() returns a String on ESP32, and we need something to - // to keep a buffer alive. - static std::string str; - str = this->client_.getString().c_str(); +#if defined(ESP32) + // The static variable is here because HTTPClient::getString() returns a String on ESP32, + // and we need something to keep a buffer alive. + static String str; +#else + // However on ESP8266, HTTPClient::getString() returns a String& to a member variable. + // Leaving this the default so that any new platform either doesn't copy, or encounters a compilation error. + auto & +#endif + str = this->client_.getString(); return str.c_str(); } From 34c229fd33b57dc08b7953c8d0b39cf0fab3bd1f Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Thu, 17 Feb 2022 11:56:14 +0100 Subject: [PATCH 0247/1729] Fix platformio docker version mismstch (#3215) --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index d97eed65f7..3de497faba 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -45,7 +45,7 @@ RUN \ # Ubuntu python3-pip is missing wheel pip3 install --no-cache-dir \ wheel==0.37.1 \ - platformio==5.2.4 \ + platformio==5.2.5 \ # Change some platformio settings && platformio settings set enable_telemetry No \ && platformio settings set check_libraries_interval 1000000 \ From 953f0569fbf060cc066a9f05b702824a04f5c2d1 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Thu, 17 Feb 2022 12:07:36 +0100 Subject: [PATCH 0248/1729] Docker ha-addon switch to nginx-light (#3218) --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 3de497faba..65e831f89b 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -95,7 +95,7 @@ RUN \ apt-get update \ # Use pinned versions so that we get updates with build caching && apt-get install -y --no-install-recommends \ - nginx=1.18.0-6.1 \ + nginx-light=1.18.0-6.1 \ && rm -rf \ /tmp/* \ /var/{cache,log}/* \ From 8cb9be7560ff370da8d70c098e5e90170f75ee62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A1n=20Panella?= Date: Thu, 17 Feb 2022 14:14:10 -0600 Subject: [PATCH 0249/1729] Analog threshold (#3190) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + .../components/analog_threshold/__init__.py | 1 + .../analog_threshold_binary_sensor.cpp | 40 +++++++++++++++++ .../analog_threshold_binary_sensor.h | 29 ++++++++++++ .../analog_threshold/binary_sensor.py | 44 +++++++++++++++++++ tests/test1.yaml | 15 +++++++ 6 files changed, 130 insertions(+) create mode 100644 esphome/components/analog_threshold/__init__.py create mode 100644 esphome/components/analog_threshold/analog_threshold_binary_sensor.cpp create mode 100644 esphome/components/analog_threshold/analog_threshold_binary_sensor.h create mode 100644 esphome/components/analog_threshold/binary_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 165ce52485..f533ff5c47 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -19,6 +19,7 @@ esphome/components/airthings_wave_mini/* @ncareau esphome/components/airthings_wave_plus/* @jeromelaban esphome/components/am43/* @buxtronix esphome/components/am43/cover/* @buxtronix +esphome/components/analog_threshold/* @ianchi esphome/components/animation/* @syndlex esphome/components/anova/* @buxtronix esphome/components/api/* @OttoWinter diff --git a/esphome/components/analog_threshold/__init__.py b/esphome/components/analog_threshold/__init__.py new file mode 100644 index 0000000000..9ae2df986d --- /dev/null +++ b/esphome/components/analog_threshold/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@ianchi"] diff --git a/esphome/components/analog_threshold/analog_threshold_binary_sensor.cpp b/esphome/components/analog_threshold/analog_threshold_binary_sensor.cpp new file mode 100644 index 0000000000..f679b9994f --- /dev/null +++ b/esphome/components/analog_threshold/analog_threshold_binary_sensor.cpp @@ -0,0 +1,40 @@ +#include "analog_threshold_binary_sensor.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace analog_threshold { + +static const char *const TAG = "analog_threshold.binary_sensor"; + +void AnalogThresholdBinarySensor::setup() { + float sensor_value = this->sensor_->get_state(); + + // TRUE state is defined to be when sensor is >= threshold + // so when undefined sensor value initialize to FALSE + if (std::isnan(sensor_value)) { + this->publish_initial_state(false); + } else { + this->publish_initial_state(sensor_value >= (this->lower_threshold_ + this->upper_threshold_) / 2.0f); + } +} + +void AnalogThresholdBinarySensor::set_sensor(sensor::Sensor *analog_sensor) { + this->sensor_ = analog_sensor; + + this->sensor_->add_on_state_callback([this](float sensor_value) { + // if there is an invalid sensor reading, ignore the change and keep the current state + if (!std::isnan(sensor_value)) { + this->publish_state(sensor_value >= (this->state ? this->lower_threshold_ : this->upper_threshold_)); + } + }); +} + +void AnalogThresholdBinarySensor::dump_config() { + LOG_BINARY_SENSOR("", "Analog Threshold Binary Sensor", this); + LOG_SENSOR(" ", "Sensor", this->sensor_); + ESP_LOGCONFIG(TAG, " Upper threshold: %.11f", this->upper_threshold_); + ESP_LOGCONFIG(TAG, " Lower threshold: %.11f", this->lower_threshold_); +} + +} // namespace analog_threshold +} // namespace esphome diff --git a/esphome/components/analog_threshold/analog_threshold_binary_sensor.h b/esphome/components/analog_threshold/analog_threshold_binary_sensor.h new file mode 100644 index 0000000000..619aef1075 --- /dev/null +++ b/esphome/components/analog_threshold/analog_threshold_binary_sensor.h @@ -0,0 +1,29 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/binary_sensor/binary_sensor.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace analog_threshold { + +class AnalogThresholdBinarySensor : public Component, public binary_sensor::BinarySensor { + public: + void dump_config() override; + void setup() override; + + float get_setup_priority() const override { return setup_priority::DATA; } + + void set_sensor(sensor::Sensor *analog_sensor); + void set_upper_threshold(float threshold) { this->upper_threshold_ = threshold; } + void set_lower_threshold(float threshold) { this->lower_threshold_ = threshold; } + + protected: + sensor::Sensor *sensor_{nullptr}; + + float upper_threshold_; + float lower_threshold_; +}; + +} // namespace analog_threshold +} // namespace esphome diff --git a/esphome/components/analog_threshold/binary_sensor.py b/esphome/components/analog_threshold/binary_sensor.py new file mode 100644 index 0000000000..ef4a6044bf --- /dev/null +++ b/esphome/components/analog_threshold/binary_sensor.py @@ -0,0 +1,44 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import binary_sensor, sensor +from esphome.const import ( + CONF_SENSOR_ID, + CONF_THRESHOLD, +) + +analog_threshold_ns = cg.esphome_ns.namespace("analog_threshold") + +AnalogThresholdBinarySensor = analog_threshold_ns.class_( + "AnalogThresholdBinarySensor", binary_sensor.BinarySensor, cg.Component +) + +CONF_UPPER = "upper" +CONF_LOWER = "lower" + +CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(AnalogThresholdBinarySensor), + cv.Required(CONF_SENSOR_ID): cv.use_id(sensor.Sensor), + cv.Required(CONF_THRESHOLD): cv.Any( + cv.float_, + cv.Schema( + {cv.Required(CONF_UPPER): cv.float_, cv.Required(CONF_LOWER): cv.float_} + ), + ), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = await binary_sensor.new_binary_sensor(config) + await cg.register_component(var, config) + + sens = await cg.get_variable(config[CONF_SENSOR_ID]) + cg.add(var.set_sensor(sens)) + + if isinstance(config[CONF_THRESHOLD], float): + cg.add(var.set_upper_threshold(config[CONF_THRESHOLD])) + cg.add(var.set_lower_threshold(config[CONF_THRESHOLD])) + else: + cg.add(var.set_upper_threshold(config[CONF_THRESHOLD][CONF_UPPER])) + cg.add(var.set_lower_threshold(config[CONF_THRESHOLD][CONF_LOWER])) diff --git a/tests/test1.yaml b/tests/test1.yaml index 54343f59c7..e2d5fdc0c5 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1307,6 +1307,21 @@ binary_sensor: ] - platform: as3935 name: "Storm Alert" + - platform: analog_threshold + name: Analog Trheshold 1 + sensor_id: template_sensor + threshold: + upper: 110 + lower: 90 + filters: + - delayed_on: 0s + - delayed_off: 10s + - platform: analog_threshold + name: Analog Trheshold 2 + sensor_id: template_sensor + threshold: 100 + filters: + - invert: pca9685: frequency: 500 From ccce4b19e8dd26f674c9b8d816627f191444c1df Mon Sep 17 00:00:00 2001 From: mipa87 <62723159+mipa87@users.noreply.github.com> Date: Thu, 17 Feb 2022 21:47:31 +0100 Subject: [PATCH 0250/1729] Fix pm1006 polling component definition (#3210) --- esphome/components/pm1006/sensor.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/components/pm1006/sensor.py b/esphome/components/pm1006/sensor.py index 1e648be199..2df9edbf45 100644 --- a/esphome/components/pm1006/sensor.py +++ b/esphome/components/pm1006/sensor.py @@ -16,7 +16,9 @@ CODEOWNERS = ["@habbie"] DEPENDENCIES = ["uart"] pm1006_ns = cg.esphome_ns.namespace("pm1006") -PM1006Component = pm1006_ns.class_("PM1006Component", uart.UARTDevice, cg.Component) +PM1006Component = pm1006_ns.class_( + "PM1006Component", uart.UARTDevice, cg.PollingComponent +) CONFIG_SCHEMA = cv.All( From 140db85d21d5030b9a4fd8f61f848e6433b302a4 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 18 Feb 2022 10:11:22 +1300 Subject: [PATCH 0251/1729] Add LONG LONG flag for arduinojson (#3212) --- esphome/components/json/json_util.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/json/json_util.h b/esphome/components/json/json_util.h index 57fe6107d8..2299a4cfed 100644 --- a/esphome/components/json/json_util.h +++ b/esphome/components/json/json_util.h @@ -4,9 +4,10 @@ #include "esphome/core/helpers.h" -#undef ARDUINOJSON_ENABLE_STD_STRING #define ARDUINOJSON_ENABLE_STD_STRING 1 // NOLINT +#define ARDUINOJSON_USE_LONG_LONG 1 // NOLINT + #include namespace esphome { From b9398897c1f11eb7ddef665a8cf841099b990399 Mon Sep 17 00:00:00 2001 From: Stewart Date: Thu, 17 Feb 2022 00:53:26 +0000 Subject: [PATCH 0252/1729] Set entity-category to diagnostic for debug component (#3209) Co-authored-by: Stewart Morgan Co-authored-by: root --- esphome/components/debug/sensor.py | 29 +++++++++++++++++++++---- esphome/components/debug/text_sensor.py | 6 +++-- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/esphome/components/debug/sensor.py b/esphome/components/debug/sensor.py index deea6fd5ed..f7ea07d138 100644 --- a/esphome/components/debug/sensor.py +++ b/esphome/components/debug/sensor.py @@ -6,6 +6,7 @@ from esphome.const import ( CONF_FRAGMENTATION, CONF_BLOCK, CONF_LOOP_TIME, + ENTITY_CATEGORY_DIAGNOSTIC, UNIT_MILLISECOND, UNIT_PERCENT, UNIT_BYTES, @@ -18,14 +19,34 @@ DEPENDENCIES = ["debug"] CONFIG_SCHEMA = { cv.GenerateID(CONF_DEBUG_ID): cv.use_id(DebugComponent), - cv.Optional(CONF_FREE): sensor.sensor_schema(UNIT_BYTES, ICON_COUNTER, 0), - cv.Optional(CONF_BLOCK): sensor.sensor_schema(UNIT_BYTES, ICON_COUNTER, 0), + cv.Optional(CONF_FREE): sensor.sensor_schema( + unit_of_measurement=UNIT_BYTES, + icon=ICON_COUNTER, + accuracy_decimals=0, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_BLOCK): sensor.sensor_schema( + unit_of_measurement=UNIT_BYTES, + icon=ICON_COUNTER, + accuracy_decimals=0, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), cv.Optional(CONF_FRAGMENTATION): cv.All( cv.only_on_esp8266, cv.require_framework_version(esp8266_arduino=cv.Version(2, 5, 2)), - sensor.sensor_schema(UNIT_PERCENT, ICON_COUNTER, 1), + sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + icon=ICON_COUNTER, + accuracy_decimals=1, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + ), + cv.Optional(CONF_LOOP_TIME): sensor.sensor_schema( + unit_of_measurement=UNIT_MILLISECOND, + icon=ICON_TIMER, + accuracy_decimals=0, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), - cv.Optional(CONF_LOOP_TIME): sensor.sensor_schema(UNIT_MILLISECOND, ICON_TIMER, 0), } diff --git a/esphome/components/debug/text_sensor.py b/esphome/components/debug/text_sensor.py index f8d1016fbf..11e6354f57 100644 --- a/esphome/components/debug/text_sensor.py +++ b/esphome/components/debug/text_sensor.py @@ -1,7 +1,7 @@ from esphome.components import text_sensor import esphome.config_validation as cv import esphome.codegen as cg -from esphome.const import CONF_DEVICE +from esphome.const import CONF_DEVICE, ENTITY_CATEGORY_DIAGNOSTIC from . import CONF_DEBUG_ID, DebugComponent @@ -11,7 +11,9 @@ DEPENDENCIES = ["debug"] CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(CONF_DEBUG_ID): cv.use_id(DebugComponent), - cv.Optional(CONF_DEVICE): text_sensor.text_sensor_schema(), + cv.Optional(CONF_DEVICE): text_sensor.text_sensor_schema( + entity_category=ENTITY_CATEGORY_DIAGNOSTIC + ), } ) From 7dcc4d030bb4eb2997eef44453981e0d1515c45c Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Thu, 17 Feb 2022 11:56:14 +0100 Subject: [PATCH 0253/1729] Fix platformio docker version mismstch (#3215) --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 5d9decbf1b..6735037c1e 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -43,7 +43,7 @@ RUN \ # Ubuntu python3-pip is missing wheel pip3 install --no-cache-dir \ wheel==0.37.1 \ - platformio==5.2.4 \ + platformio==5.2.5 \ # Change some platformio settings && platformio settings set enable_telemetry No \ && platformio settings set check_libraries_interval 1000000 \ From 8886b7e1417823bd12e1f9e37d07ee0c6a28aa1a Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 18 Feb 2022 10:11:22 +1300 Subject: [PATCH 0254/1729] Add LONG LONG flag for arduinojson (#3212) --- esphome/components/json/json_util.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/json/json_util.h b/esphome/components/json/json_util.h index 57fe6107d8..2299a4cfed 100644 --- a/esphome/components/json/json_util.h +++ b/esphome/components/json/json_util.h @@ -4,9 +4,10 @@ #include "esphome/core/helpers.h" -#undef ARDUINOJSON_ENABLE_STD_STRING #define ARDUINOJSON_ENABLE_STD_STRING 1 // NOLINT +#define ARDUINOJSON_USE_LONG_LONG 1 // NOLINT + #include namespace esphome { From 83b7181bcb22262dee690a591099aa036cd06b20 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 18 Feb 2022 10:16:11 +1300 Subject: [PATCH 0255/1729] Bump version to 2022.2.2 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 7d4fc234db..b501428f9e 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2022.2.1" +__version__ = "2022.2.2" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From 3b8bb09ae3709d844b6bc3aecc4e0b32c5b5d166 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 18 Feb 2022 10:27:20 +1300 Subject: [PATCH 0256/1729] Add class as first positional arg to sensor_schema (#3216) --- esphome/components/adc/sensor.py | 2 +- esphome/components/ads1115/sensor.py | 2 +- esphome/components/as3935/sensor.py | 3 - esphome/components/bh1750/sensor.py | 6 +- .../components/binary_sensor_map/sensor.py | 8 +- esphome/components/bl0940/sensor.py | 47 ++-- .../components/ble_client/sensor/__init__.py | 8 +- esphome/components/ble_rssi/sensor.py | 6 +- esphome/components/cd74hc4067/sensor.py | 4 +- esphome/components/ct_clamp/sensor.py | 6 +- esphome/components/dallas/sensor.py | 6 +- esphome/components/daly_bms/sensor.py | 138 +++++----- esphome/components/demo/__init__.py | 11 +- esphome/components/dsmr/sensor.py | 251 +++++++++--------- esphome/components/duty_cycle/sensor.py | 12 +- esphome/components/esp32_hall/sensor.py | 25 +- esphome/components/fingerprint_grow/sensor.py | 7 - esphome/components/gps/__init__.py | 6 - esphome/components/hmc5883l/sensor.py | 2 - .../homeassistant/sensor/__init__.py | 7 +- esphome/components/hrxl_maxsonar_wr/sensor.py | 26 +- esphome/components/hx711/sensor.py | 6 +- esphome/components/ltr390/sensor.py | 20 +- esphome/components/max31855/sensor.py | 6 +- esphome/components/max31856/sensor.py | 6 +- esphome/components/max31865/sensor.py | 6 +- esphome/components/max6675/sensor.py | 10 +- esphome/components/mcp9808/sensor.py | 10 +- .../mqtt_subscribe/sensor/__init__.py | 8 +- esphome/components/nextion/sensor/__init__.py | 2 +- esphome/components/ntc/sensor.py | 6 +- esphome/components/pid/sensor/__init__.py | 6 +- .../components/pipsolar/sensor/__init__.py | 153 +++++++---- esphome/components/pmsx003/sensor.py | 67 +++-- esphome/components/pulse_counter/sensor.py | 6 +- esphome/components/pulse_meter/sensor.py | 5 +- esphome/components/pulse_width/sensor.py | 6 +- esphome/components/qmc5883l/sensor.py | 2 - esphome/components/resistance/sensor.py | 6 +- esphome/components/rotary_encoder/sensor.py | 8 +- esphome/components/ruuvitag/sensor.py | 3 - esphome/components/sdp3x/sensor.py | 6 +- esphome/components/sensor/__init__.py | 5 + esphome/components/sgp40/sensor.py | 6 +- esphome/components/sts3x/sensor.py | 10 +- esphome/components/sun/sensor/__init__.py | 8 +- .../components/teleinfo/sensor/__init__.py | 14 +- .../components/template/sensor/__init__.py | 7 +- esphome/components/tmp102/sensor.py | 10 +- esphome/components/tmp117/sensor.py | 10 +- esphome/components/tof10120/sensor.py | 6 +- .../components/total_daily_energy/sensor.py | 6 +- esphome/components/tsl2561/sensor.py | 6 +- esphome/components/tsl2591/sensor.py | 36 +-- esphome/components/tx20/sensor.py | 2 - esphome/components/ultrasonic/sensor.py | 6 +- esphome/components/uptime/sensor.py | 27 +- esphome/components/vl53l0x/sensor.py | 6 +- esphome/components/wifi_signal/sensor.py | 27 +- .../components/xiaomi_cgpr1/binary_sensor.py | 7 +- .../xiaomi_mjyd02yla/binary_sensor.py | 4 +- esphome/core/defines.h | 1 + 62 files changed, 506 insertions(+), 629 deletions(-) diff --git a/esphome/components/adc/sensor.py b/esphome/components/adc/sensor.py index c812e67a68..5443b9875a 100644 --- a/esphome/components/adc/sensor.py +++ b/esphome/components/adc/sensor.py @@ -133,6 +133,7 @@ ADCSensor = adc_ns.class_( CONFIG_SCHEMA = cv.All( sensor.sensor_schema( + ADCSensor, unit_of_measurement=UNIT_VOLT, accuracy_decimals=2, device_class=DEVICE_CLASS_VOLTAGE, @@ -140,7 +141,6 @@ CONFIG_SCHEMA = cv.All( ) .extend( { - cv.GenerateID(): cv.declare_id(ADCSensor), cv.Required(CONF_PIN): validate_adc_pin, cv.Optional(CONF_RAW, default=False): cv.boolean, cv.SplitDefault(CONF_ATTENUATION, esp32="0db"): cv.All( diff --git a/esphome/components/ads1115/sensor.py b/esphome/components/ads1115/sensor.py index da33a39041..190e641ca3 100644 --- a/esphome/components/ads1115/sensor.py +++ b/esphome/components/ads1115/sensor.py @@ -52,6 +52,7 @@ ADS1115Sensor = ads1115_ns.class_( CONF_ADS1115_ID = "ads1115_id" CONFIG_SCHEMA = ( sensor.sensor_schema( + ADS1115Sensor, unit_of_measurement=UNIT_VOLT, accuracy_decimals=3, device_class=DEVICE_CLASS_VOLTAGE, @@ -59,7 +60,6 @@ CONFIG_SCHEMA = ( ) .extend( { - cv.GenerateID(): cv.declare_id(ADS1115Sensor), cv.GenerateID(CONF_ADS1115_ID): cv.use_id(ADS1115Component), cv.Required(CONF_MULTIPLEXER): cv.enum(MUX, upper=True, space="_"), cv.Required(CONF_GAIN): validate_gain, diff --git a/esphome/components/as3935/sensor.py b/esphome/components/as3935/sensor.py index 271a29e0fc..5a3967ed7e 100644 --- a/esphome/components/as3935/sensor.py +++ b/esphome/components/as3935/sensor.py @@ -4,7 +4,6 @@ from esphome.components import sensor from esphome.const import ( CONF_DISTANCE, CONF_LIGHTNING_ENERGY, - STATE_CLASS_NONE, UNIT_KILOMETER, ICON_SIGNAL_DISTANCE_VARIANT, ICON_FLASH, @@ -20,12 +19,10 @@ CONFIG_SCHEMA = cv.Schema( unit_of_measurement=UNIT_KILOMETER, icon=ICON_SIGNAL_DISTANCE_VARIANT, accuracy_decimals=1, - state_class=STATE_CLASS_NONE, ), cv.Optional(CONF_LIGHTNING_ENERGY): sensor.sensor_schema( icon=ICON_FLASH, accuracy_decimals=1, - state_class=STATE_CLASS_NONE, ), } ).extend(cv.COMPONENT_SCHEMA) diff --git a/esphome/components/bh1750/sensor.py b/esphome/components/bh1750/sensor.py index 156c7bb375..904e716eb8 100644 --- a/esphome/components/bh1750/sensor.py +++ b/esphome/components/bh1750/sensor.py @@ -2,7 +2,6 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor from esphome.const import ( - CONF_ID, CONF_RESOLUTION, DEVICE_CLASS_ILLUMINANCE, STATE_CLASS_MEASUREMENT, @@ -27,6 +26,7 @@ BH1750Sensor = bh1750_ns.class_( CONF_MEASUREMENT_TIME = "measurement_time" CONFIG_SCHEMA = ( sensor.sensor_schema( + BH1750Sensor, unit_of_measurement=UNIT_LUX, accuracy_decimals=1, device_class=DEVICE_CLASS_ILLUMINANCE, @@ -34,7 +34,6 @@ CONFIG_SCHEMA = ( ) .extend( { - cv.GenerateID(): cv.declare_id(BH1750Sensor), cv.Optional(CONF_RESOLUTION, default=0.5): cv.enum( BH1750_RESOLUTIONS, float=True ), @@ -52,9 +51,8 @@ CONFIG_SCHEMA = ( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await sensor.new_sensor(config) await cg.register_component(var, config) - await sensor.register_sensor(var, config) await i2c.register_i2c_device(var, config) cg.add(var.set_resolution(config[CONF_RESOLUTION])) diff --git a/esphome/components/binary_sensor_map/sensor.py b/esphome/components/binary_sensor_map/sensor.py index 946e2f9e62..7ddf0ecf2a 100644 --- a/esphome/components/binary_sensor_map/sensor.py +++ b/esphome/components/binary_sensor_map/sensor.py @@ -3,14 +3,12 @@ import esphome.config_validation as cv from esphome.components import sensor, binary_sensor from esphome.const import ( - CONF_ID, CONF_CHANNELS, CONF_VALUE, CONF_TYPE, ICON_CHECK_CIRCLE_OUTLINE, CONF_BINARY_SENSOR, CONF_GROUP, - STATE_CLASS_NONE, ) DEPENDENCIES = ["binary_sensor"] @@ -33,12 +31,11 @@ entry = { CONFIG_SCHEMA = cv.typed_schema( { CONF_GROUP: sensor.sensor_schema( + BinarySensorMap, icon=ICON_CHECK_CIRCLE_OUTLINE, accuracy_decimals=0, - state_class=STATE_CLASS_NONE, ).extend( { - cv.GenerateID(): cv.declare_id(BinarySensorMap), cv.Required(CONF_CHANNELS): cv.All( cv.ensure_list(entry), cv.Length(min=1) ), @@ -50,9 +47,8 @@ CONFIG_SCHEMA = cv.typed_schema( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await sensor.new_sensor(config) await cg.register_component(var, config) - await sensor.register_sensor(var, config) constant = SENSOR_MAP_TYPES[config[CONF_TYPE]] cg.add(var.set_sensor_type(constant)) diff --git a/esphome/components/bl0940/sensor.py b/esphome/components/bl0940/sensor.py index ce630b7408..9f516a8691 100644 --- a/esphome/components/bl0940/sensor.py +++ b/esphome/components/bl0940/sensor.py @@ -12,9 +12,7 @@ from esphome.const import ( DEVICE_CLASS_POWER, DEVICE_CLASS_VOLTAGE, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, - STATE_CLASS_NONE, UNIT_AMPERE, UNIT_CELSIUS, UNIT_KILOWATT_HOURS, @@ -35,38 +33,39 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(BL0940), cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_CURRENT): sensor.sensor_schema( - UNIT_AMPERE, - ICON_EMPTY, - 2, - DEVICE_CLASS_CURRENT, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_POWER): sensor.sensor_schema( - UNIT_WATT, ICON_EMPTY, 0, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_WATT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_ENERGY): sensor.sensor_schema( - UNIT_KILOWATT_HOURS, - ICON_EMPTY, - 0, - DEVICE_CLASS_ENERGY, - STATE_CLASS_NONE, + unit_of_measurement=UNIT_KILOWATT_HOURS, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ENERGY, ), cv.Optional(CONF_INTERNAL_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 0, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_NONE, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=0, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_EXTERNAL_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 0, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_NONE, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=0, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/ble_client/sensor/__init__.py b/esphome/components/ble_client/sensor/__init__.py index 4aa6a92ba5..71cfb03ae0 100644 --- a/esphome/components/ble_client/sensor/__init__.py +++ b/esphome/components/ble_client/sensor/__init__.py @@ -2,9 +2,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor, ble_client, esp32_ble_tracker from esphome.const import ( - CONF_ID, CONF_LAMBDA, - STATE_CLASS_NONE, CONF_TRIGGER_ID, CONF_SERVICE_UUID, ) @@ -31,12 +29,11 @@ BLESensorNotifyTrigger = ble_client_ns.class_( CONFIG_SCHEMA = cv.All( sensor.sensor_schema( + BLESensor, accuracy_decimals=0, - state_class=STATE_CLASS_NONE, ) .extend( { - cv.GenerateID(): cv.declare_id(BLESensor), cv.Required(CONF_SERVICE_UUID): esp32_ble_tracker.bt_uuid, cv.Required(CONF_CHARACTERISTIC_UUID): esp32_ble_tracker.bt_uuid, cv.Optional(CONF_DESCRIPTOR_UUID): esp32_ble_tracker.bt_uuid, @@ -57,7 +54,7 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await sensor.new_sensor(config) if len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid16_format): cg.add( var.set_service_uuid16(esp32_ble_tracker.as_hex(config[CONF_SERVICE_UUID])) @@ -124,7 +121,6 @@ async def to_code(config): await cg.register_component(var, config) await ble_client.register_ble_node(var, config) cg.add(var.set_enable_notify(config[CONF_NOTIFY])) - await sensor.register_sensor(var, config) for conf in config.get(CONF_ON_NOTIFY, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await ble_client.register_ble_node(trigger, config) diff --git a/esphome/components/ble_rssi/sensor.py b/esphome/components/ble_rssi/sensor.py index 0c4308b11a..fd2c2e5cb1 100644 --- a/esphome/components/ble_rssi/sensor.py +++ b/esphome/components/ble_rssi/sensor.py @@ -4,7 +4,6 @@ from esphome.components import sensor, esp32_ble_tracker from esphome.const import ( CONF_SERVICE_UUID, CONF_MAC_ADDRESS, - CONF_ID, DEVICE_CLASS_SIGNAL_STRENGTH, STATE_CLASS_MEASUREMENT, UNIT_DECIBEL, @@ -19,6 +18,7 @@ BLERSSISensor = ble_rssi_ns.class_( CONFIG_SCHEMA = cv.All( sensor.sensor_schema( + BLERSSISensor, unit_of_measurement=UNIT_DECIBEL, accuracy_decimals=0, device_class=DEVICE_CLASS_SIGNAL_STRENGTH, @@ -26,7 +26,6 @@ CONFIG_SCHEMA = cv.All( ) .extend( { - cv.GenerateID(): cv.declare_id(BLERSSISensor), cv.Optional(CONF_MAC_ADDRESS): cv.mac_address, cv.Optional(CONF_SERVICE_UUID): esp32_ble_tracker.bt_uuid, } @@ -38,10 +37,9 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await sensor.new_sensor(config) await cg.register_component(var, config) await esp32_ble_tracker.register_ble_device(var, config) - await sensor.register_sensor(var, config) if CONF_MAC_ADDRESS in config: cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) diff --git a/esphome/components/cd74hc4067/sensor.py b/esphome/components/cd74hc4067/sensor.py index 7c7cf9ccb7..3eee34b85e 100644 --- a/esphome/components/cd74hc4067/sensor.py +++ b/esphome/components/cd74hc4067/sensor.py @@ -25,6 +25,7 @@ CONF_CD74HC4067_ID = "cd74hc4067_id" CONFIG_SCHEMA = ( sensor.sensor_schema( + CD74HC4067Sensor, unit_of_measurement=UNIT_VOLT, accuracy_decimals=3, device_class=DEVICE_CLASS_VOLTAGE, @@ -33,7 +34,6 @@ CONFIG_SCHEMA = ( ) .extend( { - cv.GenerateID(): cv.declare_id(CD74HC4067Sensor), cv.GenerateID(CONF_CD74HC4067_ID): cv.use_id(CD74HC4067Component), cv.Required(CONF_NUMBER): cv.int_range(0, 15), cv.Required(CONF_SENSOR): cv.use_id(voltage_sampler.VoltageSampler), @@ -47,8 +47,8 @@ async def to_code(config): parent = await cg.get_variable(config[CONF_CD74HC4067_ID]) var = cg.new_Pvariable(config[CONF_ID], parent) - await cg.register_component(var, config) await sensor.register_sensor(var, config) + await cg.register_component(var, config) cg.add(var.set_pin(config[CONF_NUMBER])) sens = await cg.get_variable(config[CONF_SENSOR]) diff --git a/esphome/components/ct_clamp/sensor.py b/esphome/components/ct_clamp/sensor.py index 049905d0a7..18ea5877d2 100644 --- a/esphome/components/ct_clamp/sensor.py +++ b/esphome/components/ct_clamp/sensor.py @@ -3,7 +3,6 @@ import esphome.config_validation as cv from esphome.components import sensor, voltage_sampler from esphome.const import ( CONF_SENSOR, - CONF_ID, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT, UNIT_AMPERE, @@ -19,6 +18,7 @@ CTClampSensor = ct_clamp_ns.class_("CTClampSensor", sensor.Sensor, cg.PollingCom CONFIG_SCHEMA = ( sensor.sensor_schema( + CTClampSensor, unit_of_measurement=UNIT_AMPERE, accuracy_decimals=2, device_class=DEVICE_CLASS_CURRENT, @@ -26,7 +26,6 @@ CONFIG_SCHEMA = ( ) .extend( { - cv.GenerateID(): cv.declare_id(CTClampSensor), cv.Required(CONF_SENSOR): cv.use_id(voltage_sampler.VoltageSampler), cv.Optional( CONF_SAMPLE_DURATION, default="200ms" @@ -38,9 +37,8 @@ CONFIG_SCHEMA = ( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await sensor.new_sensor(config) await cg.register_component(var, config) - await sensor.register_sensor(var, config) sens = await cg.get_variable(config[CONF_SENSOR]) cg.add(var.set_source(sens)) diff --git a/esphome/components/dallas/sensor.py b/esphome/components/dallas/sensor.py index 14ad0efa7b..9288f0a3a6 100644 --- a/esphome/components/dallas/sensor.py +++ b/esphome/components/dallas/sensor.py @@ -9,7 +9,6 @@ from esphome.const import ( DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, - CONF_ID, ) from . import DallasComponent, dallas_ns @@ -17,13 +16,13 @@ DallasTemperatureSensor = dallas_ns.class_("DallasTemperatureSensor", sensor.Sen CONFIG_SCHEMA = cv.All( sensor.sensor_schema( + DallasTemperatureSensor, unit_of_measurement=UNIT_CELSIUS, accuracy_decimals=1, device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ).extend( { - cv.GenerateID(): cv.declare_id(DallasTemperatureSensor), cv.GenerateID(CONF_DALLAS_ID): cv.use_id(DallasComponent), cv.Optional(CONF_ADDRESS): cv.hex_int, cv.Optional(CONF_INDEX): cv.positive_int, @@ -36,7 +35,7 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): hub = await cg.get_variable(config[CONF_DALLAS_ID]) - var = cg.new_Pvariable(config[CONF_ID]) + var = await sensor.new_sensor(config) if CONF_ADDRESS in config: cg.add(var.set_address(config[CONF_ADDRESS])) @@ -49,4 +48,3 @@ async def to_code(config): cg.add(var.set_parent(hub)) cg.add(hub.register_sensor(var)) - await sensor.register_sensor(var, config) diff --git a/esphome/components/daly_bms/sensor.py b/esphome/components/daly_bms/sensor.py index 1d0ee89914..0ba68d3786 100644 --- a/esphome/components/daly_bms/sensor.py +++ b/esphome/components/daly_bms/sensor.py @@ -11,14 +11,11 @@ from esphome.const import ( DEVICE_CLASS_CURRENT, DEVICE_CLASS_BATTERY, DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT, - STATE_CLASS_NONE, UNIT_VOLT, UNIT_AMPERE, UNIT_PERCENT, UNIT_CELSIUS, - UNIT_EMPTY, ICON_FLASH, ICON_PERCENT, ICON_COUNTER, @@ -70,109 +67,94 @@ CONFIG_SCHEMA = cv.All( { cv.GenerateID(CONF_BMS_DALY_ID): cv.use_id(DalyBmsComponent), cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( - UNIT_VOLT, - ICON_FLASH, - 1, - DEVICE_CLASS_VOLTAGE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_VOLT, + icon=ICON_FLASH, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_CURRENT): sensor.sensor_schema( - UNIT_AMPERE, - ICON_CURRENT_DC, - 1, - DEVICE_CLASS_CURRENT, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_AMPERE, + icon=ICON_CURRENT_DC, + accuracy_decimals=1, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( - UNIT_PERCENT, - ICON_PERCENT, - 1, - DEVICE_CLASS_BATTERY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + icon=ICON_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_MAX_CELL_VOLTAGE): sensor.sensor_schema( - UNIT_VOLT, - ICON_FLASH, - 2, - DEVICE_CLASS_VOLTAGE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_VOLT, + icon=ICON_FLASH, + accuracy_decimals=2, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_MAX_CELL_VOLTAGE_NUMBER): sensor.sensor_schema( - UNIT_EMPTY, - ICON_COUNTER, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_NONE, + icon=ICON_COUNTER, + accuracy_decimals=0, ), cv.Optional(CONF_MIN_CELL_VOLTAGE): sensor.sensor_schema( - UNIT_VOLT, - ICON_FLASH, - 2, - DEVICE_CLASS_VOLTAGE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_VOLT, + icon=ICON_FLASH, + accuracy_decimals=2, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_MIN_CELL_VOLTAGE_NUMBER): sensor.sensor_schema( - UNIT_EMPTY, - ICON_COUNTER, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_NONE, + icon=ICON_COUNTER, + accuracy_decimals=0, ), cv.Optional(CONF_MAX_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_THERMOMETER_CHEVRON_UP, - 0, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER_CHEVRON_UP, + accuracy_decimals=0, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_MAX_TEMPERATURE_PROBE_NUMBER): sensor.sensor_schema( - UNIT_EMPTY, - ICON_COUNTER, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_NONE, + icon=ICON_COUNTER, + accuracy_decimals=0, ), cv.Optional(CONF_MIN_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_THERMOMETER_CHEVRON_DOWN, - 0, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER_CHEVRON_DOWN, + accuracy_decimals=0, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_MIN_TEMPERATURE_PROBE_NUMBER): sensor.sensor_schema( - UNIT_EMPTY, - ICON_COUNTER, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_NONE, + icon=ICON_COUNTER, + accuracy_decimals=0, ), cv.Optional(CONF_REMAINING_CAPACITY): sensor.sensor_schema( - UNIT_AMPERE_HOUR, - ICON_GAUGE, - 2, - DEVICE_CLASS_VOLTAGE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_AMPERE_HOUR, + icon=ICON_GAUGE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_CELLS_NUMBER): sensor.sensor_schema( - UNIT_EMPTY, - ICON_COUNTER, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_NONE, + icon=ICON_COUNTER, + accuracy_decimals=0, ), cv.Optional(CONF_TEMPERATURE_1): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_THERMOMETER, - 0, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_TEMPERATURE_2): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_THERMOMETER, - 0, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), } ).extend(cv.COMPONENT_SCHEMA) diff --git a/esphome/components/demo/__init__.py b/esphome/components/demo/__init__.py index f20a96ebd4..77b1680d26 100644 --- a/esphome/components/demo/__init__.py +++ b/esphome/components/demo/__init__.py @@ -337,12 +337,8 @@ CONFIG_SCHEMA = cv.Schema( }, ], ): [ - sensor.sensor_schema(accuracy_decimals=0) - .extend(cv.polling_component_schema("60s")) - .extend( - { - cv.GenerateID(): cv.declare_id(DemoSensor), - } + sensor.sensor_schema(DemoSensor, accuracy_decimals=0).extend( + cv.polling_component_schema("60s") ) ], cv.Optional( @@ -427,9 +423,8 @@ async def to_code(config): cg.add(var.set_type(conf[CONF_TYPE])) for conf in config[CONF_SENSORS]: - var = cg.new_Pvariable(conf[CONF_ID]) + var = await sensor.new_sensor(conf) await cg.register_component(var, conf) - await sensor.register_sensor(var, conf) for conf in config[CONF_SWITCHES]: var = cg.new_Pvariable(conf[CONF_ID]) diff --git a/esphome/components/dsmr/sensor.py b/esphome/components/dsmr/sensor.py index d809d0d105..bb4722655c 100644 --- a/esphome/components/dsmr/sensor.py +++ b/esphome/components/dsmr/sensor.py @@ -2,19 +2,16 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor from esphome.const import ( + CONF_ID, DEVICE_CLASS_CURRENT, - DEVICE_CLASS_EMPTY, DEVICE_CLASS_ENERGY, DEVICE_CLASS_GAS, DEVICE_CLASS_POWER, DEVICE_CLASS_VOLTAGE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, - STATE_CLASS_NONE, STATE_CLASS_TOTAL_INCREASING, UNIT_AMPERE, UNIT_CUBIC_METER, - UNIT_EMPTY, UNIT_KILOWATT, UNIT_KILOWATT_HOURS, UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, @@ -30,202 +27,214 @@ CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(CONF_DSMR_ID): cv.use_id(Dsmr), cv.Optional("energy_delivered_lux"): sensor.sensor_schema( - UNIT_KILOWATT_HOURS, - ICON_EMPTY, - 3, - DEVICE_CLASS_ENERGY, - STATE_CLASS_TOTAL_INCREASING, + unit_of_measurement=UNIT_KILOWATT_HOURS, + accuracy_decimals=3, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional("energy_delivered_tariff1"): sensor.sensor_schema( - UNIT_KILOWATT_HOURS, - ICON_EMPTY, - 3, - DEVICE_CLASS_ENERGY, - STATE_CLASS_TOTAL_INCREASING, + unit_of_measurement=UNIT_KILOWATT_HOURS, + accuracy_decimals=3, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional("energy_delivered_tariff2"): sensor.sensor_schema( - UNIT_KILOWATT_HOURS, - ICON_EMPTY, - 3, - DEVICE_CLASS_ENERGY, - STATE_CLASS_TOTAL_INCREASING, + unit_of_measurement=UNIT_KILOWATT_HOURS, + accuracy_decimals=3, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional("energy_returned_lux"): sensor.sensor_schema( - UNIT_KILOWATT_HOURS, - ICON_EMPTY, - 3, - DEVICE_CLASS_ENERGY, - STATE_CLASS_TOTAL_INCREASING, + unit_of_measurement=UNIT_KILOWATT_HOURS, + accuracy_decimals=3, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional("energy_returned_tariff1"): sensor.sensor_schema( - UNIT_KILOWATT_HOURS, - ICON_EMPTY, - 3, - DEVICE_CLASS_ENERGY, - STATE_CLASS_TOTAL_INCREASING, + unit_of_measurement=UNIT_KILOWATT_HOURS, + accuracy_decimals=3, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional("energy_returned_tariff2"): sensor.sensor_schema( - UNIT_KILOWATT_HOURS, - ICON_EMPTY, - 3, - DEVICE_CLASS_ENERGY, - STATE_CLASS_TOTAL_INCREASING, + unit_of_measurement=UNIT_KILOWATT_HOURS, + accuracy_decimals=3, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional("total_imported_energy"): sensor.sensor_schema( - UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, - ICON_EMPTY, - 3, - DEVICE_CLASS_EMPTY, - STATE_CLASS_NONE, + unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, + accuracy_decimals=3, ), cv.Optional("total_exported_energy"): sensor.sensor_schema( - UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, - ICON_EMPTY, - 3, - DEVICE_CLASS_EMPTY, - STATE_CLASS_NONE, + unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, + accuracy_decimals=3, ), cv.Optional("power_delivered"): sensor.sensor_schema( - UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_KILOWATT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("power_returned"): sensor.sensor_schema( - UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_KILOWATT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("reactive_power_delivered"): sensor.sensor_schema( - UNIT_KILOVOLT_AMPS_REACTIVE, - ICON_EMPTY, - 3, - DEVICE_CLASS_POWER, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE, + accuracy_decimals=3, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("reactive_power_returned"): sensor.sensor_schema( - UNIT_KILOVOLT_AMPS_REACTIVE, - ICON_EMPTY, - 3, - DEVICE_CLASS_POWER, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE, + accuracy_decimals=3, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("electricity_threshold"): sensor.sensor_schema( - UNIT_EMPTY, ICON_EMPTY, 3, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + accuracy_decimals=3, ), cv.Optional("electricity_switch_position"): sensor.sensor_schema( - UNIT_EMPTY, ICON_EMPTY, 3, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + accuracy_decimals=3, ), cv.Optional("electricity_failures"): sensor.sensor_schema( - UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + accuracy_decimals=0, ), cv.Optional("electricity_long_failures"): sensor.sensor_schema( - UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + accuracy_decimals=0, ), cv.Optional("electricity_sags_l1"): sensor.sensor_schema( - UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + accuracy_decimals=0, ), cv.Optional("electricity_sags_l2"): sensor.sensor_schema( - UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + accuracy_decimals=0, ), cv.Optional("electricity_sags_l3"): sensor.sensor_schema( - UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + accuracy_decimals=0, ), cv.Optional("electricity_swells_l1"): sensor.sensor_schema( - UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + accuracy_decimals=0, ), cv.Optional("electricity_swells_l2"): sensor.sensor_schema( - UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + accuracy_decimals=0, ), cv.Optional("electricity_swells_l3"): sensor.sensor_schema( - UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + accuracy_decimals=0, ), cv.Optional("current_l1"): sensor.sensor_schema( - UNIT_AMPERE, ICON_EMPTY, 1, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=1, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("current_l2"): sensor.sensor_schema( - UNIT_AMPERE, ICON_EMPTY, 1, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=1, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("current_l3"): sensor.sensor_schema( - UNIT_AMPERE, ICON_EMPTY, 1, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=1, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("power_delivered_l1"): sensor.sensor_schema( - UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_KILOWATT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("power_delivered_l2"): sensor.sensor_schema( - UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_KILOWATT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("power_delivered_l3"): sensor.sensor_schema( - UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_KILOWATT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("power_returned_l1"): sensor.sensor_schema( - UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_KILOWATT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("power_returned_l2"): sensor.sensor_schema( - UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_KILOWATT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("power_returned_l3"): sensor.sensor_schema( - UNIT_KILOWATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_KILOWATT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("reactive_power_delivered_l1"): sensor.sensor_schema( - UNIT_KILOVOLT_AMPS_REACTIVE, - ICON_EMPTY, - 3, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE, + accuracy_decimals=3, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("reactive_power_delivered_l2"): sensor.sensor_schema( - UNIT_KILOVOLT_AMPS_REACTIVE, - ICON_EMPTY, - 3, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE, + accuracy_decimals=3, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("reactive_power_delivered_l3"): sensor.sensor_schema( - UNIT_KILOVOLT_AMPS_REACTIVE, - ICON_EMPTY, - 3, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE, + accuracy_decimals=3, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("reactive_power_returned_l1"): sensor.sensor_schema( - UNIT_KILOVOLT_AMPS_REACTIVE, - ICON_EMPTY, - 3, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE, + accuracy_decimals=3, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("reactive_power_returned_l2"): sensor.sensor_schema( - UNIT_KILOVOLT_AMPS_REACTIVE, - ICON_EMPTY, - 3, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE, + accuracy_decimals=3, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("reactive_power_returned_l3"): sensor.sensor_schema( - UNIT_KILOVOLT_AMPS_REACTIVE, - ICON_EMPTY, - 3, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE, + accuracy_decimals=3, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("voltage_l1"): sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE, STATE_CLASS_NONE + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("voltage_l2"): sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE, STATE_CLASS_NONE + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("voltage_l3"): sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE, STATE_CLASS_NONE + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("gas_delivered"): sensor.sensor_schema( - UNIT_CUBIC_METER, - ICON_EMPTY, - 3, - DEVICE_CLASS_GAS, - STATE_CLASS_TOTAL_INCREASING, + unit_of_measurement=UNIT_CUBIC_METER, + accuracy_decimals=3, + device_class=DEVICE_CLASS_GAS, + state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional("gas_delivered_be"): sensor.sensor_schema( - UNIT_CUBIC_METER, - ICON_EMPTY, - 3, - DEVICE_CLASS_GAS, - STATE_CLASS_TOTAL_INCREASING, + unit_of_measurement=UNIT_CUBIC_METER, + accuracy_decimals=3, + device_class=DEVICE_CLASS_GAS, + state_class=STATE_CLASS_TOTAL_INCREASING, ), } ).extend(cv.COMPONENT_SCHEMA) @@ -238,10 +247,10 @@ async def to_code(config): for key, conf in config.items(): if not isinstance(conf, dict): continue - id = conf.get("id") + id = conf[CONF_ID] if id and id.type == sensor.Sensor: - s = await sensor.new_sensor(conf) - cg.add(getattr(hub, f"set_{key}")(s)) + sens = await sensor.new_sensor(conf) + cg.add(getattr(hub, f"set_{key}")(sens)) sensors.append(f"F({key})") if sensors: diff --git a/esphome/components/duty_cycle/sensor.py b/esphome/components/duty_cycle/sensor.py index 6a367328e6..3dcdf7a818 100644 --- a/esphome/components/duty_cycle/sensor.py +++ b/esphome/components/duty_cycle/sensor.py @@ -3,7 +3,6 @@ import esphome.config_validation as cv from esphome import pins from esphome.components import sensor from esphome.const import ( - CONF_ID, CONF_PIN, STATE_CLASS_MEASUREMENT, UNIT_PERCENT, @@ -17,25 +16,20 @@ DutyCycleSensor = duty_cycle_ns.class_( CONFIG_SCHEMA = ( sensor.sensor_schema( + DutyCycleSensor, unit_of_measurement=UNIT_PERCENT, icon=ICON_PERCENT, accuracy_decimals=1, state_class=STATE_CLASS_MEASUREMENT, ) - .extend( - { - cv.GenerateID(): cv.declare_id(DutyCycleSensor), - cv.Required(CONF_PIN): cv.All(pins.internal_gpio_input_pin_schema), - } - ) + .extend({cv.Required(CONF_PIN): cv.All(pins.internal_gpio_input_pin_schema)}) .extend(cv.polling_component_schema("60s")) ) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await sensor.new_sensor(config) await cg.register_component(var, config) - await sensor.register_sensor(var, config) pin = await cg.gpio_pin_expression(config[CONF_PIN]) cg.add(var.set_pin(pin)) diff --git a/esphome/components/esp32_hall/sensor.py b/esphome/components/esp32_hall/sensor.py index a752da2c97..0c94224ef8 100644 --- a/esphome/components/esp32_hall/sensor.py +++ b/esphome/components/esp32_hall/sensor.py @@ -2,7 +2,6 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor from esphome.const import ( - CONF_ID, STATE_CLASS_MEASUREMENT, UNIT_MICROTESLA, ICON_MAGNET, @@ -15,23 +14,15 @@ ESP32HallSensor = esp32_hall_ns.class_( "ESP32HallSensor", sensor.Sensor, cg.PollingComponent ) -CONFIG_SCHEMA = ( - sensor.sensor_schema( - unit_of_measurement=UNIT_MICROTESLA, - icon=ICON_MAGNET, - accuracy_decimals=1, - state_class=STATE_CLASS_MEASUREMENT, - ) - .extend( - { - cv.GenerateID(): cv.declare_id(ESP32HallSensor), - } - ) - .extend(cv.polling_component_schema("60s")) -) +CONFIG_SCHEMA = sensor.sensor_schema( + ESP32HallSensor, + unit_of_measurement=UNIT_MICROTESLA, + icon=ICON_MAGNET, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, +).extend(cv.polling_component_schema("60s")) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await sensor.new_sensor(config) await cg.register_component(var, config) - await sensor.register_sensor(var, config) diff --git a/esphome/components/fingerprint_grow/sensor.py b/esphome/components/fingerprint_grow/sensor.py index 4ae670743d..ed4e431dcc 100644 --- a/esphome/components/fingerprint_grow/sensor.py +++ b/esphome/components/fingerprint_grow/sensor.py @@ -14,7 +14,6 @@ from esphome.const import ( ICON_DATABASE, ICON_FINGERPRINT, ICON_SECURITY, - STATE_CLASS_NONE, ) from . import CONF_FINGERPRINT_GROW_ID, FingerprintGrowComponent @@ -26,36 +25,30 @@ CONFIG_SCHEMA = cv.Schema( cv.Optional(CONF_FINGERPRINT_COUNT): sensor.sensor_schema( icon=ICON_FINGERPRINT, accuracy_decimals=0, - state_class=STATE_CLASS_NONE, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), cv.Optional(CONF_STATUS): sensor.sensor_schema( accuracy_decimals=0, - state_class=STATE_CLASS_NONE, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), cv.Optional(CONF_CAPACITY): sensor.sensor_schema( icon=ICON_DATABASE, accuracy_decimals=0, - state_class=STATE_CLASS_NONE, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), cv.Optional(CONF_SECURITY_LEVEL): sensor.sensor_schema( icon=ICON_SECURITY, accuracy_decimals=0, - state_class=STATE_CLASS_NONE, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), cv.Optional(CONF_LAST_FINGER_ID): sensor.sensor_schema( icon=ICON_ACCOUNT, accuracy_decimals=0, - state_class=STATE_CLASS_NONE, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), cv.Optional(CONF_LAST_CONFIDENCE): sensor.sensor_schema( icon=ICON_ACCOUNT_CHECK, accuracy_decimals=0, - state_class=STATE_CLASS_NONE, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), } diff --git a/esphome/components/gps/__init__.py b/esphome/components/gps/__init__.py index e485373175..d4cf79b49e 100644 --- a/esphome/components/gps/__init__.py +++ b/esphome/components/gps/__init__.py @@ -11,7 +11,6 @@ from esphome.const import ( CONF_ALTITUDE, CONF_SATELLITES, STATE_CLASS_MEASUREMENT, - STATE_CLASS_NONE, UNIT_DEGREES, UNIT_KILOMETER_PER_HOUR, UNIT_METER, @@ -35,27 +34,22 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_LATITUDE): sensor.sensor_schema( unit_of_measurement=UNIT_DEGREES, accuracy_decimals=6, - state_class=STATE_CLASS_NONE, ), cv.Optional(CONF_LONGITUDE): sensor.sensor_schema( unit_of_measurement=UNIT_DEGREES, accuracy_decimals=6, - state_class=STATE_CLASS_NONE, ), cv.Optional(CONF_SPEED): sensor.sensor_schema( unit_of_measurement=UNIT_KILOMETER_PER_HOUR, accuracy_decimals=6, - state_class=STATE_CLASS_NONE, ), cv.Optional(CONF_COURSE): sensor.sensor_schema( unit_of_measurement=UNIT_DEGREES, accuracy_decimals=2, - state_class=STATE_CLASS_NONE, ), cv.Optional(CONF_ALTITUDE): sensor.sensor_schema( unit_of_measurement=UNIT_METER, accuracy_decimals=1, - state_class=STATE_CLASS_NONE, ), cv.Optional(CONF_SATELLITES): sensor.sensor_schema( accuracy_decimals=0, diff --git a/esphome/components/hmc5883l/sensor.py b/esphome/components/hmc5883l/sensor.py index 9d8701079e..26e8e2b60c 100644 --- a/esphome/components/hmc5883l/sensor.py +++ b/esphome/components/hmc5883l/sensor.py @@ -8,7 +8,6 @@ from esphome.const import ( CONF_RANGE, ICON_MAGNET, STATE_CLASS_MEASUREMENT, - STATE_CLASS_NONE, UNIT_MICROTESLA, UNIT_DEGREES, ICON_SCREEN_ROTATION, @@ -88,7 +87,6 @@ heading_schema = sensor.sensor_schema( unit_of_measurement=UNIT_DEGREES, icon=ICON_SCREEN_ROTATION, accuracy_decimals=1, - state_class=STATE_CLASS_NONE, ) CONFIG_SCHEMA = ( diff --git a/esphome/components/homeassistant/sensor/__init__.py b/esphome/components/homeassistant/sensor/__init__.py index cf29db8bb8..28fee9f7f6 100644 --- a/esphome/components/homeassistant/sensor/__init__.py +++ b/esphome/components/homeassistant/sensor/__init__.py @@ -5,7 +5,6 @@ from esphome.const import ( CONF_ATTRIBUTE, CONF_ENTITY_ID, CONF_ID, - STATE_CLASS_NONE, ) from .. import homeassistant_ns @@ -15,12 +14,8 @@ HomeassistantSensor = homeassistant_ns.class_( "HomeassistantSensor", sensor.Sensor, cg.Component ) -CONFIG_SCHEMA = sensor.sensor_schema( - accuracy_decimals=1, - state_class=STATE_CLASS_NONE, -).extend( +CONFIG_SCHEMA = sensor.sensor_schema(HomeassistantSensor, accuracy_decimals=1,).extend( { - cv.GenerateID(): cv.declare_id(HomeassistantSensor), cv.Required(CONF_ENTITY_ID): cv.entity_id, cv.Optional(CONF_ATTRIBUTE): cv.string, } diff --git a/esphome/components/hrxl_maxsonar_wr/sensor.py b/esphome/components/hrxl_maxsonar_wr/sensor.py index dd43bd84a7..a78ae574b1 100644 --- a/esphome/components/hrxl_maxsonar_wr/sensor.py +++ b/esphome/components/hrxl_maxsonar_wr/sensor.py @@ -1,8 +1,6 @@ import esphome.codegen as cg -import esphome.config_validation as cv from esphome.components import sensor, uart from esphome.const import ( - CONF_ID, STATE_CLASS_MEASUREMENT, UNIT_METER, ICON_ARROW_EXPAND_VERTICAL, @@ -16,24 +14,16 @@ HrxlMaxsonarWrComponent = hrxlmaxsonarwr_ns.class_( "HrxlMaxsonarWrComponent", sensor.Sensor, cg.Component, uart.UARTDevice ) -CONFIG_SCHEMA = ( - sensor.sensor_schema( - unit_of_measurement=UNIT_METER, - icon=ICON_ARROW_EXPAND_VERTICAL, - accuracy_decimals=3, - state_class=STATE_CLASS_MEASUREMENT, - ) - .extend( - { - cv.GenerateID(): cv.declare_id(HrxlMaxsonarWrComponent), - } - ) - .extend(uart.UART_DEVICE_SCHEMA) -) +CONFIG_SCHEMA = sensor.sensor_schema( + HrxlMaxsonarWrComponent, + unit_of_measurement=UNIT_METER, + icon=ICON_ARROW_EXPAND_VERTICAL, + accuracy_decimals=3, + state_class=STATE_CLASS_MEASUREMENT, +).extend(uart.UART_DEVICE_SCHEMA) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await sensor.new_sensor(config) await cg.register_component(var, config) - await sensor.register_sensor(var, config) await uart.register_uart_device(var, config) diff --git a/esphome/components/hx711/sensor.py b/esphome/components/hx711/sensor.py index cd06cc770f..88a0bb85b7 100644 --- a/esphome/components/hx711/sensor.py +++ b/esphome/components/hx711/sensor.py @@ -5,7 +5,6 @@ from esphome.components import sensor from esphome.const import ( CONF_CLK_PIN, CONF_GAIN, - CONF_ID, ICON_SCALE, STATE_CLASS_MEASUREMENT, ) @@ -24,13 +23,13 @@ GAINS = { CONFIG_SCHEMA = ( sensor.sensor_schema( + HX711Sensor, icon=ICON_SCALE, accuracy_decimals=0, state_class=STATE_CLASS_MEASUREMENT, ) .extend( { - cv.GenerateID(): cv.declare_id(HX711Sensor), cv.Required(CONF_DOUT_PIN): pins.gpio_input_pin_schema, cv.Required(CONF_CLK_PIN): pins.gpio_output_pin_schema, cv.Optional(CONF_GAIN, default=128): cv.enum(GAINS, int=True), @@ -41,9 +40,8 @@ CONFIG_SCHEMA = ( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await sensor.new_sensor(config) await cg.register_component(var, config) - await sensor.register_sensor(var, config) dout_pin = await cg.gpio_pin_expression(config[CONF_DOUT_PIN]) cg.add(var.set_dout_pin(dout_pin)) diff --git a/esphome/components/ltr390/sensor.py b/esphome/components/ltr390/sensor.py index 0e70f7bb1b..0a765dbe3d 100644 --- a/esphome/components/ltr390/sensor.py +++ b/esphome/components/ltr390/sensor.py @@ -52,16 +52,28 @@ CONFIG_SCHEMA = cv.All( { cv.GenerateID(): cv.declare_id(LTR390Component), cv.Optional(CONF_LIGHT): sensor.sensor_schema( - UNIT_LUX, ICON_BRIGHTNESS_5, 1, DEVICE_CLASS_ILLUMINANCE + unit_of_measurement=UNIT_LUX, + icon=ICON_BRIGHTNESS_5, + accuracy_decimals=1, + device_class=DEVICE_CLASS_ILLUMINANCE, ), cv.Optional(CONF_AMBIENT_LIGHT): sensor.sensor_schema( - UNIT_COUNTS, ICON_BRIGHTNESS_5, 1, DEVICE_CLASS_ILLUMINANCE + unit_of_measurement=UNIT_COUNTS, + icon=ICON_BRIGHTNESS_5, + accuracy_decimals=1, + device_class=DEVICE_CLASS_ILLUMINANCE, ), cv.Optional(CONF_UV_INDEX): sensor.sensor_schema( - UNIT_UVI, ICON_BRIGHTNESS_5, 5, DEVICE_CLASS_ILLUMINANCE + unit_of_measurement=UNIT_UVI, + icon=ICON_BRIGHTNESS_5, + accuracy_decimals=5, + device_class=DEVICE_CLASS_ILLUMINANCE, ), cv.Optional(CONF_UV): sensor.sensor_schema( - UNIT_COUNTS, ICON_BRIGHTNESS_5, 1, DEVICE_CLASS_ILLUMINANCE + unit_of_measurement=UNIT_COUNTS, + icon=ICON_BRIGHTNESS_5, + accuracy_decimals=1, + device_class=DEVICE_CLASS_ILLUMINANCE, ), cv.Optional(CONF_GAIN, default="X3"): cv.enum(GAIN_OPTIONS), cv.Optional(CONF_RESOLUTION, default=18): cv.enum(RES_OPTIONS), diff --git a/esphome/components/max31855/sensor.py b/esphome/components/max31855/sensor.py index c7732dfbe3..0cdedb5464 100644 --- a/esphome/components/max31855/sensor.py +++ b/esphome/components/max31855/sensor.py @@ -2,7 +2,6 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor, spi from esphome.const import ( - CONF_ID, CONF_REFERENCE_TEMPERATURE, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT, @@ -16,13 +15,13 @@ MAX31855Sensor = max31855_ns.class_( CONFIG_SCHEMA = ( sensor.sensor_schema( + MAX31855Sensor, unit_of_measurement=UNIT_CELSIUS, accuracy_decimals=1, device_class=DEVICE_CLASS_TEMPERATURE, ) .extend( { - cv.GenerateID(): cv.declare_id(MAX31855Sensor), cv.Optional(CONF_REFERENCE_TEMPERATURE): sensor.sensor_schema( unit_of_measurement=UNIT_CELSIUS, accuracy_decimals=2, @@ -37,10 +36,9 @@ CONFIG_SCHEMA = ( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await sensor.new_sensor(config) await cg.register_component(var, config) await spi.register_spi_device(var, config) - await sensor.register_sensor(var, config) if CONF_REFERENCE_TEMPERATURE in config: tc_ref = await sensor.new_sensor(config[CONF_REFERENCE_TEMPERATURE]) cg.add(var.set_reference_sensor(tc_ref)) diff --git a/esphome/components/max31856/sensor.py b/esphome/components/max31856/sensor.py index 083d2ac30c..71f1f3bfa5 100644 --- a/esphome/components/max31856/sensor.py +++ b/esphome/components/max31856/sensor.py @@ -2,7 +2,6 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor, spi from esphome.const import ( - CONF_ID, CONF_MAINS_FILTER, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT, @@ -22,6 +21,7 @@ FILTER = { CONFIG_SCHEMA = ( sensor.sensor_schema( + MAX31856Sensor, unit_of_measurement=UNIT_CELSIUS, accuracy_decimals=1, device_class=DEVICE_CLASS_TEMPERATURE, @@ -29,7 +29,6 @@ CONFIG_SCHEMA = ( ) .extend( { - cv.GenerateID(): cv.declare_id(MAX31856Sensor), cv.Optional(CONF_MAINS_FILTER, default="60HZ"): cv.enum( FILTER, upper=True, space="" ), @@ -41,8 +40,7 @@ CONFIG_SCHEMA = ( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await sensor.new_sensor(config) await cg.register_component(var, config) await spi.register_spi_device(var, config) - await sensor.register_sensor(var, config) cg.add(var.set_filter(config[CONF_MAINS_FILTER])) diff --git a/esphome/components/max31865/sensor.py b/esphome/components/max31865/sensor.py index 33d9c42be3..6eb8bd400d 100644 --- a/esphome/components/max31865/sensor.py +++ b/esphome/components/max31865/sensor.py @@ -2,7 +2,6 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor, spi from esphome.const import ( - CONF_ID, CONF_MAINS_FILTER, CONF_REFERENCE_RESISTANCE, CONF_RTD_NOMINAL_RESISTANCE, @@ -25,6 +24,7 @@ FILTER = { CONFIG_SCHEMA = ( sensor.sensor_schema( + MAX31865Sensor, unit_of_measurement=UNIT_CELSIUS, accuracy_decimals=2, device_class=DEVICE_CLASS_TEMPERATURE, @@ -32,7 +32,6 @@ CONFIG_SCHEMA = ( ) .extend( { - cv.GenerateID(): cv.declare_id(MAX31865Sensor), cv.Required(CONF_REFERENCE_RESISTANCE): cv.All( cv.resistance, cv.Range(min=100, max=10000) ), @@ -51,10 +50,9 @@ CONFIG_SCHEMA = ( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await sensor.new_sensor(config) await cg.register_component(var, config) await spi.register_spi_device(var, config) - await sensor.register_sensor(var, config) cg.add(var.set_reference_resistance(config[CONF_REFERENCE_RESISTANCE])) cg.add(var.set_nominal_resistance(config[CONF_RTD_NOMINAL_RESISTANCE])) cg.add(var.set_filter(config[CONF_MAINS_FILTER])) diff --git a/esphome/components/max6675/sensor.py b/esphome/components/max6675/sensor.py index dff8360226..23fc86d2c2 100644 --- a/esphome/components/max6675/sensor.py +++ b/esphome/components/max6675/sensor.py @@ -2,7 +2,6 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor, spi from esphome.const import ( - CONF_ID, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, @@ -15,23 +14,18 @@ MAX6675Sensor = max6675_ns.class_( CONFIG_SCHEMA = ( sensor.sensor_schema( + MAX6675Sensor, unit_of_measurement=UNIT_CELSIUS, accuracy_decimals=1, device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ) - .extend( - { - cv.GenerateID(): cv.declare_id(MAX6675Sensor), - } - ) .extend(cv.polling_component_schema("60s")) .extend(spi.spi_device_schema()) ) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await sensor.new_sensor(config) await cg.register_component(var, config) await spi.register_spi_device(var, config) - await sensor.register_sensor(var, config) diff --git a/esphome/components/mcp9808/sensor.py b/esphome/components/mcp9808/sensor.py index c7f6226e0b..2d7874fe20 100644 --- a/esphome/components/mcp9808/sensor.py +++ b/esphome/components/mcp9808/sensor.py @@ -2,7 +2,6 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor from esphome.const import ( - CONF_ID, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, @@ -18,23 +17,18 @@ MCP9808Sensor = mcp9808_ns.class_( CONFIG_SCHEMA = ( sensor.sensor_schema( + MCP9808Sensor, unit_of_measurement=UNIT_CELSIUS, accuracy_decimals=1, device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ) - .extend( - { - cv.GenerateID(): cv.declare_id(MCP9808Sensor), - } - ) .extend(cv.polling_component_schema("60s")) .extend(i2c.i2c_device_schema(0x18)) ) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await sensor.new_sensor(config) await cg.register_component(var, config) await i2c.register_i2c_device(var, config) - await sensor.register_sensor(var, config) diff --git a/esphome/components/mqtt_subscribe/sensor/__init__.py b/esphome/components/mqtt_subscribe/sensor/__init__.py index 420d4f152c..6fe0c48ae0 100644 --- a/esphome/components/mqtt_subscribe/sensor/__init__.py +++ b/esphome/components/mqtt_subscribe/sensor/__init__.py @@ -2,10 +2,8 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import mqtt, sensor from esphome.const import ( - CONF_ID, CONF_QOS, CONF_TOPIC, - STATE_CLASS_NONE, ) from .. import mqtt_subscribe_ns @@ -18,12 +16,11 @@ MQTTSubscribeSensor = mqtt_subscribe_ns.class_( CONFIG_SCHEMA = ( sensor.sensor_schema( + MQTTSubscribeSensor, accuracy_decimals=1, - state_class=STATE_CLASS_NONE, ) .extend( { - cv.GenerateID(): cv.declare_id(MQTTSubscribeSensor), cv.GenerateID(CONF_MQTT_PARENT_ID): cv.use_id(mqtt.MQTTClientComponent), cv.Required(CONF_TOPIC): cv.subscribe_topic, cv.Optional(CONF_QOS, default=0): cv.mqtt_qos, @@ -34,9 +31,8 @@ CONFIG_SCHEMA = ( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await sensor.new_sensor(config) await cg.register_component(var, config) - await sensor.register_sensor(var, config) parent = await cg.get_variable(config[CONF_MQTT_PARENT_ID]) cg.add(var.set_parent(parent)) diff --git a/esphome/components/nextion/sensor/__init__.py b/esphome/components/nextion/sensor/__init__.py index 8a32adc1f6..b022007ddd 100644 --- a/esphome/components/nextion/sensor/__init__.py +++ b/esphome/components/nextion/sensor/__init__.py @@ -44,11 +44,11 @@ def _validate(config): CONFIG_SCHEMA = cv.All( sensor.sensor_schema( + NextionSensor, accuracy_decimals=2, ) .extend( { - cv.GenerateID(): cv.declare_id(NextionSensor), cv.Optional(CONF_PRECISION, default=0): cv.int_range(min=0, max=8), cv.Optional(CONF_WAVE_CHANNEL_ID): CheckWaveID, cv.Optional(CONF_COMPONENT_ID): cv.uint8_t, diff --git a/esphome/components/ntc/sensor.py b/esphome/components/ntc/sensor.py index 660208635c..ba8d3df9d8 100644 --- a/esphome/components/ntc/sensor.py +++ b/esphome/components/ntc/sensor.py @@ -5,7 +5,6 @@ import esphome.codegen as cg from esphome.components import sensor from esphome.const import ( CONF_CALIBRATION, - CONF_ID, CONF_REFERENCE_RESISTANCE, CONF_REFERENCE_TEMPERATURE, CONF_SENSOR, @@ -117,6 +116,7 @@ def process_calibration(value): CONFIG_SCHEMA = ( sensor.sensor_schema( + NTC, unit_of_measurement=UNIT_CELSIUS, accuracy_decimals=1, device_class=DEVICE_CLASS_TEMPERATURE, @@ -124,7 +124,6 @@ CONFIG_SCHEMA = ( ) .extend( { - cv.GenerateID(): cv.declare_id(NTC), cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor), cv.Required(CONF_CALIBRATION): process_calibration, } @@ -134,9 +133,8 @@ CONFIG_SCHEMA = ( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await sensor.new_sensor(config) await cg.register_component(var, config) - await sensor.register_sensor(var, config) sens = await cg.get_variable(config[CONF_SENSOR]) cg.add(var.set_sensor(sens)) diff --git a/esphome/components/pid/sensor/__init__.py b/esphome/components/pid/sensor/__init__.py index d1007fcbc4..d1c65dfb39 100644 --- a/esphome/components/pid/sensor/__init__.py +++ b/esphome/components/pid/sensor/__init__.py @@ -2,7 +2,6 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor from esphome.const import ( - CONF_ID, STATE_CLASS_MEASUREMENT, UNIT_PERCENT, ICON_GAUGE, @@ -29,6 +28,7 @@ PID_CLIMATE_SENSOR_TYPES = { CONF_CLIMATE_ID = "climate_id" CONFIG_SCHEMA = ( sensor.sensor_schema( + PIDClimateSensor, unit_of_measurement=UNIT_PERCENT, icon=ICON_GAUGE, accuracy_decimals=1, @@ -36,7 +36,6 @@ CONFIG_SCHEMA = ( ) .extend( { - cv.GenerateID(): cv.declare_id(PIDClimateSensor), cv.GenerateID(CONF_CLIMATE_ID): cv.use_id(PIDClimate), cv.Required(CONF_TYPE): cv.enum(PID_CLIMATE_SENSOR_TYPES, upper=True), } @@ -47,8 +46,7 @@ CONFIG_SCHEMA = ( async def to_code(config): parent = await cg.get_variable(config[CONF_CLIMATE_ID]) - var = cg.new_Pvariable(config[CONF_ID]) - await sensor.register_sensor(var, config) + var = await sensor.new_sensor(config) await cg.register_component(var, config) cg.add(var.set_parent(parent)) diff --git a/esphome/components/pipsolar/sensor/__init__.py b/esphome/components/pipsolar/sensor/__init__.py index a206e41988..3a6f94d6ac 100644 --- a/esphome/components/pipsolar/sensor/__init__.py +++ b/esphome/components/pipsolar/sensor/__init__.py @@ -3,18 +3,15 @@ import esphome.config_validation as cv from esphome.components import sensor from esphome.const import ( DEVICE_CLASS_CURRENT, - DEVICE_CLASS_EMPTY, DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_VOLTAGE, ICON_CURRENT_AC, - ICON_EMPTY, UNIT_AMPERE, UNIT_CELSIUS, UNIT_HERTZ, UNIT_PERCENT, UNIT_VOLT, - UNIT_EMPTY, UNIT_VOLT_AMPS, UNIT_WATT, CONF_BUS_VOLTAGE, @@ -74,134 +71,196 @@ CONF_PV_CHARGING_POWER = "pv_charging_power" TYPES = { CONF_GRID_RATING_VOLTAGE: sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, ), CONF_GRID_RATING_CURRENT: sensor.sensor_schema( - UNIT_AMPERE, ICON_EMPTY, 1, DEVICE_CLASS_CURRENT + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=1, + device_class=DEVICE_CLASS_CURRENT, ), CONF_AC_OUTPUT_RATING_VOLTAGE: sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, ), CONF_AC_OUTPUT_RATING_FREQUENCY: sensor.sensor_schema( - UNIT_HERTZ, ICON_CURRENT_AC, 1, DEVICE_CLASS_EMPTY + unit_of_measurement=UNIT_HERTZ, + icon=ICON_CURRENT_AC, + accuracy_decimals=1, ), CONF_AC_OUTPUT_RATING_CURRENT: sensor.sensor_schema( - UNIT_AMPERE, ICON_EMPTY, 1, DEVICE_CLASS_CURRENT + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=1, + device_class=DEVICE_CLASS_CURRENT, ), CONF_AC_OUTPUT_RATING_APPARENT_POWER: sensor.sensor_schema( - UNIT_VOLT_AMPS, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY + unit_of_measurement=UNIT_VOLT_AMPS, + accuracy_decimals=1, ), CONF_AC_OUTPUT_RATING_ACTIVE_POWER: sensor.sensor_schema( - UNIT_WATT, ICON_EMPTY, 1, DEVICE_CLASS_POWER + unit_of_measurement=UNIT_WATT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_POWER, ), CONF_BATTERY_RATING_VOLTAGE: sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, ), CONF_BATTERY_RECHARGE_VOLTAGE: sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, ), CONF_BATTERY_UNDER_VOLTAGE: sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, ), CONF_BATTERY_BULK_VOLTAGE: sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, ), CONF_BATTERY_FLOAT_VOLTAGE: sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, ), CONF_BATTERY_TYPE: sensor.sensor_schema( - UNIT_EMPTY, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY + accuracy_decimals=1, ), CONF_CURRENT_MAX_AC_CHARGING_CURRENT: sensor.sensor_schema( - UNIT_AMPERE, ICON_EMPTY, 1, DEVICE_CLASS_CURRENT + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=1, + device_class=DEVICE_CLASS_CURRENT, ), CONF_CURRENT_MAX_CHARGING_CURRENT: sensor.sensor_schema( - UNIT_AMPERE, ICON_EMPTY, 1, DEVICE_CLASS_CURRENT + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=1, + device_class=DEVICE_CLASS_CURRENT, ), CONF_INPUT_VOLTAGE_RANGE: sensor.sensor_schema( - UNIT_EMPTY, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY + accuracy_decimals=1, ), CONF_OUTPUT_SOURCE_PRIORITY: sensor.sensor_schema( - UNIT_EMPTY, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY + accuracy_decimals=1, ), CONF_CHARGER_SOURCE_PRIORITY: sensor.sensor_schema( - UNIT_EMPTY, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY + accuracy_decimals=1, ), CONF_PARALLEL_MAX_NUM: sensor.sensor_schema( - UNIT_EMPTY, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY + accuracy_decimals=1, ), CONF_MACHINE_TYPE: sensor.sensor_schema( - UNIT_EMPTY, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY + accuracy_decimals=1, + ), + CONF_TOPOLOGY: sensor.sensor_schema( + accuracy_decimals=1, ), - CONF_TOPOLOGY: sensor.sensor_schema(UNIT_EMPTY, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY), CONF_OUTPUT_MODE: sensor.sensor_schema( - UNIT_EMPTY, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY + accuracy_decimals=1, ), CONF_BATTERY_REDISCHARGE_VOLTAGE: sensor.sensor_schema( - UNIT_EMPTY, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY + accuracy_decimals=1, ), CONF_PV_OK_CONDITION_FOR_PARALLEL: sensor.sensor_schema( - UNIT_EMPTY, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY + accuracy_decimals=1, ), CONF_PV_POWER_BALANCE: sensor.sensor_schema( - UNIT_EMPTY, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY + accuracy_decimals=1, ), CONF_GRID_VOLTAGE: sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, ), CONF_GRID_FREQUENCY: sensor.sensor_schema( - UNIT_HERTZ, ICON_CURRENT_AC, 1, DEVICE_CLASS_EMPTY + unit_of_measurement=UNIT_HERTZ, + icon=ICON_CURRENT_AC, + accuracy_decimals=1, ), CONF_AC_OUTPUT_VOLTAGE: sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, ), CONF_AC_OUTPUT_FREQUENCY: sensor.sensor_schema( - UNIT_HERTZ, ICON_CURRENT_AC, 1, DEVICE_CLASS_EMPTY + unit_of_measurement=UNIT_HERTZ, + icon=ICON_CURRENT_AC, + accuracy_decimals=1, ), CONF_AC_OUTPUT_APPARENT_POWER: sensor.sensor_schema( - UNIT_VOLT_AMPS, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY + unit_of_measurement=UNIT_VOLT_AMPS, + accuracy_decimals=1, ), CONF_AC_OUTPUT_ACTIVE_POWER: sensor.sensor_schema( - UNIT_WATT, ICON_EMPTY, 1, DEVICE_CLASS_POWER + unit_of_measurement=UNIT_WATT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_POWER, ), CONF_OUTPUT_LOAD_PERCENT: sensor.sensor_schema( - UNIT_PERCENT, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, ), CONF_BUS_VOLTAGE: sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, ), CONF_BATTERY_VOLTAGE: sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, ), CONF_BATTERY_CHARGING_CURRENT: sensor.sensor_schema( - UNIT_AMPERE, ICON_EMPTY, 1, DEVICE_CLASS_CURRENT + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=1, + device_class=DEVICE_CLASS_CURRENT, ), CONF_BATTERY_CAPACITY_PERCENT: sensor.sensor_schema( - UNIT_PERCENT, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, ), CONF_INVERTER_HEAT_SINK_TEMPERATURE: sensor.sensor_schema( - UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, ), CONF_PV_INPUT_CURRENT_FOR_BATTERY: sensor.sensor_schema( - UNIT_AMPERE, ICON_EMPTY, 1, DEVICE_CLASS_CURRENT + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=1, + device_class=DEVICE_CLASS_CURRENT, ), CONF_PV_INPUT_VOLTAGE: sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, ), CONF_BATTERY_VOLTAGE_SCC: sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, ), CONF_BATTERY_DISCHARGE_CURRENT: sensor.sensor_schema( - UNIT_AMPERE, ICON_EMPTY, 1, DEVICE_CLASS_CURRENT + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=1, + device_class=DEVICE_CLASS_CURRENT, ), CONF_BATTERY_VOLTAGE_OFFSET_FOR_FANS_ON: sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, ), CONF_EEPROM_VERSION: sensor.sensor_schema( - UNIT_EMPTY, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY + accuracy_decimals=1, ), CONF_PV_CHARGING_POWER: sensor.sensor_schema( - UNIT_WATT, ICON_EMPTY, 1, DEVICE_CLASS_POWER + unit_of_measurement=UNIT_WATT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_POWER, ), } diff --git a/esphome/components/pmsx003/sensor.py b/esphome/components/pmsx003/sensor.py index 56a91d22fc..b731e48e31 100644 --- a/esphome/components/pmsx003/sensor.py +++ b/esphome/components/pmsx003/sensor.py @@ -22,7 +22,6 @@ from esphome.const import ( DEVICE_CLASS_PM1, DEVICE_CLASS_PM10, DEVICE_CLASS_PM25, - DEVICE_CLASS_EMPTY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, ICON_CHEMICAL_WEAPON, @@ -75,22 +74,22 @@ CONFIG_SCHEMA = ( cv.GenerateID(): cv.declare_id(PMSX003Component), cv.Required(CONF_TYPE): cv.enum(PMSX003_TYPES, upper=True), cv.Optional(CONF_PM_1_0_STD): sensor.sensor_schema( - UNIT_MICROGRAMS_PER_CUBIC_METER, - ICON_CHEMICAL_WEAPON, - 0, - DEVICE_CLASS_PM1, + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=0, + device_class=DEVICE_CLASS_PM1, ), cv.Optional(CONF_PM_2_5_STD): sensor.sensor_schema( - UNIT_MICROGRAMS_PER_CUBIC_METER, - ICON_CHEMICAL_WEAPON, - 0, - DEVICE_CLASS_PM25, + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=0, + device_class=DEVICE_CLASS_PM25, ), cv.Optional(CONF_PM_10_0_STD): sensor.sensor_schema( - UNIT_MICROGRAMS_PER_CUBIC_METER, - ICON_CHEMICAL_WEAPON, - 0, - DEVICE_CLASS_PM10, + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=0, + device_class=DEVICE_CLASS_PM10, ), cv.Optional(CONF_PM_1_0): sensor.sensor_schema( unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, @@ -111,40 +110,34 @@ CONFIG_SCHEMA = ( state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_0_3UM): sensor.sensor_schema( - UNIT_COUNT_DECILITRE, - ICON_CHEMICAL_WEAPON, - 0, - DEVICE_CLASS_EMPTY, + unit_of_measurement=UNIT_COUNT_DECILITRE, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=0, ), cv.Optional(CONF_PM_0_5UM): sensor.sensor_schema( - UNIT_COUNT_DECILITRE, - ICON_CHEMICAL_WEAPON, - 0, - DEVICE_CLASS_EMPTY, + unit_of_measurement=UNIT_COUNT_DECILITRE, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=0, ), cv.Optional(CONF_PM_1_0UM): sensor.sensor_schema( - UNIT_COUNT_DECILITRE, - ICON_CHEMICAL_WEAPON, - 0, - DEVICE_CLASS_EMPTY, + unit_of_measurement=UNIT_COUNT_DECILITRE, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=0, ), cv.Optional(CONF_PM_2_5UM): sensor.sensor_schema( - UNIT_COUNT_DECILITRE, - ICON_CHEMICAL_WEAPON, - 0, - DEVICE_CLASS_EMPTY, + unit_of_measurement=UNIT_COUNT_DECILITRE, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=0, ), cv.Optional(CONF_PM_5_0UM): sensor.sensor_schema( - UNIT_COUNT_DECILITRE, - ICON_CHEMICAL_WEAPON, - 0, - DEVICE_CLASS_EMPTY, + unit_of_measurement=UNIT_COUNT_DECILITRE, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=0, ), cv.Optional(CONF_PM_10_0UM): sensor.sensor_schema( - UNIT_COUNT_DECILITRE, - ICON_CHEMICAL_WEAPON, - 0, - DEVICE_CLASS_EMPTY, + unit_of_measurement=UNIT_COUNT_DECILITRE, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=0, ), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( unit_of_measurement=UNIT_CELSIUS, diff --git a/esphome/components/pulse_counter/sensor.py b/esphome/components/pulse_counter/sensor.py index c7b89d41b0..6dcb974a1f 100644 --- a/esphome/components/pulse_counter/sensor.py +++ b/esphome/components/pulse_counter/sensor.py @@ -5,7 +5,6 @@ from esphome.components import sensor from esphome.const import ( CONF_COUNT_MODE, CONF_FALLING_EDGE, - CONF_ID, CONF_INTERNAL_FILTER, CONF_PIN, CONF_RISING_EDGE, @@ -66,6 +65,7 @@ def validate_count_mode(value): CONFIG_SCHEMA = ( sensor.sensor_schema( + PulseCounterSensor, unit_of_measurement=UNIT_PULSES_PER_MINUTE, icon=ICON_PULSE, accuracy_decimals=2, @@ -73,7 +73,6 @@ CONFIG_SCHEMA = ( ) .extend( { - cv.GenerateID(): cv.declare_id(PulseCounterSensor), cv.Required(CONF_PIN): validate_pulse_counter_pin, cv.Optional( CONF_COUNT_MODE, @@ -104,9 +103,8 @@ CONFIG_SCHEMA = ( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await sensor.new_sensor(config) await cg.register_component(var, config) - await sensor.register_sensor(var, config) pin = await cg.gpio_pin_expression(config[CONF_PIN]) cg.add(var.set_pin(pin)) diff --git a/esphome/components/pulse_meter/sensor.py b/esphome/components/pulse_meter/sensor.py index 454cb3a69d..fa753b5b05 100644 --- a/esphome/components/pulse_meter/sensor.py +++ b/esphome/components/pulse_meter/sensor.py @@ -50,13 +50,13 @@ def validate_pulse_meter_pin(value): CONFIG_SCHEMA = sensor.sensor_schema( + PulseMeterSensor, unit_of_measurement=UNIT_PULSES_PER_MINUTE, icon=ICON_PULSE, accuracy_decimals=2, state_class=STATE_CLASS_MEASUREMENT, ).extend( { - cv.GenerateID(): cv.declare_id(PulseMeterSensor), cv.Required(CONF_PIN): validate_pulse_meter_pin, cv.Optional(CONF_INTERNAL_FILTER, default="13us"): validate_internal_filter, cv.Optional(CONF_TIMEOUT, default="5min"): validate_timeout, @@ -71,9 +71,8 @@ CONFIG_SCHEMA = sensor.sensor_schema( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await sensor.new_sensor(config) await cg.register_component(var, config) - await sensor.register_sensor(var, config) pin = await cg.gpio_pin_expression(config[CONF_PIN]) cg.add(var.set_pin(pin)) diff --git a/esphome/components/pulse_width/sensor.py b/esphome/components/pulse_width/sensor.py index b090647627..47d70166d3 100644 --- a/esphome/components/pulse_width/sensor.py +++ b/esphome/components/pulse_width/sensor.py @@ -3,7 +3,6 @@ import esphome.config_validation as cv from esphome import pins from esphome.components import sensor from esphome.const import ( - CONF_ID, CONF_PIN, STATE_CLASS_MEASUREMENT, UNIT_SECOND, @@ -18,6 +17,7 @@ PulseWidthSensor = pulse_width_ns.class_( CONFIG_SCHEMA = ( sensor.sensor_schema( + PulseWidthSensor, unit_of_measurement=UNIT_SECOND, icon=ICON_TIMER, accuracy_decimals=3, @@ -25,7 +25,6 @@ CONFIG_SCHEMA = ( ) .extend( { - cv.GenerateID(): cv.declare_id(PulseWidthSensor), cv.Required(CONF_PIN): cv.All(pins.internal_gpio_input_pin_schema), } ) @@ -34,9 +33,8 @@ CONFIG_SCHEMA = ( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await sensor.new_sensor(config) await cg.register_component(var, config) - await sensor.register_sensor(var, config) pin = await cg.gpio_pin_expression(config[CONF_PIN]) cg.add(var.set_pin(pin)) diff --git a/esphome/components/qmc5883l/sensor.py b/esphome/components/qmc5883l/sensor.py index 27d1df5b29..100ac93520 100644 --- a/esphome/components/qmc5883l/sensor.py +++ b/esphome/components/qmc5883l/sensor.py @@ -8,7 +8,6 @@ from esphome.const import ( CONF_RANGE, ICON_MAGNET, STATE_CLASS_MEASUREMENT, - STATE_CLASS_NONE, UNIT_MICROTESLA, UNIT_DEGREES, ICON_SCREEN_ROTATION, @@ -79,7 +78,6 @@ heading_schema = sensor.sensor_schema( unit_of_measurement=UNIT_DEGREES, icon=ICON_SCREEN_ROTATION, accuracy_decimals=1, - state_class=STATE_CLASS_NONE, ) CONFIG_SCHEMA = ( diff --git a/esphome/components/resistance/sensor.py b/esphome/components/resistance/sensor.py index 329192e902..55e7ddfc81 100644 --- a/esphome/components/resistance/sensor.py +++ b/esphome/components/resistance/sensor.py @@ -6,7 +6,6 @@ from esphome.const import ( STATE_CLASS_MEASUREMENT, UNIT_OHM, ICON_FLASH, - CONF_ID, ) resistance_ns = cg.esphome_ns.namespace("resistance") @@ -24,6 +23,7 @@ CONFIGURATIONS = { CONFIG_SCHEMA = ( sensor.sensor_schema( + ResistanceSensor, unit_of_measurement=UNIT_OHM, icon=ICON_FLASH, accuracy_decimals=1, @@ -31,7 +31,6 @@ CONFIG_SCHEMA = ( ) .extend( { - cv.GenerateID(): cv.declare_id(ResistanceSensor), cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor), cv.Required(CONF_CONFIGURATION): cv.enum(CONFIGURATIONS, upper=True), cv.Required(CONF_RESISTOR): cv.resistance, @@ -43,9 +42,8 @@ CONFIG_SCHEMA = ( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await sensor.new_sensor(config) await cg.register_component(var, config) - await sensor.register_sensor(var, config) sens = await cg.get_variable(config[CONF_SENSOR]) cg.add(var.set_sensor(sens)) diff --git a/esphome/components/rotary_encoder/sensor.py b/esphome/components/rotary_encoder/sensor.py index cd747264b3..ae6b0ae3bf 100644 --- a/esphome/components/rotary_encoder/sensor.py +++ b/esphome/components/rotary_encoder/sensor.py @@ -7,7 +7,6 @@ from esphome.const import ( CONF_RESOLUTION, CONF_MIN_VALUE, CONF_MAX_VALUE, - STATE_CLASS_NONE, UNIT_STEPS, ICON_ROTATE_RIGHT, CONF_VALUE, @@ -65,14 +64,13 @@ def validate_min_max_value(config): CONFIG_SCHEMA = cv.All( sensor.sensor_schema( + RotaryEncoderSensor, unit_of_measurement=UNIT_STEPS, icon=ICON_ROTATE_RIGHT, accuracy_decimals=0, - state_class=STATE_CLASS_NONE, ) .extend( { - cv.GenerateID(): cv.declare_id(RotaryEncoderSensor), cv.Required(CONF_PIN_A): cv.All(pins.internal_gpio_input_pin_schema), cv.Required(CONF_PIN_B): cv.All(pins.internal_gpio_input_pin_schema), cv.Optional(CONF_PIN_RESET): pins.internal_gpio_output_pin_schema, @@ -105,9 +103,9 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await sensor.new_sensor(config) await cg.register_component(var, config) - await sensor.register_sensor(var, config) + pin_a = await cg.gpio_pin_expression(config[CONF_PIN_A]) cg.add(var.set_pin_a(pin_a)) pin_b = await cg.gpio_pin_expression(config[CONF_PIN_B]) diff --git a/esphome/components/ruuvitag/sensor.py b/esphome/components/ruuvitag/sensor.py index 2bb9549195..a46daf88ac 100644 --- a/esphome/components/ruuvitag/sensor.py +++ b/esphome/components/ruuvitag/sensor.py @@ -21,7 +21,6 @@ from esphome.const import ( DEVICE_CLASS_VOLTAGE, ENTITY_CATEGORY_DIAGNOSTIC, STATE_CLASS_MEASUREMENT, - STATE_CLASS_NONE, UNIT_CELSIUS, UNIT_PERCENT, UNIT_VOLT, @@ -108,13 +107,11 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_MOVEMENT_COUNTER): sensor.sensor_schema( icon=ICON_GAUGE, accuracy_decimals=0, - state_class=STATE_CLASS_NONE, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), cv.Optional(CONF_MEASUREMENT_SEQUENCE_NUMBER): sensor.sensor_schema( icon=ICON_GAUGE, accuracy_decimals=0, - state_class=STATE_CLASS_NONE, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), } diff --git a/esphome/components/sdp3x/sensor.py b/esphome/components/sdp3x/sensor.py index 45f5cc4d9a..66ee475b11 100644 --- a/esphome/components/sdp3x/sensor.py +++ b/esphome/components/sdp3x/sensor.py @@ -2,7 +2,6 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor from esphome.const import ( - CONF_ID, DEVICE_CLASS_PRESSURE, STATE_CLASS_MEASUREMENT, UNIT_HECTOPASCAL, @@ -24,6 +23,7 @@ CONF_MEASUREMENT_MODE = "measurement_mode" CONFIG_SCHEMA = ( sensor.sensor_schema( + SDP3XComponent, unit_of_measurement=UNIT_HECTOPASCAL, accuracy_decimals=3, device_class=DEVICE_CLASS_PRESSURE, @@ -31,7 +31,6 @@ CONFIG_SCHEMA = ( ) .extend( { - cv.GenerateID(): cv.declare_id(SDP3XComponent), cv.Optional( CONF_MEASUREMENT_MODE, default="differential_pressure" ): cv.enum(MEASUREMENT_MODE), @@ -43,8 +42,7 @@ CONFIG_SCHEMA = ( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await sensor.new_sensor(config) await cg.register_component(var, config) await i2c.register_i2c_device(var, config) - await sensor.register_sensor(var, config) cg.add(var.set_measurement_mode(config[CONF_MEASUREMENT_MODE])) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 14a15da2f1..577596f6ce 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -58,6 +58,7 @@ from esphome.const import ( DEVICE_CLASS_VOLTAGE, ) from esphome.core import CORE, coroutine_with_priority +from esphome.cpp_generator import MockObjClass from esphome.cpp_helpers import setup_entity from esphome.util import Registry @@ -223,6 +224,8 @@ _UNDEF = object() def sensor_schema( + class_: MockObjClass = _UNDEF, + *, unit_of_measurement: str = _UNDEF, icon: str = _UNDEF, accuracy_decimals: int = _UNDEF, @@ -231,6 +234,8 @@ def sensor_schema( entity_category: str = _UNDEF, ) -> cv.Schema: schema = SENSOR_SCHEMA + if class_ is not _UNDEF: + schema = schema.extend({cv.GenerateID(): cv.declare_id(class_)}) if unit_of_measurement is not _UNDEF: schema = schema.extend( { diff --git a/esphome/components/sgp40/sensor.py b/esphome/components/sgp40/sensor.py index 7b96f867af..6f27b54fb0 100644 --- a/esphome/components/sgp40/sensor.py +++ b/esphome/components/sgp40/sensor.py @@ -2,7 +2,6 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor from esphome.const import ( - CONF_ID, ICON_RADIATOR, DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, STATE_CLASS_MEASUREMENT, @@ -25,6 +24,7 @@ CONF_VOC_BASELINE = "voc_baseline" CONFIG_SCHEMA = ( sensor.sensor_schema( + SGP40Component, icon=ICON_RADIATOR, accuracy_decimals=0, device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, @@ -32,7 +32,6 @@ CONFIG_SCHEMA = ( ) .extend( { - cv.GenerateID(): cv.declare_id(SGP40Component), cv.Optional(CONF_STORE_BASELINE, default=True): cv.boolean, cv.Optional(CONF_VOC_BASELINE): cv.hex_uint16_t, cv.Optional(CONF_COMPENSATION): cv.Schema( @@ -49,10 +48,9 @@ CONFIG_SCHEMA = ( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await sensor.new_sensor(config) await cg.register_component(var, config) await i2c.register_i2c_device(var, config) - await sensor.register_sensor(var, config) if CONF_COMPENSATION in config: compensation_config = config[CONF_COMPENSATION] diff --git a/esphome/components/sts3x/sensor.py b/esphome/components/sts3x/sensor.py index b02c835ef8..aa7573aaf2 100644 --- a/esphome/components/sts3x/sensor.py +++ b/esphome/components/sts3x/sensor.py @@ -2,7 +2,6 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor from esphome.const import ( - CONF_ID, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, @@ -18,23 +17,18 @@ STS3XComponent = sts3x_ns.class_( CONFIG_SCHEMA = ( sensor.sensor_schema( + STS3XComponent, unit_of_measurement=UNIT_CELSIUS, accuracy_decimals=1, device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ) - .extend( - { - cv.GenerateID(): cv.declare_id(STS3XComponent), - } - ) .extend(cv.polling_component_schema("60s")) .extend(i2c.i2c_device_schema(0x4A)) ) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await sensor.new_sensor(config) await cg.register_component(var, config) - await sensor.register_sensor(var, config) await i2c.register_i2c_device(var, config) diff --git a/esphome/components/sun/sensor/__init__.py b/esphome/components/sun/sensor/__init__.py index 236acfadef..10c0237327 100644 --- a/esphome/components/sun/sensor/__init__.py +++ b/esphome/components/sun/sensor/__init__.py @@ -2,10 +2,8 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor from esphome.const import ( - STATE_CLASS_NONE, UNIT_DEGREES, ICON_WEATHER_SUNSET, - CONF_ID, CONF_TYPE, ) from .. import sun_ns, CONF_SUN_ID, Sun @@ -21,14 +19,13 @@ TYPES = { CONFIG_SCHEMA = ( sensor.sensor_schema( + SunSensor, unit_of_measurement=UNIT_DEGREES, icon=ICON_WEATHER_SUNSET, accuracy_decimals=1, - state_class=STATE_CLASS_NONE, ) .extend( { - cv.GenerateID(): cv.declare_id(SunSensor), cv.GenerateID(CONF_SUN_ID): cv.use_id(Sun), cv.Required(CONF_TYPE): cv.enum(TYPES, lower=True), } @@ -38,9 +35,8 @@ CONFIG_SCHEMA = ( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await sensor.new_sensor(config) await cg.register_component(var, config) - await sensor.register_sensor(var, config) cg.add(var.set_type(config[CONF_TYPE])) paren = await cg.get_variable(config[CONF_SUN_ID]) diff --git a/esphome/components/teleinfo/sensor/__init__.py b/esphome/components/teleinfo/sensor/__init__.py index aa875be157..ff2d81c95e 100644 --- a/esphome/components/teleinfo/sensor/__init__.py +++ b/esphome/components/teleinfo/sensor/__init__.py @@ -1,5 +1,4 @@ import esphome.codegen as cg -import esphome.config_validation as cv from esphome.components import sensor from esphome.const import CONF_ID, ICON_FLASH, UNIT_WATT_HOURS @@ -13,13 +12,12 @@ from .. import ( TeleInfoSensor = teleinfo_ns.class_("TeleInfoSensor", sensor.Sensor, cg.Component) -CONFIG_SCHEMA = ( - sensor.sensor_schema( - unit_of_measurement=UNIT_WATT_HOURS, icon=ICON_FLASH, accuracy_decimals=0 - ) - .extend({cv.GenerateID(): cv.declare_id(TeleInfoSensor)}) - .extend(TELEINFO_LISTENER_SCHEMA) -) +CONFIG_SCHEMA = sensor.sensor_schema( + TeleInfoSensor, + unit_of_measurement=UNIT_WATT_HOURS, + icon=ICON_FLASH, + accuracy_decimals=0, +).extend(TELEINFO_LISTENER_SCHEMA) async def to_code(config): diff --git a/esphome/components/template/sensor/__init__.py b/esphome/components/template/sensor/__init__.py index 75fb505d91..9ed7a83848 100644 --- a/esphome/components/template/sensor/__init__.py +++ b/esphome/components/template/sensor/__init__.py @@ -6,7 +6,6 @@ from esphome.const import ( CONF_ID, CONF_LAMBDA, CONF_STATE, - STATE_CLASS_NONE, ) from .. import template_ns @@ -16,12 +15,11 @@ TemplateSensor = template_ns.class_( CONFIG_SCHEMA = ( sensor.sensor_schema( + TemplateSensor, accuracy_decimals=1, - state_class=STATE_CLASS_NONE, ) .extend( { - cv.GenerateID(): cv.declare_id(TemplateSensor), cv.Optional(CONF_LAMBDA): cv.returning_lambda, } ) @@ -30,9 +28,8 @@ CONFIG_SCHEMA = ( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await sensor.new_sensor(config) await cg.register_component(var, config) - await sensor.register_sensor(var, config) if CONF_LAMBDA in config: template_ = await cg.process_lambda( diff --git a/esphome/components/tmp102/sensor.py b/esphome/components/tmp102/sensor.py index c5ffbb8df5..57d0afd5a1 100644 --- a/esphome/components/tmp102/sensor.py +++ b/esphome/components/tmp102/sensor.py @@ -11,7 +11,6 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor from esphome.const import ( - CONF_ID, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, @@ -27,23 +26,18 @@ TMP102Component = tmp102_ns.class_( CONFIG_SCHEMA = ( sensor.sensor_schema( + TMP102Component, unit_of_measurement=UNIT_CELSIUS, accuracy_decimals=1, device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ) - .extend( - { - cv.GenerateID(): cv.declare_id(TMP102Component), - } - ) .extend(cv.polling_component_schema("60s")) .extend(i2c.i2c_device_schema(0x48)) ) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await sensor.new_sensor(config) await cg.register_component(var, config) await i2c.register_i2c_device(var, config) - await sensor.register_sensor(var, config) diff --git a/esphome/components/tmp117/sensor.py b/esphome/components/tmp117/sensor.py index 054864dd83..fb97258bc1 100644 --- a/esphome/components/tmp117/sensor.py +++ b/esphome/components/tmp117/sensor.py @@ -2,7 +2,6 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor from esphome.const import ( - CONF_ID, CONF_UPDATE_INTERVAL, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT, @@ -19,16 +18,12 @@ TMP117Component = tmp117_ns.class_( CONFIG_SCHEMA = cv.All( sensor.sensor_schema( + TMP117Component, unit_of_measurement=UNIT_CELSIUS, accuracy_decimals=1, device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ) - .extend( - { - cv.GenerateID(): cv.declare_id(TMP117Component), - } - ) .extend(cv.polling_component_schema("60s")) .extend(i2c.i2c_device_schema(0x48)) ) @@ -77,10 +72,9 @@ def determine_config_register(polling_period): async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await sensor.new_sensor(config) await cg.register_component(var, config) await i2c.register_i2c_device(var, config) - await sensor.register_sensor(var, config) update_period = config[CONF_UPDATE_INTERVAL].total_seconds cg.add(var.set_config(determine_config_register(update_period))) diff --git a/esphome/components/tof10120/sensor.py b/esphome/components/tof10120/sensor.py index 2d3add2399..21d6d48659 100644 --- a/esphome/components/tof10120/sensor.py +++ b/esphome/components/tof10120/sensor.py @@ -2,7 +2,6 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor from esphome.const import ( - CONF_ID, STATE_CLASS_MEASUREMENT, UNIT_METER, ICON_ARROW_EXPAND_VERTICAL, @@ -18,19 +17,18 @@ TOF10120Sensor = tof10120_ns.class_( CONFIG_SCHEMA = ( sensor.sensor_schema( + TOF10120Sensor, unit_of_measurement=UNIT_METER, icon=ICON_ARROW_EXPAND_VERTICAL, accuracy_decimals=3, state_class=STATE_CLASS_MEASUREMENT, ) - .extend({cv.GenerateID(): cv.declare_id(TOF10120Sensor)}) .extend(cv.polling_component_schema("60s")) .extend(i2c.i2c_device_schema(0x52)) ) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await sensor.new_sensor(config) await cg.register_component(var, config) - await sensor.register_sensor(var, config) await i2c.register_i2c_device(var, config) diff --git a/esphome/components/total_daily_energy/sensor.py b/esphome/components/total_daily_energy/sensor.py index 1af8db8332..3698563aff 100644 --- a/esphome/components/total_daily_energy/sensor.py +++ b/esphome/components/total_daily_energy/sensor.py @@ -40,12 +40,12 @@ def inherit_accuracy_decimals(decimals, config): CONFIG_SCHEMA = ( sensor.sensor_schema( + TotalDailyEnergy, device_class=DEVICE_CLASS_ENERGY, state_class=STATE_CLASS_TOTAL_INCREASING, ) .extend( { - cv.GenerateID(): cv.declare_id(TotalDailyEnergy), cv.GenerateID(CONF_TIME_ID): cv.use_id(time.RealTimeClock), cv.Required(CONF_POWER_ID): cv.use_id(sensor.Sensor), cv.Optional(CONF_RESTORE, default=True): cv.boolean, @@ -82,10 +82,8 @@ FINAL_VALIDATE_SCHEMA = cv.All( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - + var = await sensor.new_sensor(config) await cg.register_component(var, config) - await sensor.register_sensor(var, config) sens = await cg.get_variable(config[CONF_POWER_ID]) cg.add(var.set_parent(sens)) diff --git a/esphome/components/tsl2561/sensor.py b/esphome/components/tsl2561/sensor.py index cf3837cb4d..fb2c00697b 100644 --- a/esphome/components/tsl2561/sensor.py +++ b/esphome/components/tsl2561/sensor.py @@ -3,7 +3,6 @@ import esphome.config_validation as cv from esphome.components import i2c, sensor from esphome.const import ( CONF_GAIN, - CONF_ID, CONF_INTEGRATION_TIME, DEVICE_CLASS_ILLUMINANCE, STATE_CLASS_MEASUREMENT, @@ -40,6 +39,7 @@ TSL2561Sensor = tsl2561_ns.class_( CONFIG_SCHEMA = ( sensor.sensor_schema( + TSL2561Sensor, unit_of_measurement=UNIT_LUX, accuracy_decimals=1, device_class=DEVICE_CLASS_ILLUMINANCE, @@ -47,7 +47,6 @@ CONFIG_SCHEMA = ( ) .extend( { - cv.GenerateID(): cv.declare_id(TSL2561Sensor), cv.Optional( CONF_INTEGRATION_TIME, default="402ms" ): validate_integration_time, @@ -61,10 +60,9 @@ CONFIG_SCHEMA = ( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await sensor.new_sensor(config) await cg.register_component(var, config) await i2c.register_i2c_device(var, config) - await sensor.register_sensor(var, config) cg.add(var.set_integration_time(config[CONF_INTEGRATION_TIME])) cg.add(var.set_gain(config[CONF_GAIN])) diff --git a/esphome/components/tsl2591/sensor.py b/esphome/components/tsl2591/sensor.py index 1ec37b5f93..63a0733365 100644 --- a/esphome/components/tsl2591/sensor.py +++ b/esphome/components/tsl2591/sensor.py @@ -35,10 +35,8 @@ from esphome.const import ( CONF_DEVICE_FACTOR, CONF_GLASS_ATTENUATION_FACTOR, ICON_BRIGHTNESS_6, - DEVICE_CLASS_EMPTY, DEVICE_CLASS_ILLUMINANCE, STATE_CLASS_MEASUREMENT, - UNIT_EMPTY, UNIT_LUX, ) @@ -87,32 +85,26 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(TSL2591Component), cv.Optional(CONF_INFRARED): sensor.sensor_schema( - UNIT_EMPTY, - ICON_BRIGHTNESS_6, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + icon=ICON_BRIGHTNESS_6, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_VISIBLE): sensor.sensor_schema( - UNIT_EMPTY, - ICON_BRIGHTNESS_6, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + icon=ICON_BRIGHTNESS_6, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_FULL_SPECTRUM): sensor.sensor_schema( - UNIT_EMPTY, - ICON_BRIGHTNESS_6, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + icon=ICON_BRIGHTNESS_6, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_CALCULATED_LUX): sensor.sensor_schema( - UNIT_LUX, - ICON_BRIGHTNESS_6, - 4, - DEVICE_CLASS_ILLUMINANCE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_LUX, + icon=ICON_BRIGHTNESS_6, + accuracy_decimals=4, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional( CONF_INTEGRATION_TIME, default="100ms" diff --git a/esphome/components/tx20/sensor.py b/esphome/components/tx20/sensor.py index 84df82b5e6..f8a0b08d99 100644 --- a/esphome/components/tx20/sensor.py +++ b/esphome/components/tx20/sensor.py @@ -8,7 +8,6 @@ from esphome.const import ( CONF_PIN, CONF_WIND_DIRECTION_DEGREES, STATE_CLASS_MEASUREMENT, - STATE_CLASS_NONE, UNIT_KILOMETER_PER_HOUR, ICON_WEATHER_WINDY, ICON_SIGN_DIRECTION, @@ -31,7 +30,6 @@ CONFIG_SCHEMA = cv.Schema( unit_of_measurement=UNIT_DEGREES, icon=ICON_SIGN_DIRECTION, accuracy_decimals=1, - state_class=STATE_CLASS_NONE, ), cv.Required(CONF_PIN): cv.All(pins.internal_gpio_input_pin_schema), } diff --git a/esphome/components/ultrasonic/sensor.py b/esphome/components/ultrasonic/sensor.py index f7026e884c..afbd1128c2 100644 --- a/esphome/components/ultrasonic/sensor.py +++ b/esphome/components/ultrasonic/sensor.py @@ -4,7 +4,6 @@ from esphome import pins from esphome.components import sensor from esphome.const import ( CONF_ECHO_PIN, - CONF_ID, CONF_TRIGGER_PIN, CONF_TIMEOUT, STATE_CLASS_MEASUREMENT, @@ -21,6 +20,7 @@ UltrasonicSensorComponent = ultrasonic_ns.class_( CONFIG_SCHEMA = ( sensor.sensor_schema( + UltrasonicSensorComponent, unit_of_measurement=UNIT_METER, icon=ICON_ARROW_EXPAND_VERTICAL, accuracy_decimals=2, @@ -28,7 +28,6 @@ CONFIG_SCHEMA = ( ) .extend( { - cv.GenerateID(): cv.declare_id(UltrasonicSensorComponent), cv.Required(CONF_TRIGGER_PIN): pins.gpio_output_pin_schema, cv.Required(CONF_ECHO_PIN): pins.internal_gpio_input_pin_schema, cv.Optional(CONF_TIMEOUT, default="2m"): cv.distance, @@ -42,9 +41,8 @@ CONFIG_SCHEMA = ( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await sensor.new_sensor(config) await cg.register_component(var, config) - await sensor.register_sensor(var, config) trigger = await cg.gpio_pin_expression(config[CONF_TRIGGER_PIN]) cg.add(var.set_trigger_pin(trigger)) diff --git a/esphome/components/uptime/sensor.py b/esphome/components/uptime/sensor.py index 16a1e4c125..103bc3a666 100644 --- a/esphome/components/uptime/sensor.py +++ b/esphome/components/uptime/sensor.py @@ -2,7 +2,6 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor from esphome.const import ( - CONF_ID, ENTITY_CATEGORY_DIAGNOSTIC, STATE_CLASS_TOTAL_INCREASING, UNIT_SECOND, @@ -12,24 +11,16 @@ from esphome.const import ( uptime_ns = cg.esphome_ns.namespace("uptime") UptimeSensor = uptime_ns.class_("UptimeSensor", sensor.Sensor, cg.PollingComponent) -CONFIG_SCHEMA = ( - sensor.sensor_schema( - unit_of_measurement=UNIT_SECOND, - icon=ICON_TIMER, - accuracy_decimals=0, - state_class=STATE_CLASS_TOTAL_INCREASING, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - ) - .extend( - { - cv.GenerateID(): cv.declare_id(UptimeSensor), - } - ) - .extend(cv.polling_component_schema("60s")) -) +CONFIG_SCHEMA = sensor.sensor_schema( + UptimeSensor, + unit_of_measurement=UNIT_SECOND, + icon=ICON_TIMER, + accuracy_decimals=0, + state_class=STATE_CLASS_TOTAL_INCREASING, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, +).extend(cv.polling_component_schema("60s")) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await sensor.new_sensor(config) await cg.register_component(var, config) - await sensor.register_sensor(var, config) diff --git a/esphome/components/vl53l0x/sensor.py b/esphome/components/vl53l0x/sensor.py index 0ce3197366..7b485e3887 100644 --- a/esphome/components/vl53l0x/sensor.py +++ b/esphome/components/vl53l0x/sensor.py @@ -2,7 +2,6 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor from esphome.const import ( - CONF_ID, STATE_CLASS_MEASUREMENT, UNIT_METER, ICON_ARROW_EXPAND_VERTICAL, @@ -41,6 +40,7 @@ def check_timeout(value): CONFIG_SCHEMA = cv.All( sensor.sensor_schema( + VL53L0XSensor, unit_of_measurement=UNIT_METER, icon=ICON_ARROW_EXPAND_VERTICAL, accuracy_decimals=2, @@ -48,7 +48,6 @@ CONFIG_SCHEMA = cv.All( ) .extend( { - cv.GenerateID(): cv.declare_id(VL53L0XSensor), cv.Optional(CONF_SIGNAL_RATE_LIMIT, default=0.25): cv.float_range( min=0.0, max=512.0, min_included=False, max_included=False ), @@ -64,7 +63,7 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await sensor.new_sensor(config) await cg.register_component(var, config) cg.add(var.set_signal_rate_limit(config[CONF_SIGNAL_RATE_LIMIT])) cg.add(var.set_long_range(config[CONF_LONG_RANGE])) @@ -74,5 +73,4 @@ async def to_code(config): enable = await cg.gpio_pin_expression(config[CONF_ENABLE_PIN]) cg.add(var.set_enable_pin(enable)) - await sensor.register_sensor(var, config) await i2c.register_i2c_device(var, config) diff --git a/esphome/components/wifi_signal/sensor.py b/esphome/components/wifi_signal/sensor.py index 2097c21bd7..77fabf272e 100644 --- a/esphome/components/wifi_signal/sensor.py +++ b/esphome/components/wifi_signal/sensor.py @@ -2,7 +2,6 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor from esphome.const import ( - CONF_ID, DEVICE_CLASS_SIGNAL_STRENGTH, ENTITY_CATEGORY_DIAGNOSTIC, STATE_CLASS_MEASUREMENT, @@ -15,24 +14,16 @@ WiFiSignalSensor = wifi_signal_ns.class_( "WiFiSignalSensor", sensor.Sensor, cg.PollingComponent ) -CONFIG_SCHEMA = ( - sensor.sensor_schema( - unit_of_measurement=UNIT_DECIBEL_MILLIWATT, - accuracy_decimals=0, - device_class=DEVICE_CLASS_SIGNAL_STRENGTH, - state_class=STATE_CLASS_MEASUREMENT, - entity_category=ENTITY_CATEGORY_DIAGNOSTIC, - ) - .extend( - { - cv.GenerateID(): cv.declare_id(WiFiSignalSensor), - } - ) - .extend(cv.polling_component_schema("60s")) -) +CONFIG_SCHEMA = sensor.sensor_schema( + WiFiSignalSensor, + unit_of_measurement=UNIT_DECIBEL_MILLIWATT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, +).extend(cv.polling_component_schema("60s")) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await sensor.new_sensor(config) await cg.register_component(var, config) - await sensor.register_sensor(var, config) diff --git a/esphome/components/xiaomi_cgpr1/binary_sensor.py b/esphome/components/xiaomi_cgpr1/binary_sensor.py index 7f0aac873d..4297b7fc3b 100644 --- a/esphome/components/xiaomi_cgpr1/binary_sensor.py +++ b/esphome/components/xiaomi_cgpr1/binary_sensor.py @@ -7,12 +7,10 @@ from esphome.const import ( CONF_DEVICE_CLASS, CONF_MAC_ADDRESS, CONF_ID, - DEVICE_CLASS_EMPTY, DEVICE_CLASS_BATTERY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_MOTION, ENTITY_CATEGORY_DIAGNOSTIC, - ICON_EMPTY, UNIT_PERCENT, CONF_IDLE_TIME, CONF_ILLUMINANCE, @@ -52,11 +50,12 @@ CONFIG_SCHEMA = cv.All( unit_of_measurement=UNIT_MINUTE, icon=ICON_TIMELAPSE, accuracy_decimals=0, - device_class=DEVICE_CLASS_EMPTY, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema( - UNIT_LUX, ICON_EMPTY, 0, DEVICE_CLASS_ILLUMINANCE + unit_of_measurement=UNIT_LUX, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ILLUMINANCE, ), } ) diff --git a/esphome/components/xiaomi_mjyd02yla/binary_sensor.py b/esphome/components/xiaomi_mjyd02yla/binary_sensor.py index 1bedae26cf..6a7d6aae79 100644 --- a/esphome/components/xiaomi_mjyd02yla/binary_sensor.py +++ b/esphome/components/xiaomi_mjyd02yla/binary_sensor.py @@ -12,7 +12,6 @@ from esphome.const import ( DEVICE_CLASS_ILLUMINANCE, ENTITY_CATEGORY_DIAGNOSTIC, STATE_CLASS_MEASUREMENT, - STATE_CLASS_NONE, UNIT_PERCENT, CONF_IDLE_TIME, CONF_ILLUMINANCE, @@ -22,7 +21,7 @@ from esphome.const import ( ) DEPENDENCIES = ["esp32_ble_tracker"] -AUTO_LOAD = ["xiaomi_ble"] +AUTO_LOAD = ["xiaomi_ble", "sensor"] xiaomi_mjyd02yla_ns = cg.esphome_ns.namespace("xiaomi_mjyd02yla") XiaomiMJYD02YLA = xiaomi_mjyd02yla_ns.class_( @@ -45,7 +44,6 @@ CONFIG_SCHEMA = cv.All( unit_of_measurement=UNIT_MINUTE, icon=ICON_TIMELAPSE, accuracy_decimals=0, - state_class=STATE_CLASS_NONE, ), cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( unit_of_measurement=UNIT_PERCENT, diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 574a8dcafe..aabb5510f4 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -57,6 +57,7 @@ // ESP32-specific feature flags #ifdef USE_ESP32 +#define USE_ESP32_BLE_CLIENT #define USE_ESP32_BLE_SERVER #define USE_ESP32_CAMERA #define USE_ESP32_IGNORE_EFUSE_MAC_CRC From 7a242bb4ed9cb78f8a78be3e8cc091c895caa52e Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 18 Feb 2022 10:39:59 +1300 Subject: [PATCH 0257/1729] Binary Sensor codegen tidyup (#3217) --- esphome/components/apds9960/binary_sensor.py | 11 +++-- esphome/components/as3935/binary_sensor.py | 2 +- esphome/components/binary_sensor/__init__.py | 40 ++++++++++++++++++- .../components/ble_presence/binary_sensor.py | 8 ++-- esphome/components/ble_scanner/text_sensor.py | 2 +- esphome/components/cap1188/binary_sensor.py | 8 ++-- .../custom/binary_sensor/__init__.py | 2 +- esphome/components/daly_bms/binary_sensor.py | 18 ++------- esphome/components/demo/__init__.py | 11 ++--- .../components/esp32_touch/binary_sensor.py | 3 +- .../fingerprint_grow/binary_sensor.py | 2 +- .../components/gpio/binary_sensor/__init__.py | 20 +++++----- .../homeassistant/binary_sensor/__init__.py | 22 +++++----- .../binary_sensor/__init__.py | 4 +- esphome/components/mpr121/binary_sensor.py | 8 ++-- .../nextion/binary_sensor/__init__.py | 4 +- .../nextion/text_sensor/__init__.py | 2 +- .../pipsolar/binary_sensor/__init__.py | 11 ++--- esphome/components/pn532/binary_sensor.py | 8 ++-- esphome/components/rc522/binary_sensor.py | 8 ++-- esphome/components/rdm6300/binary_sensor.py | 8 ++-- esphome/components/remote_base/__init__.py | 2 +- esphome/components/sim800l/binary_sensor.py | 14 ++----- esphome/components/status/binary_sensor.py | 20 +++------- .../sx1509/binary_sensor/__init__.py | 8 ++-- .../teleinfo/text_sensor/__init__.py | 2 +- .../template/binary_sensor/__init__.py | 18 +++++---- esphome/components/text_sensor/__init__.py | 7 ++-- .../touchscreen/binary_sensor/__init__.py | 11 +++-- .../components/ttp229_bsf/binary_sensor.py | 8 ++-- .../components/ttp229_lsf/binary_sensor.py | 8 ++-- .../components/tuya/binary_sensor/__init__.py | 23 ++++++----- esphome/components/wifi_info/text_sensor.py | 10 ++--- .../components/xiaomi_cgpr1/binary_sensor.py | 25 +++++------- .../xiaomi_mjyd02yla/binary_sensor.py | 24 +++++------ .../xiaomi_mue4094rt/binary_sensor.py | 18 +++++---- .../components/xiaomi_wx08zm/binary_sensor.py | 10 ++--- esphome/components/xpt2046/binary_sensor.py | 8 ++-- 38 files changed, 196 insertions(+), 222 deletions(-) diff --git a/esphome/components/apds9960/binary_sensor.py b/esphome/components/apds9960/binary_sensor.py index 4a5c69f6a9..04dc6f4d5d 100644 --- a/esphome/components/apds9960/binary_sensor.py +++ b/esphome/components/apds9960/binary_sensor.py @@ -1,7 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import binary_sensor -from esphome.const import CONF_DIRECTION, CONF_DEVICE_CLASS, DEVICE_CLASS_MOVING +from esphome.const import CONF_DIRECTION, DEVICE_CLASS_MOVING from . import APDS9960, CONF_APDS9960_ID DEPENDENCIES = ["apds9960"] @@ -13,13 +13,12 @@ DIRECTIONS = { "RIGHT": "set_right_direction", } -CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend( +CONFIG_SCHEMA = binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_MOVING +).extend( { - cv.Required(CONF_DIRECTION): cv.one_of(*DIRECTIONS, upper=True), cv.GenerateID(CONF_APDS9960_ID): cv.use_id(APDS9960), - cv.Optional( - CONF_DEVICE_CLASS, default=DEVICE_CLASS_MOVING - ): binary_sensor.device_class, + cv.Required(CONF_DIRECTION): cv.one_of(*DIRECTIONS, upper=True), } ) diff --git a/esphome/components/as3935/binary_sensor.py b/esphome/components/as3935/binary_sensor.py index 11b2ac812c..3081d2115f 100644 --- a/esphome/components/as3935/binary_sensor.py +++ b/esphome/components/as3935/binary_sensor.py @@ -5,7 +5,7 @@ from . import AS3935, CONF_AS3935_ID DEPENDENCIES = ["as3935"] -CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend( +CONFIG_SCHEMA = binary_sensor.binary_sensor_schema().extend( { cv.GenerateID(CONF_AS3935_ID): cv.use_id(AS3935), } diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index c6065ddae4..40f95d72f9 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -1,5 +1,6 @@ import esphome.codegen as cg import esphome.config_validation as cv +from esphome.cpp_generator import MockObjClass from esphome.cpp_helpers import setup_entity from esphome import automation, core from esphome.automation import Condition, maybe_simple_id @@ -7,7 +8,9 @@ from esphome.components import mqtt from esphome.const import ( CONF_DELAY, CONF_DEVICE_CLASS, + CONF_ENTITY_CATEGORY, CONF_FILTERS, + CONF_ICON, CONF_ID, CONF_INVALID_COOLDOWN, CONF_INVERTED, @@ -314,7 +317,7 @@ def validate_multi_click_timing(value): return timings -device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_") +validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_") BINARY_SENSOR_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).extend( @@ -323,7 +326,7 @@ BINARY_SENSOR_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).ex cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id( mqtt.MQTTBinarySensorComponent ), - cv.Optional(CONF_DEVICE_CLASS): device_class, + cv.Optional(CONF_DEVICE_CLASS): validate_device_class, cv.Optional(CONF_FILTERS): validate_filters, cv.Optional(CONF_ON_PRESS): automation.validate_automation( { @@ -376,6 +379,39 @@ BINARY_SENSOR_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).ex } ) +_UNDEF = object() + + +def binary_sensor_schema( + class_: MockObjClass = _UNDEF, + *, + icon: str = _UNDEF, + entity_category: str = _UNDEF, + device_class: str = _UNDEF, +) -> cv.Schema: + schema = BINARY_SENSOR_SCHEMA + if class_ is not _UNDEF: + schema = schema.extend({cv.GenerateID(): cv.declare_id(class_)}) + if icon is not _UNDEF: + schema = schema.extend({cv.Optional(CONF_ICON, default=icon): cv.icon}) + if entity_category is not _UNDEF: + schema = schema.extend( + { + cv.Optional( + CONF_ENTITY_CATEGORY, default=entity_category + ): cv.entity_category + } + ) + if device_class is not _UNDEF: + schema = schema.extend( + { + cv.Optional( + CONF_DEVICE_CLASS, default=device_class + ): validate_device_class + } + ) + return schema + async def setup_binary_sensor_core_(var, config): await setup_entity(var, config) diff --git a/esphome/components/ble_presence/binary_sensor.py b/esphome/components/ble_presence/binary_sensor.py index 2a242c3aca..67f2c3516f 100644 --- a/esphome/components/ble_presence/binary_sensor.py +++ b/esphome/components/ble_presence/binary_sensor.py @@ -7,7 +7,6 @@ from esphome.const import ( CONF_IBEACON_MAJOR, CONF_IBEACON_MINOR, CONF_IBEACON_UUID, - CONF_ID, ) DEPENDENCIES = ["esp32_ble_tracker"] @@ -30,9 +29,9 @@ def _validate(config): CONFIG_SCHEMA = cv.All( - binary_sensor.BINARY_SENSOR_SCHEMA.extend( + binary_sensor.binary_sensor_schema(BLEPresenceDevice) + .extend( { - cv.GenerateID(): cv.declare_id(BLEPresenceDevice), cv.Optional(CONF_MAC_ADDRESS): cv.mac_address, cv.Optional(CONF_SERVICE_UUID): esp32_ble_tracker.bt_uuid, cv.Optional(CONF_IBEACON_MAJOR): cv.uint16_t, @@ -48,10 +47,9 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await binary_sensor.new_binary_sensor(config) await cg.register_component(var, config) await esp32_ble_tracker.register_ble_device(var, config) - await binary_sensor.register_binary_sensor(var, config) if CONF_MAC_ADDRESS in config: cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) diff --git a/esphome/components/ble_scanner/text_sensor.py b/esphome/components/ble_scanner/text_sensor.py index 31dccdf119..743403c6a4 100644 --- a/esphome/components/ble_scanner/text_sensor.py +++ b/esphome/components/ble_scanner/text_sensor.py @@ -13,7 +13,7 @@ BLEScanner = ble_scanner_ns.class_( ) CONFIG_SCHEMA = cv.All( - text_sensor.text_sensor_schema(klass=BLEScanner) + text_sensor.text_sensor_schema(BLEScanner) .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) .extend(cv.COMPONENT_SCHEMA) ) diff --git a/esphome/components/cap1188/binary_sensor.py b/esphome/components/cap1188/binary_sensor.py index c249eb7330..7950774340 100644 --- a/esphome/components/cap1188/binary_sensor.py +++ b/esphome/components/cap1188/binary_sensor.py @@ -1,15 +1,14 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import binary_sensor -from esphome.const import CONF_CHANNEL, CONF_ID +from esphome.const import CONF_CHANNEL from . import cap1188_ns, CAP1188Component, CONF_CAP1188_ID DEPENDENCIES = ["cap1188"] CAP1188Channel = cap1188_ns.class_("CAP1188Channel", binary_sensor.BinarySensor) -CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend( +CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(CAP1188Channel).extend( { - cv.GenerateID(): cv.declare_id(CAP1188Channel), cv.GenerateID(CONF_CAP1188_ID): cv.use_id(CAP1188Component), cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=7), } @@ -17,8 +16,7 @@ CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await binary_sensor.register_binary_sensor(var, config) + var = await binary_sensor.new_binary_sensor(config) hub = await cg.get_variable(config[CONF_CAP1188_ID]) cg.add(var.set_channel(config[CONF_CHANNEL])) diff --git a/esphome/components/custom/binary_sensor/__init__.py b/esphome/components/custom/binary_sensor/__init__.py index 18d613d4c1..8d6d621b3a 100644 --- a/esphome/components/custom/binary_sensor/__init__.py +++ b/esphome/components/custom/binary_sensor/__init__.py @@ -11,7 +11,7 @@ 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 + binary_sensor.binary_sensor_schema() ), } ) diff --git a/esphome/components/daly_bms/binary_sensor.py b/esphome/components/daly_bms/binary_sensor.py index 23330cd945..7b252b5e89 100644 --- a/esphome/components/daly_bms/binary_sensor.py +++ b/esphome/components/daly_bms/binary_sensor.py @@ -1,7 +1,6 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import binary_sensor -from esphome.const import CONF_ID from . import DalyBmsComponent, CONF_BMS_DALY_ID CONF_CHARGING_MOS_ENABLED = "charging_mos_enabled" @@ -18,18 +17,10 @@ CONFIG_SCHEMA = cv.All( cv.GenerateID(CONF_BMS_DALY_ID): cv.use_id(DalyBmsComponent), cv.Optional( CONF_CHARGING_MOS_ENABLED - ): binary_sensor.BINARY_SENSOR_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(binary_sensor.BinarySensor), - } - ), + ): binary_sensor.binary_sensor_schema(), cv.Optional( CONF_DISCHARGING_MOS_ENABLED - ): binary_sensor.BINARY_SENSOR_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(binary_sensor.BinarySensor), - } - ), + ): binary_sensor.binary_sensor_schema(), } ).extend(cv.COMPONENT_SCHEMA) ) @@ -38,9 +29,8 @@ CONFIG_SCHEMA = cv.All( async def setup_conf(config, key, hub): if key in config: conf = config[key] - sens = cg.new_Pvariable(conf[CONF_ID]) - await binary_sensor.register_binary_sensor(sens, conf) - cg.add(getattr(hub, f"set_{key}_binary_sensor")(sens)) + var = await binary_sensor.new_binary_sensor(conf) + cg.add(getattr(hub, f"set_{key}_binary_sensor")(var)) async def to_code(config): diff --git a/esphome/components/demo/__init__.py b/esphome/components/demo/__init__.py index 77b1680d26..7aa742919a 100644 --- a/esphome/components/demo/__init__.py +++ b/esphome/components/demo/__init__.py @@ -132,12 +132,8 @@ CONFIG_SCHEMA = cv.Schema( }, ], ): [ - binary_sensor.BINARY_SENSOR_SCHEMA.extend( + binary_sensor.binary_sensor_schema(DemoBinarySensor).extend( cv.polling_component_schema("60s") - ).extend( - { - cv.GenerateID(): cv.declare_id(DemoBinarySensor), - } ) ], cv.Optional( @@ -372,7 +368,7 @@ CONFIG_SCHEMA = cv.Schema( }, ], ): [ - text_sensor.text_sensor_schema(klass=DemoTextSensor).extend( + text_sensor.text_sensor_schema(DemoTextSensor).extend( cv.polling_component_schema("60s") ) ], @@ -382,9 +378,8 @@ CONFIG_SCHEMA = cv.Schema( async def to_code(config): for conf in config[CONF_BINARY_SENSORS]: - var = cg.new_Pvariable(conf[CONF_ID]) + var = await binary_sensor.new_binary_sensor(conf) await cg.register_component(var, conf) - await binary_sensor.register_binary_sensor(var, conf) for conf in config[CONF_CLIMATES]: var = cg.new_Pvariable(conf[CONF_ID]) diff --git a/esphome/components/esp32_touch/binary_sensor.py b/esphome/components/esp32_touch/binary_sensor.py index bd3e06545d..326f559830 100644 --- a/esphome/components/esp32_touch/binary_sensor.py +++ b/esphome/components/esp32_touch/binary_sensor.py @@ -39,9 +39,8 @@ ESP32TouchBinarySensor = esp32_touch_ns.class_( "ESP32TouchBinarySensor", binary_sensor.BinarySensor ) -CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend( +CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(ESP32TouchBinarySensor).extend( { - cv.GenerateID(): cv.declare_id(ESP32TouchBinarySensor), cv.GenerateID(CONF_ESP32_TOUCH_ID): cv.use_id(ESP32TouchComponent), cv.Required(CONF_PIN): validate_touch_pad, cv.Required(CONF_THRESHOLD): cv.uint16_t, diff --git a/esphome/components/fingerprint_grow/binary_sensor.py b/esphome/components/fingerprint_grow/binary_sensor.py index f432ef92cc..8572919e36 100644 --- a/esphome/components/fingerprint_grow/binary_sensor.py +++ b/esphome/components/fingerprint_grow/binary_sensor.py @@ -6,7 +6,7 @@ from . import CONF_FINGERPRINT_GROW_ID, FingerprintGrowComponent DEPENDENCIES = ["fingerprint_grow"] -CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend( +CONFIG_SCHEMA = binary_sensor.binary_sensor_schema().extend( { cv.GenerateID(CONF_FINGERPRINT_GROW_ID): cv.use_id(FingerprintGrowComponent), cv.Optional(CONF_ICON, default=ICON_KEY_PLUS): cv.icon, diff --git a/esphome/components/gpio/binary_sensor/__init__.py b/esphome/components/gpio/binary_sensor/__init__.py index 4d91b81a44..786c3f4b96 100644 --- a/esphome/components/gpio/binary_sensor/__init__.py +++ b/esphome/components/gpio/binary_sensor/__init__.py @@ -2,25 +2,27 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins from esphome.components import binary_sensor -from esphome.const import CONF_ID, CONF_PIN +from esphome.const import CONF_PIN from .. import gpio_ns GPIOBinarySensor = gpio_ns.class_( "GPIOBinarySensor", binary_sensor.BinarySensor, cg.Component ) -CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(GPIOBinarySensor), - cv.Required(CONF_PIN): pins.gpio_input_pin_schema, - } -).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = ( + binary_sensor.binary_sensor_schema(GPIOBinarySensor) + .extend( + { + cv.Required(CONF_PIN): pins.gpio_input_pin_schema, + } + ) + .extend(cv.COMPONENT_SCHEMA) +) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await binary_sensor.new_binary_sensor(config) await cg.register_component(var, config) - await binary_sensor.register_binary_sensor(var, config) pin = await cg.gpio_pin_expression(config[CONF_PIN]) cg.add(var.set_pin(pin)) diff --git a/esphome/components/homeassistant/binary_sensor/__init__.py b/esphome/components/homeassistant/binary_sensor/__init__.py index 4972466aac..a4f854c16e 100644 --- a/esphome/components/homeassistant/binary_sensor/__init__.py +++ b/esphome/components/homeassistant/binary_sensor/__init__.py @@ -1,7 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import binary_sensor -from esphome.const import CONF_ATTRIBUTE, CONF_ENTITY_ID, CONF_ID +from esphome.const import CONF_ATTRIBUTE, CONF_ENTITY_ID from .. import homeassistant_ns DEPENDENCIES = ["api"] @@ -9,19 +9,21 @@ HomeassistantBinarySensor = homeassistant_ns.class_( "HomeassistantBinarySensor", binary_sensor.BinarySensor, cg.Component ) -CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(HomeassistantBinarySensor), - cv.Required(CONF_ENTITY_ID): cv.entity_id, - cv.Optional(CONF_ATTRIBUTE): cv.string, - } -).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = ( + binary_sensor.binary_sensor_schema(HomeassistantBinarySensor) + .extend( + { + cv.Required(CONF_ENTITY_ID): cv.entity_id, + cv.Optional(CONF_ATTRIBUTE): cv.string, + } + ) + .extend(cv.COMPONENT_SCHEMA) +) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await binary_sensor.new_binary_sensor(config) await cg.register_component(var, config) - await binary_sensor.register_binary_sensor(var, config) cg.add(var.set_entity_id(config[CONF_ENTITY_ID])) if CONF_ATTRIBUTE in config: diff --git a/esphome/components/modbus_controller/binary_sensor/__init__.py b/esphome/components/modbus_controller/binary_sensor/__init__.py index 557d76479d..5315167479 100644 --- a/esphome/components/modbus_controller/binary_sensor/__init__.py +++ b/esphome/components/modbus_controller/binary_sensor/__init__.py @@ -29,11 +29,11 @@ ModbusBinarySensor = modbus_controller_ns.class_( ) CONFIG_SCHEMA = cv.All( - binary_sensor.BINARY_SENSOR_SCHEMA.extend(cv.COMPONENT_SCHEMA) + binary_sensor.binary_sensor_schema(ModbusBinarySensor) + .extend(cv.COMPONENT_SCHEMA) .extend(ModbusItemBaseSchema) .extend( { - cv.GenerateID(): cv.declare_id(ModbusBinarySensor), cv.Optional(CONF_REGISTER_TYPE): cv.enum(MODBUS_REGISTER_TYPE), } ), diff --git a/esphome/components/mpr121/binary_sensor.py b/esphome/components/mpr121/binary_sensor.py index 20b80e063e..131fbcfc5b 100644 --- a/esphome/components/mpr121/binary_sensor.py +++ b/esphome/components/mpr121/binary_sensor.py @@ -1,7 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import binary_sensor -from esphome.const import CONF_CHANNEL, CONF_ID +from esphome.const import CONF_CHANNEL from . import ( mpr121_ns, MPR121Component, @@ -13,9 +13,8 @@ from . import ( DEPENDENCIES = ["mpr121"] MPR121Channel = mpr121_ns.class_("MPR121Channel", binary_sensor.BinarySensor) -CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend( +CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(MPR121Channel).extend( { - cv.GenerateID(): cv.declare_id(MPR121Channel), cv.GenerateID(CONF_MPR121_ID): cv.use_id(MPR121Component), cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=11), cv.Optional(CONF_TOUCH_THRESHOLD): cv.int_range(min=0x05, max=0x30), @@ -25,8 +24,7 @@ CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await binary_sensor.register_binary_sensor(var, config) + var = await binary_sensor.new_binary_sensor(config) hub = await cg.get_variable(config[CONF_MPR121_ID]) cg.add(var.set_channel(config[CONF_CHANNEL])) diff --git a/esphome/components/nextion/binary_sensor/__init__.py b/esphome/components/nextion/binary_sensor/__init__.py index 090fae3429..8b4a45cc60 100644 --- a/esphome/components/nextion/binary_sensor/__init__.py +++ b/esphome/components/nextion/binary_sensor/__init__.py @@ -20,9 +20,9 @@ NextionBinarySensor = nextion_ns.class_( ) CONFIG_SCHEMA = cv.All( - binary_sensor.BINARY_SENSOR_SCHEMA.extend( + binary_sensor.binary_sensor_schema(NextionBinarySensor) + .extend( { - cv.GenerateID(): cv.declare_id(NextionBinarySensor), cv.Optional(CONF_PAGE_ID): cv.uint8_t, cv.Optional(CONF_COMPONENT_ID): cv.uint8_t, } diff --git a/esphome/components/nextion/text_sensor/__init__.py b/esphome/components/nextion/text_sensor/__init__.py index 9b8518d8c4..826ff2354e 100644 --- a/esphome/components/nextion/text_sensor/__init__.py +++ b/esphome/components/nextion/text_sensor/__init__.py @@ -17,7 +17,7 @@ NextionTextSensor = nextion_ns.class_( ) CONFIG_SCHEMA = ( - text_sensor.text_sensor_schema(klass=NextionTextSensor) + text_sensor.text_sensor_schema(NextionTextSensor) .extend(CONFIG_TEXT_COMPONENT_SCHEMA) .extend(cv.polling_component_schema("never")) ) diff --git a/esphome/components/pipsolar/binary_sensor/__init__.py b/esphome/components/pipsolar/binary_sensor/__init__.py index 5c6af3bffc..f4b34fd594 100644 --- a/esphome/components/pipsolar/binary_sensor/__init__.py +++ b/esphome/components/pipsolar/binary_sensor/__init__.py @@ -1,9 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import binary_sensor -from esphome.const import ( - CONF_ID, -) + from .. import PIPSOLAR_COMPONENT_SCHEMA, CONF_PIPSOLAR_ID DEPENDENCIES = ["uart"] @@ -130,7 +128,7 @@ TYPES = [ ] CONFIG_SCHEMA = PIPSOLAR_COMPONENT_SCHEMA.extend( - {cv.Optional(type): binary_sensor.BINARY_SENSOR_SCHEMA for type in TYPES} + {cv.Optional(type): binary_sensor.binary_sensor_schema() for type in TYPES} ) @@ -139,6 +137,5 @@ async def to_code(config): for type in TYPES: if type in config: conf = config[type] - sens = cg.new_Pvariable(conf[CONF_ID]) - await binary_sensor.register_binary_sensor(sens, conf) - cg.add(getattr(paren, f"set_{type}")(sens)) + var = await binary_sensor.new_binary_sensor(conf) + cg.add(getattr(paren, f"set_{type}")(var)) diff --git a/esphome/components/pn532/binary_sensor.py b/esphome/components/pn532/binary_sensor.py index 9a5816b322..9bcae30750 100644 --- a/esphome/components/pn532/binary_sensor.py +++ b/esphome/components/pn532/binary_sensor.py @@ -1,7 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import binary_sensor -from esphome.const import CONF_UID, CONF_ID +from esphome.const import CONF_UID from esphome.core import HexInt from . import pn532_ns, PN532, CONF_PN532_ID @@ -31,9 +31,8 @@ def validate_uid(value): PN532BinarySensor = pn532_ns.class_("PN532BinarySensor", binary_sensor.BinarySensor) -CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend( +CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(PN532BinarySensor).extend( { - cv.GenerateID(): cv.declare_id(PN532BinarySensor), cv.GenerateID(CONF_PN532_ID): cv.use_id(PN532), cv.Required(CONF_UID): validate_uid, } @@ -41,8 +40,7 @@ CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await binary_sensor.register_binary_sensor(var, config) + var = await binary_sensor.new_binary_sensor(config) hub = await cg.get_variable(config[CONF_PN532_ID]) cg.add(hub.register_tag(var)) diff --git a/esphome/components/rc522/binary_sensor.py b/esphome/components/rc522/binary_sensor.py index 67d3068599..716c0eca76 100644 --- a/esphome/components/rc522/binary_sensor.py +++ b/esphome/components/rc522/binary_sensor.py @@ -1,7 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import binary_sensor -from esphome.const import CONF_UID, CONF_ID +from esphome.const import CONF_UID from esphome.core import HexInt from . import rc522_ns, RC522, CONF_RC522_ID @@ -31,9 +31,8 @@ def validate_uid(value): RC522BinarySensor = rc522_ns.class_("RC522BinarySensor", binary_sensor.BinarySensor) -CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend( +CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(RC522BinarySensor).extend( { - cv.GenerateID(): cv.declare_id(RC522BinarySensor), cv.GenerateID(CONF_RC522_ID): cv.use_id(RC522), cv.Required(CONF_UID): validate_uid, } @@ -41,8 +40,7 @@ CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await binary_sensor.register_binary_sensor(var, config) + var = await binary_sensor.new_binary_sensor(config) hub = await cg.get_variable(config[CONF_RC522_ID]) cg.add(hub.register_tag(var)) diff --git a/esphome/components/rdm6300/binary_sensor.py b/esphome/components/rdm6300/binary_sensor.py index c99a2bfc06..cd808b92cc 100644 --- a/esphome/components/rdm6300/binary_sensor.py +++ b/esphome/components/rdm6300/binary_sensor.py @@ -1,7 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import binary_sensor, rdm6300 -from esphome.const import CONF_UID, CONF_ID +from esphome.const import CONF_UID from . import rdm6300_ns DEPENDENCIES = ["rdm6300"] @@ -11,9 +11,8 @@ RDM6300BinarySensor = rdm6300_ns.class_( "RDM6300BinarySensor", binary_sensor.BinarySensor ) -CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend( +CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(RDM6300BinarySensor).extend( { - cv.GenerateID(): cv.declare_id(RDM6300BinarySensor), cv.GenerateID(CONF_RDM6300_ID): cv.use_id(rdm6300.RDM6300Component), cv.Required(CONF_UID): cv.uint32_t, } @@ -21,8 +20,7 @@ CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await binary_sensor.register_binary_sensor(var, config) + var = await binary_sensor.new_binary_sensor(config) hub = await cg.get_variable(config[CONF_RDM6300_ID]) cg.add(hub.register_card(var)) diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index 9bf03aaf28..531761ee95 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -165,7 +165,7 @@ def declare_protocol(name): BINARY_SENSOR_REGISTRY = Registry( - binary_sensor.BINARY_SENSOR_SCHEMA.extend( + binary_sensor.binary_sensor_schema().extend( { cv.GenerateID(CONF_RECEIVER_ID): cv.use_id(RemoteReceiverBase), } diff --git a/esphome/components/sim800l/binary_sensor.py b/esphome/components/sim800l/binary_sensor.py index 7cee04374b..f046d031ea 100644 --- a/esphome/components/sim800l/binary_sensor.py +++ b/esphome/components/sim800l/binary_sensor.py @@ -2,8 +2,6 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import binary_sensor from esphome.const import ( - CONF_DEVICE_CLASS, - CONF_ENTITY_CATEGORY, DEVICE_CLASS_CONNECTIVITY, ENTITY_CATEGORY_DIAGNOSTIC, ) @@ -15,15 +13,9 @@ CONF_REGISTERED = "registered" CONFIG_SCHEMA = { cv.GenerateID(CONF_SIM800L_ID): cv.use_id(Sim800LComponent), - cv.Optional(CONF_REGISTERED): binary_sensor.BINARY_SENSOR_SCHEMA.extend( - { - cv.Optional( - CONF_DEVICE_CLASS, default=DEVICE_CLASS_CONNECTIVITY - ): binary_sensor.device_class, - cv.Optional( - CONF_ENTITY_CATEGORY, default=ENTITY_CATEGORY_DIAGNOSTIC - ): cv.entity_category, - } + cv.Optional(CONF_REGISTERED): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_CONNECTIVITY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), } diff --git a/esphome/components/status/binary_sensor.py b/esphome/components/status/binary_sensor.py index 9367706388..1f2b7c9d18 100644 --- a/esphome/components/status/binary_sensor.py +++ b/esphome/components/status/binary_sensor.py @@ -2,9 +2,6 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import binary_sensor from esphome.const import ( - CONF_ENTITY_CATEGORY, - CONF_ID, - CONF_DEVICE_CLASS, DEVICE_CLASS_CONNECTIVITY, ENTITY_CATEGORY_DIAGNOSTIC, ) @@ -14,20 +11,13 @@ StatusBinarySensor = status_ns.class_( "StatusBinarySensor", binary_sensor.BinarySensor, cg.Component ) -CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(StatusBinarySensor), - cv.Optional( - CONF_DEVICE_CLASS, default=DEVICE_CLASS_CONNECTIVITY - ): binary_sensor.device_class, - cv.Optional( - CONF_ENTITY_CATEGORY, default=ENTITY_CATEGORY_DIAGNOSTIC - ): cv.entity_category, - } +CONFIG_SCHEMA = binary_sensor.binary_sensor_schema( + StatusBinarySensor, + device_class=DEVICE_CLASS_CONNECTIVITY, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ).extend(cv.COMPONENT_SCHEMA) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await binary_sensor.new_binary_sensor(config) await cg.register_component(var, config) - await binary_sensor.register_binary_sensor(var, config) diff --git a/esphome/components/sx1509/binary_sensor/__init__.py b/esphome/components/sx1509/binary_sensor/__init__.py index 1560af8e99..bbf0e5d0bc 100644 --- a/esphome/components/sx1509/binary_sensor/__init__.py +++ b/esphome/components/sx1509/binary_sensor/__init__.py @@ -1,7 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import binary_sensor -from esphome.const import CONF_ID + from .. import SX1509Component, sx1509_ns, CONF_SX1509_ID CONF_ROW = "row" @@ -11,9 +11,8 @@ DEPENDENCIES = ["sx1509"] SX1509BinarySensor = sx1509_ns.class_("SX1509BinarySensor", binary_sensor.BinarySensor) -CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend( +CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(SX1509BinarySensor).extend( { - cv.GenerateID(): cv.declare_id(SX1509BinarySensor), cv.GenerateID(CONF_SX1509_ID): cv.use_id(SX1509Component), cv.Required(CONF_ROW): cv.int_range(min=0, max=4), cv.Required(CONF_COL): cv.int_range(min=0, max=4), @@ -22,8 +21,7 @@ CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await binary_sensor.register_binary_sensor(var, config) + var = await binary_sensor.new_binary_sensor(config) hub = await cg.get_variable(config[CONF_SX1509_ID]) cg.add(var.set_row_col(config[CONF_ROW], config[CONF_COL])) diff --git a/esphome/components/teleinfo/text_sensor/__init__.py b/esphome/components/teleinfo/text_sensor/__init__.py index 848b08d742..df8e4c21fc 100644 --- a/esphome/components/teleinfo/text_sensor/__init__.py +++ b/esphome/components/teleinfo/text_sensor/__init__.py @@ -8,7 +8,7 @@ TeleInfoTextSensor = teleinfo_ns.class_( "TeleInfoTextSensor", text_sensor.TextSensor, cg.Component ) -CONFIG_SCHEMA = text_sensor.text_sensor_schema(klass=TeleInfoTextSensor).extend( +CONFIG_SCHEMA = text_sensor.text_sensor_schema(TeleInfoTextSensor).extend( TELEINFO_LISTENER_SCHEMA ) diff --git a/esphome/components/template/binary_sensor/__init__.py b/esphome/components/template/binary_sensor/__init__.py index 8f551e3215..4ce89503de 100644 --- a/esphome/components/template/binary_sensor/__init__.py +++ b/esphome/components/template/binary_sensor/__init__.py @@ -9,18 +9,20 @@ TemplateBinarySensor = template_ns.class_( "TemplateBinarySensor", binary_sensor.BinarySensor, cg.Component ) -CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(TemplateBinarySensor), - cv.Optional(CONF_LAMBDA): cv.returning_lambda, - } -).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = ( + binary_sensor.binary_sensor_schema(TemplateBinarySensor) + .extend( + { + cv.Optional(CONF_LAMBDA): cv.returning_lambda, + } + ) + .extend(cv.COMPONENT_SCHEMA) +) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await binary_sensor.new_binary_sensor(config) await cg.register_component(var, config) - await binary_sensor.register_binary_sensor(var, config) if CONF_LAMBDA in config: template_ = await cg.process_lambda( diff --git a/esphome/components/text_sensor/__init__.py b/esphome/components/text_sensor/__init__.py index de72579402..f2f382ceaa 100644 --- a/esphome/components/text_sensor/__init__.py +++ b/esphome/components/text_sensor/__init__.py @@ -137,13 +137,14 @@ _UNDEF = object() def text_sensor_schema( - klass: MockObjClass = _UNDEF, + class_: MockObjClass = _UNDEF, + *, icon: str = _UNDEF, entity_category: str = _UNDEF, ) -> cv.Schema: schema = TEXT_SENSOR_SCHEMA - if klass is not _UNDEF: - schema = schema.extend({cv.GenerateID(): cv.declare_id(klass)}) + if class_ is not _UNDEF: + schema = schema.extend({cv.GenerateID(): cv.declare_id(class_)}) if icon is not _UNDEF: schema = schema.extend({cv.Optional(CONF_ICON, default=icon): cv.icon}) if entity_category is not _UNDEF: diff --git a/esphome/components/touchscreen/binary_sensor/__init__.py b/esphome/components/touchscreen/binary_sensor/__init__.py index 9dba821d4d..73cbf1df7e 100644 --- a/esphome/components/touchscreen/binary_sensor/__init__.py +++ b/esphome/components/touchscreen/binary_sensor/__init__.py @@ -2,7 +2,6 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import binary_sensor -from esphome.const import CONF_ID from .. import touchscreen_ns, CONF_TOUCHSCREEN_ID, Touchscreen, TouchListener @@ -34,23 +33,23 @@ def validate_coords(config): CONFIG_SCHEMA = cv.All( - binary_sensor.BINARY_SENSOR_SCHEMA.extend( + binary_sensor.binary_sensor_schema(TouchscreenBinarySensor) + .extend( { - cv.GenerateID(): cv.declare_id(TouchscreenBinarySensor), cv.GenerateID(CONF_TOUCHSCREEN_ID): cv.use_id(Touchscreen), cv.Required(CONF_X_MIN): cv.int_range(min=0, max=2000), cv.Required(CONF_X_MAX): cv.int_range(min=0, max=2000), cv.Required(CONF_Y_MIN): cv.int_range(min=0, max=2000), cv.Required(CONF_Y_MAX): cv.int_range(min=0, max=2000), } - ).extend(cv.COMPONENT_SCHEMA), + ) + .extend(cv.COMPONENT_SCHEMA), validate_coords, ) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await binary_sensor.register_binary_sensor(var, config) + var = await binary_sensor.new_binary_sensor(config) await cg.register_component(var, config) await cg.register_parented(var, config[CONF_TOUCHSCREEN_ID]) diff --git a/esphome/components/ttp229_bsf/binary_sensor.py b/esphome/components/ttp229_bsf/binary_sensor.py index 75540fe0e8..8a0c7fce48 100644 --- a/esphome/components/ttp229_bsf/binary_sensor.py +++ b/esphome/components/ttp229_bsf/binary_sensor.py @@ -1,15 +1,14 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import binary_sensor -from esphome.const import CONF_CHANNEL, CONF_ID +from esphome.const import CONF_CHANNEL from . import ttp229_bsf_ns, TTP229BSFComponent, CONF_TTP229_ID DEPENDENCIES = ["ttp229_bsf"] TTP229BSFChannel = ttp229_bsf_ns.class_("TTP229BSFChannel", binary_sensor.BinarySensor) -CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend( +CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(TTP229BSFChannel).extend( { - cv.GenerateID(): cv.declare_id(TTP229BSFChannel), cv.GenerateID(CONF_TTP229_ID): cv.use_id(TTP229BSFComponent), cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=15), } @@ -17,8 +16,7 @@ CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await binary_sensor.register_binary_sensor(var, config) + var = await binary_sensor.new_binary_sensor(config) cg.add(var.set_channel(config[CONF_CHANNEL])) hub = await cg.get_variable(config[CONF_TTP229_ID]) diff --git a/esphome/components/ttp229_lsf/binary_sensor.py b/esphome/components/ttp229_lsf/binary_sensor.py index b52a9e8575..5fba0096de 100644 --- a/esphome/components/ttp229_lsf/binary_sensor.py +++ b/esphome/components/ttp229_lsf/binary_sensor.py @@ -1,15 +1,14 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import binary_sensor -from esphome.const import CONF_CHANNEL, CONF_ID +from esphome.const import CONF_CHANNEL from . import ttp229_lsf_ns, TTP229LSFComponent, CONF_TTP229_ID DEPENDENCIES = ["ttp229_lsf"] TTP229Channel = ttp229_lsf_ns.class_("TTP229Channel", binary_sensor.BinarySensor) -CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend( +CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(TTP229Channel).extend( { - cv.GenerateID(): cv.declare_id(TTP229Channel), cv.GenerateID(CONF_TTP229_ID): cv.use_id(TTP229LSFComponent), cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=15), } @@ -17,8 +16,7 @@ CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await binary_sensor.register_binary_sensor(var, config) + var = await binary_sensor.new_binary_sensor(config) cg.add(var.set_channel(config[CONF_CHANNEL])) hub = await cg.get_variable(config[CONF_TTP229_ID]) diff --git a/esphome/components/tuya/binary_sensor/__init__.py b/esphome/components/tuya/binary_sensor/__init__.py index cd4a2db89f..856b5eb323 100644 --- a/esphome/components/tuya/binary_sensor/__init__.py +++ b/esphome/components/tuya/binary_sensor/__init__.py @@ -1,7 +1,8 @@ from esphome.components import binary_sensor import esphome.config_validation as cv import esphome.codegen as cg -from esphome.const import CONF_ID, CONF_SENSOR_DATAPOINT +from esphome.const import CONF_SENSOR_DATAPOINT + from .. import tuya_ns, CONF_TUYA_ID, Tuya DEPENDENCIES = ["tuya"] @@ -11,19 +12,21 @@ TuyaBinarySensor = tuya_ns.class_( "TuyaBinarySensor", binary_sensor.BinarySensor, cg.Component ) -CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(TuyaBinarySensor), - cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya), - cv.Required(CONF_SENSOR_DATAPOINT): cv.uint8_t, - } -).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = ( + binary_sensor.binary_sensor_schema(TuyaBinarySensor) + .extend( + { + cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya), + cv.Required(CONF_SENSOR_DATAPOINT): cv.uint8_t, + } + ) + .extend(cv.COMPONENT_SCHEMA) +) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await binary_sensor.new_binary_sensor(config) await cg.register_component(var, config) - await binary_sensor.register_binary_sensor(var, config) paren = await cg.get_variable(config[CONF_TUYA_ID]) cg.add(var.set_tuya_parent(paren)) diff --git a/esphome/components/wifi_info/text_sensor.py b/esphome/components/wifi_info/text_sensor.py index 58250c3759..54993d48ee 100644 --- a/esphome/components/wifi_info/text_sensor.py +++ b/esphome/components/wifi_info/text_sensor.py @@ -32,19 +32,19 @@ MacAddressWifiInfo = wifi_info_ns.class_( CONFIG_SCHEMA = cv.Schema( { cv.Optional(CONF_IP_ADDRESS): text_sensor.text_sensor_schema( - klass=IPAddressWiFiInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC + IPAddressWiFiInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC ).extend(cv.polling_component_schema("1s")), cv.Optional(CONF_SCAN_RESULTS): text_sensor.text_sensor_schema( - klass=ScanResultsWiFiInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC + ScanResultsWiFiInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC ).extend(cv.polling_component_schema("60s")), cv.Optional(CONF_SSID): text_sensor.text_sensor_schema( - klass=SSIDWiFiInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC + SSIDWiFiInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC ).extend(cv.polling_component_schema("1s")), cv.Optional(CONF_BSSID): text_sensor.text_sensor_schema( - klass=BSSIDWiFiInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC + BSSIDWiFiInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC ).extend(cv.polling_component_schema("1s")), cv.Optional(CONF_MAC_ADDRESS): text_sensor.text_sensor_schema( - klass=MacAddressWifiInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC + MacAddressWifiInfo, entity_category=ENTITY_CATEGORY_DIAGNOSTIC ), } ) diff --git a/esphome/components/xiaomi_cgpr1/binary_sensor.py b/esphome/components/xiaomi_cgpr1/binary_sensor.py index 4297b7fc3b..1b878ca800 100644 --- a/esphome/components/xiaomi_cgpr1/binary_sensor.py +++ b/esphome/components/xiaomi_cgpr1/binary_sensor.py @@ -4,9 +4,7 @@ from esphome.components import sensor, binary_sensor, esp32_ble_tracker from esphome.const import ( CONF_BATTERY_LEVEL, CONF_BINDKEY, - CONF_DEVICE_CLASS, CONF_MAC_ADDRESS, - CONF_ID, DEVICE_CLASS_BATTERY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_MOTION, @@ -31,15 +29,11 @@ XiaomiCGPR1 = xiaomi_cgpr1_ns.class_( ) CONFIG_SCHEMA = cv.All( - binary_sensor.BINARY_SENSOR_SCHEMA.extend( + binary_sensor.binary_sensor_schema(XiaomiCGPR1, device_class=DEVICE_CLASS_MOTION) + .extend( { - cv.GenerateID(): cv.declare_id(XiaomiCGPR1), cv.Required(CONF_BINDKEY): cv.bind_key, cv.Required(CONF_MAC_ADDRESS): cv.mac_address, - cv.Optional( - CONF_DEVICE_CLASS, - default=DEVICE_CLASS_MOTION, - ): binary_sensor.device_class, cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( unit_of_measurement=UNIT_PERCENT, accuracy_decimals=0, @@ -64,21 +58,20 @@ CONFIG_SCHEMA = cv.All( ) -def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield esp32_ble_tracker.register_ble_device(var, config) - yield binary_sensor.register_binary_sensor(var, config) +async def to_code(config): + var = await binary_sensor.new_binary_sensor(config) + await cg.register_component(var, config) + await esp32_ble_tracker.register_ble_device(var, config) cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) cg.add(var.set_bindkey(config[CONF_BINDKEY])) if CONF_IDLE_TIME in config: - sens = yield sensor.new_sensor(config[CONF_IDLE_TIME]) + sens = await sensor.new_sensor(config[CONF_IDLE_TIME]) cg.add(var.set_idle_time(sens)) if CONF_BATTERY_LEVEL in config: - sens = yield sensor.new_sensor(config[CONF_BATTERY_LEVEL]) + sens = await sensor.new_sensor(config[CONF_BATTERY_LEVEL]) cg.add(var.set_battery_level(sens)) if CONF_ILLUMINANCE in config: - sens = yield sensor.new_sensor(config[CONF_ILLUMINANCE]) + sens = await sensor.new_sensor(config[CONF_ILLUMINANCE]) cg.add(var.set_illuminance(sens)) diff --git a/esphome/components/xiaomi_mjyd02yla/binary_sensor.py b/esphome/components/xiaomi_mjyd02yla/binary_sensor.py index 6a7d6aae79..9a4b50df91 100644 --- a/esphome/components/xiaomi_mjyd02yla/binary_sensor.py +++ b/esphome/components/xiaomi_mjyd02yla/binary_sensor.py @@ -3,13 +3,13 @@ import esphome.config_validation as cv from esphome.components import sensor, binary_sensor, esp32_ble_tracker from esphome.const import ( CONF_MAC_ADDRESS, - CONF_ID, CONF_BINDKEY, - CONF_DEVICE_CLASS, CONF_LIGHT, CONF_BATTERY_LEVEL, DEVICE_CLASS_BATTERY, DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_LIGHT, + DEVICE_CLASS_MOTION, ENTITY_CATEGORY_DIAGNOSTIC, STATE_CLASS_MEASUREMENT, UNIT_PERCENT, @@ -32,14 +32,13 @@ XiaomiMJYD02YLA = xiaomi_mjyd02yla_ns.class_( ) CONFIG_SCHEMA = cv.All( - binary_sensor.BINARY_SENSOR_SCHEMA.extend( + binary_sensor.binary_sensor_schema( + XiaomiMJYD02YLA, device_class=DEVICE_CLASS_MOTION + ) + .extend( { - cv.GenerateID(): cv.declare_id(XiaomiMJYD02YLA), cv.Required(CONF_MAC_ADDRESS): cv.mac_address, cv.Required(CONF_BINDKEY): cv.bind_key, - cv.Optional( - CONF_DEVICE_CLASS, default="motion" - ): binary_sensor.device_class, cv.Optional(CONF_IDLE_TIME): sensor.sensor_schema( unit_of_measurement=UNIT_MINUTE, icon=ICON_TIMELAPSE, @@ -58,12 +57,8 @@ CONFIG_SCHEMA = cv.All( device_class=DEVICE_CLASS_ILLUMINANCE, state_class=STATE_CLASS_MEASUREMENT, ), - cv.Optional(CONF_LIGHT): binary_sensor.BINARY_SENSOR_SCHEMA.extend( - { - cv.Optional( - CONF_DEVICE_CLASS, default="light" - ): binary_sensor.device_class, - } + cv.Optional(CONF_LIGHT): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_LIGHT ), } ) @@ -73,10 +68,9 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await binary_sensor.new_binary_sensor(config) await cg.register_component(var, config) await esp32_ble_tracker.register_ble_device(var, config) - await binary_sensor.register_binary_sensor(var, config) cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) cg.add(var.set_bindkey(config[CONF_BINDKEY])) diff --git a/esphome/components/xiaomi_mue4094rt/binary_sensor.py b/esphome/components/xiaomi_mue4094rt/binary_sensor.py index 5d19263c9c..94d85213ff 100644 --- a/esphome/components/xiaomi_mue4094rt/binary_sensor.py +++ b/esphome/components/xiaomi_mue4094rt/binary_sensor.py @@ -1,7 +1,11 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import binary_sensor, esp32_ble_tracker -from esphome.const import CONF_MAC_ADDRESS, CONF_DEVICE_CLASS, CONF_TIMEOUT, CONF_ID +from esphome.const import ( + CONF_MAC_ADDRESS, + CONF_TIMEOUT, + DEVICE_CLASS_MOTION, +) DEPENDENCIES = ["esp32_ble_tracker"] @@ -16,13 +20,12 @@ XiaomiMUE4094RT = xiaomi_mue4094rt_ns.class_( ) CONFIG_SCHEMA = cv.All( - binary_sensor.BINARY_SENSOR_SCHEMA.extend( + binary_sensor.binary_sensor_schema( + XiaomiMUE4094RT, device_class=DEVICE_CLASS_MOTION + ) + .extend( { - cv.GenerateID(): cv.declare_id(XiaomiMUE4094RT), cv.Required(CONF_MAC_ADDRESS): cv.mac_address, - cv.Optional( - CONF_DEVICE_CLASS, default="motion" - ): binary_sensor.device_class, cv.Optional( CONF_TIMEOUT, default="5s" ): cv.positive_time_period_milliseconds, @@ -34,10 +37,9 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await binary_sensor.new_binary_sensor(config) await cg.register_component(var, config) await esp32_ble_tracker.register_ble_device(var, config) - await binary_sensor.register_binary_sensor(var, config) cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) cg.add(var.set_time(config[CONF_TIMEOUT])) diff --git a/esphome/components/xiaomi_wx08zm/binary_sensor.py b/esphome/components/xiaomi_wx08zm/binary_sensor.py index 8667794923..504dff9d66 100644 --- a/esphome/components/xiaomi_wx08zm/binary_sensor.py +++ b/esphome/components/xiaomi_wx08zm/binary_sensor.py @@ -10,12 +10,11 @@ from esphome.const import ( STATE_CLASS_MEASUREMENT, UNIT_PERCENT, ICON_BUG, - CONF_ID, ) DEPENDENCIES = ["esp32_ble_tracker"] -AUTO_LOAD = ["xiaomi_ble"] +AUTO_LOAD = ["xiaomi_ble", "sensor"] xiaomi_wx08zm_ns = cg.esphome_ns.namespace("xiaomi_wx08zm") XiaomiWX08ZM = xiaomi_wx08zm_ns.class_( @@ -26,9 +25,9 @@ XiaomiWX08ZM = xiaomi_wx08zm_ns.class_( ) CONFIG_SCHEMA = cv.All( - binary_sensor.BINARY_SENSOR_SCHEMA.extend( + binary_sensor.binary_sensor_schema(XiaomiWX08ZM) + .extend( { - cv.GenerateID(): cv.declare_id(XiaomiWX08ZM), cv.Required(CONF_MAC_ADDRESS): cv.mac_address, cv.Optional(CONF_TABLET): sensor.sensor_schema( unit_of_measurement=UNIT_PERCENT, @@ -51,10 +50,9 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await binary_sensor.new_binary_sensor(config) await cg.register_component(var, config) await esp32_ble_tracker.register_ble_device(var, config) - await binary_sensor.register_binary_sensor(var, config) cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) diff --git a/esphome/components/xpt2046/binary_sensor.py b/esphome/components/xpt2046/binary_sensor.py index 6959d6c342..6ec09a2295 100644 --- a/esphome/components/xpt2046/binary_sensor.py +++ b/esphome/components/xpt2046/binary_sensor.py @@ -1,7 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import binary_sensor -from esphome.const import CONF_ID + from . import ( xpt2046_ns, XPT2046Component, @@ -27,9 +27,8 @@ def validate_xpt2046_button(config): CONFIG_SCHEMA = cv.All( - binary_sensor.BINARY_SENSOR_SCHEMA.extend( + binary_sensor.binary_sensor_schema(XPT2046Button).extend( { - cv.GenerateID(): cv.declare_id(XPT2046Button), cv.GenerateID(CONF_XPT2046_ID): cv.use_id(XPT2046Component), cv.Required(CONF_X_MIN): cv.int_range(min=0, max=4095), cv.Required(CONF_X_MAX): cv.int_range(min=0, max=4095), @@ -42,8 +41,7 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await binary_sensor.register_binary_sensor(var, config) + var = await binary_sensor.new_binary_sensor(config) hub = await cg.get_variable(config[CONF_XPT2046_ID]) cg.add( var.set_area( From 1a8f8adc2a2b0393cd0639d0cdf3b8088df41201 Mon Sep 17 00:00:00 2001 From: Michael Labuschke Date: Thu, 17 Feb 2022 23:00:03 +0100 Subject: [PATCH 0258/1729] Read all cell voltages from DalyBMS (#3203) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/daly_bms/daly_bms.cpp | 67 ++++++++++++++++++++++++ esphome/components/daly_bms/daly_bms.h | 33 ++++++++++++ esphome/components/daly_bms/sensor.py | 54 +++++++++++++++++++ 3 files changed, 154 insertions(+) diff --git a/esphome/components/daly_bms/daly_bms.cpp b/esphome/components/daly_bms/daly_bms.cpp index 44c05f0686..f2b4c0e92b 100644 --- a/esphome/components/daly_bms/daly_bms.cpp +++ b/esphome/components/daly_bms/daly_bms.cpp @@ -16,6 +16,7 @@ static const uint8_t DALY_REQUEST_MIN_MAX_VOLTAGE = 0x91; static const uint8_t DALY_REQUEST_MIN_MAX_TEMPERATURE = 0x92; static const uint8_t DALY_REQUEST_MOS = 0x93; static const uint8_t DALY_REQUEST_STATUS = 0x94; +static const uint8_t DALY_REQUEST_CELL_VOLTAGE = 0x95; static const uint8_t DALY_REQUEST_TEMPERATURE = 0x96; void DalyBmsComponent::setup() {} @@ -31,6 +32,7 @@ void DalyBmsComponent::update() { this->request_data_(DALY_REQUEST_MIN_MAX_TEMPERATURE); this->request_data_(DALY_REQUEST_MOS); this->request_data_(DALY_REQUEST_STATUS); + this->request_data_(DALY_REQUEST_CELL_VOLTAGE); this->request_data_(DALY_REQUEST_TEMPERATURE); std::vector get_battery_level_data; @@ -166,6 +168,71 @@ void DalyBmsComponent::decode_data_(std::vector data) { } break; + case DALY_REQUEST_CELL_VOLTAGE: + switch (it[4]) { + case 1: + if (this->cell_1_voltage_) { + this->cell_1_voltage_->publish_state((float) encode_uint16(it[5], it[6]) / 1000); + } + if (this->cell_2_voltage_) { + this->cell_2_voltage_->publish_state((float) encode_uint16(it[7], it[8]) / 1000); + } + if (this->cell_3_voltage_) { + this->cell_3_voltage_->publish_state((float) encode_uint16(it[9], it[10]) / 1000); + } + break; + case 2: + if (this->cell_4_voltage_) { + this->cell_4_voltage_->publish_state((float) encode_uint16(it[5], it[6]) / 1000); + } + if (this->cell_5_voltage_) { + this->cell_5_voltage_->publish_state((float) encode_uint16(it[7], it[8]) / 1000); + } + if (this->cell_6_voltage_) { + this->cell_6_voltage_->publish_state((float) encode_uint16(it[9], it[10]) / 1000); + } + break; + case 3: + if (this->cell_7_voltage_) { + this->cell_7_voltage_->publish_state((float) encode_uint16(it[5], it[6]) / 1000); + } + if (this->cell_8_voltage_) { + this->cell_8_voltage_->publish_state((float) encode_uint16(it[7], it[8]) / 1000); + } + if (this->cell_9_voltage_) { + this->cell_9_voltage_->publish_state((float) encode_uint16(it[9], it[10]) / 1000); + } + break; + case 4: + if (this->cell_10_voltage_) { + this->cell_10_voltage_->publish_state((float) encode_uint16(it[5], it[6]) / 1000); + } + if (this->cell_11_voltage_) { + this->cell_11_voltage_->publish_state((float) encode_uint16(it[7], it[8]) / 1000); + } + if (this->cell_12_voltage_) { + this->cell_12_voltage_->publish_state((float) encode_uint16(it[9], it[10]) / 1000); + } + break; + case 5: + if (this->cell_13_voltage_) { + this->cell_13_voltage_->publish_state((float) encode_uint16(it[5], it[6]) / 1000); + } + if (this->cell_14_voltage_) { + this->cell_14_voltage_->publish_state((float) encode_uint16(it[7], it[8]) / 1000); + } + if (this->cell_15_voltage_) { + this->cell_15_voltage_->publish_state((float) encode_uint16(it[9], it[10]) / 1000); + } + break; + case 6: + if (this->cell_16_voltage_) { + this->cell_16_voltage_->publish_state((float) encode_uint16(it[5], it[6]) / 1000); + } + break; + } + break; + default: break; } diff --git a/esphome/components/daly_bms/daly_bms.h b/esphome/components/daly_bms/daly_bms.h index b5d4c8ae39..90faab77f7 100644 --- a/esphome/components/daly_bms/daly_bms.h +++ b/esphome/components/daly_bms/daly_bms.h @@ -37,6 +37,23 @@ class DalyBmsComponent : public PollingComponent, public uart::UARTDevice { void set_cells_number_sensor(sensor::Sensor *cells_number) { cells_number_ = cells_number; } void set_temperature_1_sensor(sensor::Sensor *temperature_1_sensor) { temperature_1_sensor_ = temperature_1_sensor; } void set_temperature_2_sensor(sensor::Sensor *temperature_2_sensor) { temperature_2_sensor_ = temperature_2_sensor; } + void set_cell_1_voltage_sensor(sensor::Sensor *cell_1_voltage) { cell_1_voltage_ = cell_1_voltage; } + void set_cell_2_voltage_sensor(sensor::Sensor *cell_2_voltage) { cell_2_voltage_ = cell_2_voltage; } + void set_cell_3_voltage_sensor(sensor::Sensor *cell_3_voltage) { cell_3_voltage_ = cell_3_voltage; } + void set_cell_4_voltage_sensor(sensor::Sensor *cell_4_voltage) { cell_4_voltage_ = cell_4_voltage; } + void set_cell_5_voltage_sensor(sensor::Sensor *cell_5_voltage) { cell_5_voltage_ = cell_5_voltage; } + void set_cell_6_voltage_sensor(sensor::Sensor *cell_6_voltage) { cell_6_voltage_ = cell_6_voltage; } + void set_cell_7_voltage_sensor(sensor::Sensor *cell_7_voltage) { cell_7_voltage_ = cell_7_voltage; } + void set_cell_8_voltage_sensor(sensor::Sensor *cell_8_voltage) { cell_8_voltage_ = cell_8_voltage; } + void set_cell_9_voltage_sensor(sensor::Sensor *cell_9_voltage) { cell_9_voltage_ = cell_9_voltage; } + void set_cell_10_voltage_sensor(sensor::Sensor *cell_10_voltage) { cell_10_voltage_ = cell_10_voltage; } + void set_cell_11_voltage_sensor(sensor::Sensor *cell_11_voltage) { cell_11_voltage_ = cell_11_voltage; } + void set_cell_12_voltage_sensor(sensor::Sensor *cell_12_voltage) { cell_12_voltage_ = cell_12_voltage; } + void set_cell_13_voltage_sensor(sensor::Sensor *cell_13_voltage) { cell_13_voltage_ = cell_13_voltage; } + void set_cell_14_voltage_sensor(sensor::Sensor *cell_14_voltage) { cell_14_voltage_ = cell_14_voltage; } + void set_cell_15_voltage_sensor(sensor::Sensor *cell_15_voltage) { cell_15_voltage_ = cell_15_voltage; } + void set_cell_16_voltage_sensor(sensor::Sensor *cell_16_voltage) { cell_16_voltage_ = cell_16_voltage; } + // TEXT_SENSORS void set_status_text_sensor(text_sensor::TextSensor *status_text_sensor) { status_text_sensor_ = status_text_sensor; } // BINARY_SENSORS @@ -72,6 +89,22 @@ class DalyBmsComponent : public PollingComponent, public uart::UARTDevice { sensor::Sensor *cells_number_{nullptr}; sensor::Sensor *temperature_1_sensor_{nullptr}; sensor::Sensor *temperature_2_sensor_{nullptr}; + sensor::Sensor *cell_1_voltage_{nullptr}; + sensor::Sensor *cell_2_voltage_{nullptr}; + sensor::Sensor *cell_3_voltage_{nullptr}; + sensor::Sensor *cell_4_voltage_{nullptr}; + sensor::Sensor *cell_5_voltage_{nullptr}; + sensor::Sensor *cell_6_voltage_{nullptr}; + sensor::Sensor *cell_7_voltage_{nullptr}; + sensor::Sensor *cell_8_voltage_{nullptr}; + sensor::Sensor *cell_9_voltage_{nullptr}; + sensor::Sensor *cell_10_voltage_{nullptr}; + sensor::Sensor *cell_11_voltage_{nullptr}; + sensor::Sensor *cell_12_voltage_{nullptr}; + sensor::Sensor *cell_13_voltage_{nullptr}; + sensor::Sensor *cell_14_voltage_{nullptr}; + sensor::Sensor *cell_15_voltage_{nullptr}; + sensor::Sensor *cell_16_voltage_{nullptr}; text_sensor::TextSensor *status_text_sensor_{nullptr}; diff --git a/esphome/components/daly_bms/sensor.py b/esphome/components/daly_bms/sensor.py index 0ba68d3786..e2e8528317 100644 --- a/esphome/components/daly_bms/sensor.py +++ b/esphome/components/daly_bms/sensor.py @@ -36,6 +36,22 @@ CONF_REMAINING_CAPACITY = "remaining_capacity" CONF_TEMPERATURE_1 = "temperature_1" CONF_TEMPERATURE_2 = "temperature_2" +CONF_CELL_1_VOLTAGE = "cell_1_voltage" +CONF_CELL_2_VOLTAGE = "cell_2_voltage" +CONF_CELL_3_VOLTAGE = "cell_3_voltage" +CONF_CELL_4_VOLTAGE = "cell_4_voltage" +CONF_CELL_5_VOLTAGE = "cell_5_voltage" +CONF_CELL_6_VOLTAGE = "cell_6_voltage" +CONF_CELL_7_VOLTAGE = "cell_7_voltage" +CONF_CELL_8_VOLTAGE = "cell_8_voltage" +CONF_CELL_9_VOLTAGE = "cell_9_voltage" +CONF_CELL_10_VOLTAGE = "cell_10_voltage" +CONF_CELL_11_VOLTAGE = "cell_11_voltage" +CONF_CELL_12_VOLTAGE = "cell_12_voltage" +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" ICON_CURRENT_DC = "mdi:current-dc" ICON_BATTERY_OUTLINE = "mdi:battery-outline" ICON_THERMOMETER_CHEVRON_UP = "mdi:thermometer-chevron-up" @@ -60,8 +76,30 @@ TYPES = [ CONF_REMAINING_CAPACITY, CONF_TEMPERATURE_1, CONF_TEMPERATURE_2, + CONF_CELL_1_VOLTAGE, + CONF_CELL_2_VOLTAGE, + CONF_CELL_3_VOLTAGE, + CONF_CELL_4_VOLTAGE, + CONF_CELL_5_VOLTAGE, + CONF_CELL_6_VOLTAGE, + CONF_CELL_7_VOLTAGE, + CONF_CELL_8_VOLTAGE, + CONF_CELL_9_VOLTAGE, + CONF_CELL_10_VOLTAGE, + CONF_CELL_11_VOLTAGE, + CONF_CELL_12_VOLTAGE, + CONF_CELL_13_VOLTAGE, + CONF_CELL_14_VOLTAGE, + CONF_CELL_15_VOLTAGE, + CONF_CELL_16_VOLTAGE, ] +CELL_VOLTAGE_SCHEMA = sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, +) + CONFIG_SCHEMA = cv.All( cv.Schema( { @@ -156,6 +194,22 @@ CONFIG_SCHEMA = cv.All( device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ), + cv.Optional(CONF_CELL_1_VOLTAGE): CELL_VOLTAGE_SCHEMA, + cv.Optional(CONF_CELL_2_VOLTAGE): CELL_VOLTAGE_SCHEMA, + cv.Optional(CONF_CELL_3_VOLTAGE): CELL_VOLTAGE_SCHEMA, + cv.Optional(CONF_CELL_4_VOLTAGE): CELL_VOLTAGE_SCHEMA, + cv.Optional(CONF_CELL_5_VOLTAGE): CELL_VOLTAGE_SCHEMA, + cv.Optional(CONF_CELL_6_VOLTAGE): CELL_VOLTAGE_SCHEMA, + cv.Optional(CONF_CELL_7_VOLTAGE): CELL_VOLTAGE_SCHEMA, + cv.Optional(CONF_CELL_8_VOLTAGE): CELL_VOLTAGE_SCHEMA, + cv.Optional(CONF_CELL_9_VOLTAGE): CELL_VOLTAGE_SCHEMA, + cv.Optional(CONF_CELL_10_VOLTAGE): CELL_VOLTAGE_SCHEMA, + cv.Optional(CONF_CELL_11_VOLTAGE): CELL_VOLTAGE_SCHEMA, + cv.Optional(CONF_CELL_12_VOLTAGE): CELL_VOLTAGE_SCHEMA, + cv.Optional(CONF_CELL_13_VOLTAGE): CELL_VOLTAGE_SCHEMA, + 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, } ).extend(cv.COMPONENT_SCHEMA) ) From ccc2fbfd67aebf4179c56e8e0e4e84166aaa15d7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 11 Feb 2022 13:57:46 +0100 Subject: [PATCH 0259/1729] Bump platformio from 5.2.4 to 5.2.5 (#3188) * Bump platformio from 5.2.4 to 5.2.5 Bumps [platformio](https://github.com/platformio/platformio) from 5.2.4 to 5.2.5. - [Release notes](https://github.com/platformio/platformio/releases) - [Changelog](https://github.com/platformio/platformio-core/blob/develop/HISTORY.rst) - [Commits](https://github.com/platformio/platformio/commits) --- updated-dependencies: - dependency-name: platformio dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * Update requirements.txt Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Otto Winter --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 0c408f90b5..acbf1d9984 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,12 +6,12 @@ tornado==6.1 tzlocal==4.1 # from time tzdata>=2021.1 # from time pyserial==3.5 -platformio==5.2.4 # When updating platformio, also update Dockerfile +platformio==5.2.5 # When updating platformio, also update Dockerfile esptool==3.2 click==8.0.3 esphome-dashboard==20220209.0 aioesphomeapi==10.8.2 -zeroconf==0.37.0 +zeroconf==0.38.3 # esp-idf requires this, but doesn't bundle it by default # https://github.com/espressif/esp-idf/blob/220590d599e134d7a5e7f1e683cc4550349ffbf8/requirements.txt#L24 From 5764c988afab26b6b9c6249017f0b6575c8672e4 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 18 Feb 2022 11:51:56 +1300 Subject: [PATCH 0260/1729] Bump version to 2022.2.3 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index b501428f9e..0e8695adef 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2022.2.2" +__version__ = "2022.2.3" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From f137cc10f4124f0ab891e606fbbd096d2573c38e Mon Sep 17 00:00:00 2001 From: ImSorryButWho <91785526+ImSorryButWho@users.noreply.github.com> Date: Fri, 18 Feb 2022 16:22:41 -0500 Subject: [PATCH 0261/1729] Remote magiquest protocol (#2963) Co-authored-by: Aaron Hertz Co-authored-by: Otto Winter --- esphome/components/remote_base/__init__.py | 50 +++++++++++ .../remote_base/magiquest_protocol.cpp | 83 +++++++++++++++++++ .../remote_base/magiquest_protocol.h | 50 +++++++++++ esphome/const.py | 2 + tests/test1.yaml | 5 ++ 5 files changed, 190 insertions(+) create mode 100644 esphome/components/remote_base/magiquest_protocol.cpp create mode 100644 esphome/components/remote_base/magiquest_protocol.h diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index 531761ee95..f1b3e32c18 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -27,6 +27,8 @@ from esphome.const import ( CONF_CARRIER_FREQUENCY, CONF_RC_CODE_1, CONF_RC_CODE_2, + CONF_MAGNITUDE, + CONF_WAND_ID, CONF_LEVEL, ) from esphome.core import coroutine @@ -391,6 +393,54 @@ async def lg_action(var, config, args): cg.add(var.set_nbits(template_)) +# MagiQuest +( + MagiQuestData, + MagiQuestBinarySensor, + MagiQuestTrigger, + MagiQuestAction, + MagiQuestDumper, +) = declare_protocol("MagiQuest") + +MAGIQUEST_SCHEMA = cv.Schema( + { + cv.Required(CONF_WAND_ID): cv.hex_uint32_t, + cv.Optional(CONF_MAGNITUDE, default=0xFFFF): cv.hex_uint16_t, + } +) + + +@register_binary_sensor("magiquest", MagiQuestBinarySensor, MAGIQUEST_SCHEMA) +def magiquest_binary_sensor(var, config): + cg.add( + var.set_data( + cg.StructInitializer( + MagiQuestData, + ("magnitude", config[CONF_MAGNITUDE]), + ("wand_id", config[CONF_WAND_ID]), + ) + ) + ) + + +@register_trigger("magiquest", MagiQuestTrigger, MagiQuestData) +def magiquest_trigger(var, config): + pass + + +@register_dumper("magiquest", MagiQuestDumper) +def magiquest_dumper(var, config): + pass + + +@register_action("magiquest", MagiQuestAction, MAGIQUEST_SCHEMA) +async def magiquest_action(var, config, args): + template_ = await cg.templatable(config[CONF_WAND_ID], args, cg.uint32) + cg.add(var.set_wand_id(template_)) + template_ = await cg.templatable(config[CONF_MAGNITUDE], args, cg.uint16) + cg.add(var.set_magnitude(template_)) + + # NEC NECData, NECBinarySensor, NECTrigger, NECAction, NECDumper = declare_protocol("NEC") NEC_SCHEMA = cv.Schema( diff --git a/esphome/components/remote_base/magiquest_protocol.cpp b/esphome/components/remote_base/magiquest_protocol.cpp new file mode 100644 index 0000000000..20b40ef201 --- /dev/null +++ b/esphome/components/remote_base/magiquest_protocol.cpp @@ -0,0 +1,83 @@ +#include "magiquest_protocol.h" +#include "esphome/core/log.h" + +/* Based on protocol analysis from + * https://arduino-irremote.github.io/Arduino-IRremote/ir__MagiQuest_8cpp_source.html + */ + +namespace esphome { +namespace remote_base { + +static const char *const TAG = "remote.magiquest"; + +static const uint32_t MAGIQUEST_UNIT = 288; // us +static const uint32_t MAGIQUEST_ONE_MARK = 2 * MAGIQUEST_UNIT; +static const uint32_t MAGIQUEST_ONE_SPACE = 2 * MAGIQUEST_UNIT; +static const uint32_t MAGIQUEST_ZERO_MARK = MAGIQUEST_UNIT; +static const uint32_t MAGIQUEST_ZERO_SPACE = 3 * MAGIQUEST_UNIT; + +void MagiQuestProtocol::encode(RemoteTransmitData *dst, const MagiQuestData &data) { + dst->reserve(101); // 2 start bits, 48 data bits, 1 stop bit + dst->set_carrier_frequency(38000); + + // 2 start bits + dst->item(MAGIQUEST_ZERO_MARK, MAGIQUEST_ZERO_SPACE); + dst->item(MAGIQUEST_ZERO_MARK, MAGIQUEST_ZERO_SPACE); + for (uint32_t mask = 1 << 31; mask; mask >>= 1) { + if (data.wand_id & mask) { + dst->item(MAGIQUEST_ONE_MARK, MAGIQUEST_ONE_SPACE); + } else { + dst->item(MAGIQUEST_ZERO_MARK, MAGIQUEST_ZERO_SPACE); + } + } + + for (uint16_t mask = 1 << 15; mask; mask >>= 1) { + if (data.magnitude & mask) { + dst->item(MAGIQUEST_ONE_MARK, MAGIQUEST_ONE_SPACE); + } else { + dst->item(MAGIQUEST_ZERO_MARK, MAGIQUEST_ZERO_SPACE); + } + } + + dst->mark(MAGIQUEST_UNIT); +} +optional MagiQuestProtocol::decode(RemoteReceiveData src) { + MagiQuestData data{ + .magnitude = 0, + .wand_id = 0, + }; + // Two start bits + if (!src.expect_item(MAGIQUEST_ZERO_MARK, MAGIQUEST_ZERO_SPACE) || + !src.expect_item(MAGIQUEST_ZERO_MARK, MAGIQUEST_ZERO_SPACE)) { + return {}; + } + + for (uint32_t mask = 1 << 31; mask; mask >>= 1) { + if (src.expect_item(MAGIQUEST_ONE_MARK, MAGIQUEST_ONE_SPACE)) { + data.wand_id |= mask; + } else if (src.expect_item(MAGIQUEST_ZERO_MARK, MAGIQUEST_ZERO_SPACE)) { + data.wand_id &= ~mask; + } else { + return {}; + } + } + + for (uint16_t mask = 1 << 15; mask; mask >>= 1) { + if (src.expect_item(MAGIQUEST_ONE_MARK, MAGIQUEST_ONE_SPACE)) { + data.magnitude |= mask; + } else if (src.expect_item(MAGIQUEST_ZERO_MARK, MAGIQUEST_ZERO_SPACE)) { + data.magnitude &= ~mask; + } else { + return {}; + } + } + + src.expect_mark(MAGIQUEST_UNIT); + return data; +} +void MagiQuestProtocol::dump(const MagiQuestData &data) { + ESP_LOGD(TAG, "Received MagiQuest: wand_id=0x%08X, magnitude=0x%04X", data.wand_id, data.magnitude); +} + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/components/remote_base/magiquest_protocol.h b/esphome/components/remote_base/magiquest_protocol.h new file mode 100644 index 0000000000..909be346d0 --- /dev/null +++ b/esphome/components/remote_base/magiquest_protocol.h @@ -0,0 +1,50 @@ +#pragma once + +#include "remote_base.h" + +/* Based on protocol analysis from + * https://arduino-irremote.github.io/Arduino-IRremote/ir__MagiQuest_8cpp_source.html + */ + +namespace esphome { +namespace remote_base { + +struct MagiQuestData { + uint16_t magnitude; + uint32_t wand_id; + + bool operator==(const MagiQuestData &rhs) const { + // Treat 0xffff as a special, wildcard magnitude + // In testing, the wand never produces this value, and this allows us to match + // on just the wand_id if wanted. + if (rhs.wand_id != this->wand_id) { + return false; + } + return (this->wand_id == 0xffff || rhs.wand_id == 0xffff || this->wand_id == rhs.wand_id); + } +}; + +class MagiQuestProtocol : public RemoteProtocol { + public: + void encode(RemoteTransmitData *dst, const MagiQuestData &data) override; + optional decode(RemoteReceiveData src) override; + void dump(const MagiQuestData &data) override; +}; + +DECLARE_REMOTE_PROTOCOL(MagiQuest) + +template class MagiQuestAction : public RemoteTransmitterActionBase { + public: + TEMPLATABLE_VALUE(uint16_t, magnitude) + TEMPLATABLE_VALUE(uint32_t, wand_id) + + void encode(RemoteTransmitData *dst, Ts... x) override { + MagiQuestData data{}; + data.magnitude = this->magnitude_.value(x...); + data.wand_id = this->wand_id_.value(x...); + MagiQuestProtocol().encode(dst, data); + } +}; + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/const.py b/esphome/const.py index 1494920385..721f78f26a 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -344,6 +344,7 @@ CONF_LOOP_TIME = "loop_time" CONF_LOW = "low" CONF_LOW_VOLTAGE_REFERENCE = "low_voltage_reference" CONF_MAC_ADDRESS = "mac_address" +CONF_MAGNITUDE = "magnitude" CONF_MAINS_FILTER = "mains_filter" CONF_MAKE_ID = "make_id" CONF_MANUAL_IP = "manual_ip" @@ -735,6 +736,7 @@ CONF_VOLTAGE_DIVIDER = "voltage_divider" CONF_WAIT_TIME = "wait_time" CONF_WAIT_UNTIL = "wait_until" CONF_WAKEUP_PIN = "wakeup_pin" +CONF_WAND_ID = "wand_id" CONF_WARM_WHITE = "warm_white" CONF_WARM_WHITE_COLOR_TEMPERATURE = "warm_white_color_temperature" CONF_WATCHDOG_THRESHOLD = "watchdog_threshold" diff --git a/tests/test1.yaml b/tests/test1.yaml index e2d5fdc0c5..9b38d549b3 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1861,6 +1861,11 @@ switch: turn_on_action: remote_transmitter.transmit_jvc: data: 0x10EF + - platform: template + name: MagiQuest + turn_on_action: + remote_transmitter.transmit_magiquest: + wand_id: 0x01234567 - platform: template name: NEC id: living_room_lights_off From 231908fe9f50e33eab511a7365ec3b94bd3da53b Mon Sep 17 00:00:00 2001 From: Borys Pierov Date: Sat, 19 Feb 2022 03:45:32 -0500 Subject: [PATCH 0262/1729] Implement text_sensor based on ble_client (#3079) Co-authored-by: Otto Winter --- .../ble_client/text_sensor/__init__.py | 121 ++++++++++++++++ .../ble_client/text_sensor/automation.h | 38 +++++ .../text_sensor/ble_text_sensor.cpp | 137 ++++++++++++++++++ .../ble_client/text_sensor/ble_text_sensor.h | 47 ++++++ tests/test1.yaml | 12 ++ 5 files changed, 355 insertions(+) create mode 100644 esphome/components/ble_client/text_sensor/__init__.py create mode 100644 esphome/components/ble_client/text_sensor/automation.h create mode 100644 esphome/components/ble_client/text_sensor/ble_text_sensor.cpp create mode 100644 esphome/components/ble_client/text_sensor/ble_text_sensor.h diff --git a/esphome/components/ble_client/text_sensor/__init__.py b/esphome/components/ble_client/text_sensor/__init__.py new file mode 100644 index 0000000000..e1f97e4a01 --- /dev/null +++ b/esphome/components/ble_client/text_sensor/__init__.py @@ -0,0 +1,121 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import text_sensor, ble_client, esp32_ble_tracker +from esphome.const import ( + CONF_ID, + CONF_TRIGGER_ID, + CONF_SERVICE_UUID, +) +from esphome import automation +from .. import ble_client_ns + +DEPENDENCIES = ["ble_client"] + +CONF_CHARACTERISTIC_UUID = "characteristic_uuid" +CONF_DESCRIPTOR_UUID = "descriptor_uuid" + +CONF_NOTIFY = "notify" +CONF_ON_NOTIFY = "on_notify" + +adv_data_t = cg.std_vector.template(cg.uint8) +adv_data_t_const_ref = adv_data_t.operator("ref").operator("const") + +BLETextSensor = ble_client_ns.class_( + "BLETextSensor", + text_sensor.TextSensor, + cg.PollingComponent, + ble_client.BLEClientNode, +) +BLETextSensorNotifyTrigger = ble_client_ns.class_( + "BLETextSensorNotifyTrigger", automation.Trigger.template(cg.std_string) +) + +CONFIG_SCHEMA = cv.All( + text_sensor.TEXT_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(BLETextSensor), + cv.Required(CONF_SERVICE_UUID): esp32_ble_tracker.bt_uuid, + cv.Required(CONF_CHARACTERISTIC_UUID): esp32_ble_tracker.bt_uuid, + cv.Optional(CONF_DESCRIPTOR_UUID): esp32_ble_tracker.bt_uuid, + cv.Optional(CONF_NOTIFY, default=False): cv.boolean, + cv.Optional(CONF_ON_NOTIFY): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + BLETextSensorNotifyTrigger + ), + } + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(ble_client.BLE_CLIENT_SCHEMA) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + if len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid16_format): + cg.add( + var.set_service_uuid16(esp32_ble_tracker.as_hex(config[CONF_SERVICE_UUID])) + ) + elif len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid32_format): + cg.add( + var.set_service_uuid32(esp32_ble_tracker.as_hex(config[CONF_SERVICE_UUID])) + ) + elif len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid128_format): + uuid128 = esp32_ble_tracker.as_reversed_hex_array(config[CONF_SERVICE_UUID]) + cg.add(var.set_service_uuid128(uuid128)) + + if len(config[CONF_CHARACTERISTIC_UUID]) == len(esp32_ble_tracker.bt_uuid16_format): + cg.add( + var.set_char_uuid16( + esp32_ble_tracker.as_hex(config[CONF_CHARACTERISTIC_UUID]) + ) + ) + elif len(config[CONF_CHARACTERISTIC_UUID]) == len( + esp32_ble_tracker.bt_uuid32_format + ): + cg.add( + var.set_char_uuid32( + esp32_ble_tracker.as_hex(config[CONF_CHARACTERISTIC_UUID]) + ) + ) + elif len(config[CONF_CHARACTERISTIC_UUID]) == len( + esp32_ble_tracker.bt_uuid128_format + ): + uuid128 = esp32_ble_tracker.as_reversed_hex_array( + config[CONF_CHARACTERISTIC_UUID] + ) + cg.add(var.set_char_uuid128(uuid128)) + + if CONF_DESCRIPTOR_UUID in config: + if len(config[CONF_DESCRIPTOR_UUID]) == len(esp32_ble_tracker.bt_uuid16_format): + cg.add( + var.set_descr_uuid16( + esp32_ble_tracker.as_hex(config[CONF_DESCRIPTOR_UUID]) + ) + ) + elif len(config[CONF_DESCRIPTOR_UUID]) == len( + esp32_ble_tracker.bt_uuid32_format + ): + cg.add( + var.set_descr_uuid32( + esp32_ble_tracker.as_hex(config[CONF_DESCRIPTOR_UUID]) + ) + ) + elif len(config[CONF_DESCRIPTOR_UUID]) == len( + esp32_ble_tracker.bt_uuid128_format + ): + uuid128 = esp32_ble_tracker.as_reversed_hex_array( + config[CONF_DESCRIPTOR_UUID] + ) + cg.add(var.set_descr_uuid128(uuid128)) + + await cg.register_component(var, config) + await ble_client.register_ble_node(var, config) + cg.add(var.set_enable_notify(config[CONF_NOTIFY])) + await text_sensor.register_text_sensor(var, config) + for conf in config.get(CONF_ON_NOTIFY, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await ble_client.register_ble_node(trigger, config) + await automation.build_automation(trigger, [(cg.std_string, "x")], conf) diff --git a/esphome/components/ble_client/text_sensor/automation.h b/esphome/components/ble_client/text_sensor/automation.h new file mode 100644 index 0000000000..be85892c5a --- /dev/null +++ b/esphome/components/ble_client/text_sensor/automation.h @@ -0,0 +1,38 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/components/ble_client/text_sensor/ble_text_sensor.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace ble_client { + +class BLETextSensorNotifyTrigger : public Trigger, public BLETextSensor { + public: + explicit BLETextSensorNotifyTrigger(BLETextSensor *sensor) { sensor_ = sensor; } + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) override { + switch (event) { + case ESP_GATTC_SEARCH_CMPL_EVT: { + this->sensor_->node_state = espbt::ClientState::ESTABLISHED; + break; + } + case ESP_GATTC_NOTIFY_EVT: { + if (param->notify.conn_id != this->sensor_->parent()->conn_id || param->notify.handle != this->sensor_->handle) + break; + this->trigger(this->sensor_->parse_data(param->notify.value, param->notify.value_len)); + } + default: + break; + } + } + + protected: + BLETextSensor *sensor_; +}; + +} // namespace ble_client +} // namespace esphome + +#endif diff --git a/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp b/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp new file mode 100644 index 0000000000..c4d175faa4 --- /dev/null +++ b/esphome/components/ble_client/text_sensor/ble_text_sensor.cpp @@ -0,0 +1,137 @@ +#include "ble_text_sensor.h" + +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/core/application.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace ble_client { + +static const char *const TAG = "ble_text_sensor"; + +static const std::string EMPTY = ""; + +uint32_t BLETextSensor::hash_base() { return 193967603UL; } + +void BLETextSensor::loop() {} + +void BLETextSensor::dump_config() { + LOG_TEXT_SENSOR("", "BLE Text Sensor", this); + ESP_LOGCONFIG(TAG, " MAC address : %s", this->parent()->address_str().c_str()); + ESP_LOGCONFIG(TAG, " Service UUID : %s", this->service_uuid_.to_string().c_str()); + ESP_LOGCONFIG(TAG, " Characteristic UUID: %s", this->char_uuid_.to_string().c_str()); + ESP_LOGCONFIG(TAG, " Descriptor UUID : %s", this->descr_uuid_.to_string().c_str()); + ESP_LOGCONFIG(TAG, " Notifications : %s", YESNO(this->notify_)); + LOG_UPDATE_INTERVAL(this); +} + +void BLETextSensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) { + switch (event) { + case ESP_GATTC_OPEN_EVT: { + if (param->open.status == ESP_GATT_OK) { + ESP_LOGI(TAG, "[%s] Connected successfully!", this->get_name().c_str()); + break; + } + break; + } + case ESP_GATTC_DISCONNECT_EVT: { + ESP_LOGW(TAG, "[%s] Disconnected!", this->get_name().c_str()); + this->status_set_warning(); + this->publish_state(EMPTY); + break; + } + case ESP_GATTC_SEARCH_CMPL_EVT: { + this->handle = 0; + auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->char_uuid_); + if (chr == nullptr) { + this->status_set_warning(); + this->publish_state(EMPTY); + ESP_LOGW(TAG, "No sensor characteristic found at service %s char %s", this->service_uuid_.to_string().c_str(), + this->char_uuid_.to_string().c_str()); + break; + } + this->handle = chr->handle; + if (this->descr_uuid_.get_uuid().len > 0) { + auto *descr = chr->get_descriptor(this->descr_uuid_); + if (descr == nullptr) { + this->status_set_warning(); + this->publish_state(EMPTY); + ESP_LOGW(TAG, "No sensor descriptor found at service %s char %s descr %s", + this->service_uuid_.to_string().c_str(), this->char_uuid_.to_string().c_str(), + this->descr_uuid_.to_string().c_str()); + break; + } + this->handle = descr->handle; + } + if (this->notify_) { + auto status = + esp_ble_gattc_register_for_notify(this->parent()->gattc_if, this->parent()->remote_bda, chr->handle); + if (status) { + ESP_LOGW(TAG, "esp_ble_gattc_register_for_notify failed, status=%d", status); + } + } else { + this->node_state = espbt::ClientState::ESTABLISHED; + } + break; + } + case ESP_GATTC_READ_CHAR_EVT: { + if (param->read.conn_id != this->parent()->conn_id) + break; + if (param->read.status != ESP_GATT_OK) { + ESP_LOGW(TAG, "Error reading char at handle %d, status=%d", param->read.handle, param->read.status); + break; + } + if (param->read.handle == this->handle) { + this->status_clear_warning(); + this->publish_state(this->parse_data(param->read.value, param->read.value_len)); + } + break; + } + case ESP_GATTC_NOTIFY_EVT: { + if (param->notify.conn_id != this->parent()->conn_id || param->notify.handle != this->handle) + break; + ESP_LOGV(TAG, "[%s] ESP_GATTC_NOTIFY_EVT: handle=0x%x, value=0x%x", this->get_name().c_str(), + param->notify.handle, param->notify.value[0]); + this->publish_state(this->parse_data(param->notify.value, param->notify.value_len)); + break; + } + case ESP_GATTC_REG_FOR_NOTIFY_EVT: { + this->node_state = espbt::ClientState::ESTABLISHED; + break; + } + default: + break; + } +} + +std::string BLETextSensor::parse_data(uint8_t *value, uint16_t value_len) { + std::string text(value, value + value_len); + return text; +} + +void BLETextSensor::update() { + if (this->node_state != espbt::ClientState::ESTABLISHED) { + ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->get_name().c_str()); + return; + } + if (this->handle == 0) { + ESP_LOGW(TAG, "[%s] Cannot poll, no service or characteristic found", this->get_name().c_str()); + return; + } + + auto status = + esp_ble_gattc_read_char(this->parent()->gattc_if, this->parent()->conn_id, this->handle, ESP_GATT_AUTH_REQ_NONE); + if (status) { + this->status_set_warning(); + this->publish_state(EMPTY); + ESP_LOGW(TAG, "[%s] Error sending read request for sensor, status=%d", this->get_name().c_str(), status); + } +} + +} // namespace ble_client +} // namespace esphome +#endif diff --git a/esphome/components/ble_client/text_sensor/ble_text_sensor.h b/esphome/components/ble_client/text_sensor/ble_text_sensor.h new file mode 100644 index 0000000000..37537307de --- /dev/null +++ b/esphome/components/ble_client/text_sensor/ble_text_sensor.h @@ -0,0 +1,47 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/ble_client/ble_client.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/text_sensor/text_sensor.h" + +#ifdef USE_ESP32 +#include + +namespace esphome { +namespace ble_client { + +namespace espbt = esphome::esp32_ble_tracker; + +class BLETextSensor : public text_sensor::TextSensor, public PollingComponent, public BLEClientNode { + public: + void loop() override; + void update() override; + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void set_service_uuid16(uint16_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); } + void set_service_uuid32(uint32_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } + void set_service_uuid128(uint8_t *uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } + void set_char_uuid16(uint16_t uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); } + void set_char_uuid32(uint32_t uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } + void set_char_uuid128(uint8_t *uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } + void set_descr_uuid16(uint16_t uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); } + void set_descr_uuid32(uint32_t uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } + void set_descr_uuid128(uint8_t *uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } + void set_enable_notify(bool notify) { this->notify_ = notify; } + std::string parse_data(uint8_t *value, uint16_t value_len); + uint16_t handle; + + protected: + uint32_t hash_base() override; + bool notify_; + espbt::ESPBTUUID service_uuid_; + espbt::ESPBTUUID char_uuid_; + espbt::ESPBTUUID descr_uuid_; +}; + +} // namespace ble_client +} // namespace esphome +#endif diff --git a/tests/test1.yaml b/tests/test1.yaml index 9b38d549b3..3d533f33fc 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -2496,6 +2496,18 @@ globals: initial_value: "false" text_sensor: + - platform: ble_client + ble_client_id: ble_foo + name: 'Sensor Location' + service_uuid: '180d' + characteristic_uuid: '2a38' + descriptor_uuid: '2902' + notify: true + update_interval: never + on_notify: + then: + - lambda: |- + ESP_LOGD("green_btn", "Location changed: %s", x); - platform: mqtt_subscribe name: "MQTT Subscribe Text" topic: "the/topic" From 88fbb0ffbbc1207018e5488e95170069e5080a76 Mon Sep 17 00:00:00 2001 From: Arturo Casal Date: Sat, 19 Feb 2022 09:49:45 +0100 Subject: [PATCH 0263/1729] Add sensor support: MAX44009 (#3125) Co-authored-by: Otto Winter --- CODEOWNERS | 1 + esphome/components/max44009/__init__.py | 0 esphome/components/max44009/max44009.cpp | 144 +++++++++++++++++++++++ esphome/components/max44009/max44009.h | 37 ++++++ esphome/components/max44009/sensor.py | 53 +++++++++ tests/test1.yaml | 7 ++ 6 files changed, 242 insertions(+) create mode 100644 esphome/components/max44009/__init__.py create mode 100644 esphome/components/max44009/max44009.cpp create mode 100644 esphome/components/max44009/max44009.h create mode 100644 esphome/components/max44009/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index f533ff5c47..d2971e7c73 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -94,6 +94,7 @@ esphome/components/lilygo_t5_47/touchscreen/* @jesserockz esphome/components/lock/* @esphome/core esphome/components/logger/* @esphome/core esphome/components/ltr390/* @sjtrny +esphome/components/max44009/* @berfenger esphome/components/max7219digit/* @rspaargaren esphome/components/max9611/* @mckaymatthew esphome/components/mcp23008/* @jesserockz diff --git a/esphome/components/max44009/__init__.py b/esphome/components/max44009/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/max44009/max44009.cpp b/esphome/components/max44009/max44009.cpp new file mode 100644 index 0000000000..6f12fb6583 --- /dev/null +++ b/esphome/components/max44009/max44009.cpp @@ -0,0 +1,144 @@ +#include "max44009.h" + +#include "esphome/core/log.h" + +namespace esphome { +namespace max44009 { + +static const char *const TAG = "max44009.sensor"; + +// REGISTERS +static const uint8_t MAX44009_REGISTER_CONFIGURATION = 0x02; +static const uint8_t MAX44009_LUX_READING_HIGH = 0x03; +static const uint8_t MAX44009_LUX_READING_LOW = 0x04; +// CONFIGURATION MASKS +static const uint8_t MAX44009_CFG_CONTINUOUS = 0x80; +// ERROR CODES +static const uint8_t MAX44009_OK = 0; +static const uint8_t MAX44009_ERROR_WIRE_REQUEST = -10; +static const uint8_t MAX44009_ERROR_OVERFLOW = -20; +static const uint8_t MAX44009_ERROR_HIGH_BYTE = -30; +static const uint8_t MAX44009_ERROR_LOW_BYTE = -31; + +void MAX44009Sensor::setup() { + ESP_LOGCONFIG(TAG, "Setting up MAX44009..."); + bool state_ok = false; + if (this->mode_ == MAX44009Mode::MAX44009_MODE_LOW_POWER) { + state_ok = this->set_low_power_mode(); + } else if (this->mode_ == MAX44009Mode::MAX44009_MODE_CONTINUOUS) { + state_ok = this->set_continuous_mode(); + } else { + /* + * Mode AUTO: Set mode depending on update interval + * - On low power mode, the IC measures lux intensity only once every 800ms + * regardless of integration time + * - On continuous mode, the IC continuously measures lux intensity + */ + if (this->get_update_interval() < 800) { + state_ok = this->set_continuous_mode(); + } else { + state_ok = this->set_low_power_mode(); + } + } + if (!state_ok) + this->mark_failed(); +} + +void MAX44009Sensor::dump_config() { + ESP_LOGCONFIG(TAG, "MAX44009:"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with MAX44009 failed!"); + } +} + +float MAX44009Sensor::get_setup_priority() const { return setup_priority::DATA; } + +void MAX44009Sensor::update() { + // update sensor illuminance value + float lux = this->read_illuminance_(); + if (this->error_ != MAX44009_OK) { + this->status_set_warning(); + this->publish_state(NAN); + } else { + this->status_clear_warning(); + this->publish_state(lux); + } +} + +float MAX44009Sensor::read_illuminance_() { + uint8_t datahigh = this->read_(MAX44009_LUX_READING_HIGH); + if (error_ != MAX44009_OK) { + this->error_ = MAX44009_ERROR_HIGH_BYTE; + return this->error_; + } + uint8_t datalow = this->read_(MAX44009_LUX_READING_LOW); + if (error_ != MAX44009_OK) { + this->error_ = MAX44009_ERROR_LOW_BYTE; + return this->error_; + } + uint8_t exponent = datahigh >> 4; + if (exponent == 0x0F) { + this->error_ = MAX44009_ERROR_OVERFLOW; + return this->error_; + } + + return this->convert_to_lux_(datahigh, datalow); +} + +float MAX44009Sensor::convert_to_lux_(uint8_t data_high, uint8_t data_low) { + uint8_t exponent = data_high >> 4; + uint32_t mantissa = ((data_high & 0x0F) << 4) + (data_low & 0x0F); + return ((0x0001 << exponent) * 0.045) * mantissa; +} + +bool MAX44009Sensor::set_continuous_mode() { + uint8_t config = this->read_(MAX44009_REGISTER_CONFIGURATION); + if (this->error_ == MAX44009_OK) { + config |= MAX44009_CFG_CONTINUOUS; + this->write_(MAX44009_REGISTER_CONFIGURATION, config); + this->status_clear_warning(); + ESP_LOGV(TAG, "set to continuous mode"); + return true; + } else { + this->status_set_warning(); + return false; + } +} + +bool MAX44009Sensor::set_low_power_mode() { + uint8_t config = this->read_(MAX44009_REGISTER_CONFIGURATION); + if (this->error_ == MAX44009_OK) { + config &= ~MAX44009_CFG_CONTINUOUS; + this->write_(MAX44009_REGISTER_CONFIGURATION, config); + this->status_clear_warning(); + ESP_LOGV(TAG, "set to low power mode"); + return true; + } else { + this->status_set_warning(); + return false; + } +} + +uint8_t MAX44009Sensor::read_(uint8_t reg) { + uint8_t data = 0; + if (!this->read_byte(reg, &data)) { + this->error_ = MAX44009_ERROR_WIRE_REQUEST; + } else { + this->error_ = MAX44009_OK; + } + return data; +} + +void MAX44009Sensor::write_(uint8_t reg, uint8_t value) { + if (!this->write_byte(reg, value)) { + this->error_ = MAX44009_ERROR_WIRE_REQUEST; + } else { + this->error_ = MAX44009_OK; + } +} + +void MAX44009Sensor::set_mode(MAX44009Mode mode) { this->mode_ = mode; } + +} // namespace max44009 +} // namespace esphome diff --git a/esphome/components/max44009/max44009.h b/esphome/components/max44009/max44009.h new file mode 100644 index 0000000000..c85d1c1028 --- /dev/null +++ b/esphome/components/max44009/max44009.h @@ -0,0 +1,37 @@ +#pragma once + +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace max44009 { + +enum MAX44009Mode { MAX44009_MODE_AUTO, MAX44009_MODE_LOW_POWER, MAX44009_MODE_CONTINUOUS }; + +/// This class implements support for the MAX44009 Illuminance i2c sensor. +class MAX44009Sensor : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice { + public: + MAX44009Sensor() {} + + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + void update() override; + void set_mode(MAX44009Mode mode); + bool set_continuous_mode(); + bool set_low_power_mode(); + + protected: + /// Read the illuminance value + float read_illuminance_(); + float convert_to_lux_(uint8_t data_high, uint8_t data_low); + uint8_t read_(uint8_t reg); + void write_(uint8_t reg, uint8_t value); + + int error_; + MAX44009Mode mode_; +}; + +} // namespace max44009 +} // namespace esphome diff --git a/esphome/components/max44009/sensor.py b/esphome/components/max44009/sensor.py new file mode 100644 index 0000000000..498cccb77b --- /dev/null +++ b/esphome/components/max44009/sensor.py @@ -0,0 +1,53 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, i2c +from esphome.const import ( + CONF_ID, + CONF_MODE, + DEVICE_CLASS_ILLUMINANCE, + STATE_CLASS_MEASUREMENT, + UNIT_LUX, +) + +CODEOWNERS = ["@berfenger"] +DEPENDENCIES = ["i2c"] + +max44009_ns = cg.esphome_ns.namespace("max44009") +MAX44009Sensor = max44009_ns.class_( + "MAX44009Sensor", sensor.Sensor, cg.PollingComponent, i2c.I2CDevice +) + +MAX44009Mode = max44009_ns.enum("MAX44009Mode") +MODE_OPTIONS = { + "auto": MAX44009Mode.MAX44009_MODE_AUTO, + "low_power": MAX44009Mode.MAX44009_MODE_LOW_POWER, + "continuous": MAX44009Mode.MAX44009_MODE_CONTINUOUS, +} + +CONFIG_SCHEMA = ( + sensor.sensor_schema( + unit_of_measurement=UNIT_LUX, + accuracy_decimals=3, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend( + { + cv.GenerateID(): cv.declare_id(MAX44009Sensor), + cv.Optional(CONF_MODE, default="low_power"): cv.enum( + MODE_OPTIONS, lower=True + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x4A)) +) + + +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) + await sensor.register_sensor(var, config) + + cg.add(var.set_mode(config[CONF_MODE])) diff --git a/tests/test1.yaml b/tests/test1.yaml index 3d533f33fc..8b99f86773 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -463,6 +463,13 @@ sensor: state_topic: livingroom/custom_state_topic measurement_duration: 31 i2c_id: i2c_bus + - platform: max44009 + name: "Outside Brightness 1" + internal: true + address: 0x4A + update_interval: 30s + mode: low_power + i2c_id: i2c_bus - platform: bme280 temperature: name: "Outside Temperature" From e445d6aada8c1710758b1b4c2210ac22ed988017 Mon Sep 17 00:00:00 2001 From: Peter Valkov Date: Sat, 19 Feb 2022 11:36:19 +0200 Subject: [PATCH 0264/1729] Fix for api disconnect detection. (#2909) Co-authored-by: Otto Winter --- esphome/components/api/api_connection.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index d9ce6cd79e..b998ef5929 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -105,6 +105,7 @@ void APIConnection::loop() { ESP_LOGW(TAG, "%s didn't respond to ping request in time. Disconnecting...", this->client_info_.c_str()); } } else if (now - this->last_traffic_ > keepalive) { + ESP_LOGVV(TAG, "Sending keepalive PING..."); this->sent_ping_ = true; this->send_ping_request(PingRequest()); } @@ -908,7 +909,7 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) } return false; } - this->last_traffic_ = millis(); + // Do not set last_traffic_ on send return true; } void APIConnection::on_unauthenticated_access() { From ad2f857e15c801b9f673a6fe36621f7ea90f845d Mon Sep 17 00:00:00 2001 From: mknjc Date: Sat, 19 Feb 2022 10:59:53 +0100 Subject: [PATCH 0265/1729] [miscale] Add flag to clear last impedance reading if the newly received reading only contains weight (#3132) --- esphome/components/xiaomi_miscale/sensor.py | 3 +++ .../xiaomi_miscale/xiaomi_miscale.cpp | 25 +++++++++++-------- .../xiaomi_miscale/xiaomi_miscale.h | 2 ++ esphome/const.py | 1 + 4 files changed, 21 insertions(+), 10 deletions(-) diff --git a/esphome/components/xiaomi_miscale/sensor.py b/esphome/components/xiaomi_miscale/sensor.py index 517870cc01..a2a2f3bdfc 100644 --- a/esphome/components/xiaomi_miscale/sensor.py +++ b/esphome/components/xiaomi_miscale/sensor.py @@ -11,6 +11,7 @@ from esphome.const import ( UNIT_OHM, CONF_IMPEDANCE, ICON_OMEGA, + CONF_CLEAR_IMPEDANCE, ) DEPENDENCIES = ["esp32_ble_tracker"] @@ -25,6 +26,7 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(XiaomiMiscale), cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_CLEAR_IMPEDANCE, default=False): cv.boolean, cv.Optional(CONF_WEIGHT): sensor.sensor_schema( unit_of_measurement=UNIT_KILOGRAM, icon=ICON_SCALE_BATHROOM, @@ -50,6 +52,7 @@ async def to_code(config): await esp32_ble_tracker.register_ble_device(var, config) cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + cg.add(var.set_clear_impedance(config[CONF_CLEAR_IMPEDANCE])) if CONF_WEIGHT in config: sens = await sensor.new_sensor(config[CONF_WEIGHT]) diff --git a/esphome/components/xiaomi_miscale/xiaomi_miscale.cpp b/esphome/components/xiaomi_miscale/xiaomi_miscale.cpp index 6c3bd61cac..4ed5f587de 100644 --- a/esphome/components/xiaomi_miscale/xiaomi_miscale.cpp +++ b/esphome/components/xiaomi_miscale/xiaomi_miscale.cpp @@ -24,25 +24,30 @@ bool XiaomiMiscale::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { bool success = false; for (auto &service_data : device.get_service_datas()) { auto res = parse_header_(service_data); - if (!res.has_value()) { + if (!res.has_value()) continue; - } - if (!(parse_message_(service_data.data, *res))) { + if (!parse_message_(service_data.data, *res)) continue; - } - if (!(report_results_(res, device.address_str()))) { + if (!report_results_(res, device.address_str())) continue; - } if (res->weight.has_value() && this->weight_ != nullptr) this->weight_->publish_state(*res->weight); - if (res->version == 1 && this->impedance_ != nullptr) { - ESP_LOGW(TAG, "Impedance is only supported on version 2. Your scale was identified as verison 1."); - } else if (res->impedance.has_value() && this->impedance_ != nullptr) - this->impedance_->publish_state(*res->impedance); + if (this->impedance_ != nullptr) { + if (res->version == 1) { + ESP_LOGW(TAG, "Impedance is only supported on version 2. Your scale was identified as verison 1."); + } else { + if (res->impedance.has_value()) { + this->impedance_->publish_state(*res->impedance); + } else { + if (clear_impedance_) + this->impedance_->publish_state(NAN); + } + } + } success = true; } diff --git a/esphome/components/xiaomi_miscale/xiaomi_miscale.h b/esphome/components/xiaomi_miscale/xiaomi_miscale.h index 3e51405ddc..cff61f153b 100644 --- a/esphome/components/xiaomi_miscale/xiaomi_miscale.h +++ b/esphome/components/xiaomi_miscale/xiaomi_miscale.h @@ -24,11 +24,13 @@ class XiaomiMiscale : public Component, public esp32_ble_tracker::ESPBTDeviceLis float get_setup_priority() const override { return setup_priority::DATA; } void set_weight(sensor::Sensor *weight) { weight_ = weight; } void set_impedance(sensor::Sensor *impedance) { impedance_ = impedance; } + void set_clear_impedance(bool clear_impedance) { clear_impedance_ = clear_impedance; } protected: uint64_t address_; sensor::Sensor *weight_{nullptr}; sensor::Sensor *impedance_{nullptr}; + bool clear_impedance_{false}; optional parse_header_(const esp32_ble_tracker::ServiceData &service_data); bool parse_message_(const std::vector &message, ParseResult &result); diff --git a/esphome/const.py b/esphome/const.py index 721f78f26a..dec68e0701 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -89,6 +89,7 @@ CONF_CHANGE_MODE_EVERY = "change_mode_every" CONF_CHANNEL = "channel" CONF_CHANNELS = "channels" CONF_CHIPSET = "chipset" +CONF_CLEAR_IMPEDANCE = "clear_impedance" CONF_CLIENT_ID = "client_id" CONF_CLK_PIN = "clk_pin" CONF_CLOCK_PIN = "clock_pin" From 125c693e3f54aabdda8901c575a1c1354e919143 Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Sat, 19 Feb 2022 11:41:34 +0100 Subject: [PATCH 0266/1729] Add ESP32 variant config validator function (#3088) * Add esp32_variant config validator function * Drop unused is_esp32c3 function Co-authored-by: Otto Winter --- esphome/components/esp32/__init__.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 1229675ad8..0b2c291ba2 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -65,8 +65,26 @@ def get_esp32_variant(): return CORE.data[KEY_ESP32][KEY_VARIANT] -def is_esp32c3(): - return get_esp32_variant() == VARIANT_ESP32C3 +def only_on_variant(*, supported=None, unsupported=None): + """Config validator for features only available on some ESP32 variants.""" + if supported is not None and not isinstance(supported, list): + supported = [supported] + if unsupported is not None and not isinstance(unsupported, list): + unsupported = [unsupported] + + def validator_(obj): + variant = get_esp32_variant() + if supported is not None and variant not in supported: + raise cv.Invalid( + f"This feature is only available on {', '.join(supported)}" + ) + if unsupported is not None and variant in unsupported: + raise cv.Invalid( + f"This feature is not available on {', '.join(unsupported)}" + ) + return obj + + return validator_ @dataclass From d594f43ebdd4a580ffe8c6933dd814093341cf19 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sat, 19 Feb 2022 14:11:01 +0100 Subject: [PATCH 0267/1729] Publish NAN when dallas conversion failed (#3227) --- esphome/components/dallas/dallas_component.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/dallas/dallas_component.cpp b/esphome/components/dallas/dallas_component.cpp index 56526e98bb..b1d28b8b4c 100644 --- a/esphome/components/dallas/dallas_component.cpp +++ b/esphome/components/dallas/dallas_component.cpp @@ -111,6 +111,9 @@ void DallasComponent::update() { if (!result) { ESP_LOGE(TAG, "Requesting conversion failed"); this->status_set_warning(); + for (auto *sensor : this->sensors_) { + sensor->publish_state(NAN); + } return; } From 0c1520dd9cb8f9ed076619342e476a91f0c48a21 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sat, 19 Feb 2022 14:11:45 +0100 Subject: [PATCH 0268/1729] Fix ESP8266 climate memaccess warning (#3226) --- esphome/components/climate/climate.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index ebea20ed1f..65b5ef4eb4 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -1,4 +1,5 @@ #include "climate.h" +#include "esphome/core/macros.h" namespace esphome { namespace climate { @@ -326,14 +327,17 @@ optional Climate::restore_state_() { return recovered; } void Climate::save_state_() { -#if defined(USE_ESP_IDF) && !defined(CLANG_TIDY) +#if (defined(USE_ESP_IDF) || (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 0, 0))) && \ + !defined(CLANG_TIDY) #pragma GCC diagnostic ignored "-Wclass-memaccess" +#define TEMP_IGNORE_MEMACCESS #endif ClimateDeviceRestoreState state{}; // initialize as zero to prevent random data on stack triggering erase memset(&state, 0, sizeof(ClimateDeviceRestoreState)); -#if USE_ESP_IDF && !defined(CLANG_TIDY) +#ifdef TEMP_IGNORE_MEMACCESS #pragma GCC diagnostic pop +#undef TEMP_IGNORE_MEMACCESS #endif state.mode = this->mode; From ae57ad0c810fbf646ae2e5c2f142cfd37cafe1ee Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sat, 19 Feb 2022 14:42:54 +0100 Subject: [PATCH 0269/1729] Fix warning in test1.yaml (#3228) --- tests/test1.yaml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/test1.yaml b/tests/test1.yaml index 8b99f86773..7eb1b457db 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1097,8 +1097,8 @@ sensor: temperature: name: Max9611 Temp update_interval: 1s - - + + esp32_touch: setup_mode: False iir_filter: 10ms @@ -1317,18 +1317,18 @@ binary_sensor: - platform: analog_threshold name: Analog Trheshold 1 sensor_id: template_sensor - threshold: + threshold: upper: 110 lower: 90 - filters: + filters: - delayed_on: 0s - delayed_off: 10s - platform: analog_threshold name: Analog Trheshold 2 sensor_id: template_sensor threshold: 100 - filters: - - invert: + filters: + - invert: pca9685: frequency: 500 @@ -2514,7 +2514,7 @@ text_sensor: on_notify: then: - lambda: |- - ESP_LOGD("green_btn", "Location changed: %s", x); + ESP_LOGD("green_btn", "Location changed: %s", x.c_str()); - platform: mqtt_subscribe name: "MQTT Subscribe Text" topic: "the/topic" @@ -2582,7 +2582,7 @@ canbus: then: - lambda: |- std::string b(x.begin(), x.end()); - ESP_LOGD("canid 500", "%s", &b[0] ); + ESP_LOGD("canid 500", "%s", b.c_str()); - can_id: 23 then: - if: @@ -2620,7 +2620,7 @@ canbus: then: - lambda: |- std::string b(x.begin(), x.end()); - ESP_LOGD("canid 500", "%s", &b[0] ); + ESP_LOGD("canid 500", "%s", b.c_str() ); - can_id: 23 then: - if: From 34c9d8be50f56981a599a52ffa8e211c30d3a4bb Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sat, 19 Feb 2022 14:46:27 +0100 Subject: [PATCH 0270/1729] Lint trailing whitespace (#3230) --- .github/ISSUE_TEMPLATE/config.yml | 1 - .github/PULL_REQUEST_TEMPLATE.md | 4 ++-- CONTRIBUTING.md | 2 +- esphome/components/fujitsu_general/fujitsu_general.h | 10 +++++----- esphome/components/ili9341/ili9341_init.h | 8 ++++---- script/ci-custom.py | 5 +++++ tests/component_tests/button/test_button.yaml | 1 - 7 files changed, 17 insertions(+), 14 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 4add58dfbe..7f99701e39 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -9,4 +9,3 @@ contact_links: - name: Frequently Asked Question url: https://esphome.io/guides/faq.html about: Please view the FAQ for common questions and what to include in a bug report. - diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 25411c19f5..f9c8cce0af 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,4 +1,4 @@ -# What does this implement/fix? +# What does this implement/fix? Quick description and explanation of changes @@ -35,6 +35,6 @@ Quick description and explanation of changes ## Checklist: - [ ] The code change is tested and works locally. - [ ] Tests have been added to verify that the new code works (under `tests/` folder). - + If user exposed functionality or configuration variables are added/changed: - [ ] Documentation added/updated in [esphome-docs](https://github.com/esphome/esphome-docs). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e96bb5745b..ec23656763 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,7 +5,7 @@ For a detailed guide, please see https://esphome.io/guides/contributing.html#con Things to note when contributing: - Please test your changes :) - - If a new feature is added or an existing user-facing feature is changed, you should also + - If a new feature is added or an existing user-facing feature is changed, you should also update the [docs](https://github.com/esphome/esphome-docs). See [contributing to esphome-docs](https://esphome.io/guides/contributing.html#contributing-to-esphomedocs) for more information. - Please also update the tests in the `tests/` folder. You can do so by just adding a line in one of the YAML files diff --git a/esphome/components/fujitsu_general/fujitsu_general.h b/esphome/components/fujitsu_general/fujitsu_general.h index 8dc7a3e484..ee83ae9d19 100644 --- a/esphome/components/fujitsu_general/fujitsu_general.h +++ b/esphome/components/fujitsu_general/fujitsu_general.h @@ -17,29 +17,29 @@ const uint8_t FUJITSU_GENERAL_TEMP_MAX = 30; // Celsius * turn * on temp mode fan swing * * | | | | | | * - * + * * temperatures 1 1248 124 124 1 * auto auto 18 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 10000100 00000000 00000000 00000000 00000000 00000000 00000100 11110001 * auto auto 19 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 10001100 00000000 00000000 00000000 00000000 00000000 00000100 11111110 * auto auto 30 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 10000111 00000000 00000000 00000000 00000000 00000000 00000100 11110011 - * + * * on flag: * on at 16 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 10000000 00100000 00000000 00000000 00000000 00000000 00000100 11010101 * down to 16 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 00000000 00100000 00000000 00000000 00000000 00000000 00000100 00110101 - * + * * mode options: * auto auto 30 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 10000111 00000000 00000000 00000000 00000000 00000000 00000100 11110011 * cool auto 30 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 10000111 10000000 00000000 00000000 00000000 00000000 00000100 01110011 * dry auto 30 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 10000111 01000000 00000000 00000000 00000000 00000000 00000100 10110011 * fan (auto) (30) 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 10000111 11000000 00000000 00000000 00000000 00000000 00000100 00110011 * heat auto 30 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 10000111 00100000 00000000 00000000 00000000 00000000 00000100 11010011 - * + * * fan options: * heat 30 high 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 10000111 00100000 10000000 00000000 00000000 00000000 00000100 01010011 * heat 30 med 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 00000111 00100000 01000000 00000000 00000000 00000000 00000100 01010011 * heat 30 low 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 00000111 00100000 11000000 00000000 00000000 00000000 00000100 10010011 * heat 30 quiet 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 00000111 00100000 00100000 00000000 00000000 00000000 00000100 00010011 - * + * * swing options: * heat 30 swing vert 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 00000111 00100000 00101000 00000000 00000000 00000000 00000100 00011101 * heat 30 noswing 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 00000111 00100000 00100000 00000000 00000000 00000000 00000100 00010011 diff --git a/esphome/components/ili9341/ili9341_init.h b/esphome/components/ili9341/ili9341_init.h index 9282895e2e..b4f67ff19a 100644 --- a/esphome/components/ili9341/ili9341_init.h +++ b/esphome/components/ili9341/ili9341_init.h @@ -25,10 +25,10 @@ static const uint8_t PROGMEM INITCMD_M5STACK[] = { 0xF2, 1, 0x00, // 3Gamma Function Disable ILI9341_GAMMASET , 1, 0x01, // Gamma curve selected ILI9341_GMCTRP1 , 15, 0x0F, 0x31, 0x2B, 0x0C, 0x0E, 0x08, // Set Gamma - 0x4E, 0xF1, 0x37, 0x07, 0x10, 0x03, + 0x4E, 0xF1, 0x37, 0x07, 0x10, 0x03, 0x0E, 0x09, 0x00, ILI9341_GMCTRN1 , 15, 0x00, 0x0E, 0x14, 0x03, 0x11, 0x07, // Set Gamma - 0x31, 0xC1, 0x48, 0x08, 0x0F, 0x0C, + 0x31, 0xC1, 0x48, 0x08, 0x0F, 0x0C, 0x31, 0x36, 0x0F, ILI9341_SLPOUT , 0x80, // Exit Sleep ILI9341_DISPON , 0x80, // Display on @@ -55,10 +55,10 @@ static const uint8_t PROGMEM INITCMD_TFT[] = { 0xF2, 1, 0x00, // 3Gamma Function Disable ILI9341_GAMMASET , 1, 0x01, // Gamma curve selected ILI9341_GMCTRP1 , 15, 0x0F, 0x31, 0x2B, 0x0C, 0x0E, 0x08, // Set Gamma - 0x4E, 0xF1, 0x37, 0x07, 0x10, 0x03, + 0x4E, 0xF1, 0x37, 0x07, 0x10, 0x03, 0x0E, 0x09, 0x00, ILI9341_GMCTRN1 , 15, 0x00, 0x0E, 0x14, 0x03, 0x11, 0x07, // Set Gamma - 0x31, 0xC1, 0x48, 0x08, 0x0F, 0x0C, + 0x31, 0xC1, 0x48, 0x08, 0x0F, 0x0C, 0x31, 0x36, 0x0F, ILI9341_SLPOUT , 0x80, // Exit Sleep ILI9341_DISPON , 0x80, // Display on diff --git a/script/ci-custom.py b/script/ci-custom.py index 2703e7d311..c0737da103 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -591,6 +591,11 @@ def lint_inclusive_language(fname, match): ) +@lint_re_check(r"[\t\r\f\v ]+$") +def lint_trailing_whitespace(fname, match): + return "Trailing whitespace detected" + + @lint_content_find_check( "ESP_LOG", include=["*.h", "*.tcc"], diff --git a/tests/component_tests/button/test_button.yaml b/tests/component_tests/button/test_button.yaml index 3eac129e8c..32d2e8d93b 100644 --- a/tests/component_tests/button/test_button.yaml +++ b/tests/component_tests/button/test_button.yaml @@ -18,4 +18,3 @@ button: name: wol_test_2 id: wol_2 internal: false - From d2b209234f7f5cbf18a1747d82ca5c54c060f3b3 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sat, 19 Feb 2022 15:09:17 +0100 Subject: [PATCH 0271/1729] Improve ESP8266 iram usage (#3223) --- esphome/core/scheduler.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 7f0ed0b17c..56f823556b 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -116,7 +116,7 @@ optional HOT Scheduler::next_schedule_in() { return 0; return next_time - now; } -void IRAM_ATTR HOT Scheduler::call() { +void HOT Scheduler::call() { const uint32_t now = this->millis_(); this->process_to_add(); From b8d10a62c2436430cc235342062215a6f77b3494 Mon Sep 17 00:00:00 2001 From: Tyler Bules Date: Sat, 19 Feb 2022 09:13:48 -0500 Subject: [PATCH 0272/1729] ESP32-C3 deep sleep fix (#3066) --- esphome/components/deep_sleep/__init__.py | 40 ++++++++++++++++++- .../deep_sleep/deep_sleep_component.cpp | 14 ++++++- .../deep_sleep/deep_sleep_component.h | 5 ++- tests/test1.yaml | 2 +- tests/test2.yaml | 2 +- 5 files changed, 58 insertions(+), 5 deletions(-) diff --git a/esphome/components/deep_sleep/__init__.py b/esphome/components/deep_sleep/__init__.py index ba4c2c0d7e..2a74d0c1bb 100644 --- a/esphome/components/deep_sleep/__init__.py +++ b/esphome/components/deep_sleep/__init__.py @@ -11,9 +11,39 @@ from esphome.const import ( CONF_WAKEUP_PIN, ) +from esphome.components.esp32 import get_esp32_variant +from esphome.components.esp32.const import ( + VARIANT_ESP32, + VARIANT_ESP32C3, +) + +WAKEUP_PINS = { + VARIANT_ESP32: [ + 0, + 2, + 4, + 12, + 13, + 14, + 15, + 25, + 26, + 27, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + ], + VARIANT_ESP32C3: [0, 1, 2, 3, 4, 5], +} + def validate_pin_number(value): - valid_pins = [0, 2, 4, 12, 13, 14, 15, 25, 26, 27, 32, 33, 34, 35, 36, 37, 38, 39] + valid_pins = WAKEUP_PINS.get(get_esp32_variant(), WAKEUP_PINS[VARIANT_ESP32]) if value[CONF_NUMBER] not in valid_pins: raise cv.Invalid( f"Only pins {', '.join(str(x) for x in valid_pins)} support wakeup" @@ -21,6 +51,14 @@ def validate_pin_number(value): return value +def validate_config(config): + if get_esp32_variant() == VARIANT_ESP32C3 and CONF_ESP32_EXT1_WAKEUP in config: + raise cv.Invalid("ESP32-C3 does not support wakeup from touch.") + if get_esp32_variant() == VARIANT_ESP32C3 and CONF_TOUCH_WAKEUP in config: + raise cv.Invalid("ESP32-C3 does not support wakeup from ext1") + return config + + deep_sleep_ns = cg.esphome_ns.namespace("deep_sleep") DeepSleepComponent = deep_sleep_ns.class_("DeepSleepComponent", cg.Component) EnterDeepSleepAction = deep_sleep_ns.class_("EnterDeepSleepAction", automation.Action) diff --git a/esphome/components/deep_sleep/deep_sleep_component.cpp b/esphome/components/deep_sleep/deep_sleep_component.cpp index 7774014d3d..82751b538b 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.cpp +++ b/esphome/components/deep_sleep/deep_sleep_component.cpp @@ -104,7 +104,7 @@ void DeepSleepComponent::begin_sleep(bool manual) { App.run_safe_shutdown_hooks(); -#ifdef USE_ESP32 +#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3) if (this->sleep_duration_.has_value()) esp_sleep_enable_timer_wakeup(*this->sleep_duration_); if (this->wakeup_pin_ != nullptr) { @@ -126,6 +126,18 @@ void DeepSleepComponent::begin_sleep(bool manual) { esp_deep_sleep_start(); #endif +#ifdef USE_ESP32_VARIANT_ESP32C3 + if (this->sleep_duration_.has_value()) + esp_sleep_enable_timer_wakeup(*this->sleep_duration_); + if (this->wakeup_pin_ != nullptr) { + bool level = !this->wakeup_pin_->is_inverted(); + if (this->wakeup_pin_mode_ == WAKEUP_PIN_MODE_INVERT_WAKEUP && this->wakeup_pin_->digital_read()) { + level = !level; + } + esp_deep_sleep_enable_gpio_wakeup(gpio_num_t(this->wakeup_pin_->get_pin()), level); + } +#endif + #ifdef USE_ESP8266 ESP.deepSleep(*this->sleep_duration_); // NOLINT(readability-static-accessed-through-instance) #endif diff --git a/esphome/components/deep_sleep/deep_sleep_component.h b/esphome/components/deep_sleep/deep_sleep_component.h index 59df199a9f..057d992427 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.h +++ b/esphome/components/deep_sleep/deep_sleep_component.h @@ -57,13 +57,16 @@ class DeepSleepComponent : public Component { public: /// Set the duration in ms the component should sleep once it's in deep sleep mode. void set_sleep_duration(uint32_t time_ms); -#ifdef USE_ESP32 +#if defined(USE_ESP32) /** Set the pin to wake up to on the ESP32 once it's in deep sleep mode. * Use the inverted property to set the wakeup level. */ void set_wakeup_pin(InternalGPIOPin *pin) { this->wakeup_pin_ = pin; } void set_wakeup_pin_mode(WakeupPinMode wakeup_pin_mode); +#endif + +#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3) void set_ext1_wakeup(Ext1Wakeup ext1_wakeup); diff --git a/tests/test1.yaml b/tests/test1.yaml index 7eb1b457db..4e82d06485 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -262,7 +262,7 @@ power_supply: deep_sleep: run_duration: 20s sleep_duration: 50s - wakeup_pin: GPIO39 + wakeup_pin: GPIO2 wakeup_pin_mode: INVERT_WAKEUP ads1115: diff --git a/tests/test2.yaml b/tests/test2.yaml index 2a122b971f..76b9775c54 100644 --- a/tests/test2.yaml +++ b/tests/test2.yaml @@ -60,7 +60,7 @@ deep_sleep: gpio_wakeup_reason: 10s touch_wakeup_reason: 15s sleep_duration: 50s - wakeup_pin: GPIO39 + wakeup_pin: GPIO2 wakeup_pin_mode: INVERT_WAKEUP as3935_i2c: From debcaf6fb7602db12f98cb5c02fbed4b5e7591dc Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sat, 19 Feb 2022 15:47:50 +0100 Subject: [PATCH 0273/1729] Add ESP32C3 and ESP32S2 support to dashboard (#3152) * Add ESP32C3 and ESP32S2 support to dashboard * Format * Fix tests --- esphome/components/esp32/__init__.py | 4 +-- esphome/storage_json.py | 12 ++++--- esphome/wizard.py | 49 +++++++++++++++++++++------- tests/unit_tests/test_wizard.py | 4 +-- 4 files changed, 50 insertions(+), 19 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 0b2c291ba2..478c9701b8 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -61,8 +61,8 @@ def set_core_data(config): return config -def get_esp32_variant(): - return CORE.data[KEY_ESP32][KEY_VARIANT] +def get_esp32_variant(core_obj=None): + return (core_obj or CORE).data[KEY_ESP32][KEY_VARIANT] def only_on_variant(*, supported=None, unsupported=None): diff --git a/esphome/storage_json.py b/esphome/storage_json.py index 207a3edf57..d561de4ce6 100644 --- a/esphome/storage_json.py +++ b/esphome/storage_json.py @@ -64,7 +64,7 @@ class StorageJSON: # Web server port of the ESP, for example 80 assert web_port is None or isinstance(web_port, int) self.web_port = web_port # type: int - # The type of ESP in use, either ESP32 or ESP8266 + # The type of hardware in use, like "ESP32", "ESP32C3", "ESP8266", etc. self.target_platform = target_platform # type: str # The absolute path to the platformio project self.build_path = build_path # type: str @@ -99,6 +99,11 @@ class StorageJSON: def from_esphome_core( esph, old ): # type: (CoreType, Optional[StorageJSON]) -> StorageJSON + hardware = esph.target_platform + if esph.is_esp32: + from esphome.components import esp32 + + hardware = esp32.get_esp32_variant(esph) return StorageJSON( storage_version=1, name=esph.name, @@ -107,15 +112,14 @@ class StorageJSON: src_version=1, address=esph.address, web_port=esph.web_port, - target_platform=esph.target_platform, + target_platform=hardware, build_path=esph.build_path, firmware_bin_path=esph.firmware_bin, loaded_integrations=list(esph.loaded_integrations), ) @staticmethod - def from_wizard(name, address, esp_platform): - # type: (str, str, str) -> StorageJSON + def from_wizard(name: str, address: str, esp_platform: str) -> "StorageJSON": return StorageJSON( storage_version=1, name=name, diff --git a/esphome/wizard.py b/esphome/wizard.py index c64ad3a583..34930ff66f 100644 --- a/esphome/wizard.py +++ b/esphome/wizard.py @@ -67,6 +67,27 @@ esp32: type: arduino """ +ESP32S2_CONFIG = """ +esp32: + board: {board} + framework: + type: esp-idf +""" + +ESP32C3_CONFIG = """ +esp32: + board: {board} + framework: + type: esp-idf +""" + +HARDWARE_BASE_CONFIGS = { + "ESP8266": ESP8266_CONFIG, + "ESP32": ESP32_CONFIG, + "ESP32S2": ESP32S2_CONFIG, + "ESP32C3": ESP32C3_CONFIG, +} + def sanitize_double_quotes(value): return value.replace("\\", "\\\\").replace('"', '\\"') @@ -83,11 +104,7 @@ def wizard_file(**kwargs): config = BASE_CONFIG.format(**kwargs) - config += ( - ESP8266_CONFIG.format(**kwargs) - if kwargs["platform"] == "ESP8266" - else ESP32_CONFIG.format(**kwargs) - ) + config += HARDWARE_BASE_CONFIGS[kwargs["platform"]].format(**kwargs) config += LOGGER_API_CONFIG @@ -119,16 +136,26 @@ def wizard_file(**kwargs): """ # pylint: disable=consider-using-f-string - config += """ + if kwargs["platform"] in ["ESP8266", "ESP32"]: + config += """ # Enable fallback hotspot (captive portal) in case wifi connection fails ap: ssid: "{fallback_name}" password: "{fallback_psk}" captive_portal: -""".format( - **kwargs - ) + """.format( + **kwargs + ) + else: + config += """ + # Enable fallback hotspot in case wifi connection fails + ap: + ssid: "{fallback_name}" + password: "{fallback_psk}" + """.format( + **kwargs + ) return config @@ -147,10 +174,10 @@ def wizard_write(path, **kwargs): kwargs["platform"] = ( "ESP8266" if board in esp8266_boards.ESP8266_BOARD_PINS else "ESP32" ) - platform = kwargs["platform"] + hardware = kwargs["platform"] write_file(path, wizard_file(**kwargs)) - storage = StorageJSON.from_wizard(name, f"{name}.local", platform) + storage = StorageJSON.from_wizard(name, f"{name}.local", hardware) storage_path = ext_storage_path(os.path.dirname(path), os.path.basename(path)) storage.save(storage_path) diff --git a/tests/unit_tests/test_wizard.py b/tests/unit_tests/test_wizard.py index 59fcfbff60..79a5894075 100644 --- a/tests/unit_tests/test_wizard.py +++ b/tests/unit_tests/test_wizard.py @@ -3,14 +3,14 @@ import esphome.wizard as wz import pytest from esphome.components.esp8266.boards import ESP8266_BOARD_PINS -from mock import MagicMock +from unittest.mock import MagicMock @pytest.fixture def default_config(): return { "name": "test-name", - "platform": "test_platform", + "platform": "ESP8266", "board": "esp01_1m", "ssid": "test_ssid", "psk": "test_psk", From 5811389891e814b138eb8dd5236d730c9c0c581b Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sat, 19 Feb 2022 15:49:20 +0100 Subject: [PATCH 0274/1729] BH1750 dynamically calculate options (#3214) * BH1750 dynamically calculate options * Fix tests * Fix NAN * Convert setup to new-style * Add myself as codeowner --- CODEOWNERS | 1 + esphome/components/bh1750/bh1750.cpp | 184 +++++++++++++++++++-------- esphome/components/bh1750/bh1750.h | 27 +--- esphome/components/bh1750/sensor.py | 24 +--- tests/test1.yaml | 2 - 5 files changed, 142 insertions(+), 96 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index d2971e7c73..61469c53fa 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -28,6 +28,7 @@ esphome/components/atc_mithermometer/* @ahpohl esphome/components/b_parasite/* @rbaron esphome/components/ballu/* @bazuchan esphome/components/bang_bang/* @OttoWinter +esphome/components/bh1750/* @OttoWinter esphome/components/binary_sensor/* @esphome/core esphome/components/bl0940/* @tobias- esphome/components/ble_client/* @buxtronix diff --git a/esphome/components/bh1750/bh1750.cpp b/esphome/components/bh1750/bh1750.cpp index 4e6bb3c563..de6d811ed2 100644 --- a/esphome/components/bh1750/bh1750.cpp +++ b/esphome/components/bh1750/bh1750.cpp @@ -9,18 +9,109 @@ static const char *const TAG = "bh1750.sensor"; static const uint8_t BH1750_COMMAND_POWER_ON = 0b00000001; static const uint8_t BH1750_COMMAND_MT_REG_HI = 0b01000000; // last 3 bits static const uint8_t BH1750_COMMAND_MT_REG_LO = 0b01100000; // last 5 bits +static const uint8_t BH1750_COMMAND_ONE_TIME_L = 0b00100011; +static const uint8_t BH1750_COMMAND_ONE_TIME_H = 0b00100000; +static const uint8_t BH1750_COMMAND_ONE_TIME_H2 = 0b00100001; + +/* +bh1750 properties: + +L-resolution mode: +- resolution 4lx (@ mtreg=69) +- measurement time: typ=16ms, max=24ms, scaled by MTreg value divided by 69 +- formula: counts / 1.2 * (69 / MTreg) lx +H-resolution mode: +- resolution 1lx (@ mtreg=69) +- measurement time: typ=120ms, max=180ms, scaled by MTreg value divided by 69 +- formula: counts / 1.2 * (69 / MTreg) lx +H-resolution mode2: +- resolution 0.5lx (@ mtreg=69) +- measurement time: typ=120ms, max=180ms, scaled by MTreg value divided by 69 +- formula: counts / 1.2 * (69 / MTreg) / 2 lx + +MTreg: +- min=31, default=69, max=254 + +-> only reason to use l-resolution is faster, but offers no higher range +-> below ~7000lx, makes sense to use H-resolution2 @ MTreg=254 +-> try to maximize MTreg to get lowest noise level +*/ void BH1750Sensor::setup() { ESP_LOGCONFIG(TAG, "Setting up BH1750 '%s'...", this->name_.c_str()); - if (!this->write_bytes(BH1750_COMMAND_POWER_ON, nullptr, 0)) { + uint8_t turn_on = BH1750_COMMAND_POWER_ON; + if (this->write(&turn_on, 1) != i2c::ERROR_OK) { this->mark_failed(); return; } +} - uint8_t mtreg_hi = (this->measurement_duration_ >> 5) & 0b111; - uint8_t mtreg_lo = (this->measurement_duration_ >> 0) & 0b11111; - this->write_bytes(BH1750_COMMAND_MT_REG_HI | mtreg_hi, nullptr, 0); - this->write_bytes(BH1750_COMMAND_MT_REG_LO | mtreg_lo, nullptr, 0); +void BH1750Sensor::read_lx_(BH1750Mode mode, uint8_t mtreg, const std::function &f) { + // turn on (after one-shot sensor automatically powers down) + uint8_t turn_on = BH1750_COMMAND_POWER_ON; + if (this->write(&turn_on, 1) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Turning on BH1750 failed"); + f(NAN); + return; + } + + if (active_mtreg_ != mtreg) { + // set mtreg + uint8_t mtreg_hi = BH1750_COMMAND_MT_REG_HI | ((mtreg >> 5) & 0b111); + uint8_t mtreg_lo = BH1750_COMMAND_MT_REG_LO | ((mtreg >> 0) & 0b11111); + if (this->write(&mtreg_hi, 1) != i2c::ERROR_OK || this->write(&mtreg_lo, 1) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Setting measurement time for BH1750 failed"); + active_mtreg_ = 0; + f(NAN); + return; + } + active_mtreg_ = mtreg; + } + + uint8_t cmd; + uint16_t meas_time; + switch (mode) { + case BH1750_MODE_L: + cmd = BH1750_COMMAND_ONE_TIME_L; + meas_time = 24 * mtreg / 69; + break; + case BH1750_MODE_H: + cmd = BH1750_COMMAND_ONE_TIME_H; + meas_time = 180 * mtreg / 69; + break; + case BH1750_MODE_H2: + cmd = BH1750_COMMAND_ONE_TIME_H2; + meas_time = 180 * mtreg / 69; + break; + default: + f(NAN); + return; + } + if (this->write(&cmd, 1) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Starting measurement for BH1750 failed"); + f(NAN); + return; + } + + // probably not needed, but adjust for rounding + meas_time++; + + this->set_timeout("read", meas_time, [this, mode, mtreg, f]() { + uint16_t raw_value; + if (this->read(reinterpret_cast(&raw_value), 2) != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Reading BH1750 data failed"); + f(NAN); + return; + } + raw_value = i2c::i2ctohs(raw_value); + + float lx = float(raw_value) / 1.2f; + lx *= 69.0f / mtreg; + if (mode == BH1750_MODE_H2) + lx /= 2.0f; + + f(lx); + }); } void BH1750Sensor::dump_config() { @@ -30,64 +121,49 @@ void BH1750Sensor::dump_config() { ESP_LOGE(TAG, "Communication with BH1750 failed!"); } - const char *resolution_s; - switch (this->resolution_) { - case BH1750_RESOLUTION_0P5_LX: - resolution_s = "0.5"; - break; - case BH1750_RESOLUTION_1P0_LX: - resolution_s = "1"; - break; - case BH1750_RESOLUTION_4P0_LX: - resolution_s = "4"; - break; - default: - resolution_s = "Unknown"; - break; - } - ESP_LOGCONFIG(TAG, " Resolution: %s", resolution_s); LOG_UPDATE_INTERVAL(this); } void BH1750Sensor::update() { - if (!this->write_bytes(this->resolution_, nullptr, 0)) - return; + // first do a quick measurement in L-mode with full range + // to find right range + this->read_lx_(BH1750_MODE_L, 31, [this](float val) { + if (std::isnan(val)) { + this->status_set_warning(); + this->publish_state(NAN); + return; + } - uint32_t wait = 0; - // use max conversion times - switch (this->resolution_) { - case BH1750_RESOLUTION_0P5_LX: - case BH1750_RESOLUTION_1P0_LX: - wait = 180; - break; - case BH1750_RESOLUTION_4P0_LX: - wait = 24; - break; - } + BH1750Mode use_mode; + uint8_t use_mtreg; + if (val <= 7000) { + use_mode = BH1750_MODE_H2; + use_mtreg = 254; + } else { + use_mode = BH1750_MODE_H; + // lx = counts / 1.2 * (69 / mtreg) + // -> mtreg = counts / 1.2 * (69 / lx) + // calculate for counts=50000 (allow some range to not saturate, but maximize mtreg) + // -> mtreg = 50000*(10/12)*(69/lx) + int ideal_mtreg = 50000 * 10 * 69 / (12 * (int) val); + use_mtreg = std::min(254, std::max(31, ideal_mtreg)); + } + ESP_LOGV(TAG, "L result: %f -> Calculated mode=%d, mtreg=%d", val, (int) use_mode, use_mtreg); - this->set_timeout("illuminance", wait, [this]() { this->read_data_(); }); + this->read_lx_(use_mode, use_mtreg, [this](float val) { + if (std::isnan(val)) { + this->status_set_warning(); + this->publish_state(NAN); + return; + } + ESP_LOGD(TAG, "'%s': Got illuminance=%.1flx", this->get_name().c_str(), val); + this->status_clear_warning(); + this->publish_state(val); + }); + }); } float BH1750Sensor::get_setup_priority() const { return setup_priority::DATA; } -void BH1750Sensor::read_data_() { - uint16_t raw_value; - if (this->read(reinterpret_cast(&raw_value), 2) != i2c::ERROR_OK) { - this->status_set_warning(); - return; - } - raw_value = i2c::i2ctohs(raw_value); - - float lx = float(raw_value) / 1.2f; - lx *= 69.0f / this->measurement_duration_; - if (this->resolution_ == BH1750_RESOLUTION_0P5_LX) { - lx /= 2.0f; - } - ESP_LOGD(TAG, "'%s': Got illuminance=%.1flx", this->get_name().c_str(), lx); - this->publish_state(lx); - this->status_clear_warning(); -} - -void BH1750Sensor::set_resolution(BH1750Resolution resolution) { this->resolution_ = resolution; } } // namespace bh1750 } // namespace esphome diff --git a/esphome/components/bh1750/bh1750.h b/esphome/components/bh1750/bh1750.h index c88fa10832..a31eb33609 100644 --- a/esphome/components/bh1750/bh1750.h +++ b/esphome/components/bh1750/bh1750.h @@ -7,29 +7,15 @@ namespace esphome { namespace bh1750 { -/// Enum listing all resolutions that can be used with the BH1750 -enum BH1750Resolution { - BH1750_RESOLUTION_4P0_LX = 0b00100011, // one-time low resolution mode - BH1750_RESOLUTION_1P0_LX = 0b00100000, // one-time high resolution mode 1 - BH1750_RESOLUTION_0P5_LX = 0b00100001, // one-time high resolution mode 2 +enum BH1750Mode { + BH1750_MODE_L, + BH1750_MODE_H, + BH1750_MODE_H2, }; /// This class implements support for the i2c-based BH1750 ambient light sensor. class BH1750Sensor : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice { public: - /** Set the resolution of this sensor. - * - * Possible values are: - * - * - `BH1750_RESOLUTION_4P0_LX` - * - `BH1750_RESOLUTION_1P0_LX` - * - `BH1750_RESOLUTION_0P5_LX` (default) - * - * @param resolution The new resolution of the sensor. - */ - void set_resolution(BH1750Resolution resolution); - void set_measurement_duration(uint8_t measurement_duration) { measurement_duration_ = measurement_duration; } - // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) void setup() override; @@ -38,10 +24,9 @@ class BH1750Sensor : public sensor::Sensor, public PollingComponent, public i2c: float get_setup_priority() const override; protected: - void read_data_(); + void read_lx_(BH1750Mode mode, uint8_t mtreg, const std::function &f); - BH1750Resolution resolution_{BH1750_RESOLUTION_0P5_LX}; - uint8_t measurement_duration_; + uint8_t active_mtreg_{0}; }; } // namespace bh1750 diff --git a/esphome/components/bh1750/sensor.py b/esphome/components/bh1750/sensor.py index 904e716eb8..69778f49ce 100644 --- a/esphome/components/bh1750/sensor.py +++ b/esphome/components/bh1750/sensor.py @@ -2,28 +2,20 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor from esphome.const import ( - CONF_RESOLUTION, DEVICE_CLASS_ILLUMINANCE, STATE_CLASS_MEASUREMENT, UNIT_LUX, - CONF_MEASUREMENT_DURATION, ) DEPENDENCIES = ["i2c"] +CODEOWNERS = ["@OttoWinter"] bh1750_ns = cg.esphome_ns.namespace("bh1750") -BH1750Resolution = bh1750_ns.enum("BH1750Resolution") -BH1750_RESOLUTIONS = { - 4.0: BH1750Resolution.BH1750_RESOLUTION_4P0_LX, - 1.0: BH1750Resolution.BH1750_RESOLUTION_1P0_LX, - 0.5: BH1750Resolution.BH1750_RESOLUTION_0P5_LX, -} BH1750Sensor = bh1750_ns.class_( "BH1750Sensor", sensor.Sensor, cg.PollingComponent, i2c.I2CDevice ) -CONF_MEASUREMENT_TIME = "measurement_time" CONFIG_SCHEMA = ( sensor.sensor_schema( BH1750Sensor, @@ -34,14 +26,11 @@ CONFIG_SCHEMA = ( ) .extend( { - cv.Optional(CONF_RESOLUTION, default=0.5): cv.enum( - BH1750_RESOLUTIONS, float=True + cv.Optional("resolution"): cv.invalid( + "The 'resolution' option has been removed. The optimal value is now dynamically calculated." ), - cv.Optional(CONF_MEASUREMENT_DURATION, default=69): cv.int_range( - min=31, max=254 - ), - cv.Optional(CONF_MEASUREMENT_TIME): cv.invalid( - "The 'measurement_time' option has been replaced with 'measurement_duration' in 1.18.0" + cv.Optional("measurement_duration"): cv.invalid( + "The 'measurement_duration' option has been removed. The optimal value is now dynamically calculated." ), } ) @@ -54,6 +43,3 @@ async def to_code(config): var = await sensor.new_sensor(config) await cg.register_component(var, config) await i2c.register_i2c_device(var, config) - - cg.add(var.set_resolution(config[CONF_RESOLUTION])) - cg.add(var.set_measurement_duration(config[CONF_MEASUREMENT_DURATION])) diff --git a/tests/test1.yaml b/tests/test1.yaml index 4e82d06485..2acb420fb5 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -456,12 +456,10 @@ sensor: name: "Living Room Brightness 3" internal: true address: 0x23 - resolution: 1.0 update_interval: 30s retain: False availability: state_topic: livingroom/custom_state_topic - measurement_duration: 31 i2c_id: i2c_bus - platform: max44009 name: "Outside Brightness 1" From 8dae7f82252ed6815910090c413437865d1c3656 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sat, 19 Feb 2022 15:57:52 +0100 Subject: [PATCH 0275/1729] Bump esphome-dashboard from 20220209.0 to 20220219.0 (#3231) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index acbf1d9984..427045af02 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ pyserial==3.5 platformio==5.2.5 # When updating platformio, also update Dockerfile esptool==3.2 click==8.0.3 -esphome-dashboard==20220209.0 +esphome-dashboard==20220219.0 aioesphomeapi==10.8.2 zeroconf==0.38.3 From f59dbe4a88aa21bc829e2dd46cca3f14fc295f81 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sun, 20 Feb 2022 21:13:37 +0100 Subject: [PATCH 0276/1729] Add copy integration (#3241) --- CODEOWNERS | 1 + esphome/components/copy/__init__.py | 5 ++ .../components/copy/binary_sensor/__init__.py | 41 ++++++++++++++ .../copy/binary_sensor/copy_binary_sensor.cpp | 18 +++++++ .../copy/binary_sensor/copy_binary_sensor.h | 21 ++++++++ esphome/components/copy/button/__init__.py | 42 +++++++++++++++ .../components/copy/button/copy_button.cpp | 14 +++++ esphome/components/copy/button/copy_button.h | 22 ++++++++ esphome/components/copy/cover/__init__.py | 38 +++++++++++++ esphome/components/copy/cover/copy_cover.cpp | 50 +++++++++++++++++ esphome/components/copy/cover/copy_cover.h | 25 +++++++++ esphome/components/copy/fan/__init__.py | 36 +++++++++++++ esphome/components/copy/fan/copy_fan.cpp | 53 +++++++++++++++++++ esphome/components/copy/fan/copy_fan.h | 26 +++++++++ esphome/components/copy/lock/__init__.py | 36 +++++++++++++ esphome/components/copy/lock/copy_lock.cpp | 29 ++++++++++ esphome/components/copy/lock/copy_lock.h | 23 ++++++++ esphome/components/copy/number/__init__.py | 38 +++++++++++++ .../components/copy/number/copy_number.cpp | 29 ++++++++++ esphome/components/copy/number/copy_number.h | 23 ++++++++ esphome/components/copy/select/__init__.py | 36 +++++++++++++ .../components/copy/select/copy_select.cpp | 27 ++++++++++ esphome/components/copy/select/copy_select.h | 23 ++++++++ esphome/components/copy/sensor/__init__.py | 45 ++++++++++++++++ .../components/copy/sensor/copy_sensor.cpp | 18 +++++++ esphome/components/copy/sensor/copy_sensor.h | 21 ++++++++ esphome/components/copy/switch/__init__.py | 38 +++++++++++++ .../components/copy/switch/copy_switch.cpp | 26 +++++++++ esphome/components/copy/switch/copy_switch.h | 23 ++++++++ .../components/copy/text_sensor/__init__.py | 37 +++++++++++++ .../copy/text_sensor/copy_text_sensor.cpp | 18 +++++++ .../copy/text_sensor/copy_text_sensor.h | 21 ++++++++ esphome/const.py | 1 + tests/test1.yaml | 9 ++++ tests/test4.yaml | 21 ++++++++ 35 files changed, 934 insertions(+) create mode 100644 esphome/components/copy/__init__.py create mode 100644 esphome/components/copy/binary_sensor/__init__.py create mode 100644 esphome/components/copy/binary_sensor/copy_binary_sensor.cpp create mode 100644 esphome/components/copy/binary_sensor/copy_binary_sensor.h create mode 100644 esphome/components/copy/button/__init__.py create mode 100644 esphome/components/copy/button/copy_button.cpp create mode 100644 esphome/components/copy/button/copy_button.h create mode 100644 esphome/components/copy/cover/__init__.py create mode 100644 esphome/components/copy/cover/copy_cover.cpp create mode 100644 esphome/components/copy/cover/copy_cover.h create mode 100644 esphome/components/copy/fan/__init__.py create mode 100644 esphome/components/copy/fan/copy_fan.cpp create mode 100644 esphome/components/copy/fan/copy_fan.h create mode 100644 esphome/components/copy/lock/__init__.py create mode 100644 esphome/components/copy/lock/copy_lock.cpp create mode 100644 esphome/components/copy/lock/copy_lock.h create mode 100644 esphome/components/copy/number/__init__.py create mode 100644 esphome/components/copy/number/copy_number.cpp create mode 100644 esphome/components/copy/number/copy_number.h create mode 100644 esphome/components/copy/select/__init__.py create mode 100644 esphome/components/copy/select/copy_select.cpp create mode 100644 esphome/components/copy/select/copy_select.h create mode 100644 esphome/components/copy/sensor/__init__.py create mode 100644 esphome/components/copy/sensor/copy_sensor.cpp create mode 100644 esphome/components/copy/sensor/copy_sensor.h create mode 100644 esphome/components/copy/switch/__init__.py create mode 100644 esphome/components/copy/switch/copy_switch.cpp create mode 100644 esphome/components/copy/switch/copy_switch.h create mode 100644 esphome/components/copy/text_sensor/__init__.py create mode 100644 esphome/components/copy/text_sensor/copy_text_sensor.cpp create mode 100644 esphome/components/copy/text_sensor/copy_text_sensor.h diff --git a/CODEOWNERS b/CODEOWNERS index 61469c53fa..4edfde7711 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -44,6 +44,7 @@ esphome/components/climate/* @esphome/core esphome/components/climate_ir/* @glmnet esphome/components/color_temperature/* @jesserockz esphome/components/coolix/* @glmnet +esphome/components/copy/* @OttoWinter esphome/components/cover/* @esphome/core esphome/components/cs5460a/* @balrog-kun esphome/components/cse7761/* @berfenger diff --git a/esphome/components/copy/__init__.py b/esphome/components/copy/__init__.py new file mode 100644 index 0000000000..7594894650 --- /dev/null +++ b/esphome/components/copy/__init__.py @@ -0,0 +1,5 @@ +import esphome.codegen as cg + +CODEOWNERS = ["@OttoWinter"] + +copy_ns = cg.esphome_ns.namespace("copy") diff --git a/esphome/components/copy/binary_sensor/__init__.py b/esphome/components/copy/binary_sensor/__init__.py new file mode 100644 index 0000000000..1b6836fae7 --- /dev/null +++ b/esphome/components/copy/binary_sensor/__init__.py @@ -0,0 +1,41 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import binary_sensor +from esphome.const import ( + CONF_DEVICE_CLASS, + CONF_ENTITY_CATEGORY, + CONF_ICON, + CONF_SOURCE_ID, +) +from esphome.core.entity_helpers import inherit_property_from + +from .. import copy_ns + +CopyBinarySensor = copy_ns.class_( + "CopyBinarySensor", binary_sensor.BinarySensor, cg.Component +) + + +CONFIG_SCHEMA = ( + binary_sensor.binary_sensor_schema(CopyBinarySensor) + .extend( + { + cv.Required(CONF_SOURCE_ID): cv.use_id(binary_sensor.BinarySensor), + } + ) + .extend(cv.COMPONENT_SCHEMA) +) + +FINAL_VALIDATE_SCHEMA = cv.All( + inherit_property_from(CONF_ICON, CONF_SOURCE_ID), + inherit_property_from(CONF_ENTITY_CATEGORY, CONF_SOURCE_ID), + inherit_property_from(CONF_DEVICE_CLASS, CONF_SOURCE_ID), +) + + +async def to_code(config): + var = await binary_sensor.new_binary_sensor(config) + await cg.register_component(var, config) + + source = await cg.get_variable(config[CONF_SOURCE_ID]) + cg.add(var.set_source(source)) diff --git a/esphome/components/copy/binary_sensor/copy_binary_sensor.cpp b/esphome/components/copy/binary_sensor/copy_binary_sensor.cpp new file mode 100644 index 0000000000..0d96f58750 --- /dev/null +++ b/esphome/components/copy/binary_sensor/copy_binary_sensor.cpp @@ -0,0 +1,18 @@ +#include "copy_binary_sensor.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace copy { + +static const char *const TAG = "copy.binary_sensor"; + +void CopyBinarySensor::setup() { + source_->add_on_state_callback([this](bool value) { this->publish_state(value); }); + if (source_->has_state()) + this->publish_state(source_->state); +} + +void CopyBinarySensor::dump_config() { LOG_BINARY_SENSOR("", "Copy Binary Sensor", this); } + +} // namespace copy +} // namespace esphome diff --git a/esphome/components/copy/binary_sensor/copy_binary_sensor.h b/esphome/components/copy/binary_sensor/copy_binary_sensor.h new file mode 100644 index 0000000000..d62ed13c76 --- /dev/null +++ b/esphome/components/copy/binary_sensor/copy_binary_sensor.h @@ -0,0 +1,21 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/binary_sensor/binary_sensor.h" + +namespace esphome { +namespace copy { + +class CopyBinarySensor : public binary_sensor::BinarySensor, public Component { + public: + void set_source(binary_sensor::BinarySensor *source) { source_ = source; } + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + + protected: + binary_sensor::BinarySensor *source_; +}; + +} // namespace copy +} // namespace esphome diff --git a/esphome/components/copy/button/__init__.py b/esphome/components/copy/button/__init__.py new file mode 100644 index 0000000000..65d956601a --- /dev/null +++ b/esphome/components/copy/button/__init__.py @@ -0,0 +1,42 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import button +from esphome.const import ( + CONF_DEVICE_CLASS, + CONF_ENTITY_CATEGORY, + CONF_ICON, + CONF_ID, + CONF_SOURCE_ID, +) +from esphome.core.entity_helpers import inherit_property_from + +from .. import copy_ns + +CopyButton = copy_ns.class_("CopyButton", button.Button, cg.Component) + + +CONFIG_SCHEMA = ( + button.button_schema() + .extend( + { + cv.GenerateID(): cv.declare_id(CopyButton), + cv.Required(CONF_SOURCE_ID): cv.use_id(button.Button), + } + ) + .extend(cv.COMPONENT_SCHEMA) +) + +FINAL_VALIDATE_SCHEMA = cv.All( + inherit_property_from(CONF_ICON, CONF_SOURCE_ID), + inherit_property_from(CONF_ENTITY_CATEGORY, CONF_SOURCE_ID), + inherit_property_from(CONF_DEVICE_CLASS, CONF_SOURCE_ID), +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await button.register_button(var, config) + await cg.register_component(var, config) + + source = await cg.get_variable(config[CONF_SOURCE_ID]) + cg.add(var.set_source(source)) diff --git a/esphome/components/copy/button/copy_button.cpp b/esphome/components/copy/button/copy_button.cpp new file mode 100644 index 0000000000..595388775c --- /dev/null +++ b/esphome/components/copy/button/copy_button.cpp @@ -0,0 +1,14 @@ +#include "copy_button.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace copy { + +static const char *const TAG = "copy.button"; + +void CopyButton::dump_config() { LOG_BUTTON("", "Copy Button", this); } + +void CopyButton::press_action() { source_->press(); } + +} // namespace copy +} // namespace esphome diff --git a/esphome/components/copy/button/copy_button.h b/esphome/components/copy/button/copy_button.h new file mode 100644 index 0000000000..9996ca0c65 --- /dev/null +++ b/esphome/components/copy/button/copy_button.h @@ -0,0 +1,22 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/button/button.h" + +namespace esphome { +namespace copy { + +class CopyButton : public button::Button, public Component { + public: + void set_source(button::Button *source) { source_ = source; } + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + + protected: + void press_action() override; + + button::Button *source_; +}; + +} // namespace copy +} // namespace esphome diff --git a/esphome/components/copy/cover/__init__.py b/esphome/components/copy/cover/__init__.py new file mode 100644 index 0000000000..155e22883b --- /dev/null +++ b/esphome/components/copy/cover/__init__.py @@ -0,0 +1,38 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import cover +from esphome.const import ( + CONF_DEVICE_CLASS, + CONF_ENTITY_CATEGORY, + CONF_ICON, + CONF_ID, + CONF_SOURCE_ID, +) +from esphome.core.entity_helpers import inherit_property_from + +from .. import copy_ns + +CopyCover = copy_ns.class_("CopyCover", cover.Cover, cg.Component) + + +CONFIG_SCHEMA = cover.COVER_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(CopyCover), + cv.Required(CONF_SOURCE_ID): cv.use_id(cover.Cover), + } +).extend(cv.COMPONENT_SCHEMA) + +FINAL_VALIDATE_SCHEMA = cv.All( + inherit_property_from(CONF_ICON, CONF_SOURCE_ID), + inherit_property_from(CONF_ENTITY_CATEGORY, CONF_SOURCE_ID), + inherit_property_from(CONF_DEVICE_CLASS, CONF_SOURCE_ID), +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cover.register_cover(var, config) + await cg.register_component(var, config) + + source = await cg.get_variable(config[CONF_SOURCE_ID]) + cg.add(var.set_source(source)) diff --git a/esphome/components/copy/cover/copy_cover.cpp b/esphome/components/copy/cover/copy_cover.cpp new file mode 100644 index 0000000000..cf50473018 --- /dev/null +++ b/esphome/components/copy/cover/copy_cover.cpp @@ -0,0 +1,50 @@ +#include "copy_cover.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace copy { + +static const char *const TAG = "copy.cover"; + +void CopyCover::setup() { + source_->add_on_state_callback([this]() { + this->current_operation = this->source_->current_operation; + this->position = this->source_->position; + this->tilt = this->source_->tilt; + this->publish_state(); + }); + + this->current_operation = this->source_->current_operation; + this->position = this->source_->position; + this->tilt = this->source_->tilt; + this->publish_state(); +} + +void CopyCover::dump_config() { LOG_COVER("", "Copy Cover", this); } + +cover::CoverTraits CopyCover::get_traits() { + auto base = source_->get_traits(); + cover::CoverTraits traits{}; + // copy traits manually so it doesn't break when new options are added + // but the control() method hasn't implemented them yet. + traits.set_is_assumed_state(base.get_is_assumed_state()); + traits.set_supports_position(base.get_supports_position()); + traits.set_supports_tilt(base.get_supports_tilt()); + traits.set_supports_toggle(base.get_supports_toggle()); + return traits; +} + +void CopyCover::control(const cover::CoverCall &call) { + auto call2 = source_->make_call(); + call2.set_stop(call.get_stop()); + if (call.get_tilt().has_value()) + call2.set_tilt(*call.get_tilt()); + if (call.get_position().has_value()) + call2.set_position(*call.get_position()); + if (call.get_tilt().has_value()) + call2.set_tilt(*call.get_tilt()); + call2.perform(); +} + +} // namespace copy +} // namespace esphome diff --git a/esphome/components/copy/cover/copy_cover.h b/esphome/components/copy/cover/copy_cover.h new file mode 100644 index 0000000000..fb278523ff --- /dev/null +++ b/esphome/components/copy/cover/copy_cover.h @@ -0,0 +1,25 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/cover/cover.h" + +namespace esphome { +namespace copy { + +class CopyCover : public cover::Cover, public Component { + public: + void set_source(cover::Cover *source) { source_ = source; } + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + + cover::CoverTraits get_traits() override; + + protected: + void control(const cover::CoverCall &call) override; + + cover::Cover *source_; +}; + +} // namespace copy +} // namespace esphome diff --git a/esphome/components/copy/fan/__init__.py b/esphome/components/copy/fan/__init__.py new file mode 100644 index 0000000000..22672c02d8 --- /dev/null +++ b/esphome/components/copy/fan/__init__.py @@ -0,0 +1,36 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import fan +from esphome.const import ( + CONF_ENTITY_CATEGORY, + CONF_ICON, + CONF_ID, + CONF_SOURCE_ID, +) +from esphome.core.entity_helpers import inherit_property_from + +from .. import copy_ns + +CopyFan = copy_ns.class_("CopyFan", fan.Fan, cg.Component) + + +CONFIG_SCHEMA = fan.FAN_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(CopyFan), + cv.Required(CONF_SOURCE_ID): cv.use_id(fan.Fan), + } +).extend(cv.COMPONENT_SCHEMA) + +FINAL_VALIDATE_SCHEMA = cv.All( + inherit_property_from(CONF_ICON, CONF_SOURCE_ID), + inherit_property_from(CONF_ENTITY_CATEGORY, CONF_SOURCE_ID), +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await fan.register_fan(var, config) + await cg.register_component(var, config) + + source = await cg.get_variable(config[CONF_SOURCE_ID]) + cg.add(var.set_source(source)) diff --git a/esphome/components/copy/fan/copy_fan.cpp b/esphome/components/copy/fan/copy_fan.cpp new file mode 100644 index 0000000000..74d9da279f --- /dev/null +++ b/esphome/components/copy/fan/copy_fan.cpp @@ -0,0 +1,53 @@ +#include "copy_fan.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace copy { + +static const char *const TAG = "copy.fan"; + +void CopyFan::setup() { + source_->add_on_state_callback([this]() { + this->state = source_->state; + this->oscillating = source_->oscillating; + this->speed = source_->speed; + this->direction = source_->direction; + this->publish_state(); + }); + + this->state = source_->state; + this->oscillating = source_->oscillating; + this->speed = source_->speed; + this->direction = source_->direction; + this->publish_state(); +} + +void CopyFan::dump_config() { LOG_FAN("", "Copy Fan", this); } + +fan::FanTraits CopyFan::get_traits() { + fan::FanTraits traits; + auto base = source_->get_traits(); + // copy traits manually so it doesn't break when new options are added + // but the control() method hasn't implemented them yet. + traits.set_oscillation(base.supports_oscillation()); + traits.set_speed(base.supports_speed()); + traits.set_supported_speed_count(base.supported_speed_count()); + traits.set_direction(base.supports_direction()); + return traits; +} + +void CopyFan::control(const fan::FanCall &call) { + auto call2 = source_->make_call(); + if (call.get_state().has_value()) + call2.set_state(*call.get_state()); + if (call.get_oscillating().has_value()) + call2.set_oscillating(*call.get_oscillating()); + if (call.get_speed().has_value()) + call2.set_speed(*call.get_speed()); + if (call.get_direction().has_value()) + call2.set_direction(*call.get_direction()); + call2.perform(); +} + +} // namespace copy +} // namespace esphome diff --git a/esphome/components/copy/fan/copy_fan.h b/esphome/components/copy/fan/copy_fan.h new file mode 100644 index 0000000000..1a69810510 --- /dev/null +++ b/esphome/components/copy/fan/copy_fan.h @@ -0,0 +1,26 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/fan/fan.h" + +namespace esphome { +namespace copy { + +class CopyFan : public fan::Fan, public Component { + public: + void set_source(fan::Fan *source) { source_ = source; } + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + + fan::FanTraits get_traits() override; + + protected: + void control(const fan::FanCall &call) override; + ; + + fan::Fan *source_; +}; + +} // namespace copy +} // namespace esphome diff --git a/esphome/components/copy/lock/__init__.py b/esphome/components/copy/lock/__init__.py new file mode 100644 index 0000000000..d19e4a5807 --- /dev/null +++ b/esphome/components/copy/lock/__init__.py @@ -0,0 +1,36 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import lock +from esphome.const import ( + CONF_ENTITY_CATEGORY, + CONF_ICON, + CONF_ID, + CONF_SOURCE_ID, +) +from esphome.core.entity_helpers import inherit_property_from + +from .. import copy_ns + +CopyLock = copy_ns.class_("CopyLock", lock.Lock, cg.Component) + + +CONFIG_SCHEMA = lock.LOCK_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(CopyLock), + cv.Required(CONF_SOURCE_ID): cv.use_id(lock.Lock), + } +).extend(cv.COMPONENT_SCHEMA) + +FINAL_VALIDATE_SCHEMA = cv.All( + inherit_property_from(CONF_ICON, CONF_SOURCE_ID), + inherit_property_from(CONF_ENTITY_CATEGORY, CONF_SOURCE_ID), +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await lock.register_lock(var, config) + await cg.register_component(var, config) + + source = await cg.get_variable(config[CONF_SOURCE_ID]) + cg.add(var.set_source(source)) diff --git a/esphome/components/copy/lock/copy_lock.cpp b/esphome/components/copy/lock/copy_lock.cpp new file mode 100644 index 0000000000..67a8acffec --- /dev/null +++ b/esphome/components/copy/lock/copy_lock.cpp @@ -0,0 +1,29 @@ +#include "copy_lock.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace copy { + +static const char *const TAG = "copy.lock"; + +void CopyLock::setup() { + source_->add_on_state_callback([this]() { this->publish_state(source_->state); }); + + traits.set_assumed_state(source_->traits.get_assumed_state()); + traits.set_requires_code(source_->traits.get_requires_code()); + traits.set_supported_states(source_->traits.get_supported_states()); + traits.set_supports_open(source_->traits.get_supports_open()); + + this->publish_state(source_->state); +} + +void CopyLock::dump_config() { LOG_LOCK("", "Copy Lock", this); } + +void CopyLock::control(const lock::LockCall &call) { + auto call2 = source_->make_call(); + call2.set_state(call.get_state()); + call2.perform(); +} + +} // namespace copy +} // namespace esphome diff --git a/esphome/components/copy/lock/copy_lock.h b/esphome/components/copy/lock/copy_lock.h new file mode 100644 index 0000000000..0554013674 --- /dev/null +++ b/esphome/components/copy/lock/copy_lock.h @@ -0,0 +1,23 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/lock/lock.h" + +namespace esphome { +namespace copy { + +class CopyLock : public lock::Lock, public Component { + public: + void set_source(lock::Lock *source) { source_ = source; } + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + + protected: + void control(const lock::LockCall &call) override; + + lock::Lock *source_; +}; + +} // namespace copy +} // namespace esphome diff --git a/esphome/components/copy/number/__init__.py b/esphome/components/copy/number/__init__.py new file mode 100644 index 0000000000..4e78627a1f --- /dev/null +++ b/esphome/components/copy/number/__init__.py @@ -0,0 +1,38 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import number +from esphome.const import ( + CONF_ENTITY_CATEGORY, + CONF_ICON, + CONF_MODE, + CONF_SOURCE_ID, + CONF_UNIT_OF_MEASUREMENT, +) +from esphome.core.entity_helpers import inherit_property_from + +from .. import copy_ns + +CopyNumber = copy_ns.class_("CopyNumber", number.Number, cg.Component) + + +CONFIG_SCHEMA = number.NUMBER_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(CopyNumber), + cv.Required(CONF_SOURCE_ID): cv.use_id(number.Number), + } +).extend(cv.COMPONENT_SCHEMA) + +FINAL_VALIDATE_SCHEMA = cv.All( + inherit_property_from(CONF_ICON, CONF_SOURCE_ID), + inherit_property_from(CONF_ENTITY_CATEGORY, CONF_SOURCE_ID), + inherit_property_from(CONF_UNIT_OF_MEASUREMENT, CONF_SOURCE_ID), + inherit_property_from(CONF_MODE, CONF_SOURCE_ID), +) + + +async def to_code(config): + var = await number.new_number(config, min_value=0, max_value=0, step=0) + await cg.register_component(var, config) + + source = await cg.get_variable(config[CONF_SOURCE_ID]) + cg.add(var.set_source(source)) diff --git a/esphome/components/copy/number/copy_number.cpp b/esphome/components/copy/number/copy_number.cpp new file mode 100644 index 0000000000..46dc200b73 --- /dev/null +++ b/esphome/components/copy/number/copy_number.cpp @@ -0,0 +1,29 @@ +#include "copy_number.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace copy { + +static const char *const TAG = "copy.number"; + +void CopyNumber::setup() { + source_->add_on_state_callback([this](float value) { this->publish_state(value); }); + + traits.set_min_value(source_->traits.get_min_value()); + traits.set_max_value(source_->traits.get_max_value()); + traits.set_step(source_->traits.get_step()); + + if (source_->has_state()) + this->publish_state(source_->state); +} + +void CopyNumber::dump_config() { LOG_NUMBER("", "Copy Number", this); } + +void CopyNumber::control(float value) { + auto call2 = source_->make_call(); + call2.set_value(value); + call2.perform(); +} + +} // namespace copy +} // namespace esphome diff --git a/esphome/components/copy/number/copy_number.h b/esphome/components/copy/number/copy_number.h new file mode 100644 index 0000000000..1ad956fec4 --- /dev/null +++ b/esphome/components/copy/number/copy_number.h @@ -0,0 +1,23 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/number/number.h" + +namespace esphome { +namespace copy { + +class CopyNumber : public number::Number, public Component { + public: + void set_source(number::Number *source) { source_ = source; } + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + + protected: + void control(float value) override; + + number::Number *source_; +}; + +} // namespace copy +} // namespace esphome diff --git a/esphome/components/copy/select/__init__.py b/esphome/components/copy/select/__init__.py new file mode 100644 index 0000000000..7d4c1c7705 --- /dev/null +++ b/esphome/components/copy/select/__init__.py @@ -0,0 +1,36 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import select +from esphome.const import ( + CONF_ENTITY_CATEGORY, + CONF_ICON, + CONF_ID, + CONF_SOURCE_ID, +) +from esphome.core.entity_helpers import inherit_property_from + +from .. import copy_ns + +CopySelect = copy_ns.class_("CopySelect", select.Select, cg.Component) + + +CONFIG_SCHEMA = select.SELECT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(CopySelect), + cv.Required(CONF_SOURCE_ID): cv.use_id(select.Select), + } +).extend(cv.COMPONENT_SCHEMA) + +FINAL_VALIDATE_SCHEMA = cv.All( + inherit_property_from(CONF_ICON, CONF_SOURCE_ID), + inherit_property_from(CONF_ENTITY_CATEGORY, CONF_SOURCE_ID), +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await select.register_select(var, config, options=[]) + await cg.register_component(var, config) + + source = await cg.get_variable(config[CONF_SOURCE_ID]) + cg.add(var.set_source(source)) diff --git a/esphome/components/copy/select/copy_select.cpp b/esphome/components/copy/select/copy_select.cpp new file mode 100644 index 0000000000..0f01c2692c --- /dev/null +++ b/esphome/components/copy/select/copy_select.cpp @@ -0,0 +1,27 @@ +#include "copy_select.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace copy { + +static const char *const TAG = "copy.select"; + +void CopySelect::setup() { + source_->add_on_state_callback([this](const std::string &value) { this->publish_state(value); }); + + traits.set_options(source_->traits.get_options()); + + if (source_->has_state()) + this->publish_state(source_->state); +} + +void CopySelect::dump_config() { LOG_SELECT("", "Copy Select", this); } + +void CopySelect::control(const std::string &value) { + auto call = source_->make_call(); + call.set_option(value); + call.perform(); +} + +} // namespace copy +} // namespace esphome diff --git a/esphome/components/copy/select/copy_select.h b/esphome/components/copy/select/copy_select.h new file mode 100644 index 0000000000..c8666cd394 --- /dev/null +++ b/esphome/components/copy/select/copy_select.h @@ -0,0 +1,23 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/select/select.h" + +namespace esphome { +namespace copy { + +class CopySelect : public select::Select, public Component { + public: + void set_source(select::Select *source) { source_ = source; } + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + + protected: + void control(const std::string &value) override; + + select::Select *source_; +}; + +} // namespace copy +} // namespace esphome diff --git a/esphome/components/copy/sensor/__init__.py b/esphome/components/copy/sensor/__init__.py new file mode 100644 index 0000000000..8e78cda7c7 --- /dev/null +++ b/esphome/components/copy/sensor/__init__.py @@ -0,0 +1,45 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import ( + CONF_DEVICE_CLASS, + CONF_ENTITY_CATEGORY, + CONF_ICON, + CONF_SOURCE_ID, + CONF_STATE_CLASS, + CONF_UNIT_OF_MEASUREMENT, + CONF_ACCURACY_DECIMALS, +) +from esphome.core.entity_helpers import inherit_property_from + +from .. import copy_ns + +CopySensor = copy_ns.class_("CopySensor", sensor.Sensor, cg.Component) + + +CONFIG_SCHEMA = ( + sensor.sensor_schema(CopySensor) + .extend( + { + cv.Required(CONF_SOURCE_ID): cv.use_id(sensor.Sensor), + } + ) + .extend(cv.COMPONENT_SCHEMA) +) + +FINAL_VALIDATE_SCHEMA = cv.All( + inherit_property_from(CONF_UNIT_OF_MEASUREMENT, CONF_SOURCE_ID), + inherit_property_from(CONF_ICON, CONF_SOURCE_ID), + inherit_property_from(CONF_ACCURACY_DECIMALS, CONF_SOURCE_ID), + inherit_property_from(CONF_DEVICE_CLASS, CONF_SOURCE_ID), + inherit_property_from(CONF_STATE_CLASS, CONF_SOURCE_ID), + inherit_property_from(CONF_ENTITY_CATEGORY, CONF_SOURCE_ID), +) + + +async def to_code(config): + var = await sensor.new_sensor(config) + await cg.register_component(var, config) + + source = await cg.get_variable(config[CONF_SOURCE_ID]) + cg.add(var.set_source(source)) diff --git a/esphome/components/copy/sensor/copy_sensor.cpp b/esphome/components/copy/sensor/copy_sensor.cpp new file mode 100644 index 0000000000..a47a0cf22b --- /dev/null +++ b/esphome/components/copy/sensor/copy_sensor.cpp @@ -0,0 +1,18 @@ +#include "copy_sensor.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace copy { + +static const char *const TAG = "copy.sensor"; + +void CopySensor::setup() { + source_->add_on_state_callback([this](float value) { this->publish_state(value); }); + if (source_->has_state()) + this->publish_state(source_->state); +} + +void CopySensor::dump_config() { LOG_SENSOR("", "Copy Sensor", this); } + +} // namespace copy +} // namespace esphome diff --git a/esphome/components/copy/sensor/copy_sensor.h b/esphome/components/copy/sensor/copy_sensor.h new file mode 100644 index 0000000000..1ae790ada3 --- /dev/null +++ b/esphome/components/copy/sensor/copy_sensor.h @@ -0,0 +1,21 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace copy { + +class CopySensor : public sensor::Sensor, public Component { + public: + void set_source(sensor::Sensor *source) { source_ = source; } + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + + protected: + sensor::Sensor *source_; +}; + +} // namespace copy +} // namespace esphome diff --git a/esphome/components/copy/switch/__init__.py b/esphome/components/copy/switch/__init__.py new file mode 100644 index 0000000000..6622412123 --- /dev/null +++ b/esphome/components/copy/switch/__init__.py @@ -0,0 +1,38 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import switch +from esphome.const import ( + CONF_DEVICE_CLASS, + CONF_ENTITY_CATEGORY, + CONF_ICON, + CONF_ID, + CONF_SOURCE_ID, +) +from esphome.core.entity_helpers import inherit_property_from + +from .. import copy_ns + +CopySwitch = copy_ns.class_("CopySwitch", switch.Switch, cg.Component) + + +CONFIG_SCHEMA = switch.SWITCH_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(CopySwitch), + cv.Required(CONF_SOURCE_ID): cv.use_id(switch.Switch), + } +).extend(cv.COMPONENT_SCHEMA) + +FINAL_VALIDATE_SCHEMA = cv.All( + inherit_property_from(CONF_ICON, CONF_SOURCE_ID), + inherit_property_from(CONF_ENTITY_CATEGORY, CONF_SOURCE_ID), + inherit_property_from(CONF_DEVICE_CLASS, CONF_SOURCE_ID), +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await switch.register_switch(var, config) + await cg.register_component(var, config) + + source = await cg.get_variable(config[CONF_SOURCE_ID]) + cg.add(var.set_source(source)) diff --git a/esphome/components/copy/switch/copy_switch.cpp b/esphome/components/copy/switch/copy_switch.cpp new file mode 100644 index 0000000000..8a9fbb03dd --- /dev/null +++ b/esphome/components/copy/switch/copy_switch.cpp @@ -0,0 +1,26 @@ +#include "copy_switch.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace copy { + +static const char *const TAG = "copy.switch"; + +void CopySwitch::setup() { + source_->add_on_state_callback([this](float value) { this->publish_state(value); }); + + this->publish_state(source_->state); +} + +void CopySwitch::dump_config() { LOG_SWITCH("", "Copy Switch", this); } + +void CopySwitch::write_state(bool state) { + if (state) { + source_->turn_on(); + } else { + source_->turn_off(); + } +} + +} // namespace copy +} // namespace esphome diff --git a/esphome/components/copy/switch/copy_switch.h b/esphome/components/copy/switch/copy_switch.h new file mode 100644 index 0000000000..26cb254ab3 --- /dev/null +++ b/esphome/components/copy/switch/copy_switch.h @@ -0,0 +1,23 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/switch/switch.h" + +namespace esphome { +namespace copy { + +class CopySwitch : public switch_::Switch, public Component { + public: + void set_source(switch_::Switch *source) { source_ = source; } + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + + protected: + void write_state(bool state) override; + + switch_::Switch *source_; +}; + +} // namespace copy +} // namespace esphome diff --git a/esphome/components/copy/text_sensor/__init__.py b/esphome/components/copy/text_sensor/__init__.py new file mode 100644 index 0000000000..5b59f21319 --- /dev/null +++ b/esphome/components/copy/text_sensor/__init__.py @@ -0,0 +1,37 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import text_sensor +from esphome.const import ( + CONF_ENTITY_CATEGORY, + CONF_ICON, + CONF_SOURCE_ID, +) +from esphome.core.entity_helpers import inherit_property_from + +from .. import copy_ns + +CopyTextSensor = copy_ns.class_("CopyTextSensor", text_sensor.TextSensor, cg.Component) + + +CONFIG_SCHEMA = ( + text_sensor.text_sensor_schema(CopyTextSensor) + .extend( + { + cv.Required(CONF_SOURCE_ID): cv.use_id(text_sensor.TextSensor), + } + ) + .extend(cv.COMPONENT_SCHEMA) +) + +FINAL_VALIDATE_SCHEMA = cv.All( + inherit_property_from(CONF_ICON, CONF_SOURCE_ID), + inherit_property_from(CONF_ENTITY_CATEGORY, CONF_SOURCE_ID), +) + + +async def to_code(config): + var = await text_sensor.new_text_sensor(config) + await cg.register_component(var, config) + + source = await cg.get_variable(config[CONF_SOURCE_ID]) + cg.add(var.set_source(source)) diff --git a/esphome/components/copy/text_sensor/copy_text_sensor.cpp b/esphome/components/copy/text_sensor/copy_text_sensor.cpp new file mode 100644 index 0000000000..95fa6d7a1b --- /dev/null +++ b/esphome/components/copy/text_sensor/copy_text_sensor.cpp @@ -0,0 +1,18 @@ +#include "copy_text_sensor.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace copy { + +static const char *const TAG = "copy.text_sensor"; + +void CopyTextSensor::setup() { + source_->add_on_state_callback([this](const std::string &value) { this->publish_state(value); }); + if (source_->has_state()) + this->publish_state(source_->state); +} + +void CopyTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Copy Sensor", this); } + +} // namespace copy +} // namespace esphome diff --git a/esphome/components/copy/text_sensor/copy_text_sensor.h b/esphome/components/copy/text_sensor/copy_text_sensor.h new file mode 100644 index 0000000000..fe91fe948b --- /dev/null +++ b/esphome/components/copy/text_sensor/copy_text_sensor.h @@ -0,0 +1,21 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/text_sensor/text_sensor.h" + +namespace esphome { +namespace copy { + +class CopyTextSensor : public text_sensor::TextSensor, public Component { + public: + void set_source(text_sensor::TextSensor *source) { source_ = source; } + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + + protected: + text_sensor::TextSensor *source_; +}; + +} // namespace copy +} // namespace esphome diff --git a/esphome/const.py b/esphome/const.py index dec68e0701..00cc90ad6a 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -621,6 +621,7 @@ CONF_SLEEP_PIN = "sleep_pin" CONF_SLEEP_WHEN_DONE = "sleep_when_done" CONF_SONY = "sony" CONF_SOURCE = "source" +CONF_SOURCE_ID = "source_id" CONF_SPEED = "speed" CONF_SPEED_COMMAND_TOPIC = "speed_command_topic" CONF_SPEED_COUNT = "speed_count" diff --git a/tests/test1.yaml b/tests/test1.yaml index 2acb420fb5..c4398498c1 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -2104,6 +2104,9 @@ fan: on_speed_set: then: - logger.log: "Fan speed was changed!" + - platform: copy + source_id: fan_speed + name: "Fan Speed Copy" interval: - interval: 10s @@ -2671,6 +2674,9 @@ select: - one - two optimistic: true + - platform: copy + source_id: test_select + name: Test Select Copy qr_code: - id: homepage_qr @@ -2700,3 +2706,6 @@ lock: name: "Generic Output Lock" id: test_lock2 output: pca_6 + - platform: copy + source_id: test_lock2 + name: Generic Output Lock Copy diff --git a/tests/test4.yaml b/tests/test4.yaml index 998db8ed2d..54412222b5 100644 --- a/tests/test4.yaml +++ b/tests/test4.yaml @@ -221,6 +221,10 @@ sensor: - platform: mcp3204 name: "MCP3204 Pin 1" number: 1 + id: mcp_sensor + - platform: copy + source_id: mcp_sensor + name: "MCP binary sensor copy" # # platform sensor.apds9960 requires component apds9960 @@ -364,6 +368,9 @@ switch: name: inverter0_pv_ok_condition_for_parallel pv_power_balance: name: inverter0_pv_power_balance + - platform: copy + source_id: tuya_switch + name: Tuya Switch Copy light: - platform: fastled_clockless @@ -391,6 +398,9 @@ cover: - platform: tuya id: tuya_cover position_datapoint: 2 + - platform: copy + source_id: tuya_cover + name: "Tuya Cover copy" display: - platform: addressable_light @@ -465,6 +475,9 @@ number: min_value: 0 max_value: 17 step: 1 + - platform: copy + source_id: tuya_number + name: Tuya Number Copy text_sensor: - platform: pipsolar @@ -484,6 +497,9 @@ text_sensor: last_qflag: id: inverter0_last_qflag name: inverter0_last_qflag + - platform: copy + source_id: inverter0_device_mode + name: "Inverter Text Sensor Copy" output: - platform: pipsolar @@ -552,6 +568,11 @@ button: name: Safe Mode Button - platform: shutdown name: Shutdown Button + id: shutdown_btn + - platform: copy + source_id: shutdown_btn + name: Shutdown Button Copy + touchscreen: - platform: ektf2232 From d26141151a1cede351895468803b79b897f80d6a Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sun, 20 Feb 2022 21:17:51 +0100 Subject: [PATCH 0277/1729] Button code cleanup (#3242) --- esphome/components/button/button.cpp | 6 +----- esphome/components/button/button.h | 4 ++-- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/esphome/components/button/button.cpp b/esphome/components/button/button.cpp index d57b46e9aa..0a5c6567bc 100644 --- a/esphome/components/button/button.cpp +++ b/esphome/components/button/button.cpp @@ -18,11 +18,7 @@ void Button::add_on_press_callback(std::function &&callback) { this->pre uint32_t Button::hash_base() { return 1495763804UL; } void Button::set_device_class(const std::string &device_class) { this->device_class_ = device_class; } -std::string Button::get_device_class() { - if (this->device_class_.has_value()) - return *this->device_class_; - return ""; -} +std::string Button::get_device_class() { return this->device_class_; } } // namespace button } // namespace esphome diff --git a/esphome/components/button/button.h b/esphome/components/button/button.h index b21a96b8e1..f60c2a2bc5 100644 --- a/esphome/components/button/button.h +++ b/esphome/components/button/button.h @@ -45,12 +45,12 @@ class Button : public EntityBase { protected: /** You should implement this virtual method if you want to create your own button. */ - virtual void press_action(){}; + virtual void press_action() = 0; uint32_t hash_base() override; CallbackManager press_callback_{}; - optional device_class_{}; + std::string device_class_{}; }; } // namespace button From 07c1cf71377821499f730c6af8b13554a31697b0 Mon Sep 17 00:00:00 2001 From: cstaahl <44496349+cstaahl@users.noreply.github.com> Date: Sun, 20 Feb 2022 21:32:35 +0100 Subject: [PATCH 0278/1729] Pulse meter internal filter mode (#3082) Co-authored-by: Paul Daumlechner Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Co-authored-by: Otto Winter --- CODEOWNERS | 2 +- .../pulse_meter/pulse_meter_sensor.cpp | 70 +++++++++++++------ .../pulse_meter/pulse_meter_sensor.h | 11 ++- esphome/components/pulse_meter/sensor.py | 14 +++- esphome/const.py | 1 + esphome/cpp_generator.py | 2 +- 6 files changed, 76 insertions(+), 24 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 4edfde7711..52e80a5822 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -144,7 +144,7 @@ esphome/components/pn532_spi/* @OttoWinter @jesserockz esphome/components/power_supply/* @esphome/core esphome/components/preferences/* @esphome/core esphome/components/psram/* @esphome/core -esphome/components/pulse_meter/* @stevebaxter +esphome/components/pulse_meter/* @cstaahl @stevebaxter esphome/components/pvvx_mithermometer/* @pasiz esphome/components/qr_code/* @wjtje esphome/components/radon_eye_ble/* @jeffeb3 diff --git a/esphome/components/pulse_meter/pulse_meter_sensor.cpp b/esphome/components/pulse_meter/pulse_meter_sensor.cpp index 7d526b241b..f747f9ee40 100644 --- a/esphome/components/pulse_meter/pulse_meter_sensor.cpp +++ b/esphome/components/pulse_meter/pulse_meter_sensor.cpp @@ -12,18 +12,53 @@ void PulseMeterSensor::setup() { this->pin_->attach_interrupt(PulseMeterSensor::gpio_intr, this, gpio::INTERRUPT_ANY_EDGE); this->last_detected_edge_us_ = 0; - this->last_valid_edge_us_ = 0; + this->last_valid_low_edge_us_ = 0; + this->last_valid_high_edge_us_ = 0; + this->sensor_is_high_ = this->isr_pin_.digital_read(); } void PulseMeterSensor::loop() { const uint32_t now = micros(); + // Check to see if we should filter this edge out + if (this->filter_mode_ == FILTER_EDGE) { + if ((this->last_detected_edge_us_ - this->last_valid_high_edge_us_) >= this->filter_us_) { + // Don't measure the first valid pulse (we need at least two pulses to measure the width) + if (this->last_valid_high_edge_us_ != 0) { + this->pulse_width_us_ = (this->last_detected_edge_us_ - this->last_valid_high_edge_us_); + } + this->total_pulses_++; + this->last_valid_high_edge_us_ = this->last_detected_edge_us_; + } + } else { + // Make sure the signal has been stable long enough + if ((now - this->last_detected_edge_us_) >= this->filter_us_) { + // Only consider HIGH pulses and "new" edges if sensor state is LOW + if (!this->sensor_is_high_ && this->isr_pin_.digital_read() && + (this->last_detected_edge_us_ != this->last_valid_high_edge_us_)) { + // Don't measure the first valid pulse (we need at least two pulses to measure the width) + if (this->last_valid_high_edge_us_ != 0) { + this->pulse_width_us_ = (this->last_detected_edge_us_ - this->last_valid_high_edge_us_); + } + this->sensor_is_high_ = true; + this->total_pulses_++; + this->last_valid_high_edge_us_ = this->last_detected_edge_us_; + } + // Only consider LOW pulses and "new" edges if sensor state is HIGH + else if (this->sensor_is_high_ && !this->isr_pin_.digital_read() && + (this->last_detected_edge_us_ != this->last_valid_low_edge_us_)) { + this->sensor_is_high_ = false; + this->last_valid_low_edge_us_ = this->last_detected_edge_us_; + } + } + } + // If we've exceeded our timeout interval without receiving any pulses, assume 0 pulses/min until // we get at least two valid pulses. - const uint32_t time_since_valid_edge_us = now - this->last_valid_edge_us_; - if ((this->last_valid_edge_us_ != 0) && (time_since_valid_edge_us > this->timeout_us_)) { + const uint32_t time_since_valid_edge_us = now - this->last_valid_high_edge_us_; + if ((this->last_valid_high_edge_us_ != 0) && (time_since_valid_edge_us > this->timeout_us_) && + (this->pulse_width_us_ != 0)) { ESP_LOGD(TAG, "No pulse detected for %us, assuming 0 pulses/min", time_since_valid_edge_us / 1000000); - this->last_valid_edge_us_ = 0; this->pulse_width_us_ = 0; } @@ -52,7 +87,11 @@ void PulseMeterSensor::set_total_pulses(uint32_t pulses) { this->total_pulses_ = void PulseMeterSensor::dump_config() { LOG_SENSOR("", "Pulse Meter", this); LOG_PIN(" Pin: ", this->pin_); - ESP_LOGCONFIG(TAG, " Filtering pulses shorter than %u µs", this->filter_us_); + if (this->filter_mode_ == FILTER_EDGE) { + ESP_LOGCONFIG(TAG, " Filtering rising edges less than %u µs apart", this->filter_us_); + } else { + ESP_LOGCONFIG(TAG, " Filtering pulses shorter than %u µs", this->filter_us_); + } ESP_LOGCONFIG(TAG, " Assuming 0 pulses/min after not receiving a pulse for %us", this->timeout_us_ / 1000000); } @@ -62,23 +101,14 @@ void IRAM_ATTR PulseMeterSensor::gpio_intr(PulseMeterSensor *sensor) { // Get the current time before we do anything else so the measurements are consistent const uint32_t now = micros(); - // We only look at rising edges - if (!sensor->isr_pin_.digital_read()) { - return; - } - - // Check to see if we should filter this edge out - if ((now - sensor->last_detected_edge_us_) >= sensor->filter_us_) { - // Don't measure the first valid pulse (we need at least two pulses to measure the width) - if (sensor->last_valid_edge_us_ != 0) { - sensor->pulse_width_us_ = (now - sensor->last_valid_edge_us_); + // We only look at rising edges in EDGE mode, and all edges in PULSE mode + if (sensor->filter_mode_ == FILTER_EDGE) { + if (sensor->isr_pin_.digital_read()) { + sensor->last_detected_edge_us_ = now; } - - sensor->total_pulses_++; - sensor->last_valid_edge_us_ = now; + } else { + sensor->last_detected_edge_us_ = now; } - - sensor->last_detected_edge_us_ = now; } } // namespace pulse_meter diff --git a/esphome/components/pulse_meter/pulse_meter_sensor.h b/esphome/components/pulse_meter/pulse_meter_sensor.h index 1cebc1748e..cf08f8c92d 100644 --- a/esphome/components/pulse_meter/pulse_meter_sensor.h +++ b/esphome/components/pulse_meter/pulse_meter_sensor.h @@ -10,8 +10,14 @@ namespace pulse_meter { class PulseMeterSensor : public sensor::Sensor, public Component { public: + enum InternalFilterMode { + FILTER_EDGE = 0, + FILTER_PULSE, + }; + void set_pin(InternalGPIOPin *pin) { this->pin_ = pin; } void set_filter_us(uint32_t filter) { this->filter_us_ = filter; } + void set_filter_mode(InternalFilterMode mode) { this->filter_mode_ = mode; } void set_timeout_us(uint32_t timeout) { this->timeout_us_ = timeout; } void set_total_sensor(sensor::Sensor *sensor) { this->total_sensor_ = sensor; } @@ -30,14 +36,17 @@ class PulseMeterSensor : public sensor::Sensor, public Component { uint32_t filter_us_ = 0; uint32_t timeout_us_ = 1000000UL * 60UL * 5UL; sensor::Sensor *total_sensor_ = nullptr; + InternalFilterMode filter_mode_{FILTER_EDGE}; Deduplicator pulse_width_dedupe_; Deduplicator total_dedupe_; volatile uint32_t last_detected_edge_us_ = 0; - volatile uint32_t last_valid_edge_us_ = 0; + volatile uint32_t last_valid_low_edge_us_ = 0; + volatile uint32_t last_valid_high_edge_us_ = 0; volatile uint32_t pulse_width_us_ = 0; volatile uint32_t total_pulses_ = 0; + volatile bool sensor_is_high_ = false; }; } // namespace pulse_meter diff --git a/esphome/components/pulse_meter/sensor.py b/esphome/components/pulse_meter/sensor.py index fa753b5b05..26bc6b189b 100644 --- a/esphome/components/pulse_meter/sensor.py +++ b/esphome/components/pulse_meter/sensor.py @@ -5,6 +5,7 @@ from esphome.components import sensor from esphome.const import ( CONF_ID, CONF_INTERNAL_FILTER, + CONF_INTERNAL_FILTER_MODE, CONF_PIN, CONF_NUMBER, CONF_TIMEOUT, @@ -18,14 +19,21 @@ from esphome.const import ( ) from esphome.core import CORE -CODEOWNERS = ["@stevebaxter"] +CODEOWNERS = ["@stevebaxter", "@cstaahl"] pulse_meter_ns = cg.esphome_ns.namespace("pulse_meter") + PulseMeterSensor = pulse_meter_ns.class_( "PulseMeterSensor", sensor.Sensor, cg.Component ) +PulseMeterInternalFilterMode = PulseMeterSensor.enum("InternalFilterMode") +FILTER_MODES = { + "EDGE": PulseMeterInternalFilterMode.FILTER_EDGE, + "PULSE": PulseMeterInternalFilterMode.FILTER_PULSE, +} + SetTotalPulsesAction = pulse_meter_ns.class_("SetTotalPulsesAction", automation.Action) @@ -66,6 +74,9 @@ CONFIG_SCHEMA = sensor.sensor_schema( accuracy_decimals=0, state_class=STATE_CLASS_TOTAL_INCREASING, ), + cv.Optional(CONF_INTERNAL_FILTER_MODE, default="EDGE"): cv.enum( + FILTER_MODES, upper=True + ), } ) @@ -78,6 +89,7 @@ async def to_code(config): cg.add(var.set_pin(pin)) cg.add(var.set_filter_us(config[CONF_INTERNAL_FILTER])) cg.add(var.set_timeout_us(config[CONF_TIMEOUT])) + cg.add(var.set_filter_mode(config[CONF_INTERNAL_FILTER_MODE])) if CONF_TOTAL in config: sens = await sensor.new_sensor(config[CONF_TOTAL]) diff --git a/esphome/const.py b/esphome/const.py index 00cc90ad6a..2ec00edf7b 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -307,6 +307,7 @@ CONF_INTENSITY = "intensity" CONF_INTERLOCK = "interlock" CONF_INTERNAL = "internal" CONF_INTERNAL_FILTER = "internal_filter" +CONF_INTERNAL_FILTER_MODE = "internal_filter_mode" CONF_INTERRUPT = "interrupt" CONF_INTERVAL = "interval" CONF_INVALID_COOLDOWN = "invalid_cooldown" diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index 4ff16ba703..42828450e8 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -954,7 +954,7 @@ class MockObjEnum(MockObj): base = kwargs.pop("base") if self._is_class: base = f"{base}::{self._enum}" - kwargs["op"] = "::" + kwargs["op"] = "::" kwargs["base"] = base MockObj.__init__(self, *args, **kwargs) From 78951c197a10072c8a1846d9adc76612ea495db0 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 21 Feb 2022 09:58:53 +1300 Subject: [PATCH 0279/1729] Fix lilygo touchscreen rotation (#3221) --- .../touchscreen/lilygo_t5_47_touchscreen.cpp | 47 +++++++++++++++++-- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.cpp b/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.cpp index b5cf63980b..b92d7d6f10 100644 --- a/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.cpp +++ b/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.cpp @@ -113,8 +113,27 @@ void LilygoT547Touchscreen::loop() { if (tp.state == 0x06) tp.state = 0x07; - tp.y = (uint16_t)((buffer[i * 5 + 1 + offset] << 4) | ((buffer[i * 5 + 3 + offset] >> 4) & 0x0F)); - tp.x = (uint16_t)((buffer[i * 5 + 2 + offset] << 4) | (buffer[i * 5 + 3 + offset] & 0x0F)); + uint16_t y = (uint16_t)((buffer[i * 5 + 1 + offset] << 4) | ((buffer[i * 5 + 3 + offset] >> 4) & 0x0F)); + uint16_t x = (uint16_t)((buffer[i * 5 + 2 + offset] << 4) | (buffer[i * 5 + 3 + offset] & 0x0F)); + + switch (this->rotation_) { + case ROTATE_0_DEGREES: + tp.y = this->display_height_ - y; + tp.x = x; + break; + case ROTATE_90_DEGREES: + tp.x = this->display_height_ - y; + tp.y = this->display_width_ - x; + break; + case ROTATE_180_DEGREES: + tp.y = y; + tp.x = this->display_width_ - x; + break; + case ROTATE_270_DEGREES: + tp.x = y; + tp.y = x; + break; + } this->defer([this, tp]() { this->send_touch_(tp); }); } @@ -122,8 +141,28 @@ void LilygoT547Touchscreen::loop() { TouchPoint tp; tp.id = (buffer[0] >> 4) & 0x0F; tp.state = 0x06; - tp.y = (uint16_t)((buffer[0 * 5 + 1] << 4) | ((buffer[0 * 5 + 3] >> 4) & 0x0F)); - tp.x = (uint16_t)((buffer[0 * 5 + 2] << 4) | (buffer[0 * 5 + 3] & 0x0F)); + + uint16_t y = (uint16_t)((buffer[0 * 5 + 1] << 4) | ((buffer[0 * 5 + 3] >> 4) & 0x0F)); + uint16_t x = (uint16_t)((buffer[0 * 5 + 2] << 4) | (buffer[0 * 5 + 3] & 0x0F)); + + switch (this->rotation_) { + case ROTATE_0_DEGREES: + tp.y = this->display_height_ - y; + tp.x = x; + break; + case ROTATE_90_DEGREES: + tp.x = this->display_height_ - y; + tp.y = this->display_width_ - x; + break; + case ROTATE_180_DEGREES: + tp.y = y; + tp.x = this->display_width_ - x; + break; + case ROTATE_270_DEGREES: + tp.x = y; + tp.y = x; + break; + } this->defer([this, tp]() { this->send_touch_(tp); }); } From 2c7b104f4aa71188142c93e7983599575d1c7c3f Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 21 Feb 2022 10:01:00 +1300 Subject: [PATCH 0280/1729] Fix fatal erroring in addon startup script (#3244) --- docker/ha-addon-rootfs/etc/cont-init.d/10-requirements.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/ha-addon-rootfs/etc/cont-init.d/10-requirements.sh b/docker/ha-addon-rootfs/etc/cont-init.d/10-requirements.sh index 9d49c2b4dd..544787d568 100755 --- a/docker/ha-addon-rootfs/etc/cont-init.d/10-requirements.sh +++ b/docker/ha-addon-rootfs/etc/cont-init.d/10-requirements.sh @@ -7,12 +7,12 @@ # Check SSL requirements, if enabled if bashio::config.true 'ssl'; then if ! bashio::config.has_value 'certfile'; then - bashio::fatal 'SSL is enabled, but no certfile was specified.' + bashio::log.fatal 'SSL is enabled, but no certfile was specified.' bashio::exit.nok fi if ! bashio::config.has_value 'keyfile'; then - bashio::fatal 'SSL is enabled, but no keyfile was specified' + bashio::log.fatal 'SSL is enabled, but no keyfile was specified' bashio::exit.nok fi From 0c4de2bc97ebc75130aa04f391b202793ebba9bb Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sat, 19 Feb 2022 14:11:01 +0100 Subject: [PATCH 0281/1729] Publish NAN when dallas conversion failed (#3227) --- esphome/components/dallas/dallas_component.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/dallas/dallas_component.cpp b/esphome/components/dallas/dallas_component.cpp index 1eed2ebf78..6240983798 100644 --- a/esphome/components/dallas/dallas_component.cpp +++ b/esphome/components/dallas/dallas_component.cpp @@ -110,6 +110,9 @@ void DallasComponent::update() { if (!result) { ESP_LOGE(TAG, "Requesting conversion failed"); this->status_set_warning(); + for (auto *sensor : this->sensors_) { + sensor->publish_state(NAN); + } return; } From 616c787e377f12c83bcaf7f252d329ba4715581a Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sat, 19 Feb 2022 14:11:45 +0100 Subject: [PATCH 0282/1729] Fix ESP8266 climate memaccess warning (#3226) --- esphome/components/climate/climate.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index ebea20ed1f..65b5ef4eb4 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -1,4 +1,5 @@ #include "climate.h" +#include "esphome/core/macros.h" namespace esphome { namespace climate { @@ -326,14 +327,17 @@ optional Climate::restore_state_() { return recovered; } void Climate::save_state_() { -#if defined(USE_ESP_IDF) && !defined(CLANG_TIDY) +#if (defined(USE_ESP_IDF) || (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 0, 0))) && \ + !defined(CLANG_TIDY) #pragma GCC diagnostic ignored "-Wclass-memaccess" +#define TEMP_IGNORE_MEMACCESS #endif ClimateDeviceRestoreState state{}; // initialize as zero to prevent random data on stack triggering erase memset(&state, 0, sizeof(ClimateDeviceRestoreState)); -#if USE_ESP_IDF && !defined(CLANG_TIDY) +#ifdef TEMP_IGNORE_MEMACCESS #pragma GCC diagnostic pop +#undef TEMP_IGNORE_MEMACCESS #endif state.mode = this->mode; From 1d0395d1c7cfc546f41b4b9116df4b2b7b482f1d Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sat, 19 Feb 2022 15:09:17 +0100 Subject: [PATCH 0283/1729] Improve ESP8266 iram usage (#3223) --- esphome/core/scheduler.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 7f0ed0b17c..56f823556b 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -116,7 +116,7 @@ optional HOT Scheduler::next_schedule_in() { return 0; return next_time - now; } -void IRAM_ATTR HOT Scheduler::call() { +void HOT Scheduler::call() { const uint32_t now = this->millis_(); this->process_to_add(); From b881bc071ecd0e1493441ba981b00e7101ed3c6d Mon Sep 17 00:00:00 2001 From: Tyler Bules Date: Sat, 19 Feb 2022 09:13:48 -0500 Subject: [PATCH 0284/1729] ESP32-C3 deep sleep fix (#3066) --- esphome/components/deep_sleep/__init__.py | 40 ++++++++++++++++++- .../deep_sleep/deep_sleep_component.cpp | 14 ++++++- .../deep_sleep/deep_sleep_component.h | 5 ++- tests/test1.yaml | 2 +- tests/test2.yaml | 2 +- 5 files changed, 58 insertions(+), 5 deletions(-) diff --git a/esphome/components/deep_sleep/__init__.py b/esphome/components/deep_sleep/__init__.py index ba4c2c0d7e..2a74d0c1bb 100644 --- a/esphome/components/deep_sleep/__init__.py +++ b/esphome/components/deep_sleep/__init__.py @@ -11,9 +11,39 @@ from esphome.const import ( CONF_WAKEUP_PIN, ) +from esphome.components.esp32 import get_esp32_variant +from esphome.components.esp32.const import ( + VARIANT_ESP32, + VARIANT_ESP32C3, +) + +WAKEUP_PINS = { + VARIANT_ESP32: [ + 0, + 2, + 4, + 12, + 13, + 14, + 15, + 25, + 26, + 27, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + ], + VARIANT_ESP32C3: [0, 1, 2, 3, 4, 5], +} + def validate_pin_number(value): - valid_pins = [0, 2, 4, 12, 13, 14, 15, 25, 26, 27, 32, 33, 34, 35, 36, 37, 38, 39] + valid_pins = WAKEUP_PINS.get(get_esp32_variant(), WAKEUP_PINS[VARIANT_ESP32]) if value[CONF_NUMBER] not in valid_pins: raise cv.Invalid( f"Only pins {', '.join(str(x) for x in valid_pins)} support wakeup" @@ -21,6 +51,14 @@ def validate_pin_number(value): return value +def validate_config(config): + if get_esp32_variant() == VARIANT_ESP32C3 and CONF_ESP32_EXT1_WAKEUP in config: + raise cv.Invalid("ESP32-C3 does not support wakeup from touch.") + if get_esp32_variant() == VARIANT_ESP32C3 and CONF_TOUCH_WAKEUP in config: + raise cv.Invalid("ESP32-C3 does not support wakeup from ext1") + return config + + deep_sleep_ns = cg.esphome_ns.namespace("deep_sleep") DeepSleepComponent = deep_sleep_ns.class_("DeepSleepComponent", cg.Component) EnterDeepSleepAction = deep_sleep_ns.class_("EnterDeepSleepAction", automation.Action) diff --git a/esphome/components/deep_sleep/deep_sleep_component.cpp b/esphome/components/deep_sleep/deep_sleep_component.cpp index 7774014d3d..82751b538b 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.cpp +++ b/esphome/components/deep_sleep/deep_sleep_component.cpp @@ -104,7 +104,7 @@ void DeepSleepComponent::begin_sleep(bool manual) { App.run_safe_shutdown_hooks(); -#ifdef USE_ESP32 +#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3) if (this->sleep_duration_.has_value()) esp_sleep_enable_timer_wakeup(*this->sleep_duration_); if (this->wakeup_pin_ != nullptr) { @@ -126,6 +126,18 @@ void DeepSleepComponent::begin_sleep(bool manual) { esp_deep_sleep_start(); #endif +#ifdef USE_ESP32_VARIANT_ESP32C3 + if (this->sleep_duration_.has_value()) + esp_sleep_enable_timer_wakeup(*this->sleep_duration_); + if (this->wakeup_pin_ != nullptr) { + bool level = !this->wakeup_pin_->is_inverted(); + if (this->wakeup_pin_mode_ == WAKEUP_PIN_MODE_INVERT_WAKEUP && this->wakeup_pin_->digital_read()) { + level = !level; + } + esp_deep_sleep_enable_gpio_wakeup(gpio_num_t(this->wakeup_pin_->get_pin()), level); + } +#endif + #ifdef USE_ESP8266 ESP.deepSleep(*this->sleep_duration_); // NOLINT(readability-static-accessed-through-instance) #endif diff --git a/esphome/components/deep_sleep/deep_sleep_component.h b/esphome/components/deep_sleep/deep_sleep_component.h index 59df199a9f..057d992427 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.h +++ b/esphome/components/deep_sleep/deep_sleep_component.h @@ -57,13 +57,16 @@ class DeepSleepComponent : public Component { public: /// Set the duration in ms the component should sleep once it's in deep sleep mode. void set_sleep_duration(uint32_t time_ms); -#ifdef USE_ESP32 +#if defined(USE_ESP32) /** Set the pin to wake up to on the ESP32 once it's in deep sleep mode. * Use the inverted property to set the wakeup level. */ void set_wakeup_pin(InternalGPIOPin *pin) { this->wakeup_pin_ = pin; } void set_wakeup_pin_mode(WakeupPinMode wakeup_pin_mode); +#endif + +#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3) void set_ext1_wakeup(Ext1Wakeup ext1_wakeup); diff --git a/tests/test1.yaml b/tests/test1.yaml index d8fea223dc..b9799bdb36 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -262,7 +262,7 @@ power_supply: deep_sleep: run_duration: 20s sleep_duration: 50s - wakeup_pin: GPIO39 + wakeup_pin: GPIO2 wakeup_pin_mode: INVERT_WAKEUP ads1115: diff --git a/tests/test2.yaml b/tests/test2.yaml index 2a122b971f..76b9775c54 100644 --- a/tests/test2.yaml +++ b/tests/test2.yaml @@ -60,7 +60,7 @@ deep_sleep: gpio_wakeup_reason: 10s touch_wakeup_reason: 15s sleep_duration: 50s - wakeup_pin: GPIO39 + wakeup_pin: GPIO2 wakeup_pin_mode: INVERT_WAKEUP as3935_i2c: From e73d47918f9b3c070af75919a0418d598d77273e Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 21 Feb 2022 09:58:53 +1300 Subject: [PATCH 0285/1729] Fix lilygo touchscreen rotation (#3221) --- .../touchscreen/lilygo_t5_47_touchscreen.cpp | 47 +++++++++++++++++-- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.cpp b/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.cpp index b5cf63980b..b92d7d6f10 100644 --- a/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.cpp +++ b/esphome/components/lilygo_t5_47/touchscreen/lilygo_t5_47_touchscreen.cpp @@ -113,8 +113,27 @@ void LilygoT547Touchscreen::loop() { if (tp.state == 0x06) tp.state = 0x07; - tp.y = (uint16_t)((buffer[i * 5 + 1 + offset] << 4) | ((buffer[i * 5 + 3 + offset] >> 4) & 0x0F)); - tp.x = (uint16_t)((buffer[i * 5 + 2 + offset] << 4) | (buffer[i * 5 + 3 + offset] & 0x0F)); + uint16_t y = (uint16_t)((buffer[i * 5 + 1 + offset] << 4) | ((buffer[i * 5 + 3 + offset] >> 4) & 0x0F)); + uint16_t x = (uint16_t)((buffer[i * 5 + 2 + offset] << 4) | (buffer[i * 5 + 3 + offset] & 0x0F)); + + switch (this->rotation_) { + case ROTATE_0_DEGREES: + tp.y = this->display_height_ - y; + tp.x = x; + break; + case ROTATE_90_DEGREES: + tp.x = this->display_height_ - y; + tp.y = this->display_width_ - x; + break; + case ROTATE_180_DEGREES: + tp.y = y; + tp.x = this->display_width_ - x; + break; + case ROTATE_270_DEGREES: + tp.x = y; + tp.y = x; + break; + } this->defer([this, tp]() { this->send_touch_(tp); }); } @@ -122,8 +141,28 @@ void LilygoT547Touchscreen::loop() { TouchPoint tp; tp.id = (buffer[0] >> 4) & 0x0F; tp.state = 0x06; - tp.y = (uint16_t)((buffer[0 * 5 + 1] << 4) | ((buffer[0 * 5 + 3] >> 4) & 0x0F)); - tp.x = (uint16_t)((buffer[0 * 5 + 2] << 4) | (buffer[0 * 5 + 3] & 0x0F)); + + uint16_t y = (uint16_t)((buffer[0 * 5 + 1] << 4) | ((buffer[0 * 5 + 3] >> 4) & 0x0F)); + uint16_t x = (uint16_t)((buffer[0 * 5 + 2] << 4) | (buffer[0 * 5 + 3] & 0x0F)); + + switch (this->rotation_) { + case ROTATE_0_DEGREES: + tp.y = this->display_height_ - y; + tp.x = x; + break; + case ROTATE_90_DEGREES: + tp.x = this->display_height_ - y; + tp.y = this->display_width_ - x; + break; + case ROTATE_180_DEGREES: + tp.y = y; + tp.x = this->display_width_ - x; + break; + case ROTATE_270_DEGREES: + tp.x = y; + tp.y = x; + break; + } this->defer([this, tp]() { this->send_touch_(tp); }); } From dbd4e927d8987c319cc580df913e1d1f1c5da44d Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 21 Feb 2022 10:01:00 +1300 Subject: [PATCH 0286/1729] Fix fatal erroring in addon startup script (#3244) --- docker/ha-addon-rootfs/etc/cont-init.d/10-requirements.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/ha-addon-rootfs/etc/cont-init.d/10-requirements.sh b/docker/ha-addon-rootfs/etc/cont-init.d/10-requirements.sh index 9d49c2b4dd..544787d568 100755 --- a/docker/ha-addon-rootfs/etc/cont-init.d/10-requirements.sh +++ b/docker/ha-addon-rootfs/etc/cont-init.d/10-requirements.sh @@ -7,12 +7,12 @@ # Check SSL requirements, if enabled if bashio::config.true 'ssl'; then if ! bashio::config.has_value 'certfile'; then - bashio::fatal 'SSL is enabled, but no certfile was specified.' + bashio::log.fatal 'SSL is enabled, but no certfile was specified.' bashio::exit.nok fi if ! bashio::config.has_value 'keyfile'; then - bashio::fatal 'SSL is enabled, but no keyfile was specified' + bashio::log.fatal 'SSL is enabled, but no keyfile was specified' bashio::exit.nok fi From 2748e6ba2910d5653b45e95fbadcec64591fdbaf Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 21 Feb 2022 10:17:25 +1300 Subject: [PATCH 0287/1729] Bump version to 2022.2.4 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 0e8695adef..015a598ded 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2022.2.3" +__version__ = "2022.2.4" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From ba785e29e95c03c78fdab7ee1482eb2ec1841ce6 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 21 Feb 2022 00:23:26 +0100 Subject: [PATCH 0288/1729] Add support for MPU-6886 (#3183) --- CODEOWNERS | 1 + esphome/components/mpu6886/__init__.py | 1 + esphome/components/mpu6886/mpu6886.cpp | 153 +++++++++++++++++++++++++ esphome/components/mpu6886/mpu6886.h | 39 +++++++ esphome/components/mpu6886/sensor.py | 85 ++++++++++++++ tests/test1.yaml | 17 +++ 6 files changed, 296 insertions(+) create mode 100644 esphome/components/mpu6886/__init__.py create mode 100644 esphome/components/mpu6886/mpu6886.cpp create mode 100644 esphome/components/mpu6886/mpu6886.h create mode 100644 esphome/components/mpu6886/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 52e80a5822..eb8fb873de 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -124,6 +124,7 @@ esphome/components/modbus_controller/select/* @martgras @stegm esphome/components/modbus_controller/sensor/* @martgras esphome/components/modbus_controller/switch/* @martgras esphome/components/modbus_controller/text_sensor/* @martgras +esphome/components/mpu6886/* @fabaff esphome/components/network/* @esphome/core esphome/components/nextion/* @senexcrenshaw esphome/components/nextion/binary_sensor/* @senexcrenshaw diff --git a/esphome/components/mpu6886/__init__.py b/esphome/components/mpu6886/__init__.py new file mode 100644 index 0000000000..933f71ccd7 --- /dev/null +++ b/esphome/components/mpu6886/__init__.py @@ -0,0 +1 @@ +"""Support for MPC-6886.""" diff --git a/esphome/components/mpu6886/mpu6886.cpp b/esphome/components/mpu6886/mpu6886.cpp new file mode 100644 index 0000000000..c296653e6b --- /dev/null +++ b/esphome/components/mpu6886/mpu6886.cpp @@ -0,0 +1,153 @@ +#include "mpu6886.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace mpu6886 { + +static const char *const TAG = "mpu6886"; + +const uint8_t MPU6886_REGISTER_WHO_AM_I = 0x75; +const uint8_t MPU6886_REGISTER_POWER_MANAGEMENT_1 = 0x6B; +const uint8_t MPU6886_REGISTER_GYRO_CONFIG = 0x1B; +const uint8_t MPU6886_REGISTER_ACCEL_CONFIG = 0x1C; +const uint8_t MPU6886_REGISTER_ACCEL_XOUT_H = 0x3B; +const uint8_t MPU6886_CLOCK_SOURCE_X_GYRO = 0b001; +const uint8_t MPU6886_SCALE_2000_DPS = 0b11; +const uint8_t MPU6886_WHO_AM_I_IDENTIFIER = 0x19; +const float MPU6886_SCALE_DPS_PER_DIGIT_2000 = 0.060975f; +const uint8_t MPU6886_RANGE_2G = 0b00; +const float MPU6886_RANGE_PER_DIGIT_2G = 0.000061f; +const uint8_t MPU6886_BIT_SLEEP_ENABLED = 6; +const uint8_t MPU6886_BIT_TEMPERATURE_DISABLED = 3; +const float GRAVITY_EARTH = 9.80665f; +// See https://github.com/m5stack/M5-Schematic/blob/master/datasheet/MPU-6886-000193%2Bv1.1_GHIC.PDF.pdf +// p. 43 +const float TEMPERATURE_SENSITIVITY = 326.8; +const float TEMPERATURE_OFFSET = 25.0; + +void MPU6886Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up MPU6886..."); + uint8_t who_am_i; + if (!this->read_byte(MPU6886_REGISTER_WHO_AM_I, &who_am_i) || who_am_i != MPU6886_WHO_AM_I_IDENTIFIER) { + this->mark_failed(); + return; + } + + ESP_LOGV(TAG, " Setting up Power Management..."); + // Setup power management + uint8_t power_management; + if (!this->read_byte(MPU6886_REGISTER_POWER_MANAGEMENT_1, &power_management)) { + this->mark_failed(); + return; + } + ESP_LOGV(TAG, " Input power_management: 0b" BYTE_TO_BINARY_PATTERN, BYTE_TO_BINARY(power_management)); + // Set clock source - X-Gyro + power_management &= 0b11111000; + power_management |= MPU6886_CLOCK_SOURCE_X_GYRO; + // Disable sleep + power_management &= ~(1 << MPU6886_BIT_SLEEP_ENABLED); + // Enable temperature + power_management &= ~(1 << MPU6886_BIT_TEMPERATURE_DISABLED); + ESP_LOGV(TAG, " Output power_management: 0b" BYTE_TO_BINARY_PATTERN, BYTE_TO_BINARY(power_management)); + if (!this->write_byte(MPU6886_REGISTER_POWER_MANAGEMENT_1, power_management)) { + this->mark_failed(); + return; + } + + ESP_LOGV(TAG, " Setting up Gyroscope Config..."); + // Set scale - 2000DPS + uint8_t gyro_config; + if (!this->read_byte(MPU6886_REGISTER_GYRO_CONFIG, &gyro_config)) { + this->mark_failed(); + return; + } + ESP_LOGV(TAG, " Input gyroscope_config: 0b" BYTE_TO_BINARY_PATTERN, BYTE_TO_BINARY(gyro_config)); + gyro_config &= 0b11100111; + gyro_config |= MPU6886_SCALE_2000_DPS << 3; + ESP_LOGV(TAG, " Output gyro_config: 0b" BYTE_TO_BINARY_PATTERN, BYTE_TO_BINARY(gyro_config)); + if (!this->write_byte(MPU6886_REGISTER_GYRO_CONFIG, gyro_config)) { + this->mark_failed(); + return; + } + + ESP_LOGV(TAG, " Setting up Accelerometer Config..."); + // Set range - 2G + uint8_t accel_config; + if (!this->read_byte(MPU6886_REGISTER_ACCEL_CONFIG, &accel_config)) { + this->mark_failed(); + return; + } + ESP_LOGV(TAG, " Input accelerometer_config: 0b" BYTE_TO_BINARY_PATTERN, BYTE_TO_BINARY(accel_config)); + accel_config &= 0b11100111; + accel_config |= (MPU6886_RANGE_2G << 3); + ESP_LOGV(TAG, " Output accel_config: 0b" BYTE_TO_BINARY_PATTERN, BYTE_TO_BINARY(accel_config)); + if (!this->write_byte(MPU6886_REGISTER_GYRO_CONFIG, gyro_config)) { + this->mark_failed(); + return; + } +} + +void MPU6886Component::dump_config() { + ESP_LOGCONFIG(TAG, "MPU6886:"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with MPU6886 failed!"); + } + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "Acceleration X", this->accel_x_sensor_); + LOG_SENSOR(" ", "Acceleration Y", this->accel_y_sensor_); + LOG_SENSOR(" ", "Acceleration Z", this->accel_z_sensor_); + LOG_SENSOR(" ", "Gyro X", this->gyro_x_sensor_); + LOG_SENSOR(" ", "Gyro Y", this->gyro_y_sensor_); + LOG_SENSOR(" ", "Gyro Z", this->gyro_z_sensor_); + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); +} + +void MPU6886Component::update() { + ESP_LOGV(TAG, " Updating MPU6886..."); + uint16_t raw_data[7]; + if (!this->read_bytes_16(MPU6886_REGISTER_ACCEL_XOUT_H, raw_data, 7)) { + this->status_set_warning(); + return; + } + auto *data = reinterpret_cast(raw_data); + + float accel_x = data[0] * MPU6886_RANGE_PER_DIGIT_2G * GRAVITY_EARTH; + float accel_y = data[1] * MPU6886_RANGE_PER_DIGIT_2G * GRAVITY_EARTH; + float accel_z = data[2] * MPU6886_RANGE_PER_DIGIT_2G * GRAVITY_EARTH; + + float temperature = data[3] / TEMPERATURE_SENSITIVITY + TEMPERATURE_OFFSET; + + float gyro_x = data[4] * MPU6886_SCALE_DPS_PER_DIGIT_2000; + float gyro_y = data[5] * MPU6886_SCALE_DPS_PER_DIGIT_2000; + float gyro_z = data[6] * MPU6886_SCALE_DPS_PER_DIGIT_2000; + + ESP_LOGD(TAG, + "Got accel={x=%.3f m/s², y=%.3f m/s², z=%.3f m/s²}, " + "gyro={x=%.3f °/s, y=%.3f °/s, z=%.3f °/s}, temp=%.3f°C", + accel_x, accel_y, accel_z, gyro_x, gyro_y, gyro_z, temperature); + + if (this->accel_x_sensor_ != nullptr) + this->accel_x_sensor_->publish_state(accel_x); + if (this->accel_y_sensor_ != nullptr) + this->accel_y_sensor_->publish_state(accel_y); + if (this->accel_z_sensor_ != nullptr) + this->accel_z_sensor_->publish_state(accel_z); + + if (this->temperature_sensor_ != nullptr) + this->temperature_sensor_->publish_state(temperature); + + if (this->gyro_x_sensor_ != nullptr) + this->gyro_x_sensor_->publish_state(gyro_x); + if (this->gyro_y_sensor_ != nullptr) + this->gyro_y_sensor_->publish_state(gyro_y); + if (this->gyro_z_sensor_ != nullptr) + this->gyro_z_sensor_->publish_state(gyro_z); + + this->status_clear_warning(); +} + +float MPU6886Component::get_setup_priority() const { return setup_priority::DATA; } + +} // namespace mpu6886 +} // namespace esphome diff --git a/esphome/components/mpu6886/mpu6886.h b/esphome/components/mpu6886/mpu6886.h new file mode 100644 index 0000000000..04551ae56d --- /dev/null +++ b/esphome/components/mpu6886/mpu6886.h @@ -0,0 +1,39 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace mpu6886 { + +class MPU6886Component : public PollingComponent, public i2c::I2CDevice { + public: + void setup() override; + void dump_config() override; + + void update() override; + + float get_setup_priority() const override; + + void set_accel_x_sensor(sensor::Sensor *accel_x_sensor) { accel_x_sensor_ = accel_x_sensor; } + void set_accel_y_sensor(sensor::Sensor *accel_y_sensor) { accel_y_sensor_ = accel_y_sensor; } + void set_accel_z_sensor(sensor::Sensor *accel_z_sensor) { accel_z_sensor_ = accel_z_sensor; } + void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } + void set_gyro_x_sensor(sensor::Sensor *gyro_x_sensor) { gyro_x_sensor_ = gyro_x_sensor; } + void set_gyro_y_sensor(sensor::Sensor *gyro_y_sensor) { gyro_y_sensor_ = gyro_y_sensor; } + void set_gyro_z_sensor(sensor::Sensor *gyro_z_sensor) { gyro_z_sensor_ = gyro_z_sensor; } + + protected: + sensor::Sensor *accel_x_sensor_{nullptr}; + sensor::Sensor *accel_y_sensor_{nullptr}; + sensor::Sensor *accel_z_sensor_{nullptr}; + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *gyro_x_sensor_{nullptr}; + sensor::Sensor *gyro_y_sensor_{nullptr}; + sensor::Sensor *gyro_z_sensor_{nullptr}; +}; +; + +} // namespace mpu6886 +} // namespace esphome diff --git a/esphome/components/mpu6886/sensor.py b/esphome/components/mpu6886/sensor.py new file mode 100644 index 0000000000..535007d008 --- /dev/null +++ b/esphome/components/mpu6886/sensor.py @@ -0,0 +1,85 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_ID, + CONF_TEMPERATURE, + DEVICE_CLASS_TEMPERATURE, + ICON_BRIEFCASE_DOWNLOAD, + STATE_CLASS_MEASUREMENT, + UNIT_METER_PER_SECOND_SQUARED, + ICON_SCREEN_ROTATION, + UNIT_DEGREE_PER_SECOND, + UNIT_CELSIUS, +) + +CODEOWNERS = ["@fabaff"] +DEPENDENCIES = ["i2c"] + +CONF_ACCEL_X = "accel_x" +CONF_ACCEL_Y = "accel_y" +CONF_ACCEL_Z = "accel_z" +CONF_GYRO_X = "gyro_x" +CONF_GYRO_Y = "gyro_y" +CONF_GYRO_Z = "gyro_z" + +mpu6886_ns = cg.esphome_ns.namespace("mpu6886") +MPU6886Component = mpu6886_ns.class_( + "MPU6886Component", cg.PollingComponent, i2c.I2CDevice +) + +accel_schema = sensor.sensor_schema( + unit_of_measurement=UNIT_METER_PER_SECOND_SQUARED, + icon=ICON_BRIEFCASE_DOWNLOAD, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, +) +gyro_schema = sensor.sensor_schema( + unit_of_measurement=UNIT_DEGREE_PER_SECOND, + icon=ICON_SCREEN_ROTATION, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, +) +temperature_schema = sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(MPU6886Component), + cv.Optional(CONF_ACCEL_X): accel_schema, + cv.Optional(CONF_ACCEL_Y): accel_schema, + cv.Optional(CONF_ACCEL_Z): accel_schema, + cv.Optional(CONF_GYRO_X): gyro_schema, + cv.Optional(CONF_GYRO_Y): gyro_schema, + cv.Optional(CONF_GYRO_Z): gyro_schema, + cv.Optional(CONF_TEMPERATURE): temperature_schema, + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x68)) +) + + +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) + + for d in ["x", "y", "z"]: + accel_key = f"accel_{d}" + if accel_key in config: + sens = await sensor.new_sensor(config[accel_key]) + cg.add(getattr(var, f"set_accel_{d}_sensor")(sens)) + accel_key = f"gyro_{d}" + if accel_key in config: + sens = await sensor.new_sensor(config[accel_key]) + cg.add(getattr(var, f"set_gyro_{d}_sensor")(sens)) + + if CONF_TEMPERATURE in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature_sensor(sens)) diff --git a/tests/test1.yaml b/tests/test1.yaml index c4398498c1..0d8ba9dfe8 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -724,6 +724,23 @@ sensor: temperature: name: "MPU6050 Temperature" i2c_id: i2c_bus + - platform: mpu6886 + address: 0x68 + accel_x: + name: "MPU6886 Accel X" + accel_y: + name: "MPU6886 Accel Y" + accel_z: + name: "MPU6886 Accel z" + gyro_x: + name: "MPU6886 Gyro X" + gyro_y: + name: "MPU6886 Gyro Y" + gyro_z: + name: "MPU6886 Gyro z" + temperature: + name: "MPU6886 Temperature" + i2c_id: i2c_bus - platform: ms5611 temperature: name: "Outside Temperature" From 771162bfb113ab105ceb5edb545848e530a1fd81 Mon Sep 17 00:00:00 2001 From: Niorix Date: Mon, 21 Feb 2022 06:52:14 +0700 Subject: [PATCH 0289/1729] light: add RESTORE_AND_OFF/RESTORE_AND_ON LightRestoreMode (#3238) --- esphome/components/light/__init__.py | 2 ++ esphome/components/light/light_state.cpp | 6 ++++++ esphome/components/light/light_state.h | 2 ++ 3 files changed, 10 insertions(+) diff --git a/esphome/components/light/__init__.py b/esphome/components/light/__init__.py index fe8a90b8db..c397910ec4 100644 --- a/esphome/components/light/__init__.py +++ b/esphome/components/light/__init__.py @@ -52,6 +52,8 @@ RESTORE_MODES = { "ALWAYS_ON": LightRestoreMode.LIGHT_ALWAYS_ON, "RESTORE_INVERTED_DEFAULT_OFF": LightRestoreMode.LIGHT_RESTORE_INVERTED_DEFAULT_OFF, "RESTORE_INVERTED_DEFAULT_ON": LightRestoreMode.LIGHT_RESTORE_INVERTED_DEFAULT_ON, + "RESTORE_AND_OFF": LightRestoreMode.LIGHT_RESTORE_AND_OFF, + "RESTORE_AND_ON": LightRestoreMode.LIGHT_RESTORE_AND_ON, } LIGHT_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index 151bc58a1c..dadad38235 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -68,6 +68,12 @@ void LightState::setup() { recovered.state = !recovered.state; } break; + case LIGHT_RESTORE_AND_OFF: + case LIGHT_RESTORE_AND_ON: + this->rtc_ = global_preferences->make_preference(this->get_object_id_hash()); + this->rtc_.load(&recovered); + recovered.state = (this->restore_mode_ == LIGHT_RESTORE_AND_ON); + break; case LIGHT_ALWAYS_OFF: recovered.state = false; break; diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h index ae3711234d..2e523eda5c 100644 --- a/esphome/components/light/light_state.h +++ b/esphome/components/light/light_state.h @@ -22,6 +22,8 @@ enum LightRestoreMode { LIGHT_ALWAYS_ON, LIGHT_RESTORE_INVERTED_DEFAULT_OFF, LIGHT_RESTORE_INVERTED_DEFAULT_ON, + LIGHT_RESTORE_AND_OFF, + LIGHT_RESTORE_AND_ON, }; /** This class represents the communication layer between the front-end MQTT layer and the From 69633826bb54355680d24f47b03ed4d985c4ab2f Mon Sep 17 00:00:00 2001 From: EdJoPaTo Date: Mon, 21 Feb 2022 01:13:06 +0100 Subject: [PATCH 0290/1729] Implement send_first_at for exponential_moving_average (#3240) --- esphome/components/sensor/__init__.py | 20 ++++++++++++++++---- esphome/components/sensor/filter.cpp | 4 ++-- esphome/components/sensor/filter.h | 2 +- tests/test1.yaml | 1 + 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 577596f6ce..65ae7b2168 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -408,18 +408,30 @@ async def sliding_window_moving_average_filter_to_code(config, filter_id): ) -@FILTER_REGISTRY.register( - "exponential_moving_average", - ExponentialMovingAverageFilter, +EXPONENTIAL_AVERAGE_SCHEMA = cv.All( cv.Schema( { cv.Optional(CONF_ALPHA, default=0.1): cv.positive_float, cv.Optional(CONF_SEND_EVERY, default=15): cv.positive_not_null_int, + cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.positive_not_null_int, } ), + validate_send_first_at, +) + + +@FILTER_REGISTRY.register( + "exponential_moving_average", + ExponentialMovingAverageFilter, + EXPONENTIAL_AVERAGE_SCHEMA, ) async def exponential_moving_average_filter_to_code(config, filter_id): - return cg.new_Pvariable(filter_id, config[CONF_ALPHA], config[CONF_SEND_EVERY]) + return cg.new_Pvariable( + filter_id, + config[CONF_ALPHA], + config[CONF_SEND_EVERY], + config[CONF_SEND_FIRST_AT], + ) @FILTER_REGISTRY.register( diff --git a/esphome/components/sensor/filter.cpp b/esphome/components/sensor/filter.cpp index 49d2c648b0..d4a7a52fa1 100644 --- a/esphome/components/sensor/filter.cpp +++ b/esphome/components/sensor/filter.cpp @@ -200,8 +200,8 @@ optional SlidingWindowMovingAverageFilter::new_value(float value) { } // ExponentialMovingAverageFilter -ExponentialMovingAverageFilter::ExponentialMovingAverageFilter(float alpha, size_t send_every) - : send_every_(send_every), send_at_(send_every - 1), alpha_(alpha) {} +ExponentialMovingAverageFilter::ExponentialMovingAverageFilter(float alpha, size_t send_every, size_t send_first_at) + : send_every_(send_every), send_at_(send_every - send_first_at), alpha_(alpha) {} optional ExponentialMovingAverageFilter::new_value(float value) { if (!std::isnan(value)) { if (this->first_value_) { diff --git a/esphome/components/sensor/filter.h b/esphome/components/sensor/filter.h index 0ed7ce4801..a39c1ba25a 100644 --- a/esphome/components/sensor/filter.h +++ b/esphome/components/sensor/filter.h @@ -194,7 +194,7 @@ class SlidingWindowMovingAverageFilter : public Filter { */ class ExponentialMovingAverageFilter : public Filter { public: - ExponentialMovingAverageFilter(float alpha, size_t send_every); + ExponentialMovingAverageFilter(float alpha, size_t send_every, size_t send_first_at); optional new_value(float value) override; diff --git a/tests/test1.yaml b/tests/test1.yaml index 0d8ba9dfe8..496226565a 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -358,6 +358,7 @@ sensor: - exponential_moving_average: alpha: 0.1 send_every: 15 + send_first_at: 15 - throttle_average: 60s - throttle: 1s - heartbeat: 5s From 6919930aaa230f0437d0975d902e98cc7dbbf11f Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Mon, 21 Feb 2022 02:05:13 +0100 Subject: [PATCH 0291/1729] Respect ESPHOME_USE_SUBPROCESS in esp32 post_build script (#3246) --- esphome/components/esp32/post_build.py.script | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/esphome/components/esp32/post_build.py.script b/esphome/components/esp32/post_build.py.script index 2bb1a6c3d6..406516a102 100644 --- a/esphome/components/esp32/post_build.py.script +++ b/esphome/components/esp32/post_build.py.script @@ -1,6 +1,10 @@ # Source https://github.com/letscontrolit/ESPEasy/pull/3845#issuecomment-1005864664 -import esptool +import os +if os.environ.get("ESPHOME_USE_SUBPROCESS") is None: + import esptool +else: + import subprocess from SCons.Script import ARGUMENTS # pylint: disable=E0602 @@ -42,8 +46,11 @@ def esp32_create_combined_bin(source, target, env): print() print(f"Using esptool.py arguments: {' '.join(cmd)}") print() - esptool.main(cmd) + if os.environ.get("ESPHOME_USE_SUBPROCESS") is None: + esptool.main(cmd) + else: + subprocess.run(["esptool.py", *cmd]) # pylint: disable=E0602 env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_create_combined_bin) # noqa From d1feaa935dd587910a4e3508936e85319f99eacd Mon Sep 17 00:00:00 2001 From: Arturo Casal Date: Mon, 21 Feb 2022 12:47:03 +0100 Subject: [PATCH 0292/1729] Add device support: MCP4728 (#3174) * Added MCP4728 output component. * Added tests to test1.yaml * Added codeowners * Lint fixes * Implemented code review changes * Lint fixes * Added i2c communication check on setup() * Fixed tests * Lint fix * Update esphome/components/mcp4728/mcp4728_output.cpp Changed log function Co-authored-by: Otto Winter Co-authored-by: Otto Winter --- CODEOWNERS | 1 + esphome/components/mcp4728/__init__.py | 29 +++++ esphome/components/mcp4728/mcp4728_output.cpp | 121 ++++++++++++++++++ esphome/components/mcp4728/mcp4728_output.h | 91 +++++++++++++ esphome/components/mcp4728/output.py | 63 +++++++++ tests/test1.yaml | 31 +++++ 6 files changed, 336 insertions(+) create mode 100644 esphome/components/mcp4728/__init__.py create mode 100644 esphome/components/mcp4728/mcp4728_output.cpp create mode 100644 esphome/components/mcp4728/mcp4728_output.h create mode 100644 esphome/components/mcp4728/output.py diff --git a/CODEOWNERS b/CODEOWNERS index eb8fb873de..a53bf63c69 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -108,6 +108,7 @@ esphome/components/mcp23x17_base/* @jesserockz esphome/components/mcp23xxx_base/* @jesserockz esphome/components/mcp2515/* @danielschramm @mvturnho esphome/components/mcp3204/* @rsumner +esphome/components/mcp4728/* @berfenger esphome/components/mcp47a1/* @jesserockz esphome/components/mcp9808/* @k7hpn esphome/components/md5/* @esphome/core diff --git a/esphome/components/mcp4728/__init__.py b/esphome/components/mcp4728/__init__.py new file mode 100644 index 0000000000..d130ceb738 --- /dev/null +++ b/esphome/components/mcp4728/__init__.py @@ -0,0 +1,29 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c +from esphome.const import CONF_ID + +CODEOWNERS = ["@berfenger"] +DEPENDENCIES = ["i2c"] +MULTI_CONF = True +CONF_STORE_IN_EEPROM = "store_in_eeprom" + +mcp4728_ns = cg.esphome_ns.namespace("mcp4728") +MCP4728Component = mcp4728_ns.class_("MCP4728Component", cg.Component, i2c.I2CDevice) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(MCP4728Component), + cv.Optional(CONF_STORE_IN_EEPROM, default=False): cv.boolean, + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(0x60)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID], config[CONF_STORE_IN_EEPROM]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) diff --git a/esphome/components/mcp4728/mcp4728_output.cpp b/esphome/components/mcp4728/mcp4728_output.cpp new file mode 100644 index 0000000000..d011967624 --- /dev/null +++ b/esphome/components/mcp4728/mcp4728_output.cpp @@ -0,0 +1,121 @@ +#include "mcp4728_output.h" + +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace mcp4728 { + +static const char *const TAG = "mcp4728"; + +void MCP4728Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up MCP4728 (0x%02X)...", this->address_); + auto err = this->write(nullptr, 0); + if (err != i2c::ERROR_OK) { + this->mark_failed(); + return; + } +} + +void MCP4728Component::dump_config() { + ESP_LOGCONFIG(TAG, "MCP4728:"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with MCP4728 failed!"); + } +} + +void MCP4728Component::loop() { + if (this->update_) { + this->update_ = false; + if (this->store_in_eeprom_) { + if (!this->seq_write_()) { + this->status_set_error(); + } else { + this->status_clear_error(); + } + } else { + if (!this->multi_write_()) { + this->status_set_error(); + } else { + this->status_clear_error(); + } + } + } +} + +void MCP4728Component::set_channel_value_(MCP4728ChannelIdx channel, uint16_t value) { + uint8_t cn = 0; + if (channel == MCP4728_CHANNEL_A) { + cn = 'A'; + } else if (channel == MCP4728_CHANNEL_B) { + cn = 'B'; + } else if (channel == MCP4728_CHANNEL_C) { + cn = 'C'; + } else { + cn = 'D'; + } + ESP_LOGV(TAG, "Setting MCP4728 channel %c to %d!", cn, value); + reg_[channel].data = value; + this->update_ = true; +} + +bool MCP4728Component::multi_write_() { + i2c::ErrorCode err[4]; + for (uint8_t i = 0; i < 4; ++i) { + uint8_t wd[3]; + wd[0] = ((uint8_t) CMD::MULTI_WRITE | (i << 1)) & 0xFE; + wd[1] = ((uint8_t) reg_[i].vref << 7) | ((uint8_t) reg_[i].pd << 5) | ((uint8_t) reg_[i].gain << 4) | + (reg_[i].data >> 8); + wd[2] = reg_[i].data & 0xFF; + err[i] = this->write(wd, sizeof(wd)); + } + bool ok = true; + for (auto &e : err) { + if (e != i2c::ERROR_OK) { + ok = false; + break; + } + } + return ok; +} + +bool MCP4728Component::seq_write_() { + uint8_t wd[9]; + wd[0] = (uint8_t) CMD::SEQ_WRITE; + for (uint8_t i = 0; i < 4; i++) { + wd[i * 2 + 1] = ((uint8_t) reg_[i].vref << 7) | ((uint8_t) reg_[i].pd << 5) | ((uint8_t) reg_[i].gain << 4) | + (reg_[i].data >> 8); + wd[i * 2 + 2] = reg_[i].data & 0xFF; + } + auto err = this->write(wd, sizeof(wd)); + return err == i2c::ERROR_OK; +} + +void MCP4728Component::select_vref_(MCP4728ChannelIdx channel, MCP4728Vref vref) { + reg_[channel].vref = vref; + + this->update_ = true; +} + +void MCP4728Component::select_power_down_(MCP4728ChannelIdx channel, MCP4728PwrDown pd) { + reg_[channel].pd = pd; + + this->update_ = true; +} + +void MCP4728Component::select_gain_(MCP4728ChannelIdx channel, MCP4728Gain gain) { + reg_[channel].gain = gain; + + this->update_ = true; +} + +void MCP4728Channel::write_state(float state) { + const uint16_t max_duty = 4095; + const float duty_rounded = roundf(state * max_duty); + auto duty = static_cast(duty_rounded); + this->parent_->set_channel_value_(this->channel_, duty); +} + +} // namespace mcp4728 +} // namespace esphome diff --git a/esphome/components/mcp4728/mcp4728_output.h b/esphome/components/mcp4728/mcp4728_output.h new file mode 100644 index 0000000000..55bcfdccb6 --- /dev/null +++ b/esphome/components/mcp4728/mcp4728_output.h @@ -0,0 +1,91 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/output/float_output.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace mcp4728 { + +enum class CMD { + FAST_WRITE = 0x00, + MULTI_WRITE = 0x40, + SINGLE_WRITE = 0x58, + SEQ_WRITE = 0x50, + SELECT_VREF = 0x80, + SELECT_GAIN = 0xC0, + SELECT_POWER_DOWN = 0xA0 +}; + +enum MCP4728Vref { MCP4728_VREF_VDD = 0, MCP4728_VREF_INTERNAL_2_8V = 1 }; + +enum MCP4728PwrDown { + MCP4728_PD_NORMAL = 0, + MCP4728_PD_GND_1KOHM = 1, + MCP4728_PD_GND_100KOHM = 2, + MCP4728_PD_GND_500KOHM = 3 +}; + +enum MCP4728Gain { MCP4728_GAIN_X1 = 0, MCP4728_GAIN_X2 = 1 }; + +enum MCP4728ChannelIdx { MCP4728_CHANNEL_A = 0, MCP4728_CHANNEL_B = 1, MCP4728_CHANNEL_C = 2, MCP4728_CHANNEL_D = 3 }; + +struct DACInputData { + MCP4728Vref vref; + MCP4728PwrDown pd; + MCP4728Gain gain; + uint16_t data; +}; + +class MCP4728Channel; + +/// MCP4728 float output component. +class MCP4728Component : public Component, public i2c::I2CDevice { + public: + MCP4728Component(bool store_in_eeprom) : store_in_eeprom_(store_in_eeprom) {} + + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::HARDWARE; } + void loop() override; + + protected: + friend MCP4728Channel; + void set_channel_value_(MCP4728ChannelIdx channel, uint16_t value); + bool multi_write_(); + bool seq_write_(); + void select_vref_(MCP4728ChannelIdx channel, MCP4728Vref vref); + void select_power_down_(MCP4728ChannelIdx channel, MCP4728PwrDown pd); + void select_gain_(MCP4728ChannelIdx channel, MCP4728Gain gain); + + private: + DACInputData reg_[4]; + bool store_in_eeprom_ = false; + bool update_ = false; +}; + +class MCP4728Channel : public output::FloatOutput { + public: + MCP4728Channel(MCP4728Component *parent, MCP4728ChannelIdx channel, MCP4728Vref vref, MCP4728Gain gain, + MCP4728PwrDown pwrdown) + : parent_(parent), channel_(channel), vref_(vref), gain_(gain), pwrdown_(pwrdown) { + // update VREF + parent->select_vref_(channel, vref_); + // update PD + parent->select_power_down_(channel, pwrdown_); + // update GAIN + parent->select_gain_(channel, gain_); + } + + protected: + void write_state(float state) override; + + MCP4728Component *parent_; + MCP4728ChannelIdx channel_; + MCP4728Vref vref_; + MCP4728Gain gain_; + MCP4728PwrDown pwrdown_; +}; + +} // namespace mcp4728 +} // namespace esphome diff --git a/esphome/components/mcp4728/output.py b/esphome/components/mcp4728/output.py new file mode 100644 index 0000000000..e0913ab98a --- /dev/null +++ b/esphome/components/mcp4728/output.py @@ -0,0 +1,63 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import output +from esphome.const import CONF_CHANNEL, CONF_ID, CONF_GAIN +from . import MCP4728Component, mcp4728_ns + +DEPENDENCIES = ["mcp4728"] + +MCP4728Channel = mcp4728_ns.class_("MCP4728Channel", output.FloatOutput) +CONF_MCP4728_ID = "mcp4728_id" +CONF_VREF = "vref" +CONF_POWER_DOWN = "power_down" + +MCP4728Vref = mcp4728_ns.enum("MCP4728Vref") +VREF_OPTIONS = { + "vdd": MCP4728Vref.MCP4728_VREF_VDD, + "internal": MCP4728Vref.MCP4728_VREF_INTERNAL_2_8V, +} + +MCP4728Gain = mcp4728_ns.enum("MCP4728Gain") +GAIN_OPTIONS = {"X1": MCP4728Gain.MCP4728_GAIN_X1, "X2": MCP4728Gain.MCP4728_GAIN_X2} + +MCP4728PwrDown = mcp4728_ns.enum("MCP4728PwrDown") +PWRDOWN_OPTIONS = { + "normal": MCP4728PwrDown.MCP4728_PD_NORMAL, + "gnd_1k": MCP4728PwrDown.MCP4728_PD_GND_1KOHM, + "gnd_100k": MCP4728PwrDown.MCP4728_PD_GND_100KOHM, + "gnd_500k": MCP4728PwrDown.MCP4728_PD_GND_500KOHM, +} + +MCP4728ChannelIdx = mcp4728_ns.enum("MCP4728ChannelIdx") +CHANNEL_OPTIONS = { + "A": MCP4728ChannelIdx.MCP4728_CHANNEL_A, + "B": MCP4728ChannelIdx.MCP4728_CHANNEL_B, + "C": MCP4728ChannelIdx.MCP4728_CHANNEL_C, + "D": MCP4728ChannelIdx.MCP4728_CHANNEL_D, +} + +CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( + { + cv.Required(CONF_ID): cv.declare_id(MCP4728Channel), + cv.GenerateID(CONF_MCP4728_ID): cv.use_id(MCP4728Component), + cv.Required(CONF_CHANNEL): cv.enum(CHANNEL_OPTIONS, upper=True), + cv.Optional(CONF_VREF, default="vdd"): cv.enum(VREF_OPTIONS, upper=False), + cv.Optional(CONF_POWER_DOWN, default="normal"): cv.enum( + PWRDOWN_OPTIONS, upper=False + ), + cv.Optional(CONF_GAIN, default="X1"): cv.enum(GAIN_OPTIONS, upper=True), + } +) + + +async def to_code(config): + paren = await cg.get_variable(config[CONF_MCP4728_ID]) + var = cg.new_Pvariable( + config[CONF_ID], + paren, + config[CONF_CHANNEL], + config[CONF_VREF], + config[CONF_GAIN], + config[CONF_POWER_DOWN], + ) + await output.register_output(var, config) diff --git a/tests/test1.yaml b/tests/test1.yaml index 496226565a..65169e2fd9 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1501,6 +1501,28 @@ output: - platform: mcp4725 id: mcp4725_dac_output i2c_id: i2c_bus + - platform: mcp4728 + id: mcp4728_dac_output_a + channel: A + vref: vdd + power_down: normal + - platform: mcp4728 + id: mcp4728_dac_output_b + channel: B + vref: internal + gain: X1 + power_down: gnd_1k + - platform: mcp4728 + id: mcp4728_dac_output_c + channel: C + vref: vdd + power_down: gnd_100k + - platform: mcp4728 + id: mcp4728_dac_output_d + channel: D + vref: internal + gain: X2 + power_down: gnd_500k e131: @@ -2013,6 +2035,9 @@ switch: - output.set_level: id: mcp4725_dac_output level: !lambda "return 0.5;" + - output.set_level: + id: mcp4728_dac_output_a + level: !lambda "return 0.5;" turn_off_action: - switch.turn_on: living_room_lights_off restore_state: False @@ -2393,6 +2418,12 @@ rc522_i2c: ESP_LOGD("main", "Found tag %s", x.c_str()); i2c_id: i2c_bus +mcp4728: + - id: mcp4728_dac + store_in_eeprom: False + address: 0x60 + i2c_id: i2c_bus + gps: uart_id: uart0 From a5b4105971a6978dd842b8638d4c6abab3ccbe32 Mon Sep 17 00:00:00 2001 From: Micha Nordmann Date: Mon, 21 Feb 2022 19:35:04 +0100 Subject: [PATCH 0293/1729] support for waveshare 7.50in-hd-b (#3239) --- .../components/waveshare_epaper/display.py | 4 + .../waveshare_epaper/waveshare_epaper.cpp | 101 ++++++++++++++++++ .../waveshare_epaper/waveshare_epaper.h | 20 ++++ 3 files changed, 125 insertions(+) diff --git a/esphome/components/waveshare_epaper/display.py b/esphome/components/waveshare_epaper/display.py index 44120ebbc5..fe5b51290e 100644 --- a/esphome/components/waveshare_epaper/display.py +++ b/esphome/components/waveshare_epaper/display.py @@ -50,6 +50,9 @@ WaveshareEPaper7P5InBV2 = waveshare_epaper_ns.class_( WaveshareEPaper7P5InV2 = waveshare_epaper_ns.class_( "WaveshareEPaper7P5InV2", WaveshareEPaper ) +WaveshareEPaper7P5InHDB = waveshare_epaper_ns.class_( + "WaveshareEPaper7P5InHDB", WaveshareEPaper +) WaveshareEPaper2P13InDKE = waveshare_epaper_ns.class_( "WaveshareEPaper2P13InDKE", WaveshareEPaper ) @@ -76,6 +79,7 @@ MODELS = { "7.50in-bv2": ("b", WaveshareEPaper7P5InBV2), "7.50in-bc": ("b", WaveshareEPaper7P5InBC), "7.50inv2": ("b", WaveshareEPaper7P5InV2), + "7.50in-hd-b": ("b", WaveshareEPaper7P5InHDB), "2.13in-ttgo-dke": ("c", WaveshareEPaper2P13InDKE), } diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.cpp b/esphome/components/waveshare_epaper/waveshare_epaper.cpp index 71e3b22e7d..59b3e90b03 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.cpp +++ b/esphome/components/waveshare_epaper/waveshare_epaper.cpp @@ -1241,6 +1241,107 @@ void WaveshareEPaper7P5InBC::dump_config() { LOG_UPDATE_INTERVAL(this); } +void WaveshareEPaper7P5InHDB::initialize() { + this->command(0x12); // SWRESET + + this->wait_until_idle_(); // waiting for the electronic paper IC to release the idle signal + + this->command(0x46); // Auto Write RAM + this->data(0xF7); + + this->wait_until_idle_(); // waiting for the electronic paper IC to release the idle signal + + this->command(0x47); // Auto Write RAM + this->data(0xF7); + + this->wait_until_idle_(); // waiting for the electronic paper IC to release the idle signal + + this->command(0x0C); // Soft start setting + this->data(0xAE); + this->data(0xC7); + this->data(0xC3); + this->data(0xC0); + this->data(0x40); + + this->command(0x01); // Set MUX as 527 + this->data(0xAF); + this->data(0x02); + this->data(0x01); + + this->command(0x11); // Data entry mode + this->data(0x01); + + this->command(0x44); + this->data(0x00); // RAM x address start at 0 + this->data(0x00); + this->data(0x6F); // RAM x address end at 36Fh -> 879 + this->data(0x03); + + this->command(0x45); + this->data(0xAF); // RAM y address start at 20Fh; + this->data(0x02); + this->data(0x00); // RAM y address end at 00h; + this->data(0x00); + + this->command(0x3C); // VBD + this->data(0x01); // LUT1, for white + + this->command(0x18); + this->data(0X80); + + this->command(0x22); + this->data(0XB1); // Load Temperature and waveform setting. + + this->command(0x20); + + this->wait_until_idle_(); // waiting for the electronic paper IC to release the idle signal + + this->command(0x4E); + this->data(0x00); + this->data(0x00); + + this->command(0x4F); + this->data(0xAF); + this->data(0x02); +} + +void HOT WaveshareEPaper7P5InHDB::display() { + this->command(0x4F); + this->data(0xAf); + this->data(0x02); + + // BLACK + this->command(0x24); + this->start_data_(); + this->write_array(this->buffer_, this->get_buffer_length_()); + this->end_data_(); + + // RED + this->command(0x26); + this->start_data_(); + for (size_t i = 0; i < this->get_buffer_length_(); i++) + this->write_byte(0x00); + this->end_data_(); + + this->command(0x22); + this->data(0xC7); + this->command(0x20); + delay(100); // NOLINT +} + +int WaveshareEPaper7P5InHDB::get_width_internal() { return 880; } + +int WaveshareEPaper7P5InHDB::get_height_internal() { return 528; } + +void WaveshareEPaper7P5InHDB::dump_config() { + LOG_DISPLAY("", "Waveshare E-Paper", this); + ESP_LOGCONFIG(TAG, " Model: 7.5in-HD-b"); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + LOG_PIN(" DC Pin: ", this->dc_pin_); + LOG_PIN(" Busy Pin: ", this->busy_pin_); + LOG_UPDATE_INTERVAL(this); +} + static const uint8_t LUT_SIZE_TTGO_DKE_PART = 153; static const uint8_t PART_UPDATE_LUT_TTGO_DKE[LUT_SIZE_TTGO_DKE_PART] = { diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.h b/esphome/components/waveshare_epaper/waveshare_epaper.h index 96bd2fc782..41b93978ab 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.h +++ b/esphome/components/waveshare_epaper/waveshare_epaper.h @@ -350,6 +350,26 @@ class WaveshareEPaper7P5InV2 : public WaveshareEPaper { int get_height_internal() override; }; +class WaveshareEPaper7P5InHDB : public WaveshareEPaper { + public: + void initialize() override; + + void display() override; + + void dump_config() override; + + void deep_sleep() override { + // deep sleep + this->command(0x10); + this->data(0x01); + } + + protected: + int get_width_internal() override; + + int get_height_internal() override; +}; + class WaveshareEPaper2P13InDKE : public WaveshareEPaper { public: void initialize() override; From b55e9329d98a2b63a12134108c2236db68129d62 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 22 Feb 2022 13:47:16 +1300 Subject: [PATCH 0294/1729] Fix template button after abstract press_action (#3250) --- esphome/components/template/button/__init__.py | 5 ++++- .../components/template/button/template_button.h | 15 +++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 esphome/components/template/button/template_button.h diff --git a/esphome/components/template/button/__init__.py b/esphome/components/template/button/__init__.py index aa192d118e..a8bf595942 100644 --- a/esphome/components/template/button/__init__.py +++ b/esphome/components/template/button/__init__.py @@ -1,10 +1,13 @@ import esphome.config_validation as cv from esphome.components import button +from .. import template_ns + +TemplateButton = template_ns.class_("TemplateButton", button.Button) CONFIG_SCHEMA = button.BUTTON_SCHEMA.extend( { - cv.GenerateID(): cv.declare_id(button.Button), + cv.GenerateID(): cv.declare_id(TemplateButton), } ).extend(cv.COMPONENT_SCHEMA) diff --git a/esphome/components/template/button/template_button.h b/esphome/components/template/button/template_button.h new file mode 100644 index 0000000000..68e976f64b --- /dev/null +++ b/esphome/components/template/button/template_button.h @@ -0,0 +1,15 @@ +#pragma once + +#include "esphome/components/button/button.h" + +namespace esphome { +namespace template_ { + +class TemplateButton : public button::Button { + public: + // Implements the abstract `press_action` but the `on_press` trigger already handles the press. + void press_action() override{}; +}; + +} // namespace template_ +} // namespace esphome From 9323b3a248e151cf51478b7c3628aa5b87f9a864 Mon Sep 17 00:00:00 2001 From: Nicholas Peters Date: Mon, 21 Feb 2022 19:53:24 -0500 Subject: [PATCH 0295/1729] Fix regression caused by TSL2591 auto gain (#3249) --- esphome/components/tsl2591/tsl2591.cpp | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/esphome/components/tsl2591/tsl2591.cpp b/esphome/components/tsl2591/tsl2591.cpp index 8a540c5f13..f8c59a53c6 100644 --- a/esphome/components/tsl2591/tsl2591.cpp +++ b/esphome/components/tsl2591/tsl2591.cpp @@ -43,16 +43,34 @@ void TSL2591Component::disable_if_power_saving_() { } void TSL2591Component::setup() { - if (this->component_gain_ == TSL2591_CGAIN_AUTO) - this->gain_ = TSL2591_GAIN_MED; + switch (this->component_gain_) { + case TSL2591_CGAIN_LOW: + this->gain_ = TSL2591_GAIN_LOW; + break; + case TSL2591_CGAIN_MED: + this->gain_ = TSL2591_GAIN_MED; + break; + case TSL2591_CGAIN_HIGH: + this->gain_ = TSL2591_GAIN_HIGH; + break; + case TSL2591_CGAIN_MAX: + this->gain_ = TSL2591_GAIN_MAX; + break; + case TSL2591_CGAIN_AUTO: + this->gain_ = TSL2591_GAIN_MED; + break; + } + uint8_t address = this->address_; ESP_LOGI(TAG, "Setting up TSL2591 sensor at I2C address 0x%02X", address); + uint8_t id; if (!this->read_byte(TSL2591_COMMAND_BIT | TSL2591_REGISTER_DEVICE_ID, &id)) { ESP_LOGE(TAG, "Failed I2C read during setup()"); this->mark_failed(); return; } + if (id != 0x50) { ESP_LOGE(TAG, "Could not find the TSL2591 sensor. The ID register of the device at address 0x%02X reported 0x%02X " @@ -61,6 +79,7 @@ void TSL2591Component::setup() { this->mark_failed(); return; } + this->set_integration_time_and_gain(this->integration_time_, this->gain_); this->disable_if_power_saving_(); } From 68b3fd6b8f1f306391248ba1aec4f2f809b210c9 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 22 Feb 2022 13:54:23 +1300 Subject: [PATCH 0296/1729] Store platform as uppercase (#3251) --- esphome/storage_json.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/storage_json.py b/esphome/storage_json.py index d561de4ce6..a941fca0af 100644 --- a/esphome/storage_json.py +++ b/esphome/storage_json.py @@ -99,7 +99,7 @@ class StorageJSON: def from_esphome_core( esph, old ): # type: (CoreType, Optional[StorageJSON]) -> StorageJSON - hardware = esph.target_platform + hardware = esph.target_platform.upper() if esph.is_esp32: from esphome.components import esp32 From c9094ca537c5d14c9929eadb2c61ee8b19b5a57b Mon Sep 17 00:00:00 2001 From: RubyBailey <60991881+RubyBailey@users.noreply.github.com> Date: Tue, 22 Feb 2022 02:22:30 -0800 Subject: [PATCH 0297/1729] Add sensor support: Honeywell ABP (SPI version) (#3164) Co-authored-by: RubyBailey --- CODEOWNERS | 1 + esphome/components/honeywellabp/__init__.py | 1 + .../components/honeywellabp/honeywellabp.cpp | 102 ++++++++++++++++++ .../components/honeywellabp/honeywellabp.h | 45 ++++++++ esphome/components/honeywellabp/sensor.py | 70 ++++++++++++ tests/test1.yaml | 8 ++ 6 files changed, 227 insertions(+) create mode 100644 esphome/components/honeywellabp/__init__.py create mode 100644 esphome/components/honeywellabp/honeywellabp.cpp create mode 100644 esphome/components/honeywellabp/honeywellabp.h create mode 100644 esphome/components/honeywellabp/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index a53bf63c69..6e3801d1f3 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -80,6 +80,7 @@ esphome/components/hbridge/light/* @DotNetDann esphome/components/heatpumpir/* @rob-deutsch esphome/components/hitachi_ac424/* @sourabhjaiswal esphome/components/homeassistant/* @OttoWinter +esphome/components/honeywellabp/* @RubyBailey esphome/components/hrxl_maxsonar_wr/* @netmikey esphome/components/i2c/* @esphome/core esphome/components/improv_serial/* @esphome/core diff --git a/esphome/components/honeywellabp/__init__.py b/esphome/components/honeywellabp/__init__.py new file mode 100644 index 0000000000..03440d675d --- /dev/null +++ b/esphome/components/honeywellabp/__init__.py @@ -0,0 +1 @@ +"""Support for Honeywell ABP""" diff --git a/esphome/components/honeywellabp/honeywellabp.cpp b/esphome/components/honeywellabp/honeywellabp.cpp new file mode 100644 index 0000000000..910c39e8c8 --- /dev/null +++ b/esphome/components/honeywellabp/honeywellabp.cpp @@ -0,0 +1,102 @@ +#include "honeywellabp.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace honeywellabp { + +static const char *const TAG = "honeywellabp"; + +const float MIN_COUNT = 1638.4; // 1638 counts (10% of 2^14 counts or 0x0666) +const float MAX_COUNT = 14745.6; // 14745 counts (90% of 2^14 counts or 0x3999) + +void HONEYWELLABPSensor::setup() { + ESP_LOGD(TAG, "Setting up Honeywell ABP Sensor "); + this->spi_setup(); +} + +uint8_t HONEYWELLABPSensor::readsensor_() { + // Polls the sensor for new data. + // transfer 4 bytes (the last two are temperature only used by some sensors) + this->enable(); + buf_[0] = this->read_byte(); + buf_[1] = this->read_byte(); + buf_[2] = this->read_byte(); + buf_[3] = this->read_byte(); + this->disable(); + + // Check the status codes: + // status = 0 : normal operation + // status = 1 : device in command mode + // status = 2 : stale data + // status = 3 : diagnostic condition + status_ = buf_[0] >> 6 & 0x3; + ESP_LOGV(TAG, "Sensor status %d", status_); + + // if device is normal and there is new data, bitmask and save the raw data + if (status_ == 0) { + // 14 - bit pressure is the last 6 bits of byte 0 (high bits) & all of byte 1 (lowest 8 bits) + pressure_count_ = ((uint16_t)(buf_[0]) << 8 & 0x3F00) | ((uint16_t)(buf_[1]) & 0xFF); + // 11 - bit temperature is all of byte 2 (lowest 8 bits) and the first three bits of byte 3 + temperature_count_ = (((uint16_t)(buf_[2]) << 3) & 0x7F8) | (((uint16_t)(buf_[3]) >> 5) & 0x7); + ESP_LOGV(TAG, "Sensor pressure_count_ %d", pressure_count_); + ESP_LOGV(TAG, "Sensor temperature_count_ %d", temperature_count_); + } + return status_; +} + +// returns status +uint8_t HONEYWELLABPSensor::readstatus_() { return status_; } + +// The pressure value from the most recent reading in raw counts +int HONEYWELLABPSensor::rawpressure_() { return pressure_count_; } + +// The temperature value from the most recent reading in raw counts +int HONEYWELLABPSensor::rawtemperature_() { return temperature_count_; } + +// Converts a digital pressure measurement in counts to pressure measured +float HONEYWELLABPSensor::countstopressure_(const int counts, const float min_pressure, const float max_pressure) { + return ((((float) counts - MIN_COUNT) * (max_pressure - min_pressure)) / (MAX_COUNT - MIN_COUNT)) + min_pressure; +} + +// Converts a digital temperature measurement in counts to temperature in C +// This will be invalid if sensore daoes not have temperature measurement capability +float HONEYWELLABPSensor::countstotemperatures_(const int counts) { return (((float) counts / 2047.0) * 200.0) - 50.0; } + +// Pressure value from the most recent reading in units +float HONEYWELLABPSensor::read_pressure_() { + return countstopressure_(pressure_count_, honeywellabp_min_pressure_, honeywellabp_max_pressure_); +} + +// Temperature value from the most recent reading in degrees C +float HONEYWELLABPSensor::read_temperature_() { return countstotemperatures_(temperature_count_); } + +void HONEYWELLABPSensor::update() { + ESP_LOGV(TAG, "Update Honeywell ABP Sensor"); + if (readsensor_() == 0) { + if (this->pressure_sensor_ != nullptr) + this->pressure_sensor_->publish_state(read_pressure_() * 1.0); + if (this->temperature_sensor_ != nullptr) + this->temperature_sensor_->publish_state(read_temperature_() * 1.0); + } +} + +float HONEYWELLABPSensor::get_setup_priority() const { return setup_priority::LATE; } + +void HONEYWELLABPSensor::dump_config() { + // LOG_SENSOR("", "HONEYWELLABP", this); + LOG_PIN(" CS Pin: ", this->cs_); + ESP_LOGCONFIG(TAG, " Min Pressure Range: %0.1f", honeywellabp_min_pressure_); + ESP_LOGCONFIG(TAG, " Max Pressure Range: %0.1f", honeywellabp_max_pressure_); + LOG_UPDATE_INTERVAL(this); +} + +void HONEYWELLABPSensor::set_honeywellabp_min_pressure(float min_pressure) { + this->honeywellabp_min_pressure_ = min_pressure; +} + +void HONEYWELLABPSensor::set_honeywellabp_max_pressure(float max_pressure) { + this->honeywellabp_max_pressure_ = max_pressure; +} + +} // namespace honeywellabp +} // namespace esphome diff --git a/esphome/components/honeywellabp/honeywellabp.h b/esphome/components/honeywellabp/honeywellabp.h new file mode 100644 index 0000000000..44d5952ca6 --- /dev/null +++ b/esphome/components/honeywellabp/honeywellabp.h @@ -0,0 +1,45 @@ +// for Honeywell ABP sensor +// adapting code from https://github.com/vwls/Honeywell_pressure_sensors +#pragma once + +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/spi/spi.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace honeywellabp { + +class HONEYWELLABPSensor : public PollingComponent, + public spi::SPIDevice { + public: + void set_pressure_sensor(sensor::Sensor *pressure_sensor) { pressure_sensor_ = pressure_sensor; } + void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } + void setup() override; + void update() override; + float get_setup_priority() const override; + void dump_config() override; + void set_honeywellabp_min_pressure(float min_pressure); + void set_honeywellabp_max_pressure(float max_pressure); + + protected: + float honeywellabp_min_pressure_ = 0.0; + float honeywellabp_max_pressure_ = 0.0; + uint8_t buf_[4]; // buffer to hold sensor data + uint8_t status_ = 0; // byte to hold status information. + int pressure_count_ = 0; // hold raw pressure data (14 - bit, 0 - 16384) + int temperature_count_ = 0; // hold raw temperature data (11 - bit, 0 - 2048) + sensor::Sensor *pressure_sensor_; + sensor::Sensor *temperature_sensor_; + uint8_t readsensor_(); + uint8_t readstatus_(); + int rawpressure_(); + int rawtemperature_(); + float countstopressure_(int counts, float min_pressure, float max_pressure); + float countstotemperatures_(int counts); + float read_pressure_(); + float read_temperature_(); +}; + +} // namespace honeywellabp +} // namespace esphome diff --git a/esphome/components/honeywellabp/sensor.py b/esphome/components/honeywellabp/sensor.py new file mode 100644 index 0000000000..720a96b93c --- /dev/null +++ b/esphome/components/honeywellabp/sensor.py @@ -0,0 +1,70 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.components import spi +from esphome.const import ( + CONF_ID, + CONF_PRESSURE, + CONF_TEMPERATURE, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, +) + +DEPENDENCIES = ["spi"] +CODEOWNERS = ["@RubyBailey"] + +CONF_MIN_PRESSURE = "min_pressure" +CONF_MAX_PRESSURE = "max_pressure" + +honeywellabp_ns = cg.esphome_ns.namespace("honeywellabp") +HONEYWELLABPSensor = honeywellabp_ns.class_( + "HONEYWELLABPSensor", sensor.Sensor, cg.PollingComponent, spi.SPIDevice +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(HONEYWELLABPSensor), + cv.Optional(CONF_PRESSURE): sensor.sensor_schema( + unit_of_measurement="psi", + accuracy_decimals=1, + device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, + ).extend( + { + cv.Required(CONF_MIN_PRESSURE): cv.float_, + cv.Required(CONF_MAX_PRESSURE): cv.float_, + } + ), + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(spi.spi_device_schema(cs_pin_required=True)) +) + + +async def to_code(config): + + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await spi.register_spi_device(var, config) + + if CONF_PRESSURE in config: + conf = config[CONF_PRESSURE] + sens = await sensor.new_sensor(conf) + cg.add(var.set_pressure_sensor(sens)) + cg.add(var.set_honeywellabp_min_pressure(conf[CONF_MIN_PRESSURE])) + cg.add(var.set_honeywellabp_max_pressure(conf[CONF_MAX_PRESSURE])) + + if CONF_TEMPERATURE in config: + conf = config[CONF_TEMPERATURE] + sens = await sensor.new_sensor(conf) + cg.add(var.set_temperature_sensor(sens)) diff --git a/tests/test1.yaml b/tests/test1.yaml index 65169e2fd9..8cd01f1d6f 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -600,6 +600,14 @@ sensor: oversampling: 8x update_interval: 15s i2c_id: i2c_bus + - platform: honeywellabp + pressure: + name: "Honeywell pressure" + min_pressure: 0 + max_pressure: 15 + temperature: + name: "Honeywell temperature" + cs_pin: GPIO5 - platform: qmc5883l address: 0x0D field_strength_x: From bf60e40d0bda314bccf1dd7c870ef9af6af87e56 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 23 Feb 2022 11:05:53 +1300 Subject: [PATCH 0298/1729] Add optional display page for touchscreen binary sensors (#3247) --- esphome/components/touchscreen/__init__.py | 2 ++ esphome/components/touchscreen/binary_sensor/__init__.py | 8 +++++++- .../binary_sensor/touchscreen_binary_sensor.cpp | 4 ++++ .../touchscreen/binary_sensor/touchscreen_binary_sensor.h | 4 ++++ esphome/components/touchscreen/touchscreen.h | 1 + 5 files changed, 18 insertions(+), 1 deletion(-) diff --git a/esphome/components/touchscreen/__init__.py b/esphome/components/touchscreen/__init__.py index 8246b95187..125103e2b8 100644 --- a/esphome/components/touchscreen/__init__.py +++ b/esphome/components/touchscreen/__init__.py @@ -6,6 +6,8 @@ from esphome import automation from esphome.const import CONF_ON_TOUCH CODEOWNERS = ["@jesserockz"] +DEPENDENCIES = ["display"] + IS_PLATFORM_COMPONENT = True touchscreen_ns = cg.esphome_ns.namespace("touchscreen") diff --git a/esphome/components/touchscreen/binary_sensor/__init__.py b/esphome/components/touchscreen/binary_sensor/__init__.py index 73cbf1df7e..800bc4c2a9 100644 --- a/esphome/components/touchscreen/binary_sensor/__init__.py +++ b/esphome/components/touchscreen/binary_sensor/__init__.py @@ -1,7 +1,8 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import binary_sensor +from esphome.components import binary_sensor, display +from esphome.const import CONF_PAGE_ID from .. import touchscreen_ns, CONF_TOUCHSCREEN_ID, Touchscreen, TouchListener @@ -41,6 +42,7 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_X_MAX): cv.int_range(min=0, max=2000), cv.Required(CONF_Y_MIN): cv.int_range(min=0, max=2000), cv.Required(CONF_Y_MAX): cv.int_range(min=0, max=2000), + cv.Optional(CONF_PAGE_ID): cv.use_id(display.DisplayPage), } ) .extend(cv.COMPONENT_SCHEMA), @@ -61,3 +63,7 @@ async def to_code(config): config[CONF_Y_MAX], ) ) + + if CONF_PAGE_ID in config: + page = await cg.get_variable(config[CONF_PAGE_ID]) + cg.add(var.set_page(page)) diff --git a/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.cpp b/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.cpp index ba12aeeae0..583392cce3 100644 --- a/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.cpp +++ b/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.cpp @@ -6,6 +6,10 @@ namespace touchscreen { void TouchscreenBinarySensor::touch(TouchPoint tp) { bool touched = (tp.x >= this->x_min_ && tp.x <= this->x_max_ && tp.y >= this->y_min_ && tp.y <= this->y_max_); + if (this->page_ != nullptr) { + touched &= this->page_ == this->parent_->get_display()->get_active_page(); + } + if (touched) { this->publish_state(true); } else { diff --git a/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.h b/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.h index 7b8cac5c4c..d7e53962e2 100644 --- a/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.h +++ b/esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.h @@ -1,6 +1,7 @@ #pragma once #include "esphome/components/binary_sensor/binary_sensor.h" +#include "esphome/components/display/display_buffer.h" #include "esphome/components/touchscreen/touchscreen.h" #include "esphome/core/component.h" #include "esphome/core/helpers.h" @@ -23,11 +24,14 @@ class TouchscreenBinarySensor : public binary_sensor::BinarySensor, this->y_max_ = y_max; } + void set_page(display::DisplayPage *page) { this->page_ = page; } + void touch(TouchPoint tp) override; void release() override; protected: int16_t x_min_, x_max_, y_min_, y_max_; + display::DisplayPage *page_{nullptr}; }; } // namespace touchscreen diff --git a/esphome/components/touchscreen/touchscreen.h b/esphome/components/touchscreen/touchscreen.h index 2c0ec9e268..0597759894 100644 --- a/esphome/components/touchscreen/touchscreen.h +++ b/esphome/components/touchscreen/touchscreen.h @@ -37,6 +37,7 @@ class Touchscreen { this->display_height_ = display->get_height_internal(); this->rotation_ = static_cast(display->get_rotation()); } + display::DisplayBuffer *get_display() const { return this->display_; } Trigger *get_touch_trigger() { return &this->touch_trigger_; } From 3d0899aa58859e91b37507be391c0d4845dac005 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Mon, 21 Feb 2022 02:05:13 +0100 Subject: [PATCH 0299/1729] Respect ESPHOME_USE_SUBPROCESS in esp32 post_build script (#3246) --- esphome/components/esp32/post_build.py.script | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/esphome/components/esp32/post_build.py.script b/esphome/components/esp32/post_build.py.script index 2bb1a6c3d6..406516a102 100644 --- a/esphome/components/esp32/post_build.py.script +++ b/esphome/components/esp32/post_build.py.script @@ -1,6 +1,10 @@ # Source https://github.com/letscontrolit/ESPEasy/pull/3845#issuecomment-1005864664 -import esptool +import os +if os.environ.get("ESPHOME_USE_SUBPROCESS") is None: + import esptool +else: + import subprocess from SCons.Script import ARGUMENTS # pylint: disable=E0602 @@ -42,8 +46,11 @@ def esp32_create_combined_bin(source, target, env): print() print(f"Using esptool.py arguments: {' '.join(cmd)}") print() - esptool.main(cmd) + if os.environ.get("ESPHOME_USE_SUBPROCESS") is None: + esptool.main(cmd) + else: + subprocess.run(["esptool.py", *cmd]) # pylint: disable=E0602 env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_create_combined_bin) # noqa From 95acf1906703d5658dd9cbae8ef61afe74ac737a Mon Sep 17 00:00:00 2001 From: Nicholas Peters Date: Mon, 21 Feb 2022 19:53:24 -0500 Subject: [PATCH 0300/1729] Fix regression caused by TSL2591 auto gain (#3249) --- esphome/components/tsl2591/tsl2591.cpp | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/esphome/components/tsl2591/tsl2591.cpp b/esphome/components/tsl2591/tsl2591.cpp index 8a540c5f13..f8c59a53c6 100644 --- a/esphome/components/tsl2591/tsl2591.cpp +++ b/esphome/components/tsl2591/tsl2591.cpp @@ -43,16 +43,34 @@ void TSL2591Component::disable_if_power_saving_() { } void TSL2591Component::setup() { - if (this->component_gain_ == TSL2591_CGAIN_AUTO) - this->gain_ = TSL2591_GAIN_MED; + switch (this->component_gain_) { + case TSL2591_CGAIN_LOW: + this->gain_ = TSL2591_GAIN_LOW; + break; + case TSL2591_CGAIN_MED: + this->gain_ = TSL2591_GAIN_MED; + break; + case TSL2591_CGAIN_HIGH: + this->gain_ = TSL2591_GAIN_HIGH; + break; + case TSL2591_CGAIN_MAX: + this->gain_ = TSL2591_GAIN_MAX; + break; + case TSL2591_CGAIN_AUTO: + this->gain_ = TSL2591_GAIN_MED; + break; + } + uint8_t address = this->address_; ESP_LOGI(TAG, "Setting up TSL2591 sensor at I2C address 0x%02X", address); + uint8_t id; if (!this->read_byte(TSL2591_COMMAND_BIT | TSL2591_REGISTER_DEVICE_ID, &id)) { ESP_LOGE(TAG, "Failed I2C read during setup()"); this->mark_failed(); return; } + if (id != 0x50) { ESP_LOGE(TAG, "Could not find the TSL2591 sensor. The ID register of the device at address 0x%02X reported 0x%02X " @@ -61,6 +79,7 @@ void TSL2591Component::setup() { this->mark_failed(); return; } + this->set_integration_time_and_gain(this->integration_time_, this->gain_); this->disable_if_power_saving_(); } From 97aca8e54c62e514c7cd042c1f3c2f7c8f789a06 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 23 Feb 2022 11:20:48 +1300 Subject: [PATCH 0301/1729] Bump version to 2022.2.5 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 015a598ded..7cd88a8101 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2022.2.4" +__version__ = "2022.2.5" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From 1d2e0f74ead0aadd9654d73039ee5ef4dd4af708 Mon Sep 17 00:00:00 2001 From: Sean Brogan Date: Mon, 28 Feb 2022 14:30:33 -0800 Subject: [PATCH 0302/1729] Add Mopeka BLE and Mopeka Pro Check BLE Sensor (#2618) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 2 + esphome/components/mopeka_ble/__init__.py | 23 +++ esphome/components/mopeka_ble/mopeka_ble.cpp | 50 +++++++ esphome/components/mopeka_ble/mopeka_ble.h | 22 +++ .../components/mopeka_pro_check/__init__.py | 1 + .../mopeka_pro_check/mopeka_pro_check.cpp | 136 ++++++++++++++++++ .../mopeka_pro_check/mopeka_pro_check.h | 58 ++++++++ esphome/components/mopeka_pro_check/sensor.py | 131 +++++++++++++++++ tests/test2.yaml | 14 ++ 9 files changed, 437 insertions(+) create mode 100644 esphome/components/mopeka_ble/__init__.py create mode 100644 esphome/components/mopeka_ble/mopeka_ble.cpp create mode 100644 esphome/components/mopeka_ble/mopeka_ble.h create mode 100644 esphome/components/mopeka_pro_check/__init__.py create mode 100644 esphome/components/mopeka_pro_check/mopeka_pro_check.cpp create mode 100644 esphome/components/mopeka_pro_check/mopeka_pro_check.h create mode 100644 esphome/components/mopeka_pro_check/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 6e3801d1f3..c111fa7816 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -126,6 +126,8 @@ esphome/components/modbus_controller/select/* @martgras @stegm esphome/components/modbus_controller/sensor/* @martgras esphome/components/modbus_controller/switch/* @martgras esphome/components/modbus_controller/text_sensor/* @martgras +esphome/components/mopeka_ble/* @spbrogan +esphome/components/mopeka_pro_check/* @spbrogan esphome/components/mpu6886/* @fabaff esphome/components/network/* @esphome/core esphome/components/nextion/* @senexcrenshaw diff --git a/esphome/components/mopeka_ble/__init__.py b/esphome/components/mopeka_ble/__init__.py new file mode 100644 index 0000000000..47396435a8 --- /dev/null +++ b/esphome/components/mopeka_ble/__init__.py @@ -0,0 +1,23 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import esp32_ble_tracker +from esphome.const import CONF_ID + +CODEOWNERS = ["@spbrogan"] +DEPENDENCIES = ["esp32_ble_tracker"] + +mopeka_ble_ns = cg.esphome_ns.namespace("mopeka_ble") +MopekaListener = mopeka_ble_ns.class_( + "MopekaListener", esp32_ble_tracker.ESPBTDeviceListener +) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(MopekaListener), + } +).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await esp32_ble_tracker.register_ble_device(var, config) diff --git a/esphome/components/mopeka_ble/mopeka_ble.cpp b/esphome/components/mopeka_ble/mopeka_ble.cpp new file mode 100644 index 0000000000..844d3a7dfd --- /dev/null +++ b/esphome/components/mopeka_ble/mopeka_ble.cpp @@ -0,0 +1,50 @@ +#include "mopeka_ble.h" +#include "esphome/core/log.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace mopeka_ble { + +static const char *const TAG = "mopeka_ble"; +static const uint8_t MANUFACTURER_DATA_LENGTH = 10; +static const uint16_t MANUFACTURER_ID = 0x0059; + +/** + * Parse all incoming BLE payloads to see if it is a Mopeka BLE advertisement. + * Currently this supports the following products: + * + * Mopeka Pro Check. + * If the sync button is pressed, report the MAC so a user can add this as a sensor. + */ + +bool MopekaListener::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + const auto &manu_datas = device.get_manufacturer_datas(); + + if (manu_datas.size() != 1) { + return false; + } + + const auto &manu_data = manu_datas[0]; + + if (manu_data.data.size() != MANUFACTURER_DATA_LENGTH) { + return false; + } + + if (manu_data.uuid != esp32_ble_tracker::ESPBTUUID::from_uint16(MANUFACTURER_ID)) { + return false; + } + + if (this->parse_sync_button_(manu_data.data)) { + // button pressed + ESP_LOGI(TAG, "SENSOR FOUND: %s", device.address_str().c_str()); + } + return false; +} + +bool MopekaListener::parse_sync_button_(const std::vector &message) { return (message[2] & 0x80) != 0; } + +} // namespace mopeka_ble +} // namespace esphome + +#endif diff --git a/esphome/components/mopeka_ble/mopeka_ble.h b/esphome/components/mopeka_ble/mopeka_ble.h new file mode 100644 index 0000000000..7b797a3bbe --- /dev/null +++ b/esphome/components/mopeka_ble/mopeka_ble.h @@ -0,0 +1,22 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace mopeka_ble { + +class MopekaListener : public esp32_ble_tracker::ESPBTDeviceListener { + public: + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; + + protected: + bool parse_sync_button_(const std::vector &message); +}; + +} // namespace mopeka_ble +} // namespace esphome + +#endif diff --git a/esphome/components/mopeka_pro_check/__init__.py b/esphome/components/mopeka_pro_check/__init__.py new file mode 100644 index 0000000000..c57f60f521 --- /dev/null +++ b/esphome/components/mopeka_pro_check/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@spbrogan"] diff --git a/esphome/components/mopeka_pro_check/mopeka_pro_check.cpp b/esphome/components/mopeka_pro_check/mopeka_pro_check.cpp new file mode 100644 index 0000000000..bcfe0a80ce --- /dev/null +++ b/esphome/components/mopeka_pro_check/mopeka_pro_check.cpp @@ -0,0 +1,136 @@ +#include "mopeka_pro_check.h" +#include "esphome/core/log.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace mopeka_pro_check { + +static const char *const TAG = "mopeka_pro_check"; +static const uint8_t MANUFACTURER_DATA_LENGTH = 10; +static const uint16_t MANUFACTURER_ID = 0x0059; +static const double MOPEKA_LPG_COEF[] = {0.573045, -0.002822, -0.00000535}; // Magic numbers provided by Mopeka + +void MopekaProCheck::dump_config() { + ESP_LOGCONFIG(TAG, "Mopeka Pro Check"); + LOG_SENSOR(" ", "Level", this->level_); + LOG_SENSOR(" ", "Temperature", this->temperature_); + LOG_SENSOR(" ", "Battery Level", this->battery_level_); + LOG_SENSOR(" ", "Reading Distance", this->distance_); +} + +/** + * Main parse function that gets called for all ble advertisements. + * Check if advertisement is for our sensor and if so decode it and + * update the sensor state data. + */ +bool MopekaProCheck::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + if (device.address_uint64() != this->address_) { + return false; + } + + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + + const auto &manu_datas = device.get_manufacturer_datas(); + + if (manu_datas.size() != 1) { + ESP_LOGE(TAG, "Unexpected manu_datas size (%d)", manu_datas.size()); + return false; + } + + const auto &manu_data = manu_datas[0]; + + ESP_LOGVV(TAG, "Manufacturer data:"); + for (const uint8_t byte : manu_data.data) { + ESP_LOGVV(TAG, "0x%02x", byte); + } + + if (manu_data.data.size() != MANUFACTURER_DATA_LENGTH) { + ESP_LOGE(TAG, "Unexpected manu_data size (%d)", manu_data.data.size()); + return false; + } + + // Now parse the data - See Datasheet for definition + + if (static_cast(manu_data.data[0]) != STANDARD_BOTTOM_UP) { + ESP_LOGE(TAG, "Unsupported Sensor Type (0x%X)", manu_data.data[0]); + return false; + } + + // Get battery level first + if (this->battery_level_ != nullptr) { + uint8_t level = this->parse_battery_level_(manu_data.data); + this->battery_level_->publish_state(level); + } + + // Get distance and level if either are sensors + if ((this->distance_ != nullptr) || (this->level_ != nullptr)) { + uint32_t distance_value = this->parse_distance_(manu_data.data); + SensorReadQuality quality_value = this->parse_read_quality_(manu_data.data); + ESP_LOGD(TAG, "Distance Sensor: Quality (0x%X) Distance (%dmm)", quality_value, distance_value); + if (quality_value < QUALITY_HIGH) { + ESP_LOGW(TAG, "Poor read quality."); + } + if (quality_value < QUALITY_MED) { + // if really bad reading set to 0 + ESP_LOGW(TAG, "Setting distance to 0"); + distance_value = 0; + } + + // update distance sensor + if (this->distance_ != nullptr) { + this->distance_->publish_state(distance_value); + } + + // update level sensor + if (this->level_ != nullptr) { + uint8_t tank_level = 0; + if (distance_value >= this->full_mm_) { + tank_level = 100; // cap at 100% + } else if (distance_value > this->empty_mm_) { + tank_level = ((100.0f / (this->full_mm_ - this->empty_mm_)) * (distance_value - this->empty_mm_)); + } + this->level_->publish_state(tank_level); + } + } + + // Get temperature of sensor + if (this->temperature_ != nullptr) { + uint8_t temp_in_c = this->parse_temperature_(manu_data.data); + this->temperature_->publish_state(temp_in_c); + } + + return true; +} + +uint8_t MopekaProCheck::parse_battery_level_(const std::vector &message) { + float v = (float) ((message[1] & 0x7F) / 32.0f); + // convert voltage and scale for CR2032 + float percent = (v - 2.2f) / 0.65f * 100.0f; + if (percent < 0.0f) { + return 0; + } + if (percent > 100.0f) { + return 100; + } + return (uint8_t) percent; +} + +uint32_t MopekaProCheck::parse_distance_(const std::vector &message) { + uint16_t raw = (message[4] * 256) + message[3]; + double raw_level = raw & 0x3FFF; + double raw_t = (message[2] & 0x7F); + + return (uint32_t)(raw_level * (MOPEKA_LPG_COEF[0] + MOPEKA_LPG_COEF[1] * raw_t + MOPEKA_LPG_COEF[2] * raw_t * raw_t)); +} + +uint8_t MopekaProCheck::parse_temperature_(const std::vector &message) { return (message[2] & 0x7F) - 40; } + +SensorReadQuality MopekaProCheck::parse_read_quality_(const std::vector &message) { + return static_cast(message[4] >> 6); +} + +} // namespace mopeka_pro_check +} // namespace esphome + +#endif diff --git a/esphome/components/mopeka_pro_check/mopeka_pro_check.h b/esphome/components/mopeka_pro_check/mopeka_pro_check.h new file mode 100644 index 0000000000..59d33f7763 --- /dev/null +++ b/esphome/components/mopeka_pro_check/mopeka_pro_check.h @@ -0,0 +1,58 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace mopeka_pro_check { + +enum SensorType { + STANDARD_BOTTOM_UP = 0x03, + TOP_DOWN_AIR_ABOVE = 0x04, + BOTTOM_UP_WATER = 0x05 + // all other values are reserved +}; + +// Sensor read quality. If sensor is poorly placed or tank level +// gets too low the read quality will show and the distanace +// measurement may be inaccurate. +enum SensorReadQuality { QUALITY_HIGH = 0x3, QUALITY_MED = 0x2, QUALITY_LOW = 0x1, QUALITY_NONE = 0x0 }; + +class MopekaProCheck : public Component, public esp32_ble_tracker::ESPBTDeviceListener { + public: + void set_address(uint64_t address) { address_ = address; }; + + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + + void set_level(sensor::Sensor *level) { level_ = level; }; + void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; }; + void set_battery_level(sensor::Sensor *bat) { battery_level_ = bat; }; + void set_distance(sensor::Sensor *distance) { distance_ = distance; }; + void set_tank_full(float full) { full_mm_ = full; }; + void set_tank_empty(float empty) { empty_mm_ = empty; }; + + protected: + uint64_t address_; + sensor::Sensor *level_{nullptr}; + sensor::Sensor *temperature_{nullptr}; + sensor::Sensor *distance_{nullptr}; + sensor::Sensor *battery_level_{nullptr}; + + uint32_t full_mm_; + uint32_t empty_mm_; + + uint8_t parse_battery_level_(const std::vector &message); + uint32_t parse_distance_(const std::vector &message); + uint8_t parse_temperature_(const std::vector &message); + SensorReadQuality parse_read_quality_(const std::vector &message); +}; + +} // namespace mopeka_pro_check +} // namespace esphome + +#endif diff --git a/esphome/components/mopeka_pro_check/sensor.py b/esphome/components/mopeka_pro_check/sensor.py new file mode 100644 index 0000000000..4cd90227ab --- /dev/null +++ b/esphome/components/mopeka_pro_check/sensor.py @@ -0,0 +1,131 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, esp32_ble_tracker +from esphome.const import ( + CONF_DISTANCE, + CONF_MAC_ADDRESS, + CONF_ID, + ICON_THERMOMETER, + ICON_RULER, + UNIT_PERCENT, + CONF_LEVEL, + CONF_TEMPERATURE, + DEVICE_CLASS_TEMPERATURE, + UNIT_CELSIUS, + STATE_CLASS_MEASUREMENT, + CONF_BATTERY_LEVEL, + DEVICE_CLASS_BATTERY, +) + +CONF_TANK_TYPE = "tank_type" +CONF_CUSTOM_DISTANCE_FULL = "custom_distance_full" +CONF_CUSTOM_DISTANCE_EMPTY = "custom_distance_empty" + +ICON_PROPANE_TANK = "mdi:propane-tank" + +TANK_TYPE_CUSTOM = "CUSTOM" + +UNIT_MILLIMETER = "mm" + + +def small_distance(value): + """small_distance is stored in mm""" + meters = cv.distance(value) + return meters * 1000 + + +# +# Map of standard tank types to their +# empty and full distance values. +# Format is - tank name: (empty distance in mm, full distance in mm) +# +CONF_SUPPORTED_TANKS_MAP = { + TANK_TYPE_CUSTOM: (0, 100), + "20LB_V": (38, 254), # empty/full readings for 20lb US tank + "30LB_V": (38, 381), + "40LB_V": (38, 508), +} + +CODEOWNERS = ["@spbrogan"] +DEPENDENCIES = ["esp32_ble_tracker"] + +mopeka_pro_check_ns = cg.esphome_ns.namespace("mopeka_pro_check") +MopekaProCheck = mopeka_pro_check_ns.class_( + "MopekaProCheck", esp32_ble_tracker.ESPBTDeviceListener, cg.Component +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(MopekaProCheck), + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_CUSTOM_DISTANCE_FULL): small_distance, + cv.Optional(CONF_CUSTOM_DISTANCE_EMPTY): small_distance, + cv.Required(CONF_TANK_TYPE): cv.enum(CONF_SUPPORTED_TANKS_MAP, upper=True), + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_LEVEL): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + icon=ICON_PROPANE_TANK, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_DISTANCE): sensor.sensor_schema( + unit_of_measurement=UNIT_MILLIMETER, + icon=ICON_RULER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await esp32_ble_tracker.register_ble_device(var, config) + + cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + + if config[CONF_TANK_TYPE] == TANK_TYPE_CUSTOM: + # Support custom tank min/max + if CONF_CUSTOM_DISTANCE_EMPTY in config: + cg.add(var.set_tank_empty(config[CONF_CUSTOM_DISTANCE_EMPTY])) + else: + cg.add(var.set_tank_empty(CONF_SUPPORTED_TANKS_MAP[TANK_TYPE_CUSTOM][0])) + if CONF_CUSTOM_DISTANCE_FULL in config: + cg.add(var.set_tank_full(config[CONF_CUSTOM_DISTANCE_FULL])) + else: + cg.add(var.set_tank_full(CONF_SUPPORTED_TANKS_MAP[TANK_TYPE_CUSTOM][1])) + else: + # Set the Tank empty and full based on map - User is requesting standard tank + t = config[CONF_TANK_TYPE] + cg.add(var.set_tank_empty(CONF_SUPPORTED_TANKS_MAP[t][0])) + cg.add(var.set_tank_full(CONF_SUPPORTED_TANKS_MAP[t][1])) + + if CONF_TEMPERATURE in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature(sens)) + if CONF_LEVEL in config: + sens = await sensor.new_sensor(config[CONF_LEVEL]) + cg.add(var.set_level(sens)) + if CONF_DISTANCE in config: + sens = await sensor.new_sensor(config[CONF_DISTANCE]) + cg.add(var.set_distance(sens)) + if CONF_BATTERY_LEVEL in config: + sens = await sensor.new_sensor(config[CONF_BATTERY_LEVEL]) + cg.add(var.set_battery_level(sens)) diff --git a/tests/test2.yaml b/tests/test2.yaml index 76b9775c54..ec3ccff70c 100644 --- a/tests/test2.yaml +++ b/tests/test2.yaml @@ -331,6 +331,19 @@ sensor: name: "RD200 Radon" radon_long_term: name: "RD200 Radon Long Term" + - platform: mopeka_pro_check + mac_address: D3:75:F2:DC:16:91 + tank_type: CUSTOM + custom_distance_full: 40cm + custom_distance_empty: 10mm + temperature: + name: "Propane test temp" + level: + name: "Propane test level" + distance: + name: "Propane test distance" + battery_level: + name: "Propane test battery level" time: - platform: homeassistant @@ -442,6 +455,7 @@ ruuvi_ble: xiaomi_ble: +mopeka_ble: #esp32_ble_beacon: # type: iBeacon From 38ff66debd37a6d41862d93d3176752bae129abf Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 2 Mar 2022 16:32:45 +1300 Subject: [PATCH 0303/1729] Remove stray define (#3260) --- esphome/components/mqtt/mqtt_client.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index 148316672a..1fea0c80cc 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -1,5 +1,4 @@ #include "mqtt_client.h" -#define USE_MQTT #ifdef USE_MQTT From dc6eff83ea51e64c3c1cfc943288473718769ba0 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 2 Mar 2022 16:44:35 +1300 Subject: [PATCH 0304/1729] Only get free memory size from internal (#3259) --- esphome/components/json/json_util.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/json/json_util.cpp b/esphome/components/json/json_util.cpp index 7e88fb6e59..9acba76597 100644 --- a/esphome/components/json/json_util.cpp +++ b/esphome/components/json/json_util.cpp @@ -22,7 +22,7 @@ std::string build_json(const json_build_t &f) { #ifdef USE_ESP8266 const size_t free_heap = ESP.getMaxFreeBlockSize() - 2048; // NOLINT(readability-static-accessed-through-instance) #elif defined(USE_ESP32) - const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT) - 2048; + const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL) - 2048; #endif DynamicJsonDocument json_document(free_heap); @@ -42,7 +42,7 @@ void parse_json(const std::string &data, const json_parse_t &f) { #ifdef USE_ESP8266 const size_t free_heap = ESP.getMaxFreeBlockSize() - 2048; // NOLINT(readability-static-accessed-through-instance) #elif defined(USE_ESP32) - const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT) - 2048; + const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL) - 2048; #endif DynamicJsonDocument json_document(free_heap); From 859cca49d19a95ce3329180710cbaaf650ef7ac0 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 2 Mar 2022 16:44:35 +1300 Subject: [PATCH 0305/1729] Only get free memory size from internal (#3259) --- esphome/components/json/json_util.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/json/json_util.cpp b/esphome/components/json/json_util.cpp index 7e88fb6e59..9acba76597 100644 --- a/esphome/components/json/json_util.cpp +++ b/esphome/components/json/json_util.cpp @@ -22,7 +22,7 @@ std::string build_json(const json_build_t &f) { #ifdef USE_ESP8266 const size_t free_heap = ESP.getMaxFreeBlockSize() - 2048; // NOLINT(readability-static-accessed-through-instance) #elif defined(USE_ESP32) - const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT) - 2048; + const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL) - 2048; #endif DynamicJsonDocument json_document(free_heap); @@ -42,7 +42,7 @@ void parse_json(const std::string &data, const json_parse_t &f) { #ifdef USE_ESP8266 const size_t free_heap = ESP.getMaxFreeBlockSize() - 2048; // NOLINT(readability-static-accessed-through-instance) #elif defined(USE_ESP32) - const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT) - 2048; + const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL) - 2048; #endif DynamicJsonDocument json_document(free_heap); From 942b0de7fd7ad31d4e79bbe749ac8b928ba597cb Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 2 Mar 2022 17:07:08 +1300 Subject: [PATCH 0306/1729] Bump version to 2022.2.6 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 7cd88a8101..e49f468c30 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2022.2.5" +__version__ = "2022.2.6" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From 3b8ca809003a9e0249c1c92ae6a9158f7ed430a9 Mon Sep 17 00:00:00 2001 From: wilberforce Date: Tue, 8 Mar 2022 15:02:24 +1300 Subject: [PATCH 0307/1729] Webserver v2 (#2688) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/climate/climate_traits.h | 2 +- esphome/components/web_server/__init__.py | 45 +- esphome/components/web_server/server_index.h | 573 +++++++++++++++++++ esphome/components/web_server/web_server.cpp | 439 ++++++++++---- esphome/components/web_server/web_server.h | 38 +- tests/test1.yaml | 4 +- 6 files changed, 975 insertions(+), 126 deletions(-) create mode 100644 esphome/components/web_server/server_index.h diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index d113510eeb..3ec51bc3c2 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -141,7 +141,7 @@ class ClimateTraits { } bool supports_swing_mode(ClimateSwingMode swing_mode) const { return supported_swing_modes_.count(swing_mode); } bool get_supports_swing_modes() const { return !supported_swing_modes_.empty(); } - std::set get_supported_swing_modes() { return supported_swing_modes_; } + std::set get_supported_swing_modes() const { return supported_swing_modes_; } float get_visual_min_temperature() const { return visual_min_temperature_; } void set_visual_min_temperature(float visual_min_temperature) { visual_min_temperature_ = visual_min_temperature; } diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index eaa20ccbb4..42683c8d77 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -14,6 +14,8 @@ from esphome.const import ( CONF_PASSWORD, CONF_INCLUDE_INTERNAL, CONF_OTA, + CONF_VERSION, + CONF_LOCAL, ) from esphome.core import CORE, coroutine_with_priority @@ -22,18 +24,37 @@ AUTO_LOAD = ["json", "web_server_base"] web_server_ns = cg.esphome_ns.namespace("web_server") WebServer = web_server_ns.class_("WebServer", cg.Component, cg.Controller) + +def default_url(config): + config = config.copy() + if config[CONF_VERSION] == 1: + if not (CONF_CSS_URL in config): + config[CONF_CSS_URL] = "https://esphome.io/_static/webserver-v1.min.css" + if not (CONF_JS_URL in config): + config[CONF_JS_URL] = "https://esphome.io/_static/webserver-v1.min.js" + if config[CONF_VERSION] == 2: + if not (CONF_CSS_URL in config): + config[CONF_CSS_URL] = "" + if not (CONF_JS_URL in config): + config[CONF_JS_URL] = "https://oi.esphome.io/v2/www.js" + return config + + +def validate_local(config): + if CONF_LOCAL in config and config[CONF_VERSION] == 1: + raise cv.Invalid("'local' is not supported in version 1") + return config + + CONFIG_SCHEMA = cv.All( cv.Schema( { cv.GenerateID(): cv.declare_id(WebServer), cv.Optional(CONF_PORT, default=80): cv.port, - cv.Optional( - CONF_CSS_URL, default="https://esphome.io/_static/webserver-v1.min.css" - ): cv.string, + cv.Optional(CONF_VERSION, default=2): cv.one_of(1, 2), + cv.Optional(CONF_CSS_URL): cv.string, cv.Optional(CONF_CSS_INCLUDE): cv.file_, - cv.Optional( - CONF_JS_URL, default="https://esphome.io/_static/webserver-v1.min.js" - ): cv.string, + cv.Optional(CONF_JS_URL): cv.string, cv.Optional(CONF_JS_INCLUDE): cv.file_, cv.Optional(CONF_AUTH): cv.Schema( { @@ -50,9 +71,12 @@ CONFIG_SCHEMA = cv.All( ), cv.Optional(CONF_INCLUDE_INTERNAL, default=False): cv.boolean, cv.Optional(CONF_OTA, default=True): cv.boolean, - }, + cv.Optional(CONF_LOCAL): cv.boolean, + } ).extend(cv.COMPONENT_SCHEMA), cv.only_with_arduino, + default_url, + validate_local, ) @@ -68,6 +92,7 @@ async def to_code(config): cg.add(paren.set_port(config[CONF_PORT])) cg.add_define("USE_WEBSERVER") cg.add_define("USE_WEBSERVER_PORT", config[CONF_PORT]) + cg.add_define("USE_WEBSERVER_VERSION", config[CONF_VERSION]) cg.add(var.set_css_url(config[CONF_CSS_URL])) cg.add(var.set_js_url(config[CONF_JS_URL])) cg.add(var.set_allow_ota(config[CONF_OTA])) @@ -75,13 +100,15 @@ async def to_code(config): cg.add(paren.set_auth_username(config[CONF_AUTH][CONF_USERNAME])) cg.add(paren.set_auth_password(config[CONF_AUTH][CONF_PASSWORD])) if CONF_CSS_INCLUDE in config: - cg.add_define("WEBSERVER_CSS_INCLUDE") + cg.add_define("USE_WEBSERVER_CSS_INCLUDE") path = CORE.relative_config_path(config[CONF_CSS_INCLUDE]) with open(file=path, encoding="utf-8") as myfile: cg.add(var.set_css_include(myfile.read())) if CONF_JS_INCLUDE in config: - cg.add_define("WEBSERVER_JS_INCLUDE") + cg.add_define("USE_WEBSERVER_JS_INCLUDE") path = CORE.relative_config_path(config[CONF_JS_INCLUDE]) with open(file=path, encoding="utf-8") as myfile: cg.add(var.set_js_include(myfile.read())) cg.add(var.set_include_internal(config[CONF_INCLUDE_INTERNAL])) + if CONF_LOCAL in config and config[CONF_LOCAL]: + cg.add_define("USE_WEBSERVER_LOCAL") diff --git a/esphome/components/web_server/server_index.h b/esphome/components/web_server/server_index.h new file mode 100644 index 0000000000..719a804d0c --- /dev/null +++ b/esphome/components/web_server/server_index.h @@ -0,0 +1,573 @@ +#pragma once +// Generated from https://github.com/esphome/esphome-webserver +#include "esphome/core/hal.h" +namespace esphome { + +namespace web_server { + +const uint8_t INDEX_GZ[] PROGMEM = { + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0xbd, 0x7d, 0xd9, 0x76, 0xe3, 0xc6, 0x92, 0xe0, 0xf3, + 0x9c, 0x33, 0x7f, 0x30, 0x2f, 0x10, 0xac, 0x56, 0x01, 0x16, 0x08, 0x91, 0x54, 0x6d, 0x06, 0x05, 0xf2, 0xca, 0x55, + 0xe5, 0x5b, 0x65, 0xd7, 0xe6, 0x92, 0xaa, 0xbc, 0xc8, 0x74, 0x09, 0x22, 0x93, 0x22, 0x5c, 0x20, 0x40, 0x03, 0x49, + 0x2d, 0xa6, 0xd0, 0xa7, 0x9f, 0xfa, 0x69, 0xce, 0x99, 0xf5, 0xa1, 0x5f, 0xe6, 0xf4, 0xcb, 0x7c, 0xc4, 0x7c, 0xcf, + 0xfd, 0x81, 0xe9, 0x4f, 0x98, 0x88, 0xc8, 0x05, 0x09, 0x90, 0x5a, 0xec, 0xf6, 0xdc, 0x53, 0x8b, 0x80, 0x5c, 0x23, + 0x23, 0x23, 0x63, 0x4f, 0x68, 0x6f, 0x63, 0x9c, 0x8d, 0xf8, 0xe5, 0x9c, 0x59, 0x53, 0x3e, 0x4b, 0xfa, 0x7b, 0xf2, + 0x7f, 0x16, 0x8d, 0xfb, 0x7b, 0x49, 0x9c, 0x7e, 0xb2, 0x72, 0x96, 0x84, 0xf1, 0x28, 0x4b, 0xad, 0x69, 0xce, 0x26, + 0xe1, 0x38, 0xe2, 0x51, 0x10, 0xcf, 0xa2, 0x53, 0x66, 0xed, 0xf4, 0xf7, 0x66, 0x8c, 0x47, 0xd6, 0x68, 0x1a, 0xe5, + 0x05, 0xe3, 0xe1, 0xfb, 0xc3, 0xaf, 0x5a, 0x8f, 0xfb, 0x7b, 0xc5, 0x28, 0x8f, 0xe7, 0xdc, 0xc2, 0x21, 0xc3, 0x59, + 0x36, 0x5e, 0x24, 0xac, 0x7f, 0x16, 0xe5, 0xd6, 0x3e, 0x0b, 0xdf, 0x9c, 0xfc, 0xc2, 0x46, 0xdc, 0x1f, 0xb3, 0x49, + 0x9c, 0xb2, 0xb7, 0x79, 0x36, 0x67, 0x39, 0xbf, 0xf4, 0x2e, 0xd6, 0x57, 0xc4, 0xac, 0xf0, 0x9e, 0xe9, 0xaa, 0x53, + 0xc6, 0xdf, 0x9c, 0xa7, 0xaa, 0xcf, 0x53, 0x26, 0x26, 0xc9, 0xf2, 0xc2, 0xe3, 0xd7, 0xb4, 0x39, 0xb8, 0x9c, 0x9d, + 0x64, 0x49, 0xe1, 0x1d, 0xe8, 0xfa, 0x79, 0x9e, 0xf1, 0x0c, 0xc1, 0xf2, 0xa7, 0x51, 0x61, 0xb4, 0xf4, 0x3e, 0xad, + 0x69, 0x32, 0x97, 0x95, 0x2f, 0x8a, 0x67, 0xe9, 0x62, 0xc6, 0xf2, 0xe8, 0x24, 0x61, 0x5e, 0xc1, 0x42, 0x87, 0x79, + 0xdc, 0x8b, 0xdd, 0xb0, 0xcf, 0xad, 0x38, 0xb5, 0xd8, 0x60, 0x9f, 0x51, 0xc9, 0x92, 0xe9, 0x56, 0xc1, 0x46, 0xdb, + 0x03, 0x74, 0x4d, 0xe2, 0xd3, 0x85, 0x7e, 0x3f, 0xcf, 0x63, 0xae, 0x9e, 0xcf, 0xa2, 0x64, 0xc1, 0x82, 0xb8, 0x74, + 0x03, 0x76, 0xc4, 0x87, 0x61, 0xec, 0x3d, 0xa1, 0x41, 0x61, 0xc8, 0xe5, 0x24, 0xcb, 0x1d, 0xc4, 0x55, 0x8c, 0x63, + 0xf3, 0xab, 0x2b, 0x87, 0x87, 0xcb, 0xd2, 0x75, 0x0f, 0x98, 0x3f, 0x8a, 0x92, 0xc4, 0xc1, 0x89, 0xb7, 0xb6, 0x0a, + 0x9c, 0x31, 0xf6, 0xf8, 0x51, 0x3c, 0x74, 0x7b, 0xf1, 0xc4, 0xe1, 0xcc, 0xad, 0xfa, 0x65, 0x13, 0x8b, 0x33, 0x87, + 0xbb, 0xee, 0xa7, 0xeb, 0xfb, 0xe4, 0x8c, 0x2f, 0x72, 0x80, 0xbd, 0xf4, 0xde, 0xa8, 0x99, 0x2f, 0xb0, 0xfe, 0x19, + 0x75, 0xec, 0x01, 0xec, 0x05, 0xb7, 0x3e, 0x84, 0xe7, 0x71, 0x3a, 0xce, 0xce, 0xfd, 0x83, 0x69, 0x04, 0x3f, 0xde, + 0x65, 0x19, 0xdf, 0xda, 0x72, 0xce, 0xb2, 0x78, 0x6c, 0xb5, 0xc3, 0xd0, 0xac, 0xbc, 0x7c, 0x72, 0x70, 0x70, 0x75, + 0xd5, 0x28, 0xf0, 0xd3, 0x88, 0xc7, 0x67, 0x4c, 0x74, 0x06, 0x00, 0x6c, 0xf8, 0x39, 0xe7, 0x6c, 0x7c, 0xc0, 0x2f, + 0x13, 0x28, 0x65, 0x8c, 0x17, 0x36, 0xac, 0xf1, 0x69, 0x36, 0x02, 0xb4, 0xa5, 0x06, 0xe2, 0xa1, 0x69, 0xce, 0xe6, + 0x49, 0x34, 0x62, 0x58, 0x0f, 0x23, 0x55, 0x3d, 0xaa, 0x46, 0xde, 0x57, 0xa1, 0xd8, 0x5e, 0xc7, 0xf5, 0x62, 0x16, + 0xa6, 0xec, 0xdc, 0x7a, 0x15, 0xcd, 0x7b, 0xa3, 0x24, 0x2a, 0x0a, 0xa0, 0xd7, 0x25, 0x2d, 0x21, 0x5f, 0x8c, 0x80, + 0x40, 0x68, 0x81, 0x4b, 0x44, 0xd3, 0x34, 0x2e, 0xfc, 0x8f, 0x9b, 0xa3, 0xa2, 0x78, 0xc7, 0x8a, 0x45, 0xc2, 0x37, + 0x43, 0xd8, 0x0b, 0xbe, 0x11, 0x86, 0x5f, 0xb9, 0x7c, 0x9a, 0x67, 0xe7, 0xd6, 0xb3, 0x3c, 0x87, 0xe6, 0x36, 0x4c, + 0x29, 0x1a, 0x58, 0x71, 0x61, 0xa5, 0x19, 0xb7, 0xf4, 0x60, 0xb8, 0x81, 0xbe, 0xf5, 0xbe, 0x60, 0xd6, 0xf1, 0x22, + 0x2d, 0xa2, 0x09, 0x83, 0xa6, 0xc7, 0x56, 0x96, 0x5b, 0xc7, 0x30, 0xe8, 0x31, 0x6c, 0x59, 0xc1, 0xe1, 0xd4, 0xf8, + 0xb6, 0xdb, 0xa3, 0xb9, 0xa0, 0xf0, 0x90, 0x5d, 0xf0, 0x90, 0x95, 0x40, 0x98, 0x56, 0xa1, 0x97, 0xe1, 0xb8, 0xcb, + 0x04, 0x0a, 0x58, 0x18, 0x33, 0x24, 0x59, 0xc7, 0x6c, 0xac, 0x37, 0xe7, 0xc3, 0xd6, 0x96, 0xc6, 0x35, 0xe0, 0xc4, + 0x81, 0xb6, 0x45, 0xa3, 0xad, 0x27, 0x16, 0x5e, 0x43, 0x91, 0xeb, 0x31, 0x5f, 0xa2, 0xef, 0xe0, 0x32, 0x1d, 0xd5, + 0xc7, 0x86, 0xca, 0x92, 0x67, 0x07, 0x3c, 0x8f, 0xd3, 0x53, 0x00, 0x42, 0xce, 0x64, 0x36, 0x29, 0x4b, 0xb1, 0xf9, + 0x4f, 0x58, 0xc8, 0xc2, 0x3e, 0x8e, 0x9e, 0x33, 0xc7, 0x2e, 0xa8, 0x87, 0x1d, 0x86, 0x88, 0x7a, 0x20, 0x30, 0x36, + 0x60, 0x01, 0xdb, 0xb6, 0x6d, 0xef, 0x2b, 0xd7, 0x3b, 0x47, 0x0a, 0xf2, 0x7d, 0x9f, 0xc8, 0x57, 0x74, 0x8e, 0xc3, + 0x0e, 0x02, 0xed, 0x27, 0x2c, 0x3d, 0xe5, 0xd3, 0x01, 0x3b, 0x6a, 0x0f, 0x03, 0x0e, 0x50, 0x8d, 0x17, 0x23, 0xe6, + 0x20, 0x3d, 0x7a, 0x05, 0x1e, 0x9f, 0x6d, 0x07, 0xa6, 0xc0, 0x8d, 0xd9, 0xa0, 0x35, 0xd6, 0xb6, 0xc6, 0x55, 0x24, + 0xaa, 0x00, 0x43, 0x3a, 0xb7, 0xe1, 0x84, 0x9d, 0xb0, 0xdc, 0x80, 0x43, 0x37, 0xeb, 0xd5, 0x76, 0x70, 0x01, 0x3b, + 0x04, 0xfd, 0xac, 0xc9, 0x22, 0x1d, 0xf1, 0x18, 0x18, 0x97, 0xbd, 0x0d, 0xe0, 0x8a, 0x9d, 0xd3, 0x1b, 0x67, 0xbb, + 0xa5, 0xeb, 0xc4, 0xee, 0x36, 0x3b, 0x2a, 0xb6, 0x3b, 0x43, 0x0f, 0xa1, 0xd4, 0xc8, 0x97, 0x0b, 0x8f, 0x61, 0x81, + 0x70, 0x46, 0x98, 0x3e, 0x9e, 0x1f, 0x06, 0xcc, 0x5f, 0xa5, 0xe3, 0x90, 0xfb, 0xb3, 0x68, 0x8e, 0xab, 0x61, 0x44, + 0x03, 0x51, 0x3a, 0x42, 0xe8, 0x6a, 0xfb, 0x82, 0x18, 0xf3, 0x2b, 0x12, 0x70, 0x01, 0x21, 0x70, 0x66, 0x9f, 0x45, + 0xa3, 0x29, 0x1c, 0xf1, 0x0a, 0x71, 0x63, 0x75, 0x1c, 0x46, 0x39, 0x8b, 0x38, 0x7b, 0x96, 0x30, 0x7c, 0xc3, 0x1d, + 0x80, 0x9e, 0xb6, 0xeb, 0x15, 0xea, 0xdc, 0x25, 0x31, 0x7f, 0x9d, 0xc1, 0x3c, 0x3d, 0x41, 0x24, 0x40, 0xc5, 0xc5, + 0xd6, 0x56, 0x8c, 0x24, 0xb2, 0xcf, 0x61, 0xb7, 0x4e, 0x16, 0xc0, 0x04, 0xec, 0x14, 0x5b, 0xd8, 0x80, 0x6d, 0x2f, + 0xf6, 0x39, 0x20, 0xf1, 0x49, 0x96, 0x72, 0x18, 0x0e, 0xe0, 0xd5, 0x14, 0xe4, 0x47, 0xf3, 0x39, 0x4b, 0xc7, 0x4f, + 0xa6, 0x71, 0x32, 0x06, 0x6c, 0x94, 0xb0, 0xde, 0x8c, 0x85, 0xb0, 0x4e, 0x58, 0x4c, 0x70, 0xf3, 0x8a, 0x68, 0xfb, + 0x90, 0x90, 0x79, 0x68, 0xdb, 0x3d, 0xe4, 0x40, 0x72, 0x15, 0xc8, 0x83, 0x68, 0xe3, 0xde, 0x01, 0xeb, 0x2f, 0x5c, + 0xbe, 0x1d, 0xc6, 0x7a, 0x1b, 0x25, 0x82, 0x9f, 0x20, 0xa7, 0x01, 0xfc, 0x33, 0xe0, 0x81, 0x3d, 0x64, 0x5c, 0xdf, + 0x49, 0xae, 0x93, 0x32, 0xb5, 0x42, 0x40, 0xc0, 0x08, 0x39, 0x88, 0xc4, 0xc1, 0xdb, 0x2c, 0xb9, 0x9c, 0xc4, 0x49, + 0x72, 0xb0, 0x98, 0xcf, 0xb3, 0x9c, 0x7b, 0x5f, 0x87, 0x4b, 0x9e, 0x55, 0x6b, 0xa5, 0x43, 0x5e, 0x9c, 0xc7, 0x1c, + 0x11, 0xea, 0x2e, 0x47, 0x11, 0x6c, 0xf5, 0x97, 0x59, 0x96, 0xb0, 0x28, 0x85, 0x65, 0xb0, 0x81, 0x6d, 0x07, 0xe9, + 0x22, 0x49, 0x7a, 0x27, 0x30, 0xec, 0xa7, 0x1e, 0x55, 0x0b, 0x8e, 0x1f, 0xd0, 0xf3, 0x7e, 0x9e, 0x47, 0x97, 0xd0, + 0x10, 0xdb, 0x00, 0x2d, 0xc2, 0x6e, 0x7d, 0x7d, 0xf0, 0xe6, 0xb5, 0x2f, 0x08, 0x3f, 0x9e, 0x5c, 0x02, 0xa0, 0x65, + 0xc5, 0x35, 0x27, 0x79, 0x36, 0x6b, 0x4c, 0x8d, 0x78, 0x88, 0x43, 0xd6, 0xbb, 0x06, 0x84, 0x98, 0x46, 0x86, 0x5d, + 0x62, 0x26, 0x04, 0xaf, 0x89, 0x9e, 0x65, 0x25, 0x9e, 0x81, 0x01, 0x3e, 0x04, 0xa2, 0x18, 0xa6, 0xbc, 0x19, 0x5a, + 0x9e, 0x5f, 0x2e, 0xe3, 0x90, 0xe0, 0x9c, 0xa3, 0xfc, 0x45, 0x18, 0x47, 0x11, 0xcc, 0xbe, 0x14, 0x03, 0x96, 0x0a, + 0xe2, 0xb8, 0x2c, 0xbd, 0x44, 0x13, 0x31, 0x72, 0x3c, 0x64, 0x28, 0x1c, 0x8e, 0xd1, 0xd5, 0x15, 0x83, 0x17, 0xd7, + 0xfb, 0x26, 0x5c, 0x46, 0x6a, 0x3d, 0x28, 0xa1, 0xf0, 0x7c, 0x05, 0x82, 0x4f, 0xa0, 0x24, 0x3b, 0x03, 0x39, 0x08, + 0x70, 0x7e, 0xed, 0x81, 0xfc, 0x4f, 0x10, 0x8a, 0x8d, 0x8e, 0x07, 0x12, 0xf4, 0xc9, 0x34, 0x4a, 0x4f, 0xd9, 0x38, + 0x48, 0x58, 0x29, 0x39, 0xef, 0xbe, 0x05, 0x7b, 0x0c, 0xe4, 0x54, 0x58, 0xcf, 0x0f, 0x5f, 0xbd, 0x94, 0x3b, 0x57, + 0x63, 0xc6, 0xb0, 0x49, 0x0b, 0x10, 0xab, 0xc0, 0xb6, 0x25, 0x3b, 0x7e, 0xc6, 0x15, 0xf7, 0x16, 0x25, 0x71, 0xf1, + 0x7e, 0x0e, 0x2a, 0x06, 0x7b, 0x0b, 0xc3, 0xc0, 0xf4, 0x21, 0x4c, 0x45, 0xe5, 0x30, 0x9f, 0xa8, 0x18, 0xeb, 0x22, + 0xe8, 0x2c, 0x56, 0x2a, 0x5e, 0x33, 0xc7, 0x2d, 0x81, 0x54, 0x79, 0x3c, 0xb2, 0xa2, 0xf1, 0xf8, 0x45, 0x1a, 0xf3, + 0x38, 0x4a, 0xe2, 0xdf, 0x08, 0x93, 0x4b, 0xa4, 0x31, 0xde, 0x93, 0x9b, 0x00, 0x6b, 0xa7, 0x1e, 0x89, 0xab, 0x98, + 0xec, 0x06, 0x21, 0x43, 0x70, 0xcb, 0x24, 0x3c, 0x1a, 0x4a, 0xf0, 0x12, 0x7f, 0xbe, 0x28, 0xa6, 0x88, 0x58, 0x39, + 0x30, 0x32, 0xf2, 0xec, 0xa4, 0x60, 0xf9, 0x19, 0x1b, 0x6b, 0x0a, 0x28, 0x60, 0x55, 0xd4, 0x1c, 0x94, 0x17, 0x9a, + 0xd1, 0x51, 0x32, 0x94, 0xc1, 0x50, 0x3d, 0x93, 0xcd, 0x32, 0x49, 0xcc, 0x5a, 0xc3, 0xd1, 0x5c, 0xc0, 0x11, 0x4a, + 0x85, 0xe4, 0x04, 0x45, 0xa8, 0x56, 0x38, 0x05, 0x2e, 0x04, 0x52, 0xc1, 0x3c, 0xe6, 0x4a, 0x92, 0x3d, 0x5b, 0x90, + 0x48, 0x28, 0xa0, 0x23, 0x1c, 0x64, 0x82, 0xb4, 0x70, 0xe1, 0x54, 0x01, 0x97, 0x97, 0xe0, 0x0a, 0x2e, 0xa2, 0xd4, + 0x1c, 0x24, 0x80, 0xf0, 0x1b, 0x21, 0x0b, 0x7d, 0x6c, 0x41, 0x64, 0xe0, 0xeb, 0x9d, 0x07, 0xc4, 0xca, 0x75, 0x57, + 0x0b, 0xf1, 0xae, 0x01, 0x1b, 0x27, 0x46, 0x7a, 0xf2, 0x36, 0xb8, 0x9f, 0x66, 0xfb, 0xa3, 0x11, 0x2b, 0x8a, 0x2c, + 0xdf, 0xda, 0xda, 0xa0, 0xf6, 0xd7, 0x29, 0x5a, 0x80, 0x49, 0x57, 0xf3, 0x3a, 0xbb, 0x20, 0x09, 0x6e, 0x8a, 0x15, + 0x25, 0xd3, 0x03, 0xfb, 0xe3, 0x47, 0xe0, 0xd9, 0x9e, 0x44, 0x03, 0x60, 0x7d, 0x55, 0xf1, 0x13, 0xfa, 0x4c, 0x1d, + 0x33, 0x6b, 0xf5, 0x4b, 0xa7, 0x0e, 0x92, 0x07, 0xc3, 0xba, 0xa5, 0xb1, 0xa1, 0x6b, 0x87, 0xc6, 0xdd, 0x90, 0x02, + 0x72, 0x79, 0x4a, 0x22, 0xdb, 0xd8, 0x46, 0xd0, 0xda, 0x4a, 0x8f, 0x50, 0xaf, 0x56, 0x93, 0x13, 0xa0, 0x47, 0x6c, + 0xd8, 0x93, 0xf5, 0x61, 0x21, 0x30, 0x97, 0xb3, 0x5f, 0x17, 0xac, 0xe0, 0x82, 0x74, 0x61, 0xdc, 0x1c, 0xc6, 0x2d, + 0x57, 0xb4, 0xc3, 0x9a, 0xee, 0xb8, 0x0e, 0xb6, 0x37, 0x73, 0x94, 0x63, 0x05, 0x52, 0xf2, 0xcd, 0xe4, 0x84, 0xb0, + 0x32, 0xf7, 0xea, 0xea, 0x1b, 0x35, 0x48, 0xb5, 0x95, 0x5a, 0x07, 0x6a, 0xec, 0x89, 0xad, 0x9a, 0x8c, 0x6d, 0x57, + 0x0a, 0xd4, 0x8d, 0x4e, 0xaf, 0x46, 0x07, 0x70, 0xe6, 0xda, 0x9a, 0xa4, 0x2b, 0x65, 0xfb, 0xad, 0xc2, 0xe9, 0x1b, + 0x31, 0x32, 0x69, 0xa3, 0xec, 0x76, 0xea, 0x51, 0x27, 0x1e, 0xda, 0xae, 0xd4, 0x55, 0x8c, 0x61, 0x51, 0x67, 0x0c, + 0x4d, 0xa8, 0xe7, 0xba, 0x8b, 0xad, 0x89, 0x8a, 0x85, 0x6a, 0xaf, 0x95, 0x01, 0xc1, 0xc3, 0x23, 0x50, 0x4e, 0xd6, + 0xda, 0x07, 0xaf, 0xa3, 0x19, 0x43, 0x8c, 0x7a, 0xd7, 0x35, 0x90, 0x06, 0x04, 0x34, 0x19, 0x36, 0xc5, 0x1b, 0x77, + 0x85, 0xd6, 0x54, 0x3f, 0x5f, 0x31, 0x68, 0x11, 0xa0, 0x5f, 0x97, 0x6b, 0xb6, 0x88, 0xe4, 0xa6, 0x24, 0x67, 0x85, + 0x1f, 0x51, 0x26, 0xf6, 0x84, 0x04, 0x3c, 0x2c, 0x1e, 0xb6, 0xbf, 0xb1, 0x71, 0xb2, 0x15, 0x53, 0x6b, 0xe4, 0xc8, + 0x53, 0x00, 0xcf, 0x24, 0x04, 0x80, 0x5d, 0xd2, 0xcf, 0xda, 0xc1, 0x42, 0xb4, 0x1d, 0x20, 0x1d, 0xf8, 0x93, 0x24, + 0xe2, 0x4e, 0x67, 0xa7, 0xed, 0x02, 0x1d, 0x02, 0x13, 0x07, 0x19, 0x01, 0xea, 0x7d, 0xb5, 0x14, 0x86, 0x4b, 0x89, + 0x5d, 0xee, 0x83, 0x52, 0x34, 0x8d, 0x27, 0xdc, 0xc9, 0x50, 0x88, 0xb8, 0x25, 0x4b, 0x40, 0xc8, 0xe8, 0x73, 0x05, + 0x5c, 0x82, 0x0b, 0xee, 0x22, 0xaa, 0x35, 0x43, 0x53, 0x90, 0x12, 0x97, 0x22, 0x29, 0xa8, 0x20, 0x30, 0x98, 0x4a, + 0x4f, 0x51, 0x14, 0xc8, 0xb7, 0x78, 0x20, 0x06, 0x0d, 0x56, 0x34, 0xca, 0x78, 0x10, 0xaf, 0x16, 0x82, 0x18, 0xf6, + 0x79, 0xf6, 0x32, 0x3b, 0x67, 0xf9, 0x93, 0x08, 0x61, 0x0f, 0x44, 0xf7, 0x12, 0x38, 0x3d, 0x31, 0x74, 0xd6, 0x53, + 0xb4, 0x72, 0x46, 0x8b, 0x86, 0x8d, 0x98, 0xc5, 0x28, 0x08, 0x41, 0xca, 0x11, 0xee, 0x53, 0x3c, 0x52, 0x74, 0xf6, + 0x50, 0x94, 0x30, 0x4d, 0x5b, 0xfb, 0x2f, 0xeb, 0xb4, 0x05, 0x23, 0xcc, 0x15, 0xb5, 0xd6, 0x4f, 0xac, 0xeb, 0x49, + 0xd9, 0xec, 0x48, 0xda, 0x32, 0x84, 0x19, 0xc8, 0x8f, 0xab, 0xab, 0x4a, 0x49, 0x07, 0x61, 0xaa, 0xb9, 0x39, 0x6a, + 0x4e, 0xe2, 0x48, 0xb8, 0x25, 0x08, 0x23, 0x54, 0xbc, 0xf2, 0x2c, 0x49, 0x0c, 0x59, 0xe4, 0xc5, 0x3d, 0xa7, 0x21, + 0x8e, 0x00, 0x8a, 0x59, 0x4d, 0x22, 0x0d, 0x78, 0xa0, 0x2b, 0x50, 0x28, 0x29, 0x69, 0xe4, 0x55, 0x4d, 0x04, 0xc4, + 0xe9, 0x98, 0xe5, 0xc2, 0x40, 0x93, 0x32, 0x14, 0x26, 0x4c, 0x81, 0xa0, 0xd9, 0x18, 0x38, 0xbc, 0x5a, 0x00, 0xa8, + 0x27, 0xfe, 0x34, 0x2b, 0xb8, 0xae, 0x33, 0xa1, 0x8f, 0xaf, 0xae, 0x62, 0x61, 0x2f, 0x22, 0x01, 0xe4, 0x6c, 0x96, + 0x9d, 0xb1, 0x35, 0x50, 0xf7, 0xd4, 0x60, 0x26, 0xc8, 0xc6, 0x30, 0x20, 0x44, 0x41, 0xb4, 0xcc, 0x93, 0x78, 0xc4, + 0xb4, 0x94, 0x9a, 0xf9, 0xa0, 0xd0, 0xb1, 0x0b, 0xe0, 0x11, 0xcc, 0xed, 0xf7, 0xfb, 0x6d, 0xaf, 0xe3, 0x96, 0x02, + 0xe1, 0xcb, 0x15, 0x8c, 0xde, 0x20, 0x1f, 0xa5, 0x0a, 0xbe, 0x8e, 0x17, 0x70, 0xd7, 0x10, 0x8a, 0x5c, 0xd8, 0x49, + 0x9e, 0x64, 0xc4, 0xae, 0x37, 0x86, 0x41, 0x39, 0x53, 0x8c, 0x1b, 0x55, 0x5c, 0x71, 0x6c, 0xdf, 0x69, 0xb4, 0x69, + 0x72, 0x52, 0x27, 0x4c, 0x6d, 0x8c, 0xdc, 0xf3, 0x42, 0x5b, 0xc0, 0xe6, 0xf6, 0xa0, 0x96, 0x48, 0xd5, 0x40, 0xeb, + 0x00, 0xa1, 0xb0, 0x74, 0x9d, 0x95, 0x25, 0x55, 0x9d, 0x25, 0x13, 0xd7, 0x07, 0xe8, 0x0d, 0x93, 0x60, 0xae, 0x43, + 0xc1, 0x81, 0x64, 0x08, 0x1c, 0x2d, 0x32, 0xb1, 0x5f, 0x4f, 0x60, 0x7b, 0x4e, 0xa2, 0xd1, 0x27, 0x0d, 0x6e, 0x85, + 0xf6, 0x26, 0x19, 0x38, 0x8d, 0x92, 0xd0, 0x60, 0x57, 0xe6, 0xba, 0x15, 0x87, 0xae, 0x1d, 0x14, 0x30, 0xc8, 0x56, + 0xc8, 0xbe, 0xb9, 0xd1, 0x4d, 0x6a, 0x97, 0xe4, 0xa1, 0xec, 0x27, 0x4d, 0x25, 0x37, 0x90, 0x1c, 0x57, 0xdc, 0x80, + 0x2b, 0xc2, 0x83, 0xad, 0x69, 0x40, 0x02, 0x74, 0x57, 0x8e, 0xe3, 0xe2, 0x7a, 0x14, 0xfc, 0xa9, 0x60, 0x3e, 0x35, + 0x66, 0xba, 0x15, 0x52, 0xcd, 0xe1, 0xa4, 0x1a, 0xac, 0x41, 0x93, 0xca, 0x83, 0x62, 0x35, 0xdf, 0xa0, 0xa2, 0x42, + 0x14, 0x7f, 0x2a, 0xaa, 0x50, 0x05, 0x43, 0x30, 0x0a, 0x2f, 0x97, 0x04, 0x97, 0xad, 0xb2, 0x16, 0xc9, 0x53, 0x63, + 0x12, 0xa9, 0x9a, 0xe4, 0x32, 0x50, 0xb0, 0xe8, 0xb4, 0xfa, 0x52, 0x13, 0x57, 0x2c, 0x37, 0x0d, 0x35, 0x33, 0xc9, + 0x95, 0x35, 0xe1, 0x14, 0x68, 0x77, 0x29, 0xed, 0xdd, 0x5c, 0x4f, 0xa1, 0xd6, 0x53, 0xf8, 0x86, 0x0d, 0x65, 0xd2, + 0x76, 0x3e, 0x00, 0x75, 0xbf, 0x56, 0x89, 0xfa, 0xa9, 0x8f, 0x8c, 0xd9, 0xd5, 0x4c, 0x17, 0x18, 0x8a, 0x24, 0x93, + 0x74, 0x20, 0xe9, 0x0d, 0xd9, 0x46, 0x65, 0x19, 0x65, 0xae, 0x38, 0x20, 0x35, 0xab, 0x34, 0xf3, 0x52, 0xb7, 0xa1, + 0xbf, 0x97, 0xa5, 0xc4, 0x13, 0x17, 0x98, 0x89, 0xbd, 0x9b, 0x70, 0xe3, 0xa5, 0x61, 0x26, 0xb4, 0x5f, 0xa1, 0xec, + 0xd4, 0x30, 0x94, 0x4a, 0x16, 0x88, 0x63, 0xe3, 0x6b, 0xa5, 0x19, 0x64, 0xfe, 0x1a, 0x7d, 0x0a, 0x40, 0x49, 0x60, + 0xf3, 0x35, 0x96, 0xbc, 0x28, 0xac, 0xe3, 0x71, 0x83, 0xf0, 0x58, 0xb1, 0xd0, 0x1a, 0xcb, 0xd7, 0xf2, 0x2c, 0xf6, + 0x6b, 0x26, 0xa1, 0x89, 0xc9, 0x62, 0x50, 0x04, 0xb6, 0x72, 0x44, 0x54, 0xb2, 0x2d, 0x19, 0x24, 0x64, 0x90, 0xae, + 0x22, 0xbd, 0x36, 0x92, 0x81, 0xeb, 0x54, 0x70, 0xb4, 0x74, 0x18, 0x46, 0x0e, 0x1a, 0xee, 0xb4, 0x17, 0x2b, 0x88, + 0x6c, 0xea, 0x9b, 0x44, 0x8a, 0x68, 0x9c, 0x16, 0xa8, 0xc2, 0x99, 0x32, 0xdd, 0x71, 0x60, 0x39, 0xc0, 0xf6, 0x57, + 0x48, 0x6f, 0xad, 0xda, 0xe9, 0xfa, 0x95, 0xc1, 0x77, 0x75, 0x95, 0x20, 0x3d, 0x08, 0x85, 0x17, 0xf6, 0x6c, 0xa0, + 0x78, 0xef, 0xfe, 0x4b, 0x6c, 0x45, 0xfa, 0x67, 0x55, 0x52, 0x59, 0x0a, 0x35, 0xca, 0xad, 0xef, 0x13, 0x33, 0x5d, + 0x8b, 0xaa, 0xe2, 0xc0, 0xe0, 0xea, 0x07, 0x4a, 0x60, 0x57, 0x4b, 0x3e, 0x90, 0x43, 0xc7, 0xae, 0xeb, 0x06, 0x05, + 0x19, 0x2f, 0x1b, 0xeb, 0x4c, 0xc8, 0xad, 0x2d, 0xd3, 0x66, 0x3a, 0xd3, 0xc3, 0x3f, 0x71, 0x50, 0x38, 0x17, 0x97, + 0x29, 0x69, 0x30, 0x4f, 0x94, 0x38, 0x5a, 0x31, 0x40, 0xdb, 0x3d, 0xb4, 0xb4, 0xa3, 0xf3, 0x28, 0xe6, 0x96, 0x1e, + 0x45, 0x58, 0xda, 0xc8, 0x9f, 0xa4, 0xd2, 0x01, 0xeb, 0x42, 0x15, 0x92, 0x8c, 0x70, 0x53, 0x17, 0x2d, 0x46, 0x53, + 0x86, 0x2e, 0x70, 0xa5, 0x4f, 0x98, 0xbc, 0x67, 0x03, 0xd7, 0x2d, 0x06, 0x66, 0xeb, 0x61, 0x2f, 0x9b, 0xdd, 0x6b, + 0xea, 0x3f, 0xec, 0x11, 0xf0, 0xb6, 0x99, 0xaa, 0x2b, 0x1b, 0xef, 0x92, 0x45, 0xa2, 0x87, 0x6d, 0xdd, 0xd8, 0x52, + 0xd7, 0xef, 0x35, 0xcc, 0xeb, 0xca, 0x30, 0xaf, 0x09, 0xd5, 0x86, 0x1c, 0x56, 0x66, 0x0e, 0x33, 0x0d, 0x79, 0xb1, + 0x83, 0x6e, 0x4f, 0x38, 0x85, 0xc0, 0x88, 0xd0, 0xfa, 0xa0, 0xa2, 0x06, 0x42, 0x25, 0x57, 0x52, 0x35, 0x5b, 0x24, + 0x63, 0x09, 0x2c, 0x98, 0xb0, 0x5c, 0xd2, 0xd1, 0x79, 0x9c, 0x24, 0x55, 0xe9, 0x9f, 0xca, 0xe0, 0xc5, 0xb0, 0xb7, + 0xb1, 0x76, 0xb1, 0xa2, 0x85, 0x02, 0xc1, 0xd5, 0x4a, 0xd8, 0x7b, 0xc7, 0xad, 0xf6, 0x5d, 0x78, 0x1c, 0xb9, 0xe9, + 0x8d, 0x80, 0x7a, 0xf4, 0xb0, 0x6a, 0xd2, 0xde, 0x7f, 0x86, 0x2e, 0x35, 0x63, 0x3d, 0x28, 0xce, 0xa8, 0xf8, 0x77, + 0xe9, 0x53, 0xbf, 0x73, 0x79, 0xb7, 0x8a, 0xae, 0xa6, 0x43, 0x45, 0x39, 0x3e, 0x4c, 0x17, 0x4b, 0x5b, 0x39, 0x02, + 0x72, 0x3d, 0x2c, 0x72, 0x01, 0x13, 0x35, 0x58, 0x50, 0x8a, 0x55, 0x6b, 0x61, 0xf7, 0xf2, 0x36, 0x67, 0x0e, 0xb9, + 0xc2, 0x45, 0xff, 0x27, 0xd9, 0x6c, 0x8e, 0x9a, 0x59, 0x83, 0xa8, 0xa1, 0xc1, 0xfb, 0x46, 0x7d, 0xb9, 0xa6, 0xac, + 0xd6, 0x87, 0x4e, 0x64, 0x8d, 0x9e, 0xb4, 0xa1, 0x0c, 0x06, 0xd5, 0x42, 0x17, 0xd5, 0xf5, 0xe6, 0x26, 0x8b, 0x59, + 0x47, 0xe3, 0x3e, 0xc9, 0x6d, 0xad, 0x4d, 0x7a, 0x1a, 0x07, 0xc4, 0x93, 0x24, 0xc1, 0x9b, 0x04, 0x50, 0x56, 0xc8, + 0x59, 0x96, 0x0d, 0xf4, 0x2d, 0xcb, 0x12, 0xf7, 0xef, 0xdb, 0xde, 0x7e, 0xcd, 0xb2, 0xf6, 0xf6, 0xaf, 0x37, 0x91, + 0xab, 0x3a, 0x69, 0x41, 0x1e, 0x0d, 0xa1, 0x68, 0x45, 0xa7, 0x0c, 0x97, 0xb3, 0x6c, 0xcc, 0x02, 0x1b, 0xba, 0xa7, + 0x76, 0xa9, 0xa4, 0x32, 0x1c, 0x8e, 0x94, 0x39, 0xcb, 0x77, 0x75, 0x4f, 0x6a, 0xb0, 0x0f, 0x24, 0xa0, 0xd5, 0x85, + 0xef, 0xc2, 0xd3, 0x24, 0x3b, 0x89, 0x92, 0x43, 0x21, 0xc0, 0x6b, 0x2d, 0x3f, 0x80, 0xc9, 0x48, 0x1a, 0xab, 0x21, + 0xa4, 0xbe, 0x1b, 0x7c, 0x17, 0xdc, 0xde, 0xa3, 0xb2, 0x56, 0xec, 0x8e, 0xdf, 0xf6, 0x3b, 0xb6, 0xf2, 0x88, 0xbd, + 0x34, 0xa7, 0x03, 0x89, 0x53, 0x00, 0x66, 0x0e, 0x41, 0x92, 0x15, 0x5e, 0xc4, 0xc2, 0x97, 0x83, 0x97, 0xca, 0xa4, + 0xce, 0xc0, 0x84, 0x00, 0x23, 0x3f, 0x89, 0x79, 0x0b, 0xe3, 0x91, 0xb6, 0xb7, 0x14, 0x15, 0xe8, 0x57, 0x24, 0xbf, + 0x74, 0xa9, 0xac, 0x41, 0xef, 0x63, 0x78, 0x0c, 0xcd, 0x36, 0x37, 0x97, 0xce, 0xab, 0x88, 0x4f, 0xfd, 0x3c, 0x4a, + 0xc7, 0xd9, 0xcc, 0x71, 0xb7, 0x6d, 0xdb, 0xf5, 0x0b, 0xb2, 0x44, 0xbe, 0x70, 0xcb, 0xcd, 0x63, 0x6f, 0xca, 0x42, + 0x7b, 0x60, 0x6f, 0x7f, 0xf4, 0xde, 0xb2, 0xf0, 0x78, 0x6f, 0x73, 0x39, 0x65, 0x65, 0xff, 0xd8, 0xbb, 0xd0, 0x3e, + 0x77, 0xef, 0x2d, 0x72, 0x19, 0xe8, 0x15, 0xf6, 0x2f, 0x24, 0x18, 0x40, 0x6e, 0xe4, 0x7f, 0x07, 0x2e, 0xf7, 0x9e, + 0x02, 0x22, 0xd2, 0x4f, 0x7b, 0x75, 0x65, 0x67, 0xe4, 0x31, 0xb0, 0x37, 0xb4, 0xb1, 0xba, 0xb5, 0x55, 0x89, 0xf9, + 0xaa, 0xd4, 0x1b, 0xb1, 0xb0, 0x66, 0xa9, 0x7b, 0xef, 0x29, 0xb4, 0x52, 0x3f, 0xc8, 0x23, 0x46, 0x42, 0x73, 0x55, + 0x4f, 0x70, 0x8c, 0x23, 0xbe, 0xfe, 0x58, 0x1f, 0x09, 0x2f, 0x85, 0x1f, 0x83, 0xf6, 0x12, 0x81, 0xf8, 0x06, 0x03, + 0xc7, 0x3b, 0x0c, 0x77, 0xf6, 0x9c, 0x41, 0xe0, 0x6c, 0xb4, 0x5a, 0x57, 0x3f, 0xed, 0x1c, 0xfd, 0x1c, 0xb5, 0x7e, + 0xdb, 0x6f, 0xfd, 0x38, 0x74, 0xaf, 0x9c, 0x9f, 0x76, 0x06, 0x47, 0xf2, 0xed, 0xe8, 0xe7, 0xfe, 0x4f, 0xc5, 0xf0, + 0x73, 0x51, 0xb8, 0xe9, 0xba, 0x3b, 0xa7, 0x60, 0x29, 0x85, 0x3b, 0xad, 0x56, 0x1f, 0x9e, 0x16, 0xf0, 0x84, 0x3f, + 0x2f, 0xe1, 0xc7, 0xd5, 0x91, 0xf5, 0x1f, 0x7e, 0x4a, 0xff, 0xe3, 0x4f, 0xf9, 0x10, 0xc7, 0x3c, 0xfa, 0xf9, 0xa7, + 0xc2, 0xbe, 0xd7, 0x0f, 0x77, 0x86, 0xdb, 0xae, 0xa3, 0x6b, 0x3e, 0x0f, 0xab, 0x47, 0x68, 0x75, 0xf4, 0xb3, 0x7c, + 0xb3, 0xef, 0x1d, 0xef, 0xf5, 0xc3, 0xe1, 0x95, 0x63, 0x5f, 0xdd, 0x73, 0xaf, 0x5c, 0xf7, 0x6a, 0x13, 0xe7, 0x99, + 0xc3, 0xe8, 0xf7, 0xe0, 0xe7, 0x19, 0xfc, 0xb4, 0xe1, 0xe7, 0x29, 0xfc, 0xfc, 0x19, 0xba, 0x09, 0xff, 0xdb, 0x15, + 0xf9, 0x42, 0xae, 0x30, 0x60, 0x11, 0xc1, 0x2e, 0xb8, 0x9b, 0x3b, 0xb1, 0x37, 0x21, 0xa4, 0xc1, 0x39, 0xf4, 0x7d, + 0x1f, 0xdd, 0xa4, 0xce, 0xf2, 0xe3, 0x26, 0x6c, 0x3a, 0x52, 0xce, 0x66, 0xc0, 0x3c, 0xe1, 0x39, 0x28, 0x02, 0x2e, + 0x62, 0xab, 0x05, 0x06, 0x57, 0xbd, 0x45, 0x38, 0x61, 0x0e, 0x28, 0x05, 0x87, 0x0c, 0x1f, 0xba, 0xae, 0xf7, 0x4c, + 0xc6, 0x0c, 0xf1, 0x9c, 0x0b, 0xd2, 0x4a, 0x33, 0xa1, 0xd2, 0xd8, 0xae, 0x37, 0x5f, 0x53, 0x09, 0xc7, 0x3a, 0x3d, + 0x85, 0xba, 0x4d, 0x11, 0x68, 0xfb, 0x8e, 0x45, 0x9f, 0xf0, 0x48, 0x3e, 0x37, 0x82, 0xc0, 0x2b, 0x9a, 0x7c, 0x53, + 0x69, 0x34, 0x74, 0x44, 0x61, 0x8e, 0x7d, 0xc9, 0x60, 0x86, 0x15, 0x15, 0x91, 0x93, 0xd0, 0x14, 0x9a, 0x2d, 0x4c, + 0xfe, 0x36, 0xca, 0xf9, 0x66, 0xa5, 0xd8, 0x86, 0x35, 0x4d, 0xb6, 0xa9, 0xe9, 0xdf, 0x61, 0x0a, 0x54, 0x2d, 0x29, + 0xfe, 0x61, 0x8e, 0x1f, 0xa6, 0xb4, 0xac, 0xd7, 0x0e, 0x07, 0x0b, 0xbd, 0x00, 0xbe, 0x23, 0xfa, 0x39, 0x6f, 0x51, + 0x8c, 0xc1, 0x5f, 0xe9, 0x66, 0xf0, 0xc4, 0x7c, 0xe8, 0xa2, 0x59, 0x96, 0xda, 0xb9, 0x95, 0x22, 0xbb, 0x7f, 0x81, + 0x27, 0x23, 0x2d, 0xbd, 0x83, 0x50, 0x9d, 0x98, 0xc3, 0x9c, 0xb1, 0xef, 0xa2, 0xe4, 0x13, 0xcb, 0x9d, 0x0b, 0xaf, + 0xd3, 0xfd, 0x82, 0x3a, 0x7b, 0xa8, 0x9b, 0xbd, 0xae, 0xc2, 0x68, 0x4a, 0x2d, 0x50, 0x21, 0xc2, 0x56, 0xc7, 0x43, + 0x8e, 0x41, 0x28, 0xc8, 0xbd, 0x2c, 0xec, 0x12, 0x85, 0xdb, 0x7b, 0xc5, 0xd9, 0x69, 0xdf, 0x0e, 0x6c, 0x1b, 0x34, + 0xfe, 0x43, 0x72, 0x5b, 0x09, 0xc5, 0x02, 0x14, 0xb2, 0xbd, 0xb8, 0xc7, 0xb7, 0xb7, 0x2b, 0x87, 0x13, 0x06, 0xd2, + 0xa9, 0x7b, 0xe2, 0x45, 0xde, 0x34, 0x84, 0x01, 0x47, 0xd0, 0x0c, 0xbb, 0xf4, 0x46, 0x7b, 0xb1, 0x9c, 0x06, 0x7d, + 0x21, 0x7e, 0x12, 0x15, 0xfc, 0x05, 0xfa, 0x23, 0xc2, 0x11, 0x2a, 0xfb, 0x3e, 0xbb, 0x60, 0x23, 0xa5, 0x67, 0x00, + 0xa2, 0x22, 0xb7, 0xe7, 0x8e, 0x42, 0xa3, 0x19, 0xcc, 0x1d, 0x86, 0x87, 0x03, 0x1b, 0xce, 0x12, 0x9c, 0xca, 0x30, + 0x3a, 0xea, 0x0c, 0x07, 0x69, 0x08, 0xbc, 0x56, 0xe3, 0x56, 0x16, 0x2d, 0x6a, 0x45, 0xdd, 0xe1, 0xc0, 0x39, 0x05, + 0x25, 0x1d, 0x74, 0x71, 0x07, 0xdf, 0xd0, 0x43, 0x91, 0x87, 0xef, 0xd8, 0xe9, 0xb3, 0x8b, 0xb9, 0x63, 0xef, 0xed, + 0xd8, 0xdb, 0x58, 0xea, 0xd9, 0x40, 0x5e, 0x30, 0x77, 0x78, 0xe9, 0x9a, 0x9d, 0x77, 0x87, 0x08, 0x2a, 0x16, 0xe2, + 0xe4, 0x97, 0x03, 0xbb, 0x2f, 0xa6, 0x6e, 0xc3, 0xa0, 0xa9, 0xdc, 0x7e, 0xdc, 0xd1, 0x43, 0x5a, 0xaa, 0xea, 0xaa, + 0xa0, 0x83, 0xb2, 0x6e, 0xe0, 0x4c, 0xcd, 0x45, 0xb4, 0x70, 0x32, 0x89, 0x05, 0x30, 0x78, 0xb0, 0x19, 0x4c, 0x6a, + 0x74, 0xdb, 0x1d, 0x0e, 0x2e, 0x83, 0x7b, 0xf6, 0x3d, 0xf5, 0x72, 0xc6, 0x02, 0xb0, 0x2e, 0x68, 0xfa, 0x33, 0x94, + 0x22, 0xf0, 0x73, 0xce, 0x60, 0x91, 0x97, 0x54, 0x34, 0x96, 0x45, 0x0b, 0x2c, 0x3a, 0x0c, 0x10, 0x54, 0x2f, 0xd7, + 0xda, 0x9f, 0xd8, 0x93, 0x71, 0x48, 0xb0, 0x6f, 0x6d, 0xc1, 0xd6, 0x6c, 0x77, 0x86, 0x18, 0x6f, 0xc8, 0x79, 0xf1, + 0x5d, 0xcc, 0x41, 0x24, 0xec, 0xf4, 0x6d, 0x77, 0x60, 0x5b, 0xb8, 0xb5, 0xbd, 0x6c, 0x3b, 0x14, 0x18, 0x8e, 0xb7, + 0xdf, 0xb2, 0x60, 0xda, 0x0f, 0xdb, 0x03, 0xa7, 0x10, 0xa2, 0x23, 0xc1, 0xb8, 0xa5, 0xe0, 0xe0, 0x6d, 0x6f, 0x0a, + 0x0c, 0x1d, 0x29, 0x77, 0xd3, 0xde, 0x56, 0x85, 0x50, 0xf4, 0x71, 0x7b, 0xec, 0x06, 0x31, 0xfc, 0x70, 0x5a, 0x48, + 0x34, 0x53, 0xdd, 0x57, 0x4b, 0x66, 0x37, 0x18, 0x2b, 0x8d, 0x3c, 0x09, 0xb3, 0x6d, 0x07, 0x3d, 0xb4, 0xc0, 0x69, + 0xf7, 0x06, 0x00, 0xc3, 0xb6, 0xa3, 0x28, 0x6d, 0x47, 0x91, 0x9a, 0xd2, 0xcf, 0x8f, 0xaa, 0xed, 0x60, 0x83, 0x88, + 0xf9, 0x95, 0xf4, 0x01, 0xb0, 0x82, 0xc4, 0x2b, 0x86, 0x2a, 0xe6, 0xf5, 0xbc, 0x16, 0xdf, 0x5a, 0x2a, 0x56, 0xc4, + 0x3c, 0x83, 0x43, 0xf1, 0x52, 0x9b, 0x61, 0x42, 0xdd, 0x9e, 0x23, 0x32, 0x34, 0xc9, 0x87, 0x6d, 0x20, 0x7a, 0xe5, + 0x60, 0x4f, 0xcd, 0x63, 0x91, 0x84, 0x55, 0x73, 0xef, 0x08, 0x48, 0x7b, 0x18, 0xbe, 0x16, 0x11, 0xc7, 0x9e, 0xf2, + 0xe6, 0xb3, 0x24, 0x7c, 0xde, 0x08, 0x17, 0x47, 0x18, 0x11, 0x3a, 0xf0, 0x47, 0x8b, 0x1c, 0xf8, 0x01, 0x7f, 0x0d, + 0x9a, 0x41, 0x28, 0x9b, 0xa2, 0xa1, 0x87, 0x21, 0x60, 0x8f, 0x16, 0xde, 0x70, 0x9b, 0x1b, 0xd5, 0xa8, 0x51, 0x92, + 0xf2, 0x42, 0x81, 0xe1, 0x1e, 0x97, 0xa6, 0x3d, 0x32, 0x06, 0x19, 0x31, 0x76, 0x30, 0xe6, 0xef, 0x8f, 0xb0, 0x1a, + 0x27, 0x28, 0xdc, 0x92, 0x4e, 0x5b, 0xc5, 0xfe, 0x0e, 0xfc, 0x14, 0x38, 0x38, 0xd6, 0x81, 0x9d, 0xb5, 0xb5, 0x95, + 0xc8, 0x45, 0xed, 0xa5, 0x3d, 0x8a, 0x44, 0xa0, 0x3f, 0xb8, 0xf0, 0x53, 0xa8, 0x46, 0x14, 0x51, 0x11, 0x69, 0xa0, + 0x66, 0x54, 0xad, 0x82, 0xef, 0xc8, 0xf4, 0xc0, 0x73, 0x74, 0x5b, 0x93, 0xa2, 0xa8, 0x1b, 0x0b, 0x5f, 0xbe, 0xeb, + 0x52, 0x68, 0x0b, 0x03, 0x90, 0x82, 0xd0, 0x04, 0xc1, 0xb8, 0xe4, 0x94, 0xac, 0xe8, 0xef, 0xa3, 0xe1, 0x2b, 0x9f, + 0x1e, 0x65, 0xdb, 0xdb, 0x43, 0x11, 0xb7, 0x20, 0xc2, 0xe1, 0x86, 0x77, 0x35, 0xae, 0x00, 0xa8, 0x4f, 0xe7, 0xc4, + 0x75, 0xc7, 0xb4, 0x22, 0x4d, 0x97, 0x7c, 0x9f, 0x1c, 0x66, 0x00, 0x0c, 0xee, 0x38, 0x47, 0xfe, 0xe0, 0x2f, 0x43, + 0x30, 0x8f, 0xfd, 0xcf, 0xdd, 0x1d, 0xc5, 0x68, 0x7a, 0x32, 0xa6, 0xb8, 0xa4, 0x18, 0x6b, 0xc7, 0x23, 0xdf, 0x68, + 0x90, 0x7b, 0x29, 0xac, 0x00, 0xa4, 0x39, 0xf0, 0x84, 0x8a, 0x82, 0x90, 0xa2, 0x02, 0xdb, 0xc7, 0xc3, 0xcf, 0xf1, + 0x64, 0xbf, 0x03, 0x0d, 0x6f, 0xa0, 0xdf, 0x9e, 0xc2, 0xdb, 0x5f, 0xf4, 0xdb, 0x97, 0x2c, 0xf8, 0xa5, 0x94, 0xae, + 0xfb, 0xda, 0x14, 0x0f, 0xd5, 0x14, 0xa5, 0xd8, 0x22, 0x03, 0x87, 0xcc, 0x5d, 0xf5, 0xd9, 0x70, 0xb7, 0x04, 0x64, + 0x28, 0xd6, 0x05, 0x3a, 0x5a, 0x74, 0x8a, 0xc8, 0x75, 0x4d, 0x54, 0x18, 0xb9, 0x04, 0xe6, 0x82, 0x2b, 0xba, 0x25, + 0xe2, 0xec, 0xb7, 0xdd, 0x65, 0xad, 0x2d, 0xe9, 0x77, 0x6c, 0x36, 0xe7, 0x97, 0x07, 0x24, 0xe8, 0x03, 0x99, 0x36, + 0x20, 0x62, 0xe7, 0xed, 0x5e, 0xbc, 0xc7, 0x7b, 0x31, 0x70, 0xf5, 0x42, 0x91, 0x18, 0x9e, 0x55, 0xef, 0x2d, 0x7a, + 0x29, 0x4d, 0x62, 0xf2, 0x6a, 0xcb, 0xeb, 0xca, 0xe5, 0x6d, 0x6f, 0xc3, 0x02, 0x7b, 0x46, 0x57, 0x2e, 0xba, 0x96, + 0xa5, 0xc0, 0x09, 0x40, 0xf4, 0xb8, 0x4e, 0x72, 0x44, 0x71, 0x98, 0xcd, 0x86, 0x8c, 0x83, 0xb9, 0x6b, 0x47, 0xc5, + 0x31, 0xb1, 0xbb, 0x4c, 0xd8, 0x81, 0x95, 0x11, 0x95, 0xb7, 0x3a, 0xc2, 0x3b, 0x2c, 0xfa, 0x6b, 0xff, 0xf6, 0x47, + 0x8f, 0x6d, 0x77, 0x5c, 0x90, 0x20, 0xb5, 0xb1, 0x1e, 0x55, 0x63, 0x41, 0x7d, 0xf8, 0x51, 0x63, 0xa9, 0xcc, 0xb7, + 0xb7, 0xcb, 0x7a, 0xa8, 0x56, 0x9d, 0xe0, 0x5a, 0x34, 0xe5, 0xa2, 0x99, 0x0d, 0xc2, 0x01, 0x89, 0x09, 0x14, 0x68, + 0x6e, 0x65, 0xc5, 0x00, 0x43, 0xca, 0x72, 0xe4, 0x4f, 0x21, 0xf3, 0xe2, 0xb2, 0xd4, 0xa9, 0x2f, 0xd2, 0x1f, 0x19, + 0x62, 0xd4, 0x93, 0x94, 0x15, 0x10, 0xb0, 0x5e, 0xea, 0x25, 0xb4, 0x45, 0xb0, 0xf2, 0x67, 0x2a, 0x87, 0x46, 0x68, + 0x20, 0x51, 0x68, 0xa8, 0x25, 0x4a, 0xf9, 0xcc, 0xc3, 0x18, 0xa4, 0xfd, 0x93, 0x9a, 0xef, 0x2b, 0x57, 0x4a, 0x47, + 0x7e, 0x54, 0x0c, 0x03, 0xaa, 0x5f, 0x48, 0x0e, 0x36, 0x0d, 0xdf, 0x03, 0x19, 0x55, 0x86, 0x27, 0x31, 0xc2, 0xa7, + 0x71, 0xce, 0xc8, 0x52, 0xd8, 0x94, 0x30, 0x4b, 0xd5, 0x36, 0x52, 0xed, 0x22, 0xd3, 0x09, 0xe5, 0xc2, 0xfc, 0x53, + 0x23, 0x76, 0x91, 0x85, 0x2b, 0xad, 0x41, 0xfd, 0x78, 0x63, 0x02, 0x94, 0x5d, 0x5d, 0x65, 0xc2, 0xc6, 0x8d, 0x48, + 0xdf, 0xd0, 0x15, 0xd3, 0x81, 0x5a, 0x54, 0xe0, 0x44, 0xa4, 0xf1, 0x50, 0x0c, 0x85, 0x46, 0x38, 0xa4, 0x28, 0x72, + 0xe1, 0x1a, 0x87, 0xbe, 0x18, 0x68, 0xdb, 0x28, 0x0d, 0x9d, 0x04, 0x98, 0x80, 0x58, 0xbb, 0xa1, 0x4d, 0xa5, 0x83, + 0x34, 0x48, 0xa8, 0x14, 0xed, 0x1c, 0x58, 0x7f, 0x18, 0x49, 0x0c, 0x80, 0xfe, 0x50, 0x8d, 0x14, 0x51, 0x96, 0x05, + 0x6e, 0x00, 0xcd, 0x75, 0x80, 0x3b, 0xe1, 0x0b, 0x05, 0x15, 0xa6, 0xa7, 0x59, 0x79, 0x29, 0x84, 0xc8, 0xab, 0x35, + 0x29, 0x6b, 0xc4, 0x93, 0xcf, 0xd0, 0xe0, 0x53, 0xd6, 0xf5, 0x6b, 0xb9, 0x0e, 0x5d, 0xf0, 0x14, 0xb6, 0x55, 0x3d, + 0xbf, 0x0a, 0x39, 0x19, 0xd7, 0x20, 0x2b, 0x24, 0xd3, 0x5f, 0x31, 0x92, 0xf7, 0x5f, 0xf9, 0x55, 0x2d, 0x35, 0x86, + 0xb2, 0xf7, 0xeb, 0x9a, 0x61, 0x79, 0x39, 0xaf, 0xdc, 0x14, 0x04, 0xdc, 0x92, 0x25, 0xc1, 0x52, 0x4a, 0x08, 0xd0, + 0xb0, 0x3d, 0x92, 0x4a, 0x41, 0x51, 0x6a, 0xf7, 0xce, 0x53, 0xd0, 0x02, 0x8c, 0xa0, 0x96, 0x4a, 0xa6, 0x91, 0xc8, + 0x97, 0x42, 0x14, 0x88, 0xf2, 0x60, 0x04, 0x76, 0x6a, 0x33, 0xd2, 0x75, 0xe1, 0xfa, 0xf1, 0x0c, 0x53, 0x7b, 0x08, + 0xf4, 0xd8, 0xdb, 0x00, 0x55, 0xa2, 0x2e, 0xc3, 0x72, 0xa2, 0xd0, 0xac, 0x26, 0x59, 0x40, 0x8d, 0x69, 0x83, 0x94, + 0x6c, 0x83, 0x2e, 0x57, 0x80, 0x7e, 0x24, 0x8e, 0x67, 0xb5, 0x03, 0x42, 0xd6, 0xa0, 0x82, 0x21, 0x4f, 0xa9, 0x90, + 0xc2, 0xbc, 0xd7, 0xa5, 0x22, 0x3c, 0x9f, 0x03, 0x2e, 0xb5, 0xe0, 0xcc, 0xcb, 0x68, 0xe0, 0x83, 0xf8, 0x24, 0xc1, + 0xc4, 0x17, 0x5c, 0x15, 0xe8, 0xc1, 0x41, 0xa7, 0xd9, 0x14, 0x28, 0x15, 0x37, 0x29, 0x83, 0x6d, 0x45, 0xae, 0x0d, + 0x3f, 0x24, 0xcb, 0xd6, 0x5d, 0x1e, 0xea, 0x2e, 0x44, 0x02, 0xd8, 0xe9, 0x25, 0x7a, 0xbe, 0x65, 0xbd, 0x74, 0x18, + 0x9c, 0x69, 0x89, 0x83, 0xc0, 0x6f, 0x6f, 0x27, 0xc3, 0x32, 0x25, 0xb2, 0x6b, 0x92, 0xba, 0x80, 0x1c, 0x86, 0x6a, + 0xae, 0x1d, 0x98, 0xa5, 0xd2, 0xc7, 0xf3, 0x72, 0x86, 0xdb, 0xa5, 0x34, 0xe4, 0x66, 0xbc, 0x9a, 0xe6, 0x73, 0x2b, + 0xc9, 0xa6, 0xfd, 0xad, 0xf8, 0xa2, 0xe0, 0x1f, 0x38, 0xb1, 0xd4, 0xea, 0x29, 0xb5, 0xc2, 0xa3, 0xcc, 0x2d, 0x59, + 0xa7, 0xb8, 0x56, 0xd7, 0x0d, 0x54, 0x23, 0x8c, 0xa6, 0x61, 0x23, 0x60, 0x62, 0x82, 0x8a, 0x5f, 0x37, 0x89, 0x98, + 0xce, 0x96, 0xe0, 0x3a, 0x42, 0xef, 0xa1, 0x9c, 0xe0, 0xae, 0xa6, 0xd9, 0xe7, 0xe1, 0xfc, 0x7a, 0xe2, 0xde, 0x37, + 0x88, 0xfb, 0xcb, 0x90, 0x1b, 0x84, 0x1e, 0xcb, 0x84, 0x1f, 0xe9, 0xfb, 0x28, 0x54, 0xd5, 0x93, 0xd3, 0xb0, 0x62, + 0x59, 0xe2, 0xc9, 0x08, 0x75, 0x18, 0x51, 0xd1, 0x1a, 0x23, 0xbb, 0xba, 0xca, 0xcd, 0xb3, 0x40, 0x4e, 0x53, 0x8f, + 0xd7, 0xfd, 0xb4, 0x15, 0x39, 0x1b, 0x9e, 0xc8, 0xfd, 0x57, 0x35, 0x4f, 0x64, 0x45, 0xe7, 0x38, 0xd2, 0x35, 0x81, + 0xdc, 0x27, 0xa7, 0xab, 0x87, 0x54, 0xc8, 0x16, 0xbd, 0x6c, 0xe3, 0x8c, 0xea, 0x80, 0xa4, 0x9e, 0x51, 0x81, 0x55, + 0x8d, 0xbd, 0xb5, 0xd5, 0x11, 0xe9, 0x96, 0x4a, 0xb0, 0xc1, 0xd6, 0xc2, 0x68, 0xc6, 0x28, 0xe8, 0x94, 0x14, 0x19, + 0xa8, 0x51, 0x7e, 0x0d, 0x63, 0xd8, 0xa7, 0x06, 0x20, 0x38, 0xd7, 0x57, 0x7f, 0x59, 0x4a, 0xb2, 0x10, 0x90, 0xb8, + 0x4b, 0x06, 0x6c, 0x4d, 0x10, 0x33, 0xd2, 0xc9, 0x7b, 0xa0, 0xbc, 0x01, 0x43, 0x1b, 0x01, 0xec, 0x02, 0x71, 0xe8, + 0x41, 0xc5, 0xb6, 0x09, 0x29, 0x3a, 0x36, 0xf0, 0x1c, 0x80, 0x9d, 0x57, 0xae, 0xd1, 0x77, 0x55, 0x0a, 0x18, 0x92, + 0x81, 0x1b, 0xb0, 0xca, 0x2d, 0xb7, 0xff, 0x1c, 0xcc, 0x06, 0x78, 0x7d, 0x26, 0x9b, 0x6f, 0x62, 0x9e, 0x60, 0x15, + 0xbb, 0xf0, 0x2b, 0xcd, 0x5a, 0xc4, 0x9d, 0x0e, 0x1b, 0xf5, 0x0a, 0x13, 0xa2, 0xf6, 0x00, 0x6b, 0xdf, 0xa3, 0x87, + 0x45, 0xbc, 0xbf, 0xc2, 0x77, 0x3d, 0x6e, 0xb9, 0xaf, 0x97, 0x45, 0x2b, 0x5d, 0x45, 0x8d, 0x81, 0xc9, 0xba, 0x9d, + 0x8c, 0x6b, 0x2f, 0x0f, 0x84, 0x2f, 0xb8, 0x5a, 0x23, 0xab, 0x5c, 0x8a, 0x8d, 0x45, 0xd2, 0xd3, 0x3e, 0x05, 0xd8, + 0x37, 0x9b, 0xbd, 0x00, 0x33, 0xef, 0x2b, 0x54, 0x49, 0x48, 0x69, 0x76, 0x83, 0x25, 0x09, 0x6d, 0x45, 0x46, 0x9d, + 0x0f, 0x1c, 0x6d, 0x73, 0x2b, 0x8e, 0x60, 0x38, 0x27, 0x61, 0x3a, 0x56, 0x1e, 0x36, 0x19, 0xb8, 0xf2, 0x8e, 0x98, + 0xb6, 0x09, 0xf0, 0x6f, 0x06, 0x7c, 0x7b, 0x25, 0xb9, 0xb6, 0xd0, 0x30, 0x3c, 0x41, 0x84, 0x55, 0x9e, 0x08, 0x34, + 0x14, 0x60, 0x8d, 0x6b, 0x2d, 0x0f, 0x50, 0xe1, 0x6b, 0x67, 0x13, 0x00, 0x12, 0x59, 0x41, 0xce, 0x8a, 0xa3, 0x1b, + 0x56, 0xb9, 0xde, 0x4f, 0x8d, 0x82, 0xc4, 0xc5, 0x83, 0xe9, 0xea, 0x96, 0xfe, 0x0c, 0x35, 0x67, 0x52, 0xc4, 0xb4, + 0x13, 0x04, 0xfd, 0xa3, 0xcc, 0xc9, 0x69, 0x3a, 0xa1, 0x7d, 0xce, 0x9d, 0xda, 0xd4, 0x3d, 0x46, 0xdd, 0x3c, 0x89, + 0x2d, 0x5e, 0xc7, 0x4d, 0x29, 0x17, 0x26, 0x39, 0xe6, 0xa6, 0x48, 0xc5, 0x66, 0x8a, 0xdd, 0xb9, 0xf5, 0x83, 0x16, + 0xd2, 0x41, 0xdb, 0x14, 0x39, 0xd8, 0xac, 0xe2, 0xf7, 0x04, 0xc6, 0x73, 0x81, 0xf8, 0xf2, 0x15, 0x25, 0xe9, 0x30, + 0xc7, 0x5c, 0x60, 0xf5, 0x62, 0x0a, 0xf2, 0x77, 0x8e, 0x4e, 0xb3, 0x37, 0xf0, 0x41, 0xe2, 0x0d, 0x38, 0x66, 0x8d, + 0x7d, 0xe7, 0x52, 0x51, 0x47, 0x08, 0x54, 0x46, 0xb5, 0x4c, 0xc7, 0x89, 0x95, 0xfb, 0x46, 0xd0, 0xd5, 0x5b, 0x1d, + 0xce, 0x37, 0x9e, 0x1b, 0xbb, 0x11, 0xc4, 0x60, 0x2d, 0x14, 0x43, 0x4f, 0xb2, 0xf0, 0x1c, 0xb6, 0x67, 0x7b, 0xbb, + 0x57, 0xec, 0xf1, 0xca, 0x45, 0x52, 0xc1, 0x18, 0x63, 0x46, 0x31, 0x9e, 0x89, 0x9a, 0x58, 0x44, 0x64, 0xcb, 0xd6, + 0x61, 0x81, 0x01, 0x00, 0x68, 0x69, 0x72, 0xaf, 0x9a, 0x08, 0x95, 0xf1, 0x5c, 0x5a, 0x4f, 0x15, 0x44, 0x55, 0x8d, + 0xdf, 0xae, 0xcf, 0x40, 0x21, 0xb8, 0x37, 0x3a, 0x1e, 0x06, 0x21, 0x60, 0x17, 0x05, 0x2f, 0xd0, 0x07, 0xb4, 0x57, + 0x25, 0x42, 0x31, 0x73, 0xb2, 0x1e, 0x33, 0x8c, 0x54, 0xd0, 0x85, 0x4a, 0xd8, 0x2a, 0xcd, 0xf0, 0xab, 0x83, 0xd0, + 0x8c, 0x32, 0xee, 0xbf, 0xaa, 0xd6, 0x0c, 0xf2, 0x83, 0x79, 0xab, 0x84, 0xfa, 0x76, 0x25, 0x22, 0x53, 0x81, 0x89, + 0x87, 0x59, 0x4a, 0xbf, 0x5f, 0xd6, 0x49, 0x3f, 0x2f, 0x97, 0xe7, 0x9c, 0x24, 0x5f, 0xe7, 0x0e, 0x92, 0x4f, 0xba, + 0xfb, 0x95, 0xf0, 0x43, 0x0d, 0xa3, 0x26, 0xfc, 0xea, 0x5b, 0x1a, 0xe6, 0x9e, 0x72, 0x6f, 0xf5, 0xbb, 0xc8, 0x74, + 0x51, 0x9e, 0x83, 0x22, 0xa4, 0x1f, 0xc1, 0x34, 0x34, 0x68, 0x50, 0x24, 0x8b, 0xc5, 0xda, 0x04, 0x71, 0x7d, 0xcc, + 0xa9, 0x76, 0x28, 0x63, 0x8c, 0x68, 0x5a, 0x52, 0x90, 0x24, 0x70, 0x50, 0x7e, 0x03, 0x03, 0x62, 0x12, 0x12, 0xd2, + 0x20, 0x74, 0xd6, 0x66, 0x22, 0x2a, 0x73, 0xf1, 0x76, 0xe5, 0xb2, 0x26, 0x50, 0x84, 0x9e, 0x60, 0xa6, 0x52, 0x2a, + 0x08, 0xa4, 0xca, 0xb7, 0xd1, 0xa9, 0x39, 0x43, 0x73, 0xd7, 0x14, 0x40, 0x5e, 0xdb, 0xf5, 0xa0, 0xc9, 0x7b, 0xf2, + 0xa1, 0xaf, 0x13, 0x23, 0x5e, 0x66, 0xd0, 0x35, 0x1c, 0xfe, 0x1a, 0x2b, 0x29, 0x42, 0x26, 0x7c, 0xaf, 0x60, 0x13, + 0x21, 0x99, 0x82, 0x9e, 0x09, 0xf8, 0x43, 0xbd, 0xb2, 0x97, 0xee, 0xe5, 0x95, 0x49, 0x8b, 0xca, 0x56, 0xa2, 0x66, + 0x2d, 0x8e, 0xe2, 0xed, 0x14, 0xce, 0xb3, 0x47, 0x09, 0x04, 0x24, 0xa9, 0x9c, 0xa4, 0x9a, 0xf7, 0x28, 0x1d, 0x02, + 0x48, 0x70, 0xfa, 0x09, 0x2c, 0xb4, 0x9b, 0x12, 0x13, 0x2c, 0xaa, 0xc6, 0x6e, 0x73, 0x90, 0x9a, 0x73, 0x92, 0x7c, + 0x73, 0x94, 0xda, 0xdb, 0x4a, 0x7b, 0xc6, 0xec, 0x00, 0xdb, 0x76, 0xb7, 0xf3, 0xa3, 0x74, 0xbb, 0x33, 0x34, 0x18, + 0x17, 0x86, 0xff, 0x93, 0x12, 0xd3, 0x40, 0x0a, 0x29, 0x1b, 0x3f, 0xa1, 0x0c, 0xc3, 0xff, 0x96, 0x24, 0x80, 0x07, + 0xb5, 0xdd, 0x58, 0x31, 0xee, 0x15, 0x45, 0xc9, 0x6d, 0x55, 0xed, 0x6a, 0x09, 0x1a, 0xba, 0x91, 0xf4, 0x89, 0x62, + 0x9e, 0x13, 0x00, 0xa3, 0xc8, 0xfc, 0x1d, 0xd3, 0x49, 0xde, 0xbf, 0xac, 0x4d, 0xed, 0xf6, 0x7d, 0x3f, 0xca, 0x4f, + 0x29, 0xa4, 0xa2, 0xb2, 0x39, 0x89, 0xf8, 0x77, 0x05, 0x98, 0xe6, 0xc4, 0x47, 0x7a, 0xae, 0x61, 0x28, 0xc0, 0x57, + 0x3a, 0x94, 0x9a, 0xed, 0xe9, 0x1f, 0x9d, 0xed, 0xbe, 0x44, 0x8a, 0x20, 0x81, 0x06, 0x5e, 0xae, 0x59, 0x2f, 0xac, + 0x32, 0xb8, 0x23, 0xfe, 0x14, 0x7c, 0x5f, 0x5e, 0x07, 0x9f, 0x71, 0xfe, 0x05, 0xa0, 0x55, 0x81, 0x01, 0xe5, 0x83, + 0xa6, 0x62, 0x25, 0xd8, 0x25, 0x0a, 0xcc, 0xca, 0xcf, 0x1f, 0xd7, 0x69, 0xdd, 0xd4, 0x2c, 0xd1, 0x29, 0x3f, 0x77, + 0x0d, 0x33, 0xbe, 0xd7, 0xc8, 0x1f, 0xdf, 0x7f, 0x0e, 0xb2, 0x9d, 0x50, 0xbb, 0xb5, 0x55, 0x6c, 0x90, 0x86, 0x86, + 0xf7, 0xc2, 0xe6, 0xd0, 0x16, 0xf1, 0x52, 0xa8, 0x67, 0x2c, 0xc6, 0xdb, 0x22, 0x54, 0x86, 0x0f, 0x58, 0x30, 0x87, + 0x15, 0x82, 0xc5, 0x4e, 0x65, 0xf2, 0x19, 0x0e, 0x9a, 0x22, 0xd7, 0x42, 0x28, 0x7c, 0x39, 0x88, 0x4a, 0x49, 0x8b, + 0x75, 0xb4, 0x3d, 0x3b, 0x83, 0xe7, 0x97, 0x71, 0x01, 0xd8, 0x81, 0xe5, 0x57, 0x58, 0x16, 0x07, 0xc8, 0xc5, 0x43, + 0x59, 0xeb, 0x15, 0x8d, 0xc7, 0x37, 0x76, 0x61, 0x75, 0x01, 0x3e, 0x8d, 0xd2, 0x71, 0x22, 0x26, 0x31, 0x93, 0x2a, + 0xd7, 0xe4, 0xda, 0xe8, 0x5e, 0x5a, 0xa3, 0x79, 0x2e, 0x38, 0x78, 0x85, 0xe0, 0x06, 0xd3, 0x57, 0xf2, 0x72, 0xbd, + 0x82, 0x82, 0xa1, 0xf6, 0xe6, 0x26, 0x98, 0x2b, 0xf1, 0x98, 0xc1, 0x35, 0xfd, 0x3a, 0x9c, 0x8a, 0x6e, 0x5e, 0xae, + 0x18, 0xfc, 0x3a, 0x67, 0xac, 0x21, 0x00, 0x88, 0x4e, 0x1e, 0x5e, 0x6f, 0x26, 0xbd, 0x52, 0xd2, 0x41, 0x49, 0x84, + 0xf8, 0xae, 0xcc, 0xd7, 0x5d, 0x2a, 0xba, 0x72, 0xd5, 0xbd, 0xaf, 0x19, 0x33, 0x2e, 0x18, 0x3d, 0xe7, 0xb3, 0xa4, + 0x71, 0xed, 0x86, 0xee, 0xea, 0xfc, 0xe8, 0xfd, 0x20, 0xf3, 0x16, 0x66, 0x40, 0x26, 0x20, 0x0a, 0x9e, 0x7b, 0xaf, + 0x8d, 0x88, 0xf2, 0xb7, 0x66, 0x88, 0x57, 0x0e, 0xb3, 0x2e, 0x92, 0xfc, 0xed, 0xe0, 0xdb, 0xe0, 0xfa, 0x96, 0x46, + 0x04, 0xb9, 0xab, 0x22, 0xc8, 0x84, 0xb9, 0x99, 0x3e, 0x70, 0xfb, 0x77, 0x65, 0x08, 0x22, 0x2a, 0xa6, 0x43, 0xe5, + 0xb8, 0x7f, 0xb4, 0x41, 0xa5, 0x42, 0xe2, 0x53, 0x95, 0xbb, 0x72, 0x6d, 0x6a, 0xa8, 0xc7, 0x75, 0x32, 0x0b, 0x4d, + 0xb3, 0x26, 0x97, 0xb2, 0x69, 0x31, 0x32, 0x4d, 0x4e, 0xb5, 0xf9, 0xdd, 0x6b, 0x83, 0x74, 0x0c, 0xd5, 0xc5, 0x5a, + 0x2d, 0x98, 0xdf, 0x95, 0x17, 0xde, 0xf5, 0x62, 0x23, 0x95, 0xa1, 0xa6, 0x3d, 0x8a, 0x3e, 0x8e, 0xdb, 0xcc, 0xe5, + 0x51, 0xfa, 0x67, 0x0d, 0x00, 0xd3, 0x10, 0x16, 0xdd, 0x4d, 0xcb, 0xd8, 0x13, 0xcb, 0xd3, 0x13, 0x19, 0x28, 0x7a, + 0xae, 0xf3, 0x55, 0xab, 0xc4, 0xd2, 0x35, 0x08, 0x76, 0x6f, 0xc8, 0x58, 0x95, 0xb8, 0x5b, 0xad, 0x5f, 0xcd, 0xf3, + 0x79, 0xca, 0x57, 0xf2, 0x7c, 0x6a, 0x1a, 0xdd, 0x46, 0xdb, 0xbd, 0x39, 0x35, 0x54, 0xcc, 0xb5, 0xbe, 0xc9, 0x1f, + 0x98, 0xae, 0x83, 0xae, 0x16, 0x81, 0x66, 0x75, 0xaa, 0x9e, 0x95, 0xe5, 0xac, 0x9e, 0xc9, 0x31, 0x13, 0xb6, 0xa9, + 0x34, 0x87, 0xe8, 0x86, 0xa9, 0x9a, 0xe9, 0xc7, 0xc6, 0xb1, 0x90, 0x6d, 0x9e, 0x5f, 0x8e, 0x73, 0xc0, 0xb4, 0x3c, + 0x5f, 0x26, 0x0c, 0x3f, 0x5e, 0x5d, 0xfd, 0x28, 0xf8, 0x54, 0xd5, 0xd1, 0x5b, 0xbe, 0xd4, 0x3d, 0x83, 0x59, 0xa9, + 0x8c, 0x88, 0x13, 0xb6, 0x7e, 0xf0, 0xe6, 0xe9, 0x15, 0xb0, 0x9c, 0xc0, 0xea, 0x4e, 0x98, 0xd3, 0x18, 0xaa, 0x3a, + 0xc0, 0x3f, 0xac, 0x1f, 0x6c, 0xdd, 0x19, 0xfe, 0x61, 0xf0, 0x43, 0x70, 0x63, 0x63, 0xe3, 0x18, 0xef, 0xd6, 0x12, + 0x41, 0x5e, 0x61, 0x40, 0x1f, 0xaf, 0x3e, 0x0a, 0x5c, 0xae, 0x63, 0xdb, 0x03, 0x87, 0xdc, 0xd6, 0xc0, 0xdf, 0x24, + 0x4f, 0x1a, 0x2d, 0x0a, 0x9e, 0xcd, 0xe4, 0x0c, 0x85, 0xbc, 0xe6, 0xe3, 0xa0, 0xee, 0x08, 0x7f, 0x03, 0xa7, 0x16, + 0x5e, 0x5e, 0x7e, 0x82, 0x3e, 0x60, 0xe9, 0x4a, 0x6e, 0x2a, 0xfc, 0x94, 0xf2, 0x88, 0xae, 0xd6, 0x79, 0x30, 0x52, + 0x5c, 0x4c, 0x51, 0xe8, 0xb8, 0xcb, 0x1b, 0x67, 0x23, 0xa3, 0xbf, 0xc4, 0xab, 0x8b, 0x74, 0xf9, 0x48, 0x64, 0xab, + 0x96, 0xde, 0x2f, 0x3a, 0xba, 0x6d, 0xcf, 0x18, 0x9f, 0x66, 0x63, 0x0a, 0xcc, 0xf8, 0x38, 0x11, 0x5e, 0x9f, 0x18, + 0xeb, 0xbb, 0x45, 0xa0, 0xba, 0x39, 0x36, 0xd9, 0xe1, 0x78, 0xbd, 0xd9, 0xac, 0x71, 0x07, 0x6f, 0x9c, 0x27, 0xce, + 0xb2, 0x44, 0x8f, 0xca, 0x52, 0xc3, 0x03, 0x52, 0x21, 0x6e, 0xde, 0x33, 0x81, 0x71, 0xd9, 0x25, 0x71, 0x6d, 0x37, + 0x10, 0x6b, 0xb1, 0x27, 0x31, 0x4b, 0xc6, 0xb6, 0x07, 0xe5, 0x81, 0xbe, 0x18, 0x4d, 0xb7, 0x80, 0x69, 0x7b, 0xed, + 0xec, 0x3c, 0xb5, 0xbd, 0x6a, 0xaa, 0x00, 0x66, 0xc9, 0xf2, 0xf8, 0x14, 0x49, 0xf7, 0x1b, 0xe8, 0x22, 0x06, 0x8c, + 0x8d, 0x2b, 0x73, 0xee, 0x72, 0xdd, 0x8e, 0xf8, 0x46, 0x13, 0xa9, 0x52, 0x1f, 0x51, 0xdf, 0x61, 0x58, 0xab, 0xab, + 0x0c, 0x24, 0x30, 0x8f, 0xbc, 0x3b, 0xae, 0xa5, 0xa7, 0x63, 0x16, 0x93, 0x2a, 0x7d, 0x4b, 0x5d, 0x8b, 0x6b, 0xba, + 0xbd, 0xe2, 0x01, 0xe8, 0x1f, 0xe8, 0xb7, 0x88, 0x85, 0xbf, 0x9d, 0xd7, 0x52, 0x58, 0x1b, 0x73, 0xe4, 0xe8, 0x6b, + 0x0f, 0x7e, 0x61, 0xd5, 0x9e, 0x81, 0x1a, 0x66, 0xc4, 0x48, 0x7e, 0x33, 0xee, 0x55, 0x4d, 0x1c, 0xb9, 0x0b, 0xc0, + 0xfa, 0x96, 0x74, 0x49, 0x0e, 0xaf, 0x64, 0xb9, 0x2a, 0x86, 0xfc, 0x1b, 0xec, 0xb3, 0xde, 0x9c, 0x80, 0x99, 0x38, + 0xe5, 0x25, 0x26, 0xa6, 0x88, 0xcb, 0xcd, 0xd2, 0xe7, 0x69, 0xda, 0x2c, 0xda, 0xc0, 0x29, 0x8c, 0x04, 0x8e, 0xd8, + 0x37, 0xb6, 0xa1, 0x99, 0xb0, 0x11, 0x13, 0x6a, 0x54, 0x4a, 0x09, 0x1f, 0xc8, 0xad, 0x96, 0xf4, 0x65, 0x6e, 0xaf, + 0xbe, 0xdc, 0x26, 0x28, 0xa0, 0xa8, 0x81, 0xe5, 0xd0, 0x38, 0x6e, 0x19, 0xc8, 0x85, 0xc5, 0xb0, 0x30, 0x6a, 0x55, + 0xae, 0x26, 0xa3, 0x3a, 0x99, 0xaf, 0x16, 0x17, 0x2a, 0xf4, 0xe0, 0x91, 0x40, 0xce, 0x5f, 0x60, 0xea, 0x60, 0x56, + 0x6a, 0x33, 0x2d, 0x36, 0x51, 0xde, 0x33, 0x1d, 0x92, 0xeb, 0xaf, 0xe1, 0xa1, 0xf2, 0x8b, 0x57, 0xe6, 0x14, 0xf3, + 0x45, 0x1e, 0x4b, 0x5b, 0x63, 0x6e, 0xfd, 0xaf, 0xf2, 0x3e, 0xad, 0x04, 0xec, 0x37, 0x60, 0x53, 0xc6, 0x5a, 0x62, + 0xe3, 0x82, 0xa4, 0xbc, 0x96, 0xa7, 0xf4, 0xbe, 0x86, 0xf0, 0x5d, 0x51, 0xe9, 0x2a, 0x91, 0x75, 0x8d, 0x56, 0xf7, + 0xeb, 0x82, 0xe5, 0x97, 0x07, 0x0c, 0x73, 0x93, 0x51, 0x21, 0x5b, 0x51, 0xb3, 0x29, 0xbf, 0xda, 0xbb, 0xf1, 0x2b, + 0x0f, 0x25, 0x05, 0xd5, 0x2a, 0xd9, 0xbc, 0x72, 0xc3, 0x31, 0x6e, 0xdc, 0x70, 0x8c, 0x7b, 0x14, 0x57, 0xae, 0x50, + 0xad, 0xf3, 0xdf, 0x57, 0xdd, 0x4f, 0x74, 0xd6, 0x86, 0xfa, 0xd4, 0x0d, 0xd7, 0xa6, 0xa7, 0xdf, 0xb0, 0x54, 0x23, + 0x4b, 0xe8, 0xa6, 0xa5, 0x62, 0x32, 0x12, 0x25, 0xa6, 0xab, 0x94, 0x47, 0x7d, 0x8d, 0xb8, 0x00, 0x76, 0x43, 0xf9, + 0x8b, 0x7f, 0x0d, 0xcf, 0x8f, 0x03, 0x54, 0xa2, 0x96, 0x93, 0x2c, 0xe5, 0xad, 0x49, 0x34, 0x8b, 0x93, 0xcb, 0x60, + 0x11, 0xb7, 0x66, 0x59, 0x9a, 0x15, 0x73, 0xa0, 0x4a, 0xaf, 0xb8, 0x04, 0x1d, 0x7e, 0xd6, 0x5a, 0xc4, 0xde, 0x73, + 0x96, 0x9c, 0x31, 0x1e, 0x8f, 0x22, 0xcf, 0xde, 0xcf, 0x81, 0x3d, 0x58, 0xaf, 0xa3, 0x3c, 0xcf, 0xce, 0x6d, 0xef, + 0x5d, 0x76, 0x02, 0x44, 0xeb, 0xbd, 0xb9, 0xb8, 0x3c, 0x65, 0xa9, 0xf7, 0xfe, 0x64, 0x91, 0xf2, 0x85, 0x57, 0x44, + 0x69, 0xd1, 0x2a, 0x58, 0x1e, 0x4f, 0x40, 0x4c, 0x24, 0x59, 0xde, 0xc2, 0xfc, 0xe7, 0x19, 0x0b, 0x92, 0xf8, 0x74, + 0xca, 0xad, 0x71, 0x94, 0x7f, 0xea, 0xb5, 0x5a, 0xf3, 0x3c, 0x9e, 0x45, 0xf9, 0x65, 0x8b, 0x5a, 0x04, 0x9f, 0xb5, + 0x77, 0xa3, 0x2f, 0x26, 0xf7, 0x7b, 0x3c, 0x87, 0xbe, 0x31, 0x62, 0x31, 0x00, 0xe6, 0x63, 0xed, 0x3e, 0x68, 0xcf, + 0x8a, 0x0d, 0x11, 0x51, 0x8a, 0x52, 0x5e, 0x1e, 0x7b, 0x1f, 0x41, 0xb7, 0x3d, 0xf6, 0x4f, 0x78, 0xea, 0x81, 0x2d, + 0xc7, 0xb3, 0x74, 0x39, 0x5a, 0xe4, 0x05, 0x0c, 0x30, 0xcf, 0xe2, 0x94, 0xb3, 0xbc, 0x77, 0x92, 0xe5, 0x80, 0xb6, + 0x56, 0x1e, 0x8d, 0xe3, 0x45, 0x11, 0xdc, 0x9f, 0x5f, 0xf4, 0x50, 0x57, 0x38, 0xcd, 0xb3, 0x45, 0x3a, 0x96, 0x73, + 0xc5, 0x29, 0x1c, 0x8c, 0x98, 0x9b, 0x15, 0xf4, 0x25, 0x14, 0x80, 0x2f, 0x65, 0x51, 0xde, 0x3a, 0xc5, 0xce, 0xa8, + 0xe8, 0xb7, 0xc7, 0xec, 0xd4, 0xcb, 0x4f, 0x4f, 0x22, 0xa7, 0xd3, 0x7d, 0xe4, 0xa9, 0x7f, 0xfe, 0x03, 0x17, 0x14, + 0xf7, 0xb5, 0xc5, 0x9d, 0x76, 0xfb, 0x1f, 0xdc, 0x5e, 0x63, 0x16, 0x02, 0x28, 0xe8, 0xcc, 0x2f, 0xac, 0x22, 0x4b, + 0x60, 0x7f, 0xd6, 0xf5, 0xec, 0xcd, 0xc1, 0x6e, 0x8a, 0xd3, 0xd3, 0xa0, 0x3b, 0xbf, 0x28, 0x71, 0x75, 0x81, 0x48, + 0xc8, 0x94, 0x8b, 0x94, 0x6f, 0xcb, 0x3f, 0x0a, 0xf1, 0xe3, 0xf5, 0x10, 0x77, 0x15, 0xc4, 0x15, 0xd6, 0x5b, 0x63, + 0x38, 0x07, 0x84, 0xfe, 0x4e, 0x21, 0x00, 0x99, 0x82, 0x11, 0x98, 0x2b, 0x38, 0xe8, 0xe5, 0x0f, 0x83, 0xd1, 0x5d, + 0x0f, 0xc6, 0xe3, 0xdb, 0xc0, 0xc8, 0xd3, 0xf1, 0xb2, 0xbe, 0xaf, 0x1d, 0x30, 0x4e, 0x7b, 0x53, 0x86, 0xf4, 0x14, + 0x74, 0xf1, 0xf9, 0x3c, 0x1e, 0xf3, 0xa9, 0x78, 0x24, 0x72, 0x3e, 0x17, 0x75, 0x0f, 0xda, 0x6d, 0xf1, 0x5e, 0x80, + 0x40, 0x0b, 0x3a, 0x3e, 0x36, 0x00, 0x22, 0x7a, 0x71, 0xdd, 0x47, 0x6c, 0x3e, 0xdc, 0xfa, 0xa5, 0x1a, 0xef, 0x52, + 0xe5, 0x0d, 0x0a, 0x11, 0xa1, 0xbe, 0xd9, 0x82, 0x19, 0x6f, 0x45, 0xbf, 0xa3, 0x03, 0x55, 0x83, 0x0f, 0x8c, 0xa4, + 0x5e, 0xc0, 0x3d, 0x33, 0x17, 0xa8, 0x97, 0xf6, 0xd1, 0x25, 0xd5, 0x6a, 0xb9, 0x20, 0x37, 0x18, 0xba, 0x90, 0x28, + 0x20, 0xe8, 0x14, 0x83, 0x9c, 0xbe, 0xa9, 0x91, 0xb9, 0x41, 0xee, 0x64, 0x2e, 0x1c, 0xf9, 0x4c, 0xf3, 0xf5, 0x62, + 0x6b, 0x0b, 0xac, 0xec, 0x17, 0x4c, 0x36, 0x00, 0xee, 0x4d, 0xae, 0xae, 0xef, 0x43, 0x61, 0x4a, 0x29, 0x43, 0x6a, + 0x76, 0xd3, 0x15, 0x7d, 0xd8, 0x95, 0x98, 0x32, 0x92, 0x8f, 0x86, 0xff, 0x0e, 0xc5, 0xde, 0xd1, 0x86, 0x65, 0x91, + 0x2d, 0xf2, 0x11, 0x79, 0xea, 0x56, 0x2d, 0x7e, 0x9b, 0x04, 0xae, 0xed, 0x31, 0xcd, 0xe7, 0xd1, 0x0c, 0xae, 0x7d, + 0xe4, 0x80, 0x53, 0x10, 0x44, 0xdc, 0x31, 0x90, 0x5e, 0x0e, 0x05, 0x21, 0x8a, 0xae, 0x31, 0xe5, 0xbb, 0xd1, 0xfd, + 0x4b, 0x7f, 0x91, 0xc6, 0xc0, 0xe9, 0x3e, 0xc6, 0x63, 0xba, 0x77, 0x12, 0x8f, 0x29, 0x10, 0xd1, 0xa2, 0xc4, 0x23, + 0xf4, 0x6c, 0x43, 0x81, 0xfa, 0x0e, 0x0b, 0x3c, 0xcb, 0x44, 0x16, 0xbb, 0x65, 0x63, 0x30, 0xc1, 0x10, 0x95, 0xe3, + 0x6c, 0x16, 0xc5, 0x69, 0x80, 0xdf, 0x07, 0xf1, 0xf4, 0x88, 0x01, 0x76, 0xf1, 0xe0, 0x27, 0x93, 0xb9, 0x68, 0x1d, + 0xd7, 0xff, 0x05, 0xf8, 0x08, 0xf5, 0x2f, 0xa5, 0x1d, 0xa6, 0xe1, 0x52, 0x61, 0xde, 0x7a, 0x29, 0xf0, 0x1e, 0xae, + 0x74, 0x56, 0x46, 0x7e, 0x8e, 0x3d, 0x4e, 0x3f, 0x06, 0xad, 0x4e, 0xd0, 0xd1, 0xa6, 0x6b, 0xed, 0x36, 0xaa, 0xc8, + 0x65, 0x91, 0x37, 0x1a, 0x09, 0x06, 0xfd, 0x2c, 0xe0, 0xac, 0xde, 0x35, 0xac, 0x9e, 0xa4, 0x4b, 0x74, 0xe0, 0x9c, + 0xa6, 0x4e, 0x0d, 0x08, 0x8a, 0x05, 0x5c, 0x33, 0x95, 0x5b, 0x46, 0x24, 0x94, 0xbe, 0xa4, 0x03, 0x5c, 0xbf, 0x4b, + 0x84, 0xf7, 0x86, 0xea, 0x29, 0x50, 0x8a, 0xe4, 0x16, 0xc7, 0x7b, 0xe2, 0xc4, 0x5b, 0x44, 0x63, 0xa1, 0x0d, 0x47, + 0xd0, 0xb6, 0xfe, 0x32, 0x02, 0x2c, 0x7d, 0x0a, 0xed, 0xcd, 0xa5, 0xa3, 0x12, 0xeb, 0x73, 0x98, 0x6b, 0x5f, 0x48, + 0x3d, 0xba, 0x91, 0x6f, 0xf7, 0x37, 0x97, 0xbc, 0xdc, 0xdb, 0x11, 0xbd, 0xfb, 0xc7, 0x65, 0x41, 0x02, 0xca, 0x74, + 0xa4, 0x55, 0x53, 0x88, 0x3a, 0x18, 0x96, 0xd2, 0x77, 0x71, 0xdc, 0x42, 0x2b, 0x5d, 0xc2, 0x63, 0x2c, 0xc9, 0x2e, + 0xc7, 0x74, 0xa5, 0x28, 0x87, 0x33, 0xa9, 0x13, 0x52, 0x72, 0x91, 0x83, 0xd1, 0x5b, 0x85, 0xe2, 0x18, 0x21, 0x18, + 0x6c, 0x2e, 0xe3, 0x32, 0xdc, 0x5c, 0x66, 0xe5, 0x31, 0x68, 0x26, 0x08, 0x55, 0xa1, 0x3e, 0xef, 0x02, 0x13, 0x0b, + 0x27, 0x8b, 0x45, 0x23, 0xe0, 0xb4, 0xac, 0xb4, 0xad, 0x81, 0x80, 0x06, 0x2c, 0x40, 0x2c, 0x00, 0xdd, 0x8d, 0x7a, + 0x31, 0x58, 0x8b, 0x68, 0xdd, 0x87, 0x81, 0xf6, 0x76, 0x44, 0x23, 0x58, 0x57, 0x8e, 0x20, 0x57, 0xcb, 0xc2, 0x74, + 0x1c, 0x73, 0x69, 0x49, 0x74, 0xc2, 0x12, 0xe8, 0x9f, 0x5f, 0x5d, 0xb5, 0xa1, 0x9b, 0x78, 0xb5, 0xf6, 0xe2, 0x74, + 0xbe, 0x90, 0xdf, 0xd4, 0x82, 0x59, 0x3a, 0x18, 0xe6, 0xc4, 0x94, 0xff, 0x81, 0x8a, 0xdb, 0x05, 0x36, 0x8d, 0x6b, + 0x03, 0x3c, 0x14, 0x32, 0x40, 0x50, 0x2a, 0x1a, 0x80, 0xd2, 0x78, 0xbc, 0x5a, 0xa6, 0x97, 0x51, 0xc0, 0x0b, 0x9c, + 0xc1, 0x39, 0x3e, 0xa7, 0xf0, 0x3c, 0x8b, 0x53, 0x7c, 0xcc, 0xf1, 0x31, 0xba, 0xc0, 0xc7, 0xac, 0xb4, 0xff, 0x2e, + 0xe8, 0xb6, 0x34, 0x02, 0xb2, 0xab, 0x2b, 0x60, 0xee, 0x1a, 0x05, 0x40, 0x10, 0xe2, 0xdb, 0x2a, 0xcc, 0xc4, 0x16, + 0x2b, 0xe6, 0x2d, 0x51, 0x6e, 0x91, 0xf0, 0x0c, 0xc1, 0xb6, 0xca, 0x9d, 0x86, 0x8e, 0xe0, 0xc9, 0x2c, 0x92, 0x27, + 0xf8, 0xe2, 0xda, 0x96, 0xf8, 0xf8, 0x85, 0x40, 0x07, 0x3d, 0xe2, 0xda, 0x74, 0x19, 0x97, 0x9f, 0xb5, 0x89, 0x43, + 0x1b, 0x67, 0x01, 0x35, 0x0d, 0x99, 0x3d, 0x8f, 0xe2, 0x44, 0x34, 0x5e, 0xb3, 0x92, 0x46, 0x3a, 0x20, 0x2d, 0x64, + 0x6f, 0xa7, 0x82, 0x0d, 0x80, 0x1f, 0x89, 0xcb, 0xd4, 0x15, 0xf4, 0xb6, 0xa8, 0xa2, 0x28, 0xb9, 0x3c, 0xbc, 0x03, + 0xe1, 0x0f, 0xd7, 0xeb, 0x1c, 0x82, 0x5d, 0x17, 0xa5, 0xf5, 0x16, 0x00, 0xf1, 0x9c, 0xb1, 0xb1, 0x67, 0x5b, 0xc0, + 0x26, 0xc5, 0xf3, 0xc7, 0x84, 0x9d, 0x31, 0xf9, 0x11, 0x14, 0xdd, 0x57, 0x57, 0x8e, 0x40, 0xda, 0x72, 0x79, 0x3f, + 0x53, 0x52, 0x9e, 0x5a, 0x97, 0x5c, 0x7d, 0x1d, 0x78, 0xcf, 0x36, 0x06, 0x6d, 0xce, 0xd1, 0xae, 0x0f, 0xeb, 0x75, + 0x40, 0x91, 0xb5, 0x01, 0x4c, 0xd2, 0xcf, 0x6e, 0x5a, 0x0a, 0xf4, 0x63, 0x93, 0x09, 0x1c, 0x00, 0x15, 0xb7, 0xd0, + 0xa7, 0x5b, 0x00, 0x03, 0x66, 0xa6, 0x67, 0x8b, 0x16, 0x76, 0xd5, 0x56, 0x3f, 0x21, 0x2a, 0x92, 0x6c, 0xf4, 0xa9, + 0x36, 0xc5, 0x02, 0x09, 0x08, 0xc7, 0x6a, 0xf0, 0x29, 0xfb, 0xdf, 0xfe, 0xf5, 0x7f, 0xfe, 0x57, 0x18, 0x8e, 0x3a, + 0xb8, 0xa5, 0x75, 0x7d, 0xab, 0xff, 0x01, 0xad, 0x16, 0xe9, 0x2d, 0xed, 0xfe, 0xf6, 0xcf, 0xff, 0x0d, 0x9a, 0xd1, + 0xcd, 0x1a, 0xb7, 0x3c, 0x0e, 0xec, 0x11, 0x6a, 0x32, 0x77, 0x03, 0xa4, 0xd6, 0xf5, 0xda, 0xf1, 0xff, 0x05, 0x81, + 0x2d, 0x78, 0x36, 0xbf, 0x11, 0x08, 0x84, 0x75, 0x94, 0x64, 0x05, 0x13, 0x50, 0x08, 0x36, 0x79, 0x47, 0x30, 0x68, + 0x86, 0x39, 0x90, 0x6c, 0x61, 0x89, 0xde, 0x02, 0xfb, 0xb5, 0xde, 0x8d, 0x5d, 0x29, 0x18, 0x27, 0xd0, 0xc9, 0x03, + 0x00, 0xfb, 0x20, 0x9e, 0xe0, 0x81, 0x4e, 0x33, 0x6c, 0xbb, 0xce, 0x17, 0x68, 0x0c, 0xa1, 0x89, 0x4c, 0x8c, 0x20, + 0x5c, 0x1d, 0xaa, 0x1f, 0xfc, 0x04, 0xd6, 0xf2, 0x51, 0x3f, 0x47, 0x17, 0xfa, 0x19, 0xd9, 0x0f, 0x0c, 0x0b, 0x82, + 0x62, 0x86, 0x3a, 0x40, 0x73, 0x61, 0xea, 0xa4, 0x56, 0xfc, 0x81, 0xa9, 0xe4, 0xb0, 0x8f, 0x98, 0x0f, 0x89, 0xb7, + 0x5f, 0x16, 0x39, 0xab, 0x38, 0x26, 0x36, 0x10, 0xac, 0xc8, 0xac, 0xff, 0x98, 0x64, 0xe7, 0xe5, 0x75, 0x75, 0x53, + 0xa0, 0xe2, 0x72, 0x6f, 0x1c, 0x9f, 0xf5, 0x25, 0x22, 0x1b, 0x6b, 0x59, 0xed, 0xd2, 0x5c, 0x18, 0x56, 0xc9, 0x75, + 0xc9, 0x47, 0x5c, 0x96, 0xd7, 0x46, 0x01, 0x80, 0xe3, 0xee, 0x9d, 0xe4, 0x7d, 0xb9, 0x80, 0x57, 0x78, 0x61, 0x8b, + 0x20, 0x41, 0x3e, 0x2e, 0x64, 0x0c, 0x27, 0x19, 0x63, 0xb2, 0x7a, 0xd4, 0x5a, 0x33, 0xc5, 0xd2, 0xb1, 0x61, 0x8d, + 0x0b, 0x73, 0xc9, 0x85, 0x63, 0xa9, 0x0e, 0x49, 0x2e, 0x8c, 0x1f, 0xe0, 0x68, 0x70, 0xe1, 0xf8, 0x5a, 0x2e, 0x8c, + 0x6b, 0x1b, 0xe0, 0xc8, 0xa1, 0xbd, 0x8d, 0xb6, 0xb8, 0x21, 0x15, 0x38, 0x0a, 0x37, 0x30, 0xc0, 0x46, 0x9f, 0xa4, + 0x6c, 0x23, 0x50, 0xd1, 0x18, 0x95, 0xd2, 0x9a, 0x04, 0x9b, 0x64, 0xcf, 0xc1, 0xe2, 0x18, 0x64, 0x9b, 0x39, 0x32, + 0x58, 0xc2, 0x13, 0x86, 0xc7, 0xff, 0x78, 0x07, 0xfb, 0x8a, 0xcd, 0x2c, 0xe9, 0x19, 0xa4, 0xcf, 0x0e, 0x0d, 0xe0, + 0x2d, 0x85, 0x3b, 0x23, 0xb0, 0xdf, 0xbe, 0x39, 0x38, 0xb4, 0xbd, 0x93, 0x6c, 0x7c, 0x19, 0xd8, 0xa0, 0x8a, 0x82, + 0x24, 0x73, 0x7d, 0x3e, 0x65, 0xa9, 0xa3, 0x94, 0xc1, 0x2c, 0x01, 0x65, 0x38, 0x3b, 0x15, 0xb7, 0xef, 0x9b, 0xae, + 0x58, 0x40, 0x1b, 0x7d, 0x9e, 0xaf, 0xbf, 0xc7, 0xc5, 0x97, 0x2b, 0x79, 0x8e, 0x8f, 0x7d, 0x0c, 0x46, 0xef, 0xed, + 0xc0, 0x03, 0xbe, 0x1c, 0x20, 0x05, 0xe9, 0x37, 0x01, 0x67, 0x21, 0xde, 0x77, 0xb0, 0xfd, 0x8e, 0xea, 0x8b, 0x50, + 0x28, 0x1a, 0xd0, 0xfa, 0x5a, 0xa5, 0x04, 0xd0, 0xd8, 0x63, 0x22, 0x41, 0xdc, 0x18, 0xc0, 0x01, 0x1f, 0xeb, 0x12, + 0x41, 0xa6, 0x46, 0x11, 0x8d, 0x52, 0xb1, 0x7f, 0x59, 0x85, 0x13, 0x12, 0xfa, 0xc4, 0x64, 0xf0, 0x93, 0xc0, 0x3f, + 0x36, 0xbf, 0x34, 0x25, 0x3e, 0x0a, 0xa3, 0x17, 0x79, 0xf4, 0x57, 0xb0, 0x61, 0xbd, 0xf3, 0x63, 0x6a, 0xa9, 0xcc, + 0x1a, 0xb4, 0xb7, 0xd1, 0xfc, 0x6b, 0x2b, 0xfb, 0x15, 0x24, 0x5e, 0x12, 0xcd, 0x0b, 0x16, 0xa8, 0x07, 0x69, 0xe1, + 0xa0, 0xa1, 0xb4, 0x6a, 0x52, 0x9a, 0x92, 0xb1, 0xe4, 0xd3, 0xa5, 0x69, 0x02, 0x3d, 0x04, 0x13, 0x08, 0xd3, 0xb7, + 0x5b, 0x11, 0xb0, 0xf7, 0x34, 0x48, 0xd8, 0x84, 0x97, 0x1c, 0xef, 0x07, 0x2f, 0x95, 0xcd, 0xe9, 0x77, 0x1f, 0x80, + 0x59, 0x64, 0xf9, 0xf8, 0xff, 0x6d, 0x63, 0x8f, 0x83, 0x14, 0xcc, 0x18, 0xba, 0x30, 0x80, 0x97, 0xb1, 0x00, 0x22, + 0xf3, 0x7d, 0x69, 0x4c, 0x34, 0x62, 0x68, 0x8f, 0x97, 0x3c, 0xb7, 0xf8, 0xd4, 0xe3, 0xb9, 0xd9, 0x0e, 0x34, 0xa5, + 0x15, 0xa3, 0x7c, 0xd5, 0x2c, 0xdc, 0x75, 0xa5, 0xf2, 0xb8, 0xda, 0x58, 0xd9, 0xd6, 0xf5, 0xb7, 0x15, 0x0c, 0x19, + 0x5e, 0x80, 0x52, 0x70, 0xbe, 0xa5, 0xe8, 0x61, 0xae, 0x69, 0xd5, 0x3f, 0x70, 0xab, 0xee, 0x51, 0xd2, 0xd9, 0x3e, + 0xa2, 0xb3, 0x4d, 0xcc, 0x65, 0xb8, 0x14, 0x73, 0x8f, 0xa2, 0x64, 0xe4, 0x20, 0x00, 0x56, 0xcb, 0xba, 0x0f, 0xd8, + 0x04, 0x2e, 0x3d, 0x2c, 0xcb, 0xde, 0x25, 0x73, 0x8e, 0x7e, 0x93, 0x79, 0xe4, 0xe2, 0xfa, 0xa0, 0xfe, 0x04, 0x5b, + 0xbb, 0x74, 0x87, 0xde, 0xf7, 0xc6, 0x77, 0xad, 0x6c, 0x45, 0xa9, 0xb6, 0x07, 0xf8, 0xfd, 0x3e, 0xc4, 0xbe, 0xaf, + 0x1c, 0x1b, 0xb5, 0x10, 0xaa, 0xb9, 0x6c, 0x11, 0xe1, 0xd8, 0xd8, 0x4d, 0x78, 0x41, 0xbf, 0xba, 0xce, 0x98, 0xfd, + 0xee, 0x76, 0x63, 0x96, 0xdd, 0xd1, 0x98, 0xfd, 0xee, 0x4f, 0x36, 0x66, 0xbf, 0x6a, 0x1a, 0xb3, 0xbf, 0xfe, 0x1e, + 0x63, 0x36, 0xcf, 0xce, 0x8b, 0xb0, 0x23, 0x83, 0xa7, 0xc0, 0x4c, 0xfe, 0x3e, 0x56, 0x2d, 0x4c, 0xd4, 0xb0, 0x69, + 0xc9, 0x88, 0x15, 0xf9, 0x5e, 0xc0, 0xab, 0xa5, 0x09, 0xd9, 0xd6, 0x89, 0x55, 0xad, 0xfb, 0xea, 0x26, 0x09, 0xe8, + 0xf5, 0xae, 0xbe, 0x03, 0xd5, 0x55, 0x46, 0x66, 0x40, 0x9f, 0x82, 0xd4, 0x1d, 0xbb, 0xdb, 0x2a, 0xa3, 0xc7, 0x1c, + 0xa1, 0xa7, 0x1c, 0xb5, 0x82, 0x7c, 0x96, 0xf6, 0x7f, 0x3a, 0xea, 0xf4, 0x76, 0x3b, 0x33, 0xe8, 0x0d, 0x72, 0x0b, + 0xde, 0xda, 0xbd, 0xdd, 0x5d, 0x7c, 0x3b, 0x57, 0x6f, 0x5d, 0x7c, 0x8b, 0xd5, 0xdb, 0x03, 0x7c, 0x1b, 0xa9, 0xb7, + 0x87, 0xf8, 0x36, 0x56, 0x6f, 0x8f, 0xf0, 0xed, 0xcc, 0x2e, 0x8f, 0xb8, 0x06, 0xee, 0x11, 0xd0, 0x15, 0x29, 0x89, + 0x81, 0x2a, 0x83, 0xd3, 0x88, 0x37, 0xb0, 0xa2, 0xd3, 0x20, 0xf6, 0x84, 0x02, 0x1d, 0x14, 0xde, 0x39, 0xb0, 0xf4, + 0x80, 0x12, 0x8e, 0x9e, 0xe2, 0x55, 0x7c, 0xd0, 0x3d, 0x0f, 0xe3, 0x19, 0x53, 0xdf, 0x24, 0x55, 0xab, 0x06, 0x35, + 0x05, 0xec, 0xed, 0xb2, 0xa7, 0xf7, 0x49, 0xd8, 0xd0, 0x2a, 0x77, 0x82, 0x76, 0xae, 0xaa, 0x13, 0xd3, 0xb5, 0xf4, + 0x0e, 0x5f, 0x23, 0x20, 0x40, 0x00, 0x2b, 0xa3, 0x74, 0x02, 0x6a, 0x40, 0xeb, 0x02, 0x94, 0xf4, 0xb5, 0x42, 0x03, + 0x21, 0xd2, 0x62, 0x82, 0xd6, 0xa4, 0xdf, 0x0e, 0xa3, 0x53, 0xfd, 0xfc, 0x0a, 0xf4, 0xa9, 0xe8, 0x94, 0xdd, 0x26, + 0x40, 0x08, 0x44, 0x53, 0x78, 0x28, 0x20, 0x48, 0x0b, 0x81, 0xad, 0x41, 0x63, 0x41, 0x0a, 0x0f, 0xc4, 0x4e, 0x5d, + 0x9c, 0xd0, 0xf4, 0xf5, 0x22, 0xc0, 0x68, 0x55, 0xb0, 0x07, 0x6a, 0x1d, 0x95, 0x0a, 0x0c, 0x43, 0x05, 0x16, 0xdc, + 0x28, 0x63, 0x84, 0x2a, 0x72, 0x93, 0xa4, 0xb1, 0x94, 0x90, 0x31, 0x1d, 0xbc, 0xda, 0xbb, 0xbb, 0xca, 0xf7, 0x3e, + 0xeb, 0x8c, 0xf0, 0x8f, 0xe4, 0xaa, 0x9f, 0x4d, 0x26, 0x93, 0x1b, 0x85, 0xce, 0x67, 0xe3, 0x09, 0xeb, 0xb2, 0x07, + 0x3d, 0x74, 0xfe, 0xb5, 0xa4, 0x2f, 0xae, 0x53, 0x12, 0xee, 0x96, 0x77, 0x6b, 0x8c, 0xce, 0x38, 0x90, 0x43, 0x77, + 0x97, 0x4e, 0x25, 0x60, 0x65, 0x09, 0x5c, 0xf9, 0x34, 0x4e, 0x83, 0x76, 0xe9, 0x9f, 0x49, 0x76, 0xfe, 0xd9, 0xe3, + 0xc7, 0x8f, 0x4b, 0x7f, 0xac, 0xde, 0xda, 0xe3, 0x71, 0xe9, 0x8f, 0x96, 0x7a, 0x19, 0xed, 0xf6, 0x64, 0x52, 0xfa, + 0xb1, 0x2a, 0xd8, 0xed, 0x8e, 0xc6, 0xbb, 0xdd, 0xd2, 0x3f, 0x37, 0x5a, 0x94, 0x3e, 0x93, 0x6f, 0x39, 0x1b, 0xd7, + 0x3c, 0x88, 0x8f, 0xc0, 0x78, 0xf5, 0x05, 0xa1, 0x2d, 0xd1, 0x64, 0x10, 0x8f, 0x41, 0xb4, 0xe0, 0x60, 0xeb, 0x02, + 0x6f, 0x67, 0xc0, 0x9f, 0x27, 0x92, 0xb7, 0x8b, 0x4f, 0x7e, 0x22, 0x47, 0xff, 0xd5, 0xe4, 0xe8, 0x48, 0xcc, 0xc4, + 0xcd, 0x19, 0xc9, 0x81, 0x66, 0x35, 0x52, 0x16, 0x55, 0xff, 0x1a, 0xb2, 0x8a, 0xd9, 0x23, 0xb7, 0xc1, 0x96, 0x82, + 0xc7, 0x7f, 0x7d, 0x1d, 0x8f, 0xff, 0xe6, 0x76, 0x1e, 0x7f, 0x72, 0x37, 0x16, 0xff, 0xcd, 0x9f, 0xcc, 0xe2, 0xbf, + 0x6e, 0xb2, 0xf8, 0xcd, 0x3b, 0xb1, 0xf8, 0x35, 0x89, 0x1f, 0xa4, 0x9a, 0xbe, 0x49, 0x43, 0xfb, 0x0d, 0xd8, 0x30, + 0x46, 0xc9, 0x64, 0x02, 0x45, 0x93, 0x89, 0xad, 0x92, 0x1d, 0x81, 0x13, 0x51, 0xab, 0xd7, 0xb5, 0x12, 0x6a, 0xf5, + 0xd5, 0x57, 0x66, 0x99, 0x59, 0x20, 0xfd, 0x0d, 0xa6, 0x7c, 0x57, 0x35, 0x52, 0x65, 0x56, 0x9f, 0x06, 0x19, 0xc7, + 0x05, 0x9e, 0x26, 0x2c, 0x28, 0x79, 0x76, 0x7a, 0x9a, 0x30, 0xfd, 0xed, 0x33, 0xd5, 0xd2, 0x7c, 0x33, 0xe7, 0x33, + 0xcb, 0x07, 0x26, 0xb4, 0x41, 0x0d, 0xd0, 0x9e, 0x70, 0x64, 0xd2, 0xe7, 0xa0, 0x45, 0xd8, 0xfa, 0x4c, 0x7e, 0x37, + 0x98, 0xfc, 0xa9, 0x4b, 0xc9, 0x7e, 0x65, 0x40, 0xb3, 0xea, 0x8a, 0x2e, 0x4c, 0x91, 0x02, 0x32, 0x2e, 0x95, 0xdb, + 0x12, 0xa0, 0x9d, 0xe3, 0x47, 0x4e, 0x74, 0xca, 0xd2, 0xca, 0x37, 0x85, 0x34, 0x9b, 0xc0, 0x8f, 0x1e, 0x88, 0x29, + 0xc4, 0x67, 0x02, 0xf5, 0xb8, 0x22, 0x0e, 0xe8, 0xd4, 0xd6, 0x68, 0xac, 0x2a, 0x0c, 0xcd, 0xa5, 0xa8, 0x9c, 0x93, + 0xd5, 0x79, 0xd6, 0x8a, 0xe6, 0xeb, 0x85, 0xf2, 0xdd, 0xa6, 0xbb, 0x45, 0x34, 0x14, 0xe7, 0x76, 0x5f, 0xdb, 0x98, + 0x35, 0x9a, 0x29, 0xeb, 0x5e, 0x38, 0x9a, 0xe8, 0x24, 0xbb, 0xa8, 0xdb, 0x48, 0x26, 0x0c, 0x68, 0x3e, 0xe9, 0xbd, + 0x57, 0x75, 0xaa, 0xa0, 0x34, 0xbd, 0xa2, 0x22, 0xd3, 0x8b, 0x48, 0x83, 0x7c, 0x60, 0xb0, 0x03, 0xa9, 0x60, 0xca, + 0x30, 0x0f, 0x71, 0x17, 0x6d, 0x47, 0xa0, 0x32, 0x6d, 0x2b, 0x60, 0x51, 0x3a, 0xe4, 0xe8, 0x6b, 0xc2, 0x0e, 0x7d, + 0xab, 0x06, 0x70, 0xaa, 0x6d, 0xb3, 0xdb, 0x19, 0x3e, 0x98, 0x16, 0xe7, 0xc7, 0x7e, 0x71, 0xee, 0xc1, 0x3f, 0xeb, + 0xf3, 0x25, 0xb0, 0xb0, 0x93, 0x4f, 0x31, 0x07, 0x85, 0x71, 0xde, 0x42, 0xa3, 0x98, 0xdc, 0x3b, 0x92, 0xd7, 0x53, + 0xa8, 0x45, 0x5c, 0x89, 0xe8, 0x2d, 0x0a, 0xb4, 0x40, 0x48, 0xd5, 0x0e, 0xd2, 0x2c, 0x65, 0xbd, 0x7a, 0x48, 0xcd, + 0xd4, 0x76, 0x15, 0xb6, 0x86, 0xcb, 0x0c, 0x2d, 0x16, 0x7e, 0x09, 0x16, 0x8b, 0x90, 0x11, 0x6d, 0x15, 0x8e, 0x69, + 0xaf, 0x6d, 0x1f, 0x48, 0x64, 0x6e, 0x93, 0x28, 0xcc, 0x57, 0x55, 0xfa, 0xeb, 0x54, 0xf2, 0xdb, 0x02, 0x4c, 0xdd, + 0x07, 0x0f, 0x3c, 0xf5, 0xcf, 0x88, 0xcc, 0x35, 0x8b, 0x29, 0xc0, 0x74, 0x17, 0xc8, 0x82, 0x68, 0x82, 0x5f, 0x10, + 0xbb, 0x4b, 0xcb, 0x13, 0xca, 0xee, 0x5a, 0xa2, 0xcc, 0x0a, 0x3a, 0x8f, 0xc1, 0xc6, 0xb8, 0xf3, 0xf0, 0x37, 0x2f, + 0xbf, 0x94, 0x38, 0x52, 0x97, 0xf4, 0x6c, 0xbb, 0x87, 0xa7, 0x39, 0x89, 0x2e, 0xc1, 0xd4, 0x21, 0x01, 0x7a, 0x82, + 0xce, 0xae, 0xde, 0x3c, 0x93, 0x91, 0xd2, 0x9c, 0x25, 0xf4, 0x99, 0x7e, 0xb9, 0x15, 0xbb, 0x0f, 0xe7, 0x17, 0x6a, + 0x37, 0x3a, 0x8d, 0x08, 0xe8, 0x9f, 0x1a, 0xe8, 0xbc, 0x3e, 0xb2, 0x5a, 0x0f, 0xd6, 0x3d, 0x00, 0x18, 0x84, 0xd4, + 0x6e, 0xe5, 0x02, 0xaa, 0x36, 0x94, 0x18, 0xa1, 0xde, 0x6a, 0x20, 0xcb, 0xdf, 0x05, 0x09, 0x11, 0x81, 0xbd, 0x8b, + 0x9f, 0x72, 0x8b, 0xc1, 0xa0, 0x92, 0x9a, 0xc1, 0x2c, 0x1e, 0x8f, 0x13, 0xd6, 0x53, 0xc2, 0xdf, 0xea, 0x3c, 0xc4, + 0x48, 0xa9, 0xb9, 0x65, 0xf5, 0x5d, 0x31, 0x90, 0xa7, 0xf1, 0x14, 0x9d, 0x80, 0x32, 0x82, 0xdf, 0x63, 0x5b, 0x8b, + 0x4e, 0x19, 0x42, 0x6c, 0x57, 0xc8, 0xa3, 0xe7, 0xfa, 0x5a, 0x1e, 0x80, 0x26, 0x44, 0x1b, 0x0e, 0x46, 0x75, 0x36, + 0x0f, 0x5a, 0xbb, 0xf5, 0x85, 0x60, 0x95, 0x5e, 0x82, 0xb7, 0x66, 0x59, 0x1e, 0xd0, 0x44, 0x4b, 0x7c, 0xf8, 0xc7, + 0xf2, 0x3b, 0xb2, 0x8c, 0x06, 0xc0, 0x6f, 0x7e, 0xe9, 0xa2, 0xb2, 0xbe, 0x98, 0xff, 0x3f, 0xa7, 0xe5, 0x8b, 0xf5, + 0xa7, 0xe5, 0x0b, 0x75, 0x5a, 0x6e, 0xa6, 0xd8, 0xcf, 0x26, 0x1d, 0xfc, 0xd3, 0xab, 0x16, 0x04, 0xbb, 0x02, 0xe8, + 0xb0, 0x50, 0xe9, 0x6b, 0x75, 0xe1, 0x3f, 0x1a, 0xba, 0xed, 0xe1, 0x1f, 0x1f, 0xd4, 0x9b, 0xb6, 0x85, 0x85, 0xf8, + 0xaf, 0x5d, 0xab, 0xea, 0xdc, 0xc7, 0x3a, 0xec, 0xf5, 0x60, 0xb5, 0xae, 0x7b, 0xf3, 0xa1, 0x05, 0x7e, 0xc5, 0x9d, + 0x40, 0x31, 0x63, 0xb0, 0x43, 0xa2, 0x93, 0x13, 0x28, 0x9d, 0x64, 0xa3, 0x45, 0xf1, 0x8f, 0x12, 0x7e, 0x89, 0xc4, + 0x1b, 0x8f, 0x74, 0x63, 0x1c, 0xd5, 0x55, 0x84, 0xdd, 0xd5, 0x08, 0x4b, 0xbd, 0x4f, 0x41, 0x01, 0x84, 0xc9, 0x9c, + 0xae, 0x7f, 0x7f, 0xcd, 0x21, 0xf8, 0xbb, 0xec, 0xcd, 0xda, 0xc5, 0xfc, 0x7b, 0x91, 0x71, 0x23, 0x12, 0x7e, 0x17, + 0x0e, 0xcc, 0x3d, 0x6c, 0x3f, 0x5e, 0x0f, 0xee, 0x91, 0x9a, 0x69, 0xa8, 0x84, 0x82, 0x94, 0x3b, 0xa0, 0xe2, 0x46, + 0x8b, 0x84, 0xdf, 0x3c, 0xea, 0x75, 0x94, 0xb1, 0x32, 0xea, 0x0d, 0x0c, 0xbd, 0x6a, 0x7b, 0x47, 0x2e, 0xfd, 0xd9, + 0x17, 0xf7, 0xf1, 0x8f, 0xf0, 0xea, 0x9c, 0x54, 0x8a, 0xbf, 0x30, 0x7c, 0x51, 0xf1, 0xdf, 0xac, 0x69, 0xf6, 0x42, + 0x82, 0x93, 0x72, 0x7f, 0xd7, 0xd6, 0xa8, 0xcf, 0xde, 0xa9, 0xb9, 0xd4, 0x83, 0x7e, 0x57, 0xeb, 0xdf, 0x37, 0xf8, + 0x1d, 0xdb, 0x8e, 0x84, 0xce, 0x5c, 0x6f, 0x2b, 0x7f, 0x65, 0xc2, 0x6a, 0x63, 0x81, 0xe7, 0xbb, 0x36, 0x57, 0x1b, + 0x44, 0xed, 0x37, 0xc3, 0x13, 0x6d, 0x1e, 0xc9, 0xb0, 0x1b, 0xb6, 0x17, 0x16, 0xd2, 0xb7, 0x2c, 0xbc, 0x87, 0x9f, + 0x1a, 0xb2, 0x2e, 0x66, 0x49, 0x0a, 0x3a, 0xd5, 0x94, 0xf3, 0x79, 0xb0, 0xb3, 0x73, 0x7e, 0x7e, 0xee, 0x9f, 0xef, + 0xfa, 0x59, 0x7e, 0xba, 0xd3, 0x6d, 0xb7, 0xdb, 0xf8, 0x85, 0x18, 0xdb, 0x3a, 0x8b, 0xd9, 0xf9, 0x97, 0xd9, 0x45, + 0x68, 0x3f, 0xb2, 0x1e, 0x5b, 0x8f, 0x76, 0xad, 0x07, 0x0f, 0x6d, 0x8b, 0xb8, 0x3f, 0x94, 0xec, 0xda, 0x96, 0xe0, + 0xfe, 0xa1, 0x0d, 0xc5, 0xfd, 0xbd, 0x53, 0xa5, 0xc0, 0x61, 0x06, 0xae, 0x50, 0x8f, 0xc0, 0x66, 0xc9, 0x3e, 0xb1, + 0xfa, 0x39, 0x17, 0x65, 0x2d, 0x29, 0x43, 0xd4, 0x2b, 0x1e, 0xf6, 0x51, 0x34, 0x0f, 0x88, 0x86, 0xcc, 0x42, 0x74, + 0x00, 0x89, 0x52, 0x9a, 0x02, 0xa3, 0xba, 0x27, 0xf0, 0x04, 0x1a, 0xfb, 0xd4, 0x82, 0xe7, 0x57, 0xdd, 0x47, 0x20, + 0xe0, 0xce, 0x5a, 0xf7, 0x47, 0xed, 0x56, 0xc7, 0xea, 0xb4, 0xba, 0xfe, 0x23, 0xab, 0x2b, 0xfe, 0x07, 0x06, 0xb9, + 0x6b, 0x75, 0xe0, 0x69, 0xd7, 0x82, 0xf7, 0xb3, 0xfb, 0x22, 0x24, 0x1c, 0xd9, 0x3b, 0xfd, 0x3d, 0xfc, 0x85, 0x29, + 0xb0, 0xa8, 0x2f, 0x6c, 0xf1, 0x2b, 0x9e, 0xec, 0xcf, 0xcc, 0xd2, 0xce, 0xe3, 0xb5, 0xc5, 0xdd, 0x47, 0x6b, 0x8b, + 0x77, 0x1f, 0xae, 0x2d, 0xbe, 0xff, 0xa0, 0x5e, 0xbc, 0x73, 0x2a, 0xaa, 0x34, 0x53, 0x08, 0xed, 0x59, 0x04, 0x54, + 0x72, 0xe1, 0x74, 0x00, 0xce, 0xb6, 0xd5, 0xc2, 0x1f, 0x8f, 0xba, 0xae, 0xee, 0x75, 0x82, 0xbd, 0xf4, 0x2a, 0x1f, + 0x3d, 0x86, 0x55, 0x3e, 0xef, 0x3e, 0x1c, 0x61, 0x3b, 0x5a, 0x28, 0xfc, 0x3b, 0xdb, 0x7d, 0x3c, 0x02, 0x71, 0x60, + 0xe1, 0x3f, 0xf8, 0x33, 0x7d, 0xd0, 0x1d, 0x89, 0x97, 0x36, 0xd6, 0x7f, 0xe8, 0x3c, 0x2a, 0xa0, 0x29, 0xfe, 0xf9, + 0x4d, 0xeb, 0xcf, 0xa8, 0xbe, 0x9b, 0xe3, 0xde, 0x07, 0x1c, 0x3d, 0x9e, 0x76, 0xfd, 0x2f, 0xce, 0x1e, 0xf9, 0x8f, + 0xa7, 0x9d, 0x47, 0x1f, 0xc4, 0x5b, 0x02, 0x18, 0xfc, 0x02, 0xff, 0x7d, 0xd8, 0x6d, 0x83, 0x69, 0xeb, 0x3f, 0x3e, + 0xdb, 0xf5, 0x77, 0x93, 0xd6, 0x43, 0xff, 0x31, 0xfe, 0xab, 0x86, 0x9b, 0x66, 0x33, 0x66, 0x5b, 0xb8, 0xdf, 0x0d, + 0xbb, 0xd0, 0x9c, 0xa3, 0x7b, 0xdf, 0x7a, 0x70, 0xff, 0xf9, 0x63, 0xd8, 0xa3, 0x69, 0xa7, 0x0b, 0xff, 0x5f, 0xf7, + 0xf8, 0x01, 0x11, 0x2f, 0x07, 0x8e, 0x18, 0xe6, 0xce, 0x29, 0xc4, 0xd1, 0xd7, 0x8a, 0xee, 0x79, 0x3f, 0x5e, 0x67, + 0xda, 0xff, 0x70, 0xbb, 0x69, 0xff, 0xd7, 0x3b, 0xba, 0x6f, 0x7f, 0xf8, 0x93, 0x6d, 0xfb, 0x1f, 0x9b, 0xb6, 0xfd, + 0x39, 0x5b, 0x31, 0xee, 0x9b, 0xf6, 0xd9, 0x21, 0x73, 0x8e, 0xbe, 0x65, 0x43, 0xcc, 0x13, 0x85, 0xd6, 0x7f, 0xad, + 0x79, 0x3a, 0x32, 0x3c, 0xc8, 0xe7, 0x4c, 0x9c, 0xe4, 0xef, 0xaf, 0x43, 0x08, 0xe3, 0xb7, 0x22, 0xe4, 0xc5, 0xdd, + 0xf0, 0x41, 0x9f, 0x16, 0xff, 0x13, 0xf1, 0xf1, 0xbd, 0x89, 0x8f, 0x9a, 0x2f, 0x99, 0x8c, 0x79, 0xb2, 0xc1, 0x0f, + 0xe8, 0xdd, 0xb1, 0x77, 0x18, 0xbe, 0x15, 0xb6, 0x48, 0x7e, 0x7a, 0xf7, 0x7b, 0xfc, 0xde, 0x22, 0x8d, 0x32, 0xb4, + 0xa5, 0x83, 0x62, 0x8e, 0x1f, 0xe3, 0x54, 0x2f, 0x67, 0x22, 0x55, 0x3f, 0xa4, 0x7b, 0x36, 0xf7, 0xb5, 0x73, 0x03, + 0x33, 0x5b, 0xc3, 0x65, 0xc6, 0x23, 0xfc, 0x6d, 0x2f, 0x3c, 0xe6, 0x09, 0xde, 0x02, 0x94, 0x37, 0x66, 0x30, 0x11, + 0xf3, 0x5b, 0x4c, 0x22, 0x55, 0xee, 0xef, 0x19, 0x3a, 0x0c, 0x5e, 0xb1, 0x71, 0x1c, 0x39, 0xb6, 0x33, 0x87, 0x13, + 0x0b, 0x63, 0xb6, 0x6a, 0x19, 0x9c, 0x94, 0xbc, 0xe9, 0xda, 0xea, 0x17, 0x8c, 0xe4, 0xf8, 0xc1, 0xa6, 0xf0, 0x48, + 0xba, 0xce, 0x6c, 0xa9, 0xfe, 0xc3, 0xf8, 0xaa, 0x24, 0x47, 0xd6, 0x5d, 0xa9, 0x0c, 0xb6, 0xd0, 0x19, 0x3a, 0x7e, + 0x17, 0x6c, 0x08, 0x2a, 0xc6, 0x0f, 0xe0, 0xfc, 0xe0, 0xb4, 0x76, 0x41, 0xa7, 0x31, 0xba, 0xe9, 0x81, 0x86, 0x2b, + 0x1f, 0xdf, 0x14, 0x7e, 0x83, 0x46, 0xa9, 0xa7, 0x7f, 0xe3, 0x12, 0x50, 0x86, 0xca, 0xf5, 0xff, 0xf2, 0xf2, 0x50, + 0x5e, 0x72, 0xb5, 0xd1, 0x27, 0x49, 0xbe, 0xe8, 0xea, 0x03, 0x3b, 0xdb, 0x20, 0x2e, 0xe8, 0xd7, 0xde, 0x51, 0x50, + 0x16, 0x25, 0x02, 0xe6, 0x98, 0x5a, 0xd2, 0x6c, 0x08, 0x6d, 0x21, 0x0f, 0xc6, 0xec, 0x2c, 0x1e, 0x49, 0xb6, 0xee, + 0x59, 0x32, 0x37, 0xbe, 0x45, 0xab, 0x08, 0x3b, 0x9e, 0x30, 0x9c, 0xe1, 0x05, 0x65, 0x54, 0x98, 0x66, 0x76, 0xff, + 0x5e, 0x4f, 0x43, 0x52, 0x4f, 0xcf, 0xb5, 0xf1, 0x77, 0xf0, 0x1d, 0x81, 0xa1, 0xf6, 0x8f, 0xe1, 0x3d, 0xfc, 0x2d, + 0x7c, 0xf7, 0x86, 0xb6, 0xeb, 0x13, 0x53, 0xbc, 0x57, 0xfd, 0x2a, 0x3e, 0xe4, 0x08, 0xdb, 0x20, 0xbf, 0xbc, 0xbb, + 0x0a, 0x32, 0x29, 0xb4, 0xba, 0x0f, 0x2a, 0xa1, 0x05, 0xcf, 0x06, 0x97, 0x02, 0x06, 0xda, 0xf5, 0x1f, 0x18, 0xac, + 0xf0, 0xac, 0x85, 0x3f, 0x6b, 0xcc, 0xf0, 0x3e, 0x34, 0x50, 0xdc, 0xf0, 0x25, 0x34, 0xdf, 0x15, 0x8c, 0x17, 0xfa, + 0xfd, 0x48, 0xac, 0x4a, 0xb0, 0xa9, 0x3a, 0xc5, 0xac, 0x09, 0x8f, 0x88, 0x78, 0xb6, 0xed, 0x39, 0xfa, 0xfb, 0xfe, + 0x92, 0x5c, 0xe5, 0xe5, 0xa4, 0xa7, 0xd0, 0xd7, 0xd1, 0xdf, 0xad, 0x5d, 0x57, 0xe7, 0xd5, 0x4e, 0xce, 0x9a, 0x29, + 0x90, 0xe0, 0x1b, 0x21, 0x18, 0xca, 0xd5, 0x16, 0xdf, 0x6f, 0x12, 0xc7, 0xb8, 0xfa, 0xc2, 0xd5, 0x9a, 0x74, 0x43, + 0xf3, 0x50, 0xb0, 0x8a, 0x68, 0xe8, 0x5c, 0x00, 0x23, 0xa0, 0x9f, 0x55, 0xb1, 0x7a, 0x90, 0x04, 0xe5, 0x27, 0x11, + 0xfe, 0xfa, 0x09, 0xfa, 0x51, 0x56, 0x07, 0x90, 0xd3, 0x07, 0xfa, 0x08, 0xd2, 0x17, 0xe3, 0xb2, 0xb9, 0x08, 0xd0, + 0x17, 0xf0, 0xb7, 0x99, 0x55, 0xb9, 0xe1, 0xf2, 0xd2, 0x17, 0x86, 0xc1, 0xc7, 0x71, 0x4e, 0x77, 0x09, 0xd5, 0xfa, + 0x6b, 0xd7, 0xfc, 0x2a, 0x54, 0xd3, 0xa9, 0x64, 0xc5, 0xc0, 0xc6, 0x22, 0x5b, 0x65, 0xe9, 0x98, 0x5f, 0xa8, 0x35, + 0x2f, 0x7b, 0x8d, 0x45, 0x9a, 0x0e, 0x7e, 0xc1, 0xdb, 0x16, 0x48, 0xb6, 0x81, 0x8d, 0x5d, 0xbb, 0x26, 0x52, 0x6e, + 0xf0, 0x8e, 0x54, 0xf5, 0x2b, 0x59, 0xcc, 0x03, 0x6f, 0x9b, 0xbb, 0xa5, 0xc7, 0xa5, 0x7d, 0x70, 0xa5, 0xa7, 0xf0, + 0x84, 0x45, 0xdc, 0x8f, 0x52, 0xca, 0xf7, 0x70, 0x0c, 0xb6, 0xe0, 0x75, 0xd8, 0xae, 0x5b, 0x02, 0xe7, 0x31, 0x7e, + 0x67, 0x8d, 0x40, 0xbd, 0x0f, 0x85, 0x6e, 0xe5, 0xb5, 0x9b, 0x76, 0xfb, 0x6f, 0x0e, 0xf7, 0x2d, 0x71, 0x9a, 0xf7, + 0x76, 0xe0, 0x75, 0x8f, 0x6c, 0x61, 0x91, 0x52, 0x10, 0x8a, 0x94, 0x02, 0x4b, 0x64, 0xc3, 0x84, 0xf6, 0x8e, 0x58, + 0xa6, 0x6d, 0xb1, 0x74, 0x24, 0x3c, 0x78, 0x33, 0xb0, 0x15, 0x62, 0xfc, 0x8a, 0xd1, 0x0e, 0x76, 0x6b, 0xe1, 0x4e, + 0xc3, 0x11, 0x10, 0x3e, 0x3e, 0xa5, 0x20, 0xf0, 0xd4, 0x96, 0xfe, 0x3e, 0x10, 0xeb, 0x4c, 0x65, 0x62, 0xc8, 0xa1, + 0x74, 0x5e, 0xde, 0x6a, 0xeb, 0x62, 0x71, 0x32, 0x03, 0x3e, 0xa4, 0x92, 0x29, 0xde, 0xcb, 0x0e, 0x7b, 0x34, 0x15, + 0x66, 0x01, 0xae, 0x3a, 0x21, 0xa7, 0x9d, 0xfe, 0x5e, 0x24, 0xf5, 0x1d, 0x3c, 0xbb, 0x05, 0x1c, 0x5e, 0x10, 0x73, + 0xa8, 0x54, 0xf8, 0x71, 0xb6, 0x73, 0xce, 0x4e, 0x5a, 0xd1, 0x3c, 0xae, 0x7c, 0x7f, 0x28, 0xfd, 0xfa, 0x7b, 0x4a, + 0x10, 0xca, 0x84, 0x33, 0xf9, 0x18, 0x19, 0x89, 0x07, 0x88, 0x38, 0x22, 0xd0, 0x52, 0x3a, 0x16, 0x49, 0x69, 0x04, + 0xe4, 0x03, 0xac, 0x44, 0xbf, 0xca, 0x01, 0x29, 0x25, 0x41, 0x69, 0xf7, 0xff, 0xf6, 0xbf, 0xfe, 0xb7, 0xf4, 0x29, + 0x02, 0x5a, 0x01, 0x2c, 0xcc, 0xdc, 0xa8, 0x62, 0x67, 0xec, 0x02, 0xac, 0xd0, 0x78, 0xdc, 0x9a, 0x46, 0xc9, 0x04, + 0x20, 0x28, 0x98, 0xb8, 0xbb, 0x21, 0xeb, 0x81, 0x0a, 0x24, 0x58, 0x66, 0xd8, 0x59, 0x82, 0x57, 0x2f, 0xc2, 0x1d, + 0xfb, 0x43, 0x19, 0x7c, 0x2a, 0xb7, 0x94, 0x08, 0xda, 0xc8, 0xe7, 0x33, 0x68, 0xae, 0x96, 0xd3, 0xa7, 0x7e, 0x23, + 0x8c, 0x64, 0x1e, 0xac, 0x96, 0xd0, 0x07, 0x2d, 0x75, 0xa0, 0xe0, 0xdf, 0xfe, 0xf5, 0x3f, 0xff, 0x77, 0xf5, 0x8a, + 0xfe, 0xff, 0xbf, 0xfd, 0xcb, 0x3f, 0xfd, 0xdf, 0xff, 0xf3, 0x5f, 0x30, 0x39, 0x52, 0xc6, 0x08, 0xe8, 0x28, 0x59, + 0x55, 0x80, 0x40, 0x9c, 0xa9, 0x7a, 0xb6, 0xdf, 0x01, 0xcd, 0x42, 0x04, 0x29, 0x41, 0x22, 0x62, 0xa6, 0x24, 0x50, + 0x42, 0xd5, 0x0d, 0x38, 0x83, 0xfd, 0xb3, 0x28, 0x4a, 0x6d, 0x3f, 0x68, 0xdb, 0xd5, 0x9e, 0xf6, 0x8d, 0xbe, 0x3b, + 0xb8, 0x1b, 0x77, 0xca, 0x14, 0xf1, 0xf5, 0x5e, 0x2d, 0x95, 0xe3, 0x0a, 0x4b, 0xca, 0xaa, 0xdc, 0x42, 0x8f, 0xf2, + 0x12, 0x5f, 0x83, 0xae, 0x51, 0x4c, 0x5b, 0x5b, 0xeb, 0xd3, 0xfb, 0x65, 0x51, 0xf0, 0x78, 0x82, 0xfb, 0x21, 0xdc, + 0x63, 0x14, 0x0a, 0x6c, 0xa1, 0x4a, 0x92, 0x5c, 0x96, 0x34, 0x8a, 0x30, 0x61, 0xee, 0x3f, 0xfe, 0x87, 0xf2, 0x2f, + 0x33, 0x54, 0x05, 0x2c, 0x67, 0x16, 0x5d, 0x48, 0xc3, 0xe6, 0x61, 0xbb, 0x3d, 0xbf, 0x70, 0x97, 0xd5, 0x0c, 0xde, + 0x75, 0x93, 0x91, 0x4b, 0xcd, 0x1c, 0x90, 0x62, 0x88, 0xda, 0x7b, 0x07, 0xba, 0x7c, 0x1b, 0x9d, 0x3d, 0x65, 0xf9, + 0xf9, 0x92, 0x1c, 0x48, 0xf1, 0x6f, 0x18, 0xeb, 0x93, 0xbe, 0x36, 0x28, 0x31, 0x56, 0xb1, 0x34, 0x7a, 0x75, 0x45, + 0xaf, 0x69, 0x67, 0x35, 0xd3, 0xc4, 0x8c, 0x55, 0x9a, 0x51, 0x46, 0xcc, 0xc3, 0x80, 0x0e, 0xde, 0xb4, 0xbb, 0xd4, + 0xc3, 0x73, 0x9e, 0xcd, 0xcc, 0xe0, 0x24, 0x8b, 0xd8, 0x88, 0x4d, 0x94, 0x8f, 0x52, 0xd6, 0x8b, 0xc0, 0x63, 0xf9, + 0x19, 0x9e, 0x31, 0xc0, 0x6d, 0x16, 0xf1, 0x80, 0x28, 0xb5, 0x67, 0x86, 0x2f, 0x23, 0x0c, 0x0c, 0x67, 0x4b, 0x63, + 0xae, 0x9e, 0x68, 0x8a, 0x9e, 0xc0, 0x7a, 0x7e, 0x4a, 0xe9, 0x53, 0x77, 0x73, 0x28, 0xe1, 0x48, 0x78, 0x51, 0x65, + 0x87, 0x54, 0x26, 0xf6, 0xbb, 0x9a, 0x39, 0x2e, 0x99, 0x31, 0x18, 0xc1, 0xb7, 0x37, 0x16, 0x52, 0x52, 0x34, 0xfd, + 0x15, 0x94, 0x1f, 0x5a, 0x80, 0xdd, 0x6c, 0x45, 0x85, 0xd8, 0xea, 0x5d, 0xf8, 0x42, 0xab, 0xe2, 0xd1, 0x7c, 0x4e, + 0x0d, 0x5d, 0xa0, 0x53, 0x52, 0xa9, 0x91, 0x71, 0x50, 0x2c, 0x5c, 0x84, 0x9e, 0x65, 0x1b, 0x49, 0xd0, 0xe2, 0x49, + 0x06, 0xa5, 0xe9, 0xf7, 0x0d, 0xff, 0x3f, 0xdf, 0x8d, 0x21, 0x2b, 0x85, 0x78, 0x00, 0x00}; + +} // namespace web_server +} // namespace esphome diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 44d044750e..af7273fbde 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -24,11 +24,20 @@ #include "esphome/components/fan/fan_helpers.h" #endif +#ifdef USE_CLIMATE +#include "esphome/components/climate/climate.h" +#endif + +#ifdef USE_WEBSERVER_LOCAL +#include "server_index.h" +#endif + namespace esphome { namespace web_server { static const char *const TAG = "web_server"; +#if USE_WEBSERVER_VERSION == 1 void write_row(AsyncResponseStream *stream, EntityBase *obj, const std::string &klass, const std::string &action, const std::function &action_func = nullptr) { stream->print("print(""); stream->print(""); } +#endif UrlMatch match_url(const std::string &url, bool only_domain = false) { UrlMatch match; @@ -87,76 +97,94 @@ void WebServer::setup() { this->base_->init(); this->events_.onConnect([this](AsyncEventSourceClient *client) { - // Configure reconnect timeout - client->send("", "ping", millis(), 30000); + // Configure reconnect timeout and send config + + client->send(json::build_json([this](JsonObject root) { + root["title"] = App.get_name(); + root["ota"] = this->allow_ota_; + root["lang"] = "en"; + }).c_str(), + "ping", millis(), 30000); #ifdef USE_SENSOR for (auto *obj : App.get_sensors()) { if (this->include_internal_ || !obj->is_internal()) - client->send(this->sensor_json(obj, obj->state).c_str(), "state"); + client->send(this->sensor_json(obj, obj->state, DETAIL_ALL).c_str(), "state"); } #endif #ifdef USE_SWITCH for (auto *obj : App.get_switches()) { if (this->include_internal_ || !obj->is_internal()) - client->send(this->switch_json(obj, obj->state).c_str(), "state"); + client->send(this->switch_json(obj, obj->state, DETAIL_ALL).c_str(), "state"); } #endif +#ifdef USE_BUTTON + for (auto *obj : App.get_buttons()) + client->send(this->button_json(obj, DETAIL_ALL).c_str(), "state"); +#endif + #ifdef USE_BINARY_SENSOR for (auto *obj : App.get_binary_sensors()) { if (this->include_internal_ || !obj->is_internal()) - client->send(this->binary_sensor_json(obj, obj->state).c_str(), "state"); + client->send(this->binary_sensor_json(obj, obj->state, DETAIL_ALL).c_str(), "state"); } #endif #ifdef USE_FAN for (auto *obj : App.get_fans()) { if (this->include_internal_ || !obj->is_internal()) - client->send(this->fan_json(obj).c_str(), "state"); + client->send(this->fan_json(obj, DETAIL_ALL).c_str(), "state"); } #endif #ifdef USE_LIGHT for (auto *obj : App.get_lights()) { if (this->include_internal_ || !obj->is_internal()) - client->send(this->light_json(obj).c_str(), "state"); + client->send(this->light_json(obj, DETAIL_ALL).c_str(), "state"); } #endif #ifdef USE_TEXT_SENSOR for (auto *obj : App.get_text_sensors()) { if (this->include_internal_ || !obj->is_internal()) - client->send(this->text_sensor_json(obj, obj->state).c_str(), "state"); + client->send(this->text_sensor_json(obj, obj->state, DETAIL_ALL).c_str(), "state"); } #endif #ifdef USE_COVER for (auto *obj : App.get_covers()) { if (this->include_internal_ || !obj->is_internal()) - client->send(this->cover_json(obj).c_str(), "state"); + client->send(this->cover_json(obj, DETAIL_ALL).c_str(), "state"); } #endif #ifdef USE_NUMBER for (auto *obj : App.get_numbers()) { if (this->include_internal_ || !obj->is_internal()) - client->send(this->number_json(obj, obj->state).c_str(), "state"); + client->send(this->number_json(obj, obj->state, DETAIL_ALL).c_str(), "state"); } #endif #ifdef USE_SELECT for (auto *obj : App.get_selects()) { if (this->include_internal_ || !obj->is_internal()) - client->send(this->select_json(obj, obj->state).c_str(), "state"); + client->send(this->select_json(obj, obj->state, DETAIL_ALL).c_str(), "state"); + } +#endif + +#ifdef USE_CLIMATE + for (auto *obj : App.get_climates()) { + if (this->include_internal_ || !obj->is_internal()) + client->send(this->climate_json(obj, DETAIL_ALL).c_str(), "state"); } #endif #ifdef USE_LOCK for (auto *obj : App.get_locks()) { if (this->include_internal_ || !obj->is_internal()) - client->send(this->lock_json(obj, obj->state).c_str(), "state"); + client->send(this->lock_json(obj, obj->state, DETAIL_ALL).c_str(), "state"); } #endif }); @@ -181,15 +209,27 @@ void WebServer::dump_config() { } float WebServer::get_setup_priority() const { return setup_priority::WIFI - 1.0f; } +#ifdef USE_WEBSERVER_LOCAL +void WebServer::handle_index_request(AsyncWebServerRequest *request) { + AsyncWebServerResponse *response = request->beginResponse_P(200, "text/html", INDEX_GZ, sizeof(INDEX_GZ)); + response->addHeader("Content-Encoding", "gzip"); + request->send(response); +} +#else void WebServer::handle_index_request(AsyncWebServerRequest *request) { AsyncResponseStream *stream = request->beginResponseStream("text/html"); - std::string title = App.get_name() + " Web Server"; - stream->print(F("" - "")); + // All content is controlled and created by user - so allowing all origins is fine here. + stream->addHeader("Access-Control-Allow-Origin", "*"); +#if USE_WEBSERVER_VERSION == 1 + const std::string &title = App.get_name(); + stream->print(F("<!DOCTYPE html><html lang=\"en\"><head><meta charset=UTF-8><meta " + "name=viewport content=\"width=device-width, initial-scale=1,user-scalable=no\"><title>")); stream->print(title.c_str()); stream->print(F("")); -#ifdef WEBSERVER_CSS_INCLUDE +#else + stream->print(F("")); +#endif +#ifdef USE_WEBSERVER_CSS_INCLUDE stream->print(F("")); #endif if (strlen(this->css_url_) > 0) { @@ -197,11 +237,12 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) { stream->print(this->css_url_); stream->print(F("\">")); } - stream->print(F("

")); + stream->print(F("")); +#if USE_WEBSERVER_VERSION == 1 + stream->print(F("

")); stream->print(title.c_str()); - stream->print(F("

States

")); - // All content is controlled and created by user - so allowing all origins is fine here. - stream->addHeader("Access-Control-Allow-Origin", "*"); + stream->print(F("")); + stream->print(F("

States

NameStateActions
")); #ifdef USE_SENSOR for (auto *obj : App.get_sensors()) { @@ -308,6 +349,13 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) { } #endif +#ifdef USE_CLIMATE + for (auto *obj : App.get_climates()) { + if (this->include_internal_ || !obj->is_internal()) + write_row(stream, obj, "climate", ""); + } +#endif + stream->print(F("
NameStateActions

See ESPHome Web API for " "REST API documentation.

")); if (this->allow_ota_) { @@ -316,23 +364,30 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) { "type=\"file\" name=\"update\">")); } stream->print(F("

Debug Log

"));
-
-#ifdef WEBSERVER_JS_INCLUDE
+#endif
+#ifdef USE_WEBSERVER_JS_INCLUDE
   if (this->js_include_ != nullptr) {
-    stream->print(F(""));
+    stream->print(F(""));
   }
+#endif
+#if USE_WEBSERVER_VERSION == 2
+  stream->print(F(""));
 #endif
   if (strlen(this->js_url_) > 0) {
     stream->print(F(""));
   }
+#if USE_WEBSERVER_VERSION == 1
   stream->print(F("
")); +#else + stream->print(F("")); +#endif request->send(stream); } - -#ifdef WEBSERVER_CSS_INCLUDE +#endif +#ifdef USE_WEBSERVER_CSS_INCLUDE void WebServer::handle_css_request(AsyncWebServerRequest *request) { AsyncResponseStream *stream = request->beginResponseStream("text/css"); if (this->css_include_ != nullptr) { @@ -343,10 +398,11 @@ void WebServer::handle_css_request(AsyncWebServerRequest *request) { } #endif -#ifdef WEBSERVER_JS_INCLUDE +#ifdef USE_WEBSERVER_JS_INCLUDE void WebServer::handle_js_request(AsyncWebServerRequest *request) { AsyncResponseStream *stream = request->beginResponseStream("text/javascript"); if (this->js_include_ != nullptr) { + stream->addHeader("Access-Control-Allow-Origin", "*"); stream->print(this->js_include_); } @@ -354,64 +410,75 @@ void WebServer::handle_js_request(AsyncWebServerRequest *request) { } #endif +#define set_json_id(root, obj, sensor, start_config) \ + (root)["id"] = sensor; \ + if (((start_config) == DETAIL_ALL)) \ + (root)["name"] = (obj)->get_name(); + +#define set_json_value(root, obj, sensor, value, start_config) \ + set_json_id((root), (obj), sensor, start_config)(root)["value"] = value; + +#define set_json_state_value(root, obj, sensor, state, value, start_config) \ + set_json_value(root, obj, sensor, value, start_config)(root)["state"] = state; + +#define set_json_icon_state_value(root, obj, sensor, state, value, start_config) \ + set_json_value(root, obj, sensor, value, start_config)(root)["state"] = state; \ + if (((start_config) == DETAIL_ALL)) \ + (root)["icon"] = (obj)->get_icon(); + #ifdef USE_SENSOR void WebServer::on_sensor_update(sensor::Sensor *obj, float state) { - this->events_.send(this->sensor_json(obj, state).c_str(), "state"); + this->events_.send(this->sensor_json(obj, state, DETAIL_STATE).c_str(), "state"); } void WebServer::handle_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (sensor::Sensor *obj : App.get_sensors()) { if (obj->get_object_id() != match.id) continue; - std::string data = this->sensor_json(obj, obj->state); + std::string data = this->sensor_json(obj, obj->state, DETAIL_STATE); request->send(200, "text/json", data.c_str()); return; } request->send(404); } -std::string WebServer::sensor_json(sensor::Sensor *obj, float value) { - return json::build_json([obj, value](JsonObject root) { - root["id"] = "sensor-" + obj->get_object_id(); +std::string WebServer::sensor_json(sensor::Sensor *obj, float value, JsonDetail start_config) { + return json::build_json([obj, value, start_config](JsonObject root) { std::string state = value_accuracy_to_string(value, obj->get_accuracy_decimals()); if (!obj->get_unit_of_measurement().empty()) state += " " + obj->get_unit_of_measurement(); - root["state"] = state; - root["value"] = value; + set_json_icon_state_value(root, obj, "sensor-" + obj->get_object_id(), state, value, start_config); }); } #endif #ifdef USE_TEXT_SENSOR void WebServer::on_text_sensor_update(text_sensor::TextSensor *obj, const std::string &state) { - this->events_.send(this->text_sensor_json(obj, state).c_str(), "state"); + this->events_.send(this->text_sensor_json(obj, state, DETAIL_STATE).c_str(), "state"); } void WebServer::handle_text_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (text_sensor::TextSensor *obj : App.get_text_sensors()) { if (obj->get_object_id() != match.id) continue; - std::string data = this->text_sensor_json(obj, obj->state); + std::string data = this->text_sensor_json(obj, obj->state, DETAIL_STATE); request->send(200, "text/json", data.c_str()); return; } request->send(404); } -std::string WebServer::text_sensor_json(text_sensor::TextSensor *obj, const std::string &value) { - return json::build_json([obj, value](JsonObject root) { - root["id"] = "text_sensor-" + obj->get_object_id(); - root["state"] = value; - root["value"] = value; +std::string WebServer::text_sensor_json(text_sensor::TextSensor *obj, const std::string &value, + JsonDetail start_config) { + return json::build_json([obj, value, start_config](JsonObject root) { + set_json_icon_state_value(root, obj, "text_sensor-" + obj->get_object_id(), value, value, start_config); }); } #endif #ifdef USE_SWITCH void WebServer::on_switch_update(switch_::Switch *obj, bool state) { - this->events_.send(this->switch_json(obj, state).c_str(), "state"); + this->events_.send(this->switch_json(obj, state, DETAIL_STATE).c_str(), "state"); } -std::string WebServer::switch_json(switch_::Switch *obj, bool value) { - return json::build_json([obj, value](JsonObject root) { - root["id"] = "switch-" + obj->get_object_id(); - root["state"] = value ? "ON" : "OFF"; - root["value"] = value; +std::string WebServer::switch_json(switch_::Switch *obj, bool value, JsonDetail start_config) { + return json::build_json([obj, value, start_config](JsonObject root) { + set_json_icon_state_value(root, obj, "switch-" + obj->get_object_id(), value ? "ON" : "OFF", value, start_config); }); } void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -420,7 +487,7 @@ void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlM continue; if (request->method() == HTTP_GET) { - std::string data = this->switch_json(obj, obj->state); + std::string data = this->switch_json(obj, obj->state, DETAIL_STATE); request->send(200, "text/json", data.c_str()); } else if (match.method == "toggle") { this->defer([obj]() { obj->toggle(); }); @@ -441,14 +508,19 @@ void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlM #endif #ifdef USE_BUTTON +std::string WebServer::button_json(button::Button *obj, JsonDetail start_config) { + return json::build_json( + [obj, start_config](JsonObject root) { set_json_id(root, obj, "button-" + obj->get_object_id(), start_config); }); +} + void WebServer::handle_button_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (button::Button *obj : App.get_buttons()) { if (obj->get_object_id() != match.id) continue; - if (request->method() == HTTP_POST && match.method == "press") { this->defer([obj]() { obj->press(); }); request->send(200); + return; } else { request->send(404); } @@ -460,20 +532,18 @@ void WebServer::handle_button_request(AsyncWebServerRequest *request, const UrlM #ifdef USE_BINARY_SENSOR void WebServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state) { - this->events_.send(this->binary_sensor_json(obj, state).c_str(), "state"); + this->events_.send(this->binary_sensor_json(obj, state, DETAIL_STATE).c_str(), "state"); } -std::string WebServer::binary_sensor_json(binary_sensor::BinarySensor *obj, bool value) { - return json::build_json([obj, value](JsonObject root) { - root["id"] = "binary_sensor-" + obj->get_object_id(); - root["state"] = value ? "ON" : "OFF"; - root["value"] = value; +std::string WebServer::binary_sensor_json(binary_sensor::BinarySensor *obj, bool value, JsonDetail start_config) { + return json::build_json([obj, value, start_config](JsonObject root) { + set_json_state_value(root, obj, "binary_sensor-" + obj->get_object_id(), value ? "ON" : "OFF", value, start_config); }); } void WebServer::handle_binary_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (binary_sensor::BinarySensor *obj : App.get_binary_sensors()) { if (obj->get_object_id() != match.id) continue; - std::string data = this->binary_sensor_json(obj, obj->state); + std::string data = this->binary_sensor_json(obj, obj->state, DETAIL_STATE); request->send(200, "text/json", data.c_str()); return; } @@ -482,15 +552,15 @@ void WebServer::handle_binary_sensor_request(AsyncWebServerRequest *request, con #endif #ifdef USE_FAN -void WebServer::on_fan_update(fan::Fan *obj) { this->events_.send(this->fan_json(obj).c_str(), "state"); } -std::string WebServer::fan_json(fan::Fan *obj) { - return json::build_json([obj](JsonObject root) { - root["id"] = "fan-" + obj->get_object_id(); - root["state"] = obj->state ? "ON" : "OFF"; - root["value"] = obj->state; +void WebServer::on_fan_update(fan::Fan *obj) { this->events_.send(this->fan_json(obj, DETAIL_STATE).c_str(), "state"); } +std::string WebServer::fan_json(fan::Fan *obj, JsonDetail start_config) { + return json::build_json([obj, start_config](JsonObject root) { + set_json_state_value(root, obj, "fan-" + obj->get_object_id(), obj->state ? "ON" : "OFF", obj->state, start_config); const auto traits = obj->get_traits(); if (traits.supports_speed()) { root["speed_level"] = obj->speed; + root["speed_count"] = traits.supported_speed_count(); + #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdeprecated-declarations" // NOLINTNEXTLINE(clang-diagnostic-deprecated-declarations) @@ -517,7 +587,7 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc continue; if (request->method() == HTTP_GET) { - std::string data = this->fan_json(obj); + std::string data = this->fan_json(obj, DETAIL_STATE); request->send(200, "text/json", data.c_str()); } else if (match.method == "toggle") { this->defer([obj]() { obj->toggle().perform(); }); @@ -573,14 +643,16 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc #endif #ifdef USE_LIGHT -void WebServer::on_light_update(light::LightState *obj) { this->events_.send(this->light_json(obj).c_str(), "state"); } +void WebServer::on_light_update(light::LightState *obj) { + this->events_.send(this->light_json(obj, DETAIL_STATE).c_str(), "state"); +} void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (light::LightState *obj : App.get_lights()) { if (obj->get_object_id() != match.id) continue; if (request->method() == HTTP_GET) { - std::string data = this->light_json(obj); + std::string data = this->light_json(obj, DETAIL_STATE); request->send(200, "text/json", data.c_str()); } else if (match.method == "toggle") { this->defer([obj]() { obj->toggle().perform(); }); @@ -632,24 +704,34 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa } request->send(404); } -std::string WebServer::light_json(light::LightState *obj) { - return json::build_json([obj](JsonObject root) { - root["id"] = "light-" + obj->get_object_id(); +std::string WebServer::light_json(light::LightState *obj, JsonDetail start_config) { + return json::build_json([obj, start_config](JsonObject root) { + set_json_id(root, obj, "light-" + obj->get_object_id(), start_config); root["state"] = obj->remote_values.is_on() ? "ON" : "OFF"; + light::LightJSONSchema::dump_json(*obj, root); + if (start_config == DETAIL_ALL) { + JsonArray opt = root.createNestedArray("effects"); + opt.add("None"); + for (auto const &option : obj->get_effects()) { + opt.add(option->get_name()); + } + } }); } #endif #ifdef USE_COVER -void WebServer::on_cover_update(cover::Cover *obj) { this->events_.send(this->cover_json(obj).c_str(), "state"); } +void WebServer::on_cover_update(cover::Cover *obj) { + this->events_.send(this->cover_json(obj, DETAIL_STATE).c_str(), "state"); +} void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (cover::Cover *obj : App.get_covers()) { if (obj->get_object_id() != match.id) continue; if (request->method() == HTTP_GET) { - std::string data = this->cover_json(obj); + std::string data = this->cover_json(obj, DETAIL_STATE); request->send(200, "text/json", data.c_str()); continue; } @@ -684,11 +766,10 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa } request->send(404); } -std::string WebServer::cover_json(cover::Cover *obj) { - return json::build_json([obj](JsonObject root) { - root["id"] = "cover-" + obj->get_object_id(); - root["state"] = obj->is_fully_closed() ? "CLOSED" : "OPEN"; - root["value"] = obj->position; +std::string WebServer::cover_json(cover::Cover *obj, JsonDetail start_config) { + return json::build_json([obj, start_config](JsonObject root) { + set_json_state_value(root, obj, "cover-" + obj->get_object_id(), obj->is_fully_closed() ? "CLOSED" : "OPEN", + obj->position, start_config); root["current_operation"] = cover::cover_operation_to_str(obj->current_operation); if (obj->get_traits().get_supports_tilt()) @@ -699,7 +780,7 @@ std::string WebServer::cover_json(cover::Cover *obj) { #ifdef USE_NUMBER void WebServer::on_number_update(number::Number *obj, float state) { - this->events_.send(this->number_json(obj, state).c_str(), "state"); + this->events_.send(this->number_json(obj, state, DETAIL_STATE).c_str(), "state"); } void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_numbers()) { @@ -707,18 +788,16 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM continue; if (request->method() == HTTP_GET) { - std::string data = this->number_json(obj, obj->state); + std::string data = this->number_json(obj, obj->state, DETAIL_STATE); request->send(200, "text/json", data.c_str()); return; } - if (match.method != "set") { request->send(404); return; } auto call = obj->make_call(); - if (request->hasParam("value")) { String value = request->getParam("value")->value(); optional value_f = parse_number(value.c_str()); @@ -732,19 +811,30 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM } request->send(404); } -std::string WebServer::number_json(number::Number *obj, float value) { - return json::build_json([obj, value](JsonObject root) { - root["id"] = "number-" + obj->get_object_id(); + +std::string WebServer::number_json(number::Number *obj, float value, JsonDetail start_config) { + return json::build_json([obj, value, start_config](JsonObject root) { + set_json_id(root, obj, "number-" + obj->get_object_id(), start_config); + if (start_config == DETAIL_ALL) { + root["min_value"] = obj->traits.get_min_value(); + root["max_value"] = obj->traits.get_max_value(); + root["step"] = obj->traits.get_step(); + root["mode"] = (int) obj->traits.get_mode(); + } std::string state = str_sprintf("%f", value); root["state"] = state; - root["value"] = value; + if (isnan(value)) { + root["value"] = "\"NaN\""; + } else { + root["value"] = value; + } }); } #endif #ifdef USE_SELECT void WebServer::on_select_update(select::Select *obj, const std::string &state) { - this->events_.send(this->select_json(obj, state).c_str(), "state"); + this->events_.send(this->select_json(obj, state, DETAIL_STATE).c_str(), "state"); } void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlMatch &match) { for (auto *obj : App.get_selects()) { @@ -752,7 +842,7 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM continue; if (request->method() == HTTP_GET) { - std::string data = this->select_json(obj, obj->state); + std::string data = this->select_json(obj, obj->state, DETAIL_STATE); request->send(200, "text/json", data.c_str()); return; } @@ -775,24 +865,158 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM } request->send(404); } -std::string WebServer::select_json(select::Select *obj, const std::string &value) { - return json::build_json([obj, value](JsonObject root) { - root["id"] = "select-" + obj->get_object_id(); - root["state"] = value; - root["value"] = value; +std::string WebServer::select_json(select::Select *obj, const std::string &value, JsonDetail start_config) { + return json::build_json([obj, value, start_config](JsonObject root) { + set_json_state_value(root, obj, "select-" + obj->get_object_id(), value, value, start_config); + if (start_config == DETAIL_ALL) { + JsonArray opt = root.createNestedArray("option"); + for (auto &option : obj->traits.get_options()) { + opt.add(option); + } + } + }); +} +#endif + +#ifdef USE_CLIMATE +void WebServer::on_climate_update(climate::Climate *obj) { + this->events_.send(this->climate_json(obj, DETAIL_STATE).c_str(), "state"); +} + +void WebServer::handle_climate_request(AsyncWebServerRequest *request, const UrlMatch &match) { + for (auto *obj : App.get_climates()) { + if (obj->get_object_id() != match.id) + continue; + + if (request->method() == HTTP_GET) { + std::string data = this->climate_json(obj, DETAIL_STATE); + request->send(200, "text/json", data.c_str()); + return; + } + + if (match.method != "set") { + request->send(404); + return; + } + + auto call = obj->make_call(); + + if (request->hasParam("mode")) { + String mode = request->getParam("mode")->value(); + call.set_mode(mode.c_str()); + } + + if (request->hasParam("target_temperature_high")) { + String value = request->getParam("target_temperature_high")->value(); + optional value_f = parse_number(value.c_str()); + if (value_f.has_value()) + call.set_target_temperature_high(*value_f); + } + + if (request->hasParam("target_temperature_low")) { + String value = request->getParam("target_temperature_low")->value(); + optional value_f = parse_number(value.c_str()); + if (value_f.has_value()) + call.set_target_temperature_low(*value_f); + } + + if (request->hasParam("target_temperature")) { + String value = request->getParam("target_temperature")->value(); + optional value_f = parse_number(value.c_str()); + if (value_f.has_value()) + call.set_target_temperature(*value_f); + } + + this->defer([call]() mutable { call.perform(); }); + request->send(200); + return; + } + request->send(404); +} + +// Longest: HORIZONTAL +#define PSTR_LOCAL(mode_s) strncpy_P(__buf, (PGM_P)((mode_s)), 15) + +std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_config) { + return json::build_json([obj, start_config](JsonObject root) { + set_json_id(root, obj, "climate-" + obj->get_object_id(), start_config); + const auto traits = obj->get_traits(); + char __buf[16]; + + if (start_config == DETAIL_ALL) { + JsonArray opt = root.createNestedArray("modes"); + for (climate::ClimateMode m : traits.get_supported_modes()) + opt.add(PSTR_LOCAL(climate::climate_mode_to_string(m))); + if (!traits.get_supported_custom_fan_modes().empty()) { + JsonArray opt = root.createNestedArray("fan_modes"); + for (climate::ClimateFanMode m : traits.get_supported_fan_modes()) + opt.add(PSTR_LOCAL(climate::climate_fan_mode_to_string(m))); + } + + if (!traits.get_supported_custom_fan_modes().empty()) { + JsonArray opt = root.createNestedArray("custom_fan_modes"); + for (auto const &custom_fan_mode : traits.get_supported_custom_fan_modes()) + opt.add(custom_fan_mode); + } + if (traits.get_supports_swing_modes()) { + JsonArray opt = root.createNestedArray("swing_modes"); + for (auto swing_mode : traits.get_supported_swing_modes()) + opt.add(PSTR_LOCAL(climate::climate_swing_mode_to_string(swing_mode))); + } + if (traits.get_supports_presets() && obj->preset.has_value()) { + JsonArray opt = root.createNestedArray("presets"); + for (climate::ClimatePreset m : traits.get_supported_presets()) + opt.add(PSTR_LOCAL(climate::climate_preset_to_string(m))); + } + if (!traits.get_supported_custom_presets().empty() && obj->custom_preset.has_value()) { + JsonArray opt = root.createNestedArray("custom_presets"); + for (auto const &custom_preset : traits.get_supported_custom_presets()) + opt.add(custom_preset); + } + } + + root["mode"] = PSTR_LOCAL(climate_mode_to_string(obj->mode)); + + if (traits.get_supports_action()) { + root["action"] = PSTR_LOCAL(climate_action_to_string(obj->action)); + } + if (traits.get_supports_fan_modes() && obj->fan_mode.has_value()) { + root["fan_mode"] = PSTR_LOCAL(climate_fan_mode_to_string(obj->fan_mode.value())); + } + if (!traits.get_supported_custom_fan_modes().empty() && obj->custom_fan_mode.has_value()) { + root["custom_fan_mode"] = obj->custom_fan_mode.value().c_str(); + } + if (traits.get_supports_presets() && obj->preset.has_value()) { + root["preset"] = PSTR_LOCAL(climate_preset_to_string(obj->preset.value())); + } + if (!traits.get_supported_custom_presets().empty() && obj->custom_preset.has_value()) { + root["custom_preset"] = obj->custom_preset.value().c_str(); + } + if (traits.get_supports_swing_modes()) { + root["swing_mode"] = PSTR_LOCAL(climate_swing_mode_to_string(obj->swing_mode)); + } + if (traits.get_supports_current_temperature()) { + root["current_temperature"] = obj->current_temperature; + } + if (traits.get_supports_two_point_target_temperature()) { + root["current_temperature_low"] = obj->target_temperature_low; + root["current_temperature_high"] = obj->target_temperature_low; + } else { + root["target_temperature"] = obj->target_temperature; + root["state"] = obj->target_temperature; + } }); } #endif #ifdef USE_LOCK void WebServer::on_lock_update(lock::Lock *obj) { - this->events_.send(this->lock_json(obj, obj->state).c_str(), "state"); + this->events_.send(this->lock_json(obj, obj->state, DETAIL_STATE).c_str(), "state"); } -std::string WebServer::lock_json(lock::Lock *obj, lock::LockState value) { - return json::build_json([obj, value](JsonObject root) { - root["id"] = "lock-" + obj->get_object_id(); - root["state"] = lock::lock_state_to_string(value); - root["value"] = value; +std::string WebServer::lock_json(lock::Lock *obj, lock::LockState value, JsonDetail start_config) { + return json::build_json([obj, value, start_config](JsonObject root) { + set_json_icon_state_value(root, obj, "lock-" + obj->get_object_id(), lock::lock_state_to_string(value), value, + start_config); }); } void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMatch &match) { @@ -801,7 +1025,7 @@ void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMat continue; if (request->method() == HTTP_GET) { - std::string data = this->lock_json(obj, obj->state); + std::string data = this->lock_json(obj, obj->state, DETAIL_STATE); request->send(200, "text/json", data.c_str()); } else if (match.method == "lock") { this->defer([obj]() { obj->lock(); }); @@ -825,12 +1049,12 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) { if (request->url() == "/") return true; -#ifdef WEBSERVER_CSS_INCLUDE +#ifdef USE_WEBSERVER_CSS_INCLUDE if (request->url() == "/0.css") return true; #endif -#ifdef WEBSERVER_JS_INCLUDE +#ifdef USE_WEBSERVER_JS_INCLUDE if (request->url() == "/0.js") return true; #endif @@ -888,6 +1112,11 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) { return true; #endif +#ifdef USE_CLIMATE + if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "climate") + return true; +#endif + #ifdef USE_LOCK if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "lock") return true; @@ -901,14 +1130,14 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) { return; } -#ifdef WEBSERVER_CSS_INCLUDE +#ifdef USE_WEBSERVER_CSS_INCLUDE if (request->url() == "/0.css") { this->handle_css_request(request); return; } #endif -#ifdef WEBSERVER_JS_INCLUDE +#ifdef USE_WEBSERVER_JS_INCLUDE if (request->url() == "/0.js") { this->handle_js_request(request); return; @@ -986,9 +1215,17 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) { } #endif +#ifdef USE_CLIMATE + if (match.domain == "climate") { + this->handle_climate_request(request, match); + return; + } +#endif + #ifdef USE_LOCK if (match.domain == "lock") { this->handle_lock_request(request, match); + return; } #endif diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 3dd5c93f59..2717997f60 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -2,9 +2,9 @@ #ifdef USE_ARDUINO +#include "esphome/components/web_server_base/web_server_base.h" #include "esphome/core/component.h" #include "esphome/core/controller.h" -#include "esphome/components/web_server_base/web_server_base.h" #include @@ -19,6 +19,8 @@ struct UrlMatch { bool valid; ///< Whether this match is valid }; +enum JsonDetail { DETAIL_ALL, DETAIL_STATE }; + /** This class allows users to create a web server with their ESP nodes. * * Behind the scenes it's using AsyncWebServer to set up the server. It exposes 3 things: @@ -99,7 +101,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { void handle_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match); /// Dump the sensor state with its value as a JSON string. - std::string sensor_json(sensor::Sensor *obj, float value); + std::string sensor_json(sensor::Sensor *obj, float value, JsonDetail start_config); #endif #ifdef USE_SWITCH @@ -109,12 +111,15 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { void handle_switch_request(AsyncWebServerRequest *request, const UrlMatch &match); /// Dump the switch state with its value as a JSON string. - std::string switch_json(switch_::Switch *obj, bool value); + std::string switch_json(switch_::Switch *obj, bool value, JsonDetail start_config); #endif #ifdef USE_BUTTON /// Handle a button request under '/button//press'. void handle_button_request(AsyncWebServerRequest *request, const UrlMatch &match); + + /// Dump the button details with its value as a JSON string. + std::string button_json(button::Button *obj, JsonDetail start_config); #endif #ifdef USE_BINARY_SENSOR @@ -124,7 +129,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { void handle_binary_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match); /// Dump the binary sensor state with its value as a JSON string. - std::string binary_sensor_json(binary_sensor::BinarySensor *obj, bool value); + std::string binary_sensor_json(binary_sensor::BinarySensor *obj, bool value, JsonDetail start_config); #endif #ifdef USE_FAN @@ -134,7 +139,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { void handle_fan_request(AsyncWebServerRequest *request, const UrlMatch &match); /// Dump the fan state as a JSON string. - std::string fan_json(fan::Fan *obj); + std::string fan_json(fan::Fan *obj, JsonDetail start_config); #endif #ifdef USE_LIGHT @@ -144,7 +149,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { void handle_light_request(AsyncWebServerRequest *request, const UrlMatch &match); /// Dump the light state as a JSON string. - std::string light_json(light::LightState *obj); + std::string light_json(light::LightState *obj, JsonDetail start_config); #endif #ifdef USE_TEXT_SENSOR @@ -154,7 +159,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { void handle_text_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match); /// Dump the text sensor state with its value as a JSON string. - std::string text_sensor_json(text_sensor::TextSensor *obj, const std::string &value); + std::string text_sensor_json(text_sensor::TextSensor *obj, const std::string &value, JsonDetail start_config); #endif #ifdef USE_COVER @@ -164,7 +169,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { void handle_cover_request(AsyncWebServerRequest *request, const UrlMatch &match); /// Dump the cover state as a JSON string. - std::string cover_json(cover::Cover *obj); + std::string cover_json(cover::Cover *obj, JsonDetail start_config); #endif #ifdef USE_NUMBER @@ -173,7 +178,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { void handle_number_request(AsyncWebServerRequest *request, const UrlMatch &match); /// Dump the number state with its value as a JSON string. - std::string number_json(number::Number *obj, float value); + std::string number_json(number::Number *obj, float value, JsonDetail start_config); #endif #ifdef USE_SELECT @@ -181,8 +186,17 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle a select request under '/select/'. void handle_select_request(AsyncWebServerRequest *request, const UrlMatch &match); - /// Dump the number state with its value as a JSON string. - std::string select_json(select::Select *obj, const std::string &value); + /// Dump the select state with its value as a JSON string. + std::string select_json(select::Select *obj, const std::string &value, JsonDetail start_config); +#endif + +#ifdef USE_CLIMATE + void on_climate_update(climate::Climate *obj) override; + /// Handle a climate request under '/climate/'. + void handle_climate_request(AsyncWebServerRequest *request, const UrlMatch &match); + + /// Dump the climate details + std::string climate_json(climate::Climate *obj, JsonDetail start_config); #endif #ifdef USE_LOCK @@ -192,7 +206,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { void handle_lock_request(AsyncWebServerRequest *request, const UrlMatch &match); /// Dump the lock state with its value as a JSON string. - std::string lock_json(lock::Lock *obj, lock::LockState value); + std::string lock_json(lock::Lock *obj, lock::LockState value, JsonDetail start_config); #endif /// Override the web handler's canHandle method. diff --git a/tests/test1.yaml b/tests/test1.yaml index 8cd01f1d6f..3763fd3fa5 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -247,9 +247,7 @@ logger: web_server: port: 8080 - ota: true - css_url: https://esphome.io/_static/webserver-v1.min.css - js_url: https://esphome.io/_static/webserver-v1.min.js + version: 2 power_supply: id: "atx_power_supply" From 4c22a98b0b20c94911c840b97fe54c5938baab37 Mon Sep 17 00:00:00 2001 From: JasperPlant <78851352+JasperPlant@users.noreply.github.com> Date: Tue, 8 Mar 2022 03:21:13 +0100 Subject: [PATCH 0308/1729] Add entity_category_diagnostics to SGP30 baseline sensors (#3272) --- esphome/components/sgp30/sensor.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/sgp30/sensor.py b/esphome/components/sgp30/sensor.py index 2596e0065d..14a078b501 100644 --- a/esphome/components/sgp30/sensor.py +++ b/esphome/components/sgp30/sensor.py @@ -13,6 +13,7 @@ from esphome.const import ( UNIT_PARTS_PER_MILLION, UNIT_PARTS_PER_BILLION, ICON_MOLECULE_CO2, + ENTITY_CATEGORY_DIAGNOSTIC, ) DEPENDENCIES = ["i2c"] @@ -49,10 +50,12 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_ECO2_BASELINE): sensor.sensor_schema( icon=ICON_MOLECULE_CO2, accuracy_decimals=0, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), cv.Optional(CONF_TVOC_BASELINE): sensor.sensor_schema( icon=ICON_RADIATOR, accuracy_decimals=0, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, ), cv.Optional(CONF_STORE_BASELINE, default=True): cv.boolean, cv.Optional(CONF_BASELINE): cv.Schema( From 900b4f1af96221181f5a21b9193e2a2266a57c1b Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 9 Mar 2022 10:27:54 +1300 Subject: [PATCH 0309/1729] Bump esphome-dashboard to 20220309.0 (#3277) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 427045af02..739ad79098 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ pyserial==3.5 platformio==5.2.5 # When updating platformio, also update Dockerfile esptool==3.2 click==8.0.3 -esphome-dashboard==20220219.0 +esphome-dashboard==20220309.0 aioesphomeapi==10.8.2 zeroconf==0.38.3 From 5b2457af0b26be0f93b7f9cf51fced6e7cb61cf4 Mon Sep 17 00:00:00 2001 From: wilberforce Date: Wed, 9 Mar 2022 10:28:16 +1300 Subject: [PATCH 0310/1729] Add visual step/min/max for webserver climate (#3275) --- esphome/components/web_server/web_server.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index af7273fbde..278aeab937 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -976,7 +976,9 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf } root["mode"] = PSTR_LOCAL(climate_mode_to_string(obj->mode)); - + root["max_temp"] = traits.get_visual_max_temperature(); + root["min_temp"] = traits.get_visual_min_temperature(); + root["step"] = traits.get_visual_temperature_step(); if (traits.get_supports_action()) { root["action"] = PSTR_LOCAL(climate_action_to_string(obj->action)); } From 023d26f5218c8bcafee022873e7737d5cf5838f2 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 9 Mar 2022 20:07:50 +1300 Subject: [PATCH 0311/1729] Bump version to 2022.3.0b1 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 2ec00edf7b..26444baffd 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2022.3.0-dev" +__version__ = "2022.3.0b1" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From 0af1edefffbd283f1a40425a11e3d4c6e3a94000 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 9 Mar 2022 20:07:50 +1300 Subject: [PATCH 0312/1729] Bump version to 2022.4.0-dev --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 2ec00edf7b..46c9a659be 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2022.3.0-dev" +__version__ = "2022.4.0-dev" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From a29d65d47ca83c025ac05625d16417c8a7da1387 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Mar 2022 13:27:15 +0100 Subject: [PATCH 0313/1729] Bump pytest-asyncio from 0.18.1 to 0.18.2 (#3262) Bumps [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio) from 0.18.1 to 0.18.2. - [Release notes](https://github.com/pytest-dev/pytest-asyncio/releases) - [Commits](https://github.com/pytest-dev/pytest-asyncio/compare/v0.18.1...v0.18.2) --- updated-dependencies: - dependency-name: pytest-asyncio dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index afc4fd9d2a..8da8945c8b 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -8,6 +8,6 @@ pre-commit pytest==7.0.1 pytest-cov==3.0.0 pytest-mock==3.7.0 -pytest-asyncio==0.18.1 +pytest-asyncio==0.18.2 asyncmock==0.4.2 hypothesis==5.49.0 From 65d3e8fbfcda4b408897f3591865aa1ff3738da3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Mar 2022 13:27:33 +0100 Subject: [PATCH 0314/1729] Bump zeroconf from 0.38.3 to 0.38.4 (#3257) Bumps [zeroconf](https://github.com/jstasiak/python-zeroconf) from 0.38.3 to 0.38.4. - [Release notes](https://github.com/jstasiak/python-zeroconf/releases) - [Commits](https://github.com/jstasiak/python-zeroconf/compare/0.38.3...0.38.4) --- updated-dependencies: - dependency-name: zeroconf dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 739ad79098..bcc7c2a911 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ esptool==3.2 click==8.0.3 esphome-dashboard==20220309.0 aioesphomeapi==10.8.2 -zeroconf==0.38.3 +zeroconf==0.38.4 # esp-idf requires this, but doesn't bundle it by default # https://github.com/espressif/esp-idf/blob/220590d599e134d7a5e7f1e683cc4550349ffbf8/requirements.txt#L24 From 6bf733e24e4e2dbe5e3c01aa91ba8154517cad57 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Mar 2022 13:27:56 +0100 Subject: [PATCH 0315/1729] Bump click from 8.0.3 to 8.0.4 (#3248) Bumps [click](https://github.com/pallets/click) from 8.0.3 to 8.0.4. - [Release notes](https://github.com/pallets/click/releases) - [Changelog](https://github.com/pallets/click/blob/main/CHANGES.rst) - [Commits](https://github.com/pallets/click/compare/8.0.3...8.0.4) --- updated-dependencies: - dependency-name: click dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index bcc7c2a911..c23cdcd33f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ tzdata>=2021.1 # from time pyserial==3.5 platformio==5.2.5 # When updating platformio, also update Dockerfile esptool==3.2 -click==8.0.3 +click==8.0.4 esphome-dashboard==20220309.0 aioesphomeapi==10.8.2 zeroconf==0.38.4 From 3208c8ed1e488c10f28a3fffdd6f5f5d627848b3 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Wed, 9 Mar 2022 13:48:02 +0100 Subject: [PATCH 0316/1729] Bump docker dependencies (#3281) --- docker/Dockerfile | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 65e831f89b..ad4891d62e 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -6,13 +6,13 @@ ARG BASEIMGTYPE=docker # https://github.com/hassio-addons/addon-debian-base/releases -FROM ghcr.io/hassio-addons/debian-base/amd64:5.2.3 AS base-hassio-amd64 -FROM ghcr.io/hassio-addons/debian-base/aarch64:5.2.3 AS base-hassio-arm64 -FROM ghcr.io/hassio-addons/debian-base/armv7:5.2.3 AS base-hassio-armv7 +FROM ghcr.io/hassio-addons/debian-base/amd64:5.3.0 AS base-hassio-amd64 +FROM ghcr.io/hassio-addons/debian-base/aarch64:5.3.0 AS base-hassio-arm64 +FROM ghcr.io/hassio-addons/debian-base/armv7:5.3.0 AS base-hassio-armv7 # https://hub.docker.com/_/debian?tab=tags&page=1&name=bullseye -FROM debian:bullseye-20220125-slim AS base-docker-amd64 -FROM debian:bullseye-20220125-slim AS base-docker-arm64 -FROM debian:bullseye-20220125-slim AS base-docker-armv7 +FROM debian:bullseye-20220228-slim AS base-docker-amd64 +FROM debian:bullseye-20220228-slim AS base-docker-arm64 +FROM debian:bullseye-20220228-slim AS base-docker-armv7 # Use TARGETARCH/TARGETVARIANT defined by docker # https://docs.docker.com/engine/reference/builder/#automatic-platform-args-in-the-global-scope From aafdfa933e82706e2ddad140217e39cd084c6fe3 Mon Sep 17 00:00:00 2001 From: stegm Date: Wed, 9 Mar 2022 20:40:43 +0100 Subject: [PATCH 0317/1729] Add optimistic config flag to modbus select. (#3267) --- esphome/components/modbus_controller/select/__init__.py | 4 +++- esphome/components/modbus_controller/select/modbus_select.cpp | 3 +++ esphome/components/modbus_controller/select/modbus_select.h | 2 ++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/esphome/components/modbus_controller/select/__init__.py b/esphome/components/modbus_controller/select/__init__.py index 7d03064fa5..6f194ef2a3 100644 --- a/esphome/components/modbus_controller/select/__init__.py +++ b/esphome/components/modbus_controller/select/__init__.py @@ -1,7 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import select -from esphome.const import CONF_ADDRESS, CONF_ID, CONF_LAMBDA +from esphome.const import CONF_ADDRESS, CONF_ID, CONF_LAMBDA, CONF_OPTIMISTIC from esphome.jsonschema import jschema_composite from .. import ( @@ -79,6 +79,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_FORCE_NEW_RANGE, default=False): cv.boolean, cv.Required(CONF_OPTIONSMAP): ensure_option_map(), cv.Optional(CONF_USE_WRITE_MULTIPLE, default=False): cv.boolean, + cv.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, cv.Optional(CONF_LAMBDA): cv.returning_lambda, cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda, }, @@ -112,6 +113,7 @@ async def to_code(config): cg.add(parent.add_sensor_item(var)) cg.add(var.set_parent(parent)) cg.add(var.set_use_write_mutiple(config[CONF_USE_WRITE_MULTIPLE])) + cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) if CONF_LAMBDA in config: template_ = await cg.process_lambda( diff --git a/esphome/components/modbus_controller/select/modbus_select.cpp b/esphome/components/modbus_controller/select/modbus_select.cpp index 2c6b32f545..33cef39a18 100644 --- a/esphome/components/modbus_controller/select/modbus_select.cpp +++ b/esphome/components/modbus_controller/select/modbus_select.cpp @@ -80,6 +80,9 @@ void ModbusSelect::control(const std::string &value) { } parent_->queue_command(write_cmd); + + if (this->optimistic_) + this->publish_state(value); } } // namespace modbus_controller diff --git a/esphome/components/modbus_controller/select/modbus_select.h b/esphome/components/modbus_controller/select/modbus_select.h index 0875194768..2a31dfd7cc 100644 --- a/esphome/components/modbus_controller/select/modbus_select.h +++ b/esphome/components/modbus_controller/select/modbus_select.h @@ -32,6 +32,7 @@ class ModbusSelect : public Component, public select::Select, public SensorItem void set_parent(ModbusController *const parent) { this->parent_ = parent; } void set_use_write_mutiple(bool use_write_multiple) { this->use_write_multiple_ = use_write_multiple; } + void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } void set_template(transform_func_t &&f) { this->transform_func_ = f; } void set_write_template(write_transform_func_t &&f) { this->write_transform_func_ = f; } @@ -43,6 +44,7 @@ class ModbusSelect : public Component, public select::Select, public SensorItem std::vector mapping_; ModbusController *parent_; bool use_write_multiple_{false}; + bool optimistic_{false}; optional transform_func_; optional write_transform_func_; }; From 59f67796dcf94a05d21bdcee5450bec68f890106 Mon Sep 17 00:00:00 2001 From: Rai-Rai Date: Sun, 13 Mar 2022 20:00:00 +0100 Subject: [PATCH 0318/1729] Fixed wrong comment (#3286) --- esphome/components/climate/climate_mode.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/climate/climate_mode.h b/esphome/components/climate/climate_mode.h index 3e5626919c..139400a08a 100644 --- a/esphome/components/climate/climate_mode.h +++ b/esphome/components/climate/climate_mode.h @@ -76,7 +76,7 @@ enum ClimateSwingMode : uint8_t { CLIMATE_SWING_HORIZONTAL = 3, }; -/// Enum for all modes a climate swing can be in +/// Enum for all preset modes enum ClimatePreset : uint8_t { /// No preset is active CLIMATE_PRESET_NONE = 0, @@ -108,7 +108,7 @@ const LogString *climate_fan_mode_to_string(ClimateFanMode mode); /// Convert the given ClimateSwingMode to a human-readable string. const LogString *climate_swing_mode_to_string(ClimateSwingMode mode); -/// Convert the given ClimateSwingMode to a human-readable string. +/// Convert the given PresetMode to a human-readable string. const LogString *climate_preset_to_string(ClimatePreset preset); } // namespace climate From 99f5ed14614ae792174bb85f6b0b9579d15e7461 Mon Sep 17 00:00:00 2001 From: andrewpc Date: Tue, 15 Mar 2022 06:09:17 +1100 Subject: [PATCH 0319/1729] Add support for QMP6988 Pressure sensor (#3192) --- CODEOWNERS | 1 + esphome/components/qmp6988/__init__.py | 1 + esphome/components/qmp6988/qmp6988.cpp | 397 +++++++++++++++++++++++++ esphome/components/qmp6988/qmp6988.h | 116 ++++++++ esphome/components/qmp6988/sensor.py | 101 +++++++ tests/test1.yaml | 11 + 6 files changed, 627 insertions(+) create mode 100644 esphome/components/qmp6988/__init__.py create mode 100644 esphome/components/qmp6988/qmp6988.cpp create mode 100644 esphome/components/qmp6988/qmp6988.h create mode 100644 esphome/components/qmp6988/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index c111fa7816..8958aa2928 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -151,6 +151,7 @@ esphome/components/preferences/* @esphome/core esphome/components/psram/* @esphome/core esphome/components/pulse_meter/* @cstaahl @stevebaxter esphome/components/pvvx_mithermometer/* @pasiz +esphome/components/qmp6988/* @andrewpc esphome/components/qr_code/* @wjtje esphome/components/radon_eye_ble/* @jeffeb3 esphome/components/radon_eye_rd200/* @jeffeb3 diff --git a/esphome/components/qmp6988/__init__.py b/esphome/components/qmp6988/__init__.py new file mode 100644 index 0000000000..09bcf51589 --- /dev/null +++ b/esphome/components/qmp6988/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@andrewpc"] diff --git a/esphome/components/qmp6988/qmp6988.cpp b/esphome/components/qmp6988/qmp6988.cpp new file mode 100644 index 0000000000..5bad1e4a47 --- /dev/null +++ b/esphome/components/qmp6988/qmp6988.cpp @@ -0,0 +1,397 @@ +#include "qmp6988.h" +#include + +namespace esphome { +namespace qmp6988 { + +static const uint8_t QMP6988_CHIP_ID = 0x5C; + +static const uint8_t QMP6988_CHIP_ID_REG = 0xD1; /* Chip ID confirmation Register */ +static const uint8_t QMP6988_RESET_REG = 0xE0; /* Device reset register */ +static const uint8_t QMP6988_DEVICE_STAT_REG = 0xF3; /* Device state register */ +static const uint8_t QMP6988_CTRLMEAS_REG = 0xF4; /* Measurement Condition Control Register */ +/* data */ +static const uint8_t QMP6988_PRESSURE_MSB_REG = 0xF7; /* Pressure MSB Register */ +static const uint8_t QMP6988_TEMPERATURE_MSB_REG = 0xFA; /* Temperature MSB Reg */ + +/* compensation calculation */ +static const uint8_t QMP6988_CALIBRATION_DATA_START = 0xA0; /* QMP6988 compensation coefficients */ +static const uint8_t QMP6988_CALIBRATION_DATA_LENGTH = 25; + +static const uint8_t SHIFT_RIGHT_4_POSITION = 4; +static const uint8_t SHIFT_LEFT_2_POSITION = 2; +static const uint8_t SHIFT_LEFT_4_POSITION = 4; +static const uint8_t SHIFT_LEFT_5_POSITION = 5; +static const uint8_t SHIFT_LEFT_8_POSITION = 8; +static const uint8_t SHIFT_LEFT_12_POSITION = 12; +static const uint8_t SHIFT_LEFT_16_POSITION = 16; + +/* power mode */ +static const uint8_t QMP6988_SLEEP_MODE = 0x00; +static const uint8_t QMP6988_FORCED_MODE = 0x01; +static const uint8_t QMP6988_NORMAL_MODE = 0x03; + +static const uint8_t QMP6988_CTRLMEAS_REG_MODE_POS = 0; +static const uint8_t QMP6988_CTRLMEAS_REG_MODE_MSK = 0x03; +static const uint8_t QMP6988_CTRLMEAS_REG_MODE_LEN = 2; + +static const uint8_t QMP6988_CTRLMEAS_REG_OSRST_POS = 5; +static const uint8_t QMP6988_CTRLMEAS_REG_OSRST_MSK = 0xE0; +static const uint8_t QMP6988_CTRLMEAS_REG_OSRST_LEN = 3; + +static const uint8_t QMP6988_CTRLMEAS_REG_OSRSP_POS = 2; +static const uint8_t QMP6988_CTRLMEAS_REG_OSRSP_MSK = 0x1C; +static const uint8_t QMP6988_CTRLMEAS_REG_OSRSP_LEN = 3; + +static const uint8_t QMP6988_CONFIG_REG = 0xF1; /*IIR filter co-efficient setting Register*/ +static const uint8_t QMP6988_CONFIG_REG_FILTER_POS = 0; +static const uint8_t QMP6988_CONFIG_REG_FILTER_MSK = 0x07; +static const uint8_t QMP6988_CONFIG_REG_FILTER_LEN = 3; + +static const uint32_t SUBTRACTOR = 8388608; + +static const char *const TAG = "qmp6988"; + +static const char *oversampling_to_str(QMP6988Oversampling oversampling) { + switch (oversampling) { + case QMP6988_OVERSAMPLING_SKIPPED: + return "None"; + case QMP6988_OVERSAMPLING_1X: + return "1x"; + case QMP6988_OVERSAMPLING_2X: + return "2x"; + case QMP6988_OVERSAMPLING_4X: + return "4x"; + case QMP6988_OVERSAMPLING_8X: + return "8x"; + case QMP6988_OVERSAMPLING_16X: + return "16x"; + case QMP6988_OVERSAMPLING_32X: + return "32x"; + case QMP6988_OVERSAMPLING_64X: + return "64x"; + default: + return "UNKNOWN"; + } +} + +static const char *iir_filter_to_str(QMP6988IIRFilter filter) { + switch (filter) { + case QMP6988_IIR_FILTER_OFF: + return "OFF"; + case QMP6988_IIR_FILTER_2X: + return "2x"; + case QMP6988_IIR_FILTER_4X: + return "4x"; + case QMP6988_IIR_FILTER_8X: + return "8x"; + case QMP6988_IIR_FILTER_16X: + return "16x"; + case QMP6988_IIR_FILTER_32X: + return "32x"; + default: + return "UNKNOWN"; + } +} + +bool QMP6988Component::device_check_() { + uint8_t ret = 0; + + ret = this->read_register(QMP6988_CHIP_ID_REG, &(qmp6988_data_.chip_id), 1); + if (ret != i2c::ERROR_OK) { + ESP_LOGE(TAG, "%s: read chip ID (0xD1) failed", __func__); + } + ESP_LOGD(TAG, "qmp6988 read chip id = 0x%x", qmp6988_data_.chip_id); + + return qmp6988_data_.chip_id == QMP6988_CHIP_ID; +} + +bool QMP6988Component::get_calibration_data_() { + uint8_t status = 0; + // BITFIELDS temp_COE; + uint8_t a_data_uint8_tr[QMP6988_CALIBRATION_DATA_LENGTH] = {0}; + int len; + + for (len = 0; len < QMP6988_CALIBRATION_DATA_LENGTH; len += 1) { + status = this->read_register(QMP6988_CALIBRATION_DATA_START + len, &a_data_uint8_tr[len], 1); + if (status != i2c::ERROR_OK) { + ESP_LOGE(TAG, "qmp6988 read calibration data (0xA0) error!"); + return false; + } + } + + qmp6988_data_.qmp6988_cali.COE_a0 = + (QMP6988_S32_t)(((a_data_uint8_tr[18] << SHIFT_LEFT_12_POSITION) | + (a_data_uint8_tr[19] << SHIFT_LEFT_4_POSITION) | (a_data_uint8_tr[24] & 0x0f)) + << 12); + qmp6988_data_.qmp6988_cali.COE_a0 = qmp6988_data_.qmp6988_cali.COE_a0 >> 12; + + qmp6988_data_.qmp6988_cali.COE_a1 = + (QMP6988_S16_t)(((a_data_uint8_tr[20]) << SHIFT_LEFT_8_POSITION) | a_data_uint8_tr[21]); + qmp6988_data_.qmp6988_cali.COE_a2 = + (QMP6988_S16_t)(((a_data_uint8_tr[22]) << SHIFT_LEFT_8_POSITION) | a_data_uint8_tr[23]); + + qmp6988_data_.qmp6988_cali.COE_b00 = + (QMP6988_S32_t)(((a_data_uint8_tr[0] << SHIFT_LEFT_12_POSITION) | (a_data_uint8_tr[1] << SHIFT_LEFT_4_POSITION) | + ((a_data_uint8_tr[24] & 0xf0) >> SHIFT_RIGHT_4_POSITION)) + << 12); + qmp6988_data_.qmp6988_cali.COE_b00 = qmp6988_data_.qmp6988_cali.COE_b00 >> 12; + + qmp6988_data_.qmp6988_cali.COE_bt1 = + (QMP6988_S16_t)(((a_data_uint8_tr[2]) << SHIFT_LEFT_8_POSITION) | a_data_uint8_tr[3]); + qmp6988_data_.qmp6988_cali.COE_bt2 = + (QMP6988_S16_t)(((a_data_uint8_tr[4]) << SHIFT_LEFT_8_POSITION) | a_data_uint8_tr[5]); + qmp6988_data_.qmp6988_cali.COE_bp1 = + (QMP6988_S16_t)(((a_data_uint8_tr[6]) << SHIFT_LEFT_8_POSITION) | a_data_uint8_tr[7]); + qmp6988_data_.qmp6988_cali.COE_b11 = + (QMP6988_S16_t)(((a_data_uint8_tr[8]) << SHIFT_LEFT_8_POSITION) | a_data_uint8_tr[9]); + qmp6988_data_.qmp6988_cali.COE_bp2 = + (QMP6988_S16_t)(((a_data_uint8_tr[10]) << SHIFT_LEFT_8_POSITION) | a_data_uint8_tr[11]); + qmp6988_data_.qmp6988_cali.COE_b12 = + (QMP6988_S16_t)(((a_data_uint8_tr[12]) << SHIFT_LEFT_8_POSITION) | a_data_uint8_tr[13]); + qmp6988_data_.qmp6988_cali.COE_b21 = + (QMP6988_S16_t)(((a_data_uint8_tr[14]) << SHIFT_LEFT_8_POSITION) | a_data_uint8_tr[15]); + qmp6988_data_.qmp6988_cali.COE_bp3 = + (QMP6988_S16_t)(((a_data_uint8_tr[16]) << SHIFT_LEFT_8_POSITION) | a_data_uint8_tr[17]); + + ESP_LOGV(TAG, "<-----------calibration data-------------->\r\n"); + ESP_LOGV(TAG, "COE_a0[%d] COE_a1[%d] COE_a2[%d] COE_b00[%d]\r\n", qmp6988_data_.qmp6988_cali.COE_a0, + qmp6988_data_.qmp6988_cali.COE_a1, qmp6988_data_.qmp6988_cali.COE_a2, qmp6988_data_.qmp6988_cali.COE_b00); + ESP_LOGV(TAG, "COE_bt1[%d] COE_bt2[%d] COE_bp1[%d] COE_b11[%d]\r\n", qmp6988_data_.qmp6988_cali.COE_bt1, + qmp6988_data_.qmp6988_cali.COE_bt2, qmp6988_data_.qmp6988_cali.COE_bp1, qmp6988_data_.qmp6988_cali.COE_b11); + ESP_LOGV(TAG, "COE_bp2[%d] COE_b12[%d] COE_b21[%d] COE_bp3[%d]\r\n", qmp6988_data_.qmp6988_cali.COE_bp2, + qmp6988_data_.qmp6988_cali.COE_b12, qmp6988_data_.qmp6988_cali.COE_b21, qmp6988_data_.qmp6988_cali.COE_bp3); + ESP_LOGV(TAG, "<-----------calibration data-------------->\r\n"); + + qmp6988_data_.ik.a0 = qmp6988_data_.qmp6988_cali.COE_a0; // 20Q4 + qmp6988_data_.ik.b00 = qmp6988_data_.qmp6988_cali.COE_b00; // 20Q4 + + qmp6988_data_.ik.a1 = 3608L * (QMP6988_S32_t) qmp6988_data_.qmp6988_cali.COE_a1 - 1731677965L; // 31Q23 + qmp6988_data_.ik.a2 = 16889L * (QMP6988_S32_t) qmp6988_data_.qmp6988_cali.COE_a2 - 87619360L; // 30Q47 + + qmp6988_data_.ik.bt1 = 2982L * (QMP6988_S64_t) qmp6988_data_.qmp6988_cali.COE_bt1 + 107370906L; // 28Q15 + qmp6988_data_.ik.bt2 = 329854L * (QMP6988_S64_t) qmp6988_data_.qmp6988_cali.COE_bt2 + 108083093L; // 34Q38 + qmp6988_data_.ik.bp1 = 19923L * (QMP6988_S64_t) qmp6988_data_.qmp6988_cali.COE_bp1 + 1133836764L; // 31Q20 + qmp6988_data_.ik.b11 = 2406L * (QMP6988_S64_t) qmp6988_data_.qmp6988_cali.COE_b11 + 118215883L; // 28Q34 + qmp6988_data_.ik.bp2 = 3079L * (QMP6988_S64_t) qmp6988_data_.qmp6988_cali.COE_bp2 - 181579595L; // 29Q43 + qmp6988_data_.ik.b12 = 6846L * (QMP6988_S64_t) qmp6988_data_.qmp6988_cali.COE_b12 + 85590281L; // 29Q53 + qmp6988_data_.ik.b21 = 13836L * (QMP6988_S64_t) qmp6988_data_.qmp6988_cali.COE_b21 + 79333336L; // 29Q60 + qmp6988_data_.ik.bp3 = 2915L * (QMP6988_S64_t) qmp6988_data_.qmp6988_cali.COE_bp3 + 157155561L; // 28Q65 + ESP_LOGV(TAG, "<----------- int calibration data -------------->\r\n"); + ESP_LOGV(TAG, "a0[%d] a1[%d] a2[%d] b00[%d]\r\n", qmp6988_data_.ik.a0, qmp6988_data_.ik.a1, qmp6988_data_.ik.a2, + qmp6988_data_.ik.b00); + ESP_LOGV(TAG, "bt1[%lld] bt2[%lld] bp1[%lld] b11[%lld]\r\n", qmp6988_data_.ik.bt1, qmp6988_data_.ik.bt2, + qmp6988_data_.ik.bp1, qmp6988_data_.ik.b11); + ESP_LOGV(TAG, "bp2[%lld] b12[%lld] b21[%lld] bp3[%lld]\r\n", qmp6988_data_.ik.bp2, qmp6988_data_.ik.b12, + qmp6988_data_.ik.b21, qmp6988_data_.ik.bp3); + ESP_LOGV(TAG, "<----------- int calibration data -------------->\r\n"); + return true; +} + +QMP6988_S16_t QMP6988Component::get_compensated_temperature_(qmp6988_ik_data_t *ik, QMP6988_S32_t dt) { + QMP6988_S16_t ret; + QMP6988_S64_t wk1, wk2; + + // wk1: 60Q4 // bit size + wk1 = ((QMP6988_S64_t) ik->a1 * (QMP6988_S64_t) dt); // 31Q23+24-1=54 (54Q23) + wk2 = ((QMP6988_S64_t) ik->a2 * (QMP6988_S64_t) dt) >> 14; // 30Q47+24-1=53 (39Q33) + wk2 = (wk2 * (QMP6988_S64_t) dt) >> 10; // 39Q33+24-1=62 (52Q23) + wk2 = ((wk1 + wk2) / 32767) >> 19; // 54,52->55Q23 (20Q04) + ret = (QMP6988_S16_t)((ik->a0 + wk2) >> 4); // 21Q4 -> 17Q0 + return ret; +} + +QMP6988_S32_t QMP6988Component::get_compensated_pressure_(qmp6988_ik_data_t *ik, QMP6988_S32_t dp, QMP6988_S16_t tx) { + QMP6988_S32_t ret; + QMP6988_S64_t wk1, wk2, wk3; + + // wk1 = 48Q16 // bit size + wk1 = ((QMP6988_S64_t) ik->bt1 * (QMP6988_S64_t) tx); // 28Q15+16-1=43 (43Q15) + wk2 = ((QMP6988_S64_t) ik->bp1 * (QMP6988_S64_t) dp) >> 5; // 31Q20+24-1=54 (49Q15) + wk1 += wk2; // 43,49->50Q15 + wk2 = ((QMP6988_S64_t) ik->bt2 * (QMP6988_S64_t) tx) >> 1; // 34Q38+16-1=49 (48Q37) + wk2 = (wk2 * (QMP6988_S64_t) tx) >> 8; // 48Q37+16-1=63 (55Q29) + wk3 = wk2; // 55Q29 + wk2 = ((QMP6988_S64_t) ik->b11 * (QMP6988_S64_t) tx) >> 4; // 28Q34+16-1=43 (39Q30) + wk2 = (wk2 * (QMP6988_S64_t) dp) >> 1; // 39Q30+24-1=62 (61Q29) + wk3 += wk2; // 55,61->62Q29 + wk2 = ((QMP6988_S64_t) ik->bp2 * (QMP6988_S64_t) dp) >> 13; // 29Q43+24-1=52 (39Q30) + wk2 = (wk2 * (QMP6988_S64_t) dp) >> 1; // 39Q30+24-1=62 (61Q29) + wk3 += wk2; // 62,61->63Q29 + wk1 += wk3 >> 14; // Q29 >> 14 -> Q15 + wk2 = ((QMP6988_S64_t) ik->b12 * (QMP6988_S64_t) tx); // 29Q53+16-1=45 (45Q53) + wk2 = (wk2 * (QMP6988_S64_t) tx) >> 22; // 45Q53+16-1=61 (39Q31) + wk2 = (wk2 * (QMP6988_S64_t) dp) >> 1; // 39Q31+24-1=62 (61Q30) + wk3 = wk2; // 61Q30 + wk2 = ((QMP6988_S64_t) ik->b21 * (QMP6988_S64_t) tx) >> 6; // 29Q60+16-1=45 (39Q54) + wk2 = (wk2 * (QMP6988_S64_t) dp) >> 23; // 39Q54+24-1=62 (39Q31) + wk2 = (wk2 * (QMP6988_S64_t) dp) >> 1; // 39Q31+24-1=62 (61Q20) + wk3 += wk2; // 61,61->62Q30 + wk2 = ((QMP6988_S64_t) ik->bp3 * (QMP6988_S64_t) dp) >> 12; // 28Q65+24-1=51 (39Q53) + wk2 = (wk2 * (QMP6988_S64_t) dp) >> 23; // 39Q53+24-1=62 (39Q30) + wk2 = (wk2 * (QMP6988_S64_t) dp); // 39Q30+24-1=62 (62Q30) + wk3 += wk2; // 62,62->63Q30 + wk1 += wk3 >> 15; // Q30 >> 15 = Q15 + wk1 /= 32767L; + wk1 >>= 11; // Q15 >> 7 = Q4 + wk1 += ik->b00; // Q4 + 20Q4 + // wk1 >>= 4; // 28Q4 -> 24Q0 + ret = (QMP6988_S32_t) wk1; + return ret; +} + +void QMP6988Component::software_reset_() { + uint8_t ret = 0; + + ret = this->write_byte(QMP6988_RESET_REG, 0xe6); + if (ret != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Software Reset (0xe6) failed"); + } + delay(10); + + this->write_byte(QMP6988_RESET_REG, 0x00); +} + +void QMP6988Component::set_power_mode_(uint8_t power_mode) { + uint8_t data; + + ESP_LOGD(TAG, "Setting Power mode to: %d", power_mode); + + qmp6988_data_.power_mode = power_mode; + this->read_register(QMP6988_CTRLMEAS_REG, &data, 1); + data = data & 0xfc; + if (power_mode == QMP6988_SLEEP_MODE) { + data |= 0x00; + } else if (power_mode == QMP6988_FORCED_MODE) { + data |= 0x01; + } else if (power_mode == QMP6988_NORMAL_MODE) { + data |= 0x03; + } + this->write_byte(QMP6988_CTRLMEAS_REG, data); + + ESP_LOGD(TAG, "Set Power mode 0xf4=0x%x \r\n", data); + + delay(10); +} + +void QMP6988Component::write_filter_(unsigned char filter) { + uint8_t data; + + data = (filter & 0x03); + this->write_byte(QMP6988_CONFIG_REG, data); + delay(10); +} + +void QMP6988Component::write_oversampling_pressure_(unsigned char oversampling_p) { + uint8_t data; + + this->read_register(QMP6988_CTRLMEAS_REG, &data, 1); + data &= 0xe3; + data |= (oversampling_p << 2); + this->write_byte(QMP6988_CTRLMEAS_REG, data); + delay(10); +} + +void QMP6988Component::write_oversampling_temperature_(unsigned char oversampling_t) { + uint8_t data; + + this->read_register(QMP6988_CTRLMEAS_REG, &data, 1); + data &= 0x1f; + data |= (oversampling_t << 5); + this->write_byte(QMP6988_CTRLMEAS_REG, data); + delay(10); +} + +void QMP6988Component::set_temperature_oversampling(QMP6988Oversampling oversampling_t) { + this->temperature_oversampling_ = oversampling_t; +} + +void QMP6988Component::set_pressure_oversampling(QMP6988Oversampling oversampling_p) { + this->pressure_oversampling_ = oversampling_p; +} + +void QMP6988Component::set_iir_filter(QMP6988IIRFilter iirfilter) { this->iir_filter_ = iirfilter; } + +void QMP6988Component::calculate_altitude_(float pressure, float temp) { + float altitude; + altitude = (pow((101325 / pressure), 1 / 5.257) - 1) * (temp + 273.15) / 0.0065; + this->qmp6988_data_.altitude = altitude; +} + +void QMP6988Component::calculate_pressure_() { + uint8_t err = 0; + QMP6988_U32_t p_read, t_read; + QMP6988_S32_t p_raw, t_raw; + uint8_t a_data_uint8_tr[6] = {0}; + QMP6988_S32_t t_int, p_int; + this->qmp6988_data_.temperature = 0; + this->qmp6988_data_.pressure = 0; + + err = this->read_register(QMP6988_PRESSURE_MSB_REG, a_data_uint8_tr, 6); + if (err != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Error reading raw pressure/temp values"); + return; + } + p_read = (QMP6988_U32_t)((((QMP6988_U32_t)(a_data_uint8_tr[0])) << SHIFT_LEFT_16_POSITION) | + (((QMP6988_U16_t)(a_data_uint8_tr[1])) << SHIFT_LEFT_8_POSITION) | (a_data_uint8_tr[2])); + p_raw = (QMP6988_S32_t)(p_read - SUBTRACTOR); + + t_read = (QMP6988_U32_t)((((QMP6988_U32_t)(a_data_uint8_tr[3])) << SHIFT_LEFT_16_POSITION) | + (((QMP6988_U16_t)(a_data_uint8_tr[4])) << SHIFT_LEFT_8_POSITION) | (a_data_uint8_tr[5])); + t_raw = (QMP6988_S32_t)(t_read - SUBTRACTOR); + + t_int = this->get_compensated_temperature_(&(qmp6988_data_.ik), t_raw); + p_int = this->get_compensated_pressure_(&(qmp6988_data_.ik), p_raw, t_int); + + this->qmp6988_data_.temperature = (float) t_int / 256.0f; + this->qmp6988_data_.pressure = (float) p_int / 16.0f; +} + +void QMP6988Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up QMP6988"); + + bool ret; + ret = this->device_check_(); + if (!ret) { + ESP_LOGCONFIG(TAG, "Setup failed - device not found"); + } + + this->software_reset_(); + this->get_calibration_data_(); + this->set_power_mode_(QMP6988_NORMAL_MODE); + this->write_filter_(iir_filter_); + this->write_oversampling_pressure_(this->pressure_oversampling_); + this->write_oversampling_temperature_(this->temperature_oversampling_); +} + +void QMP6988Component::dump_config() { + ESP_LOGCONFIG(TAG, "QMP6988:"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with QMP6988 failed!"); + } + LOG_UPDATE_INTERVAL(this); + + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + ESP_LOGCONFIG(TAG, " Temperature Oversampling: %s", oversampling_to_str(this->temperature_oversampling_)); + LOG_SENSOR(" ", "Pressure", this->pressure_sensor_); + ESP_LOGCONFIG(TAG, " Pressure Oversampling: %s", oversampling_to_str(this->pressure_oversampling_)); + ESP_LOGCONFIG(TAG, " IIR Filter: %s", iir_filter_to_str(this->iir_filter_)); +} + +float QMP6988Component::get_setup_priority() const { return setup_priority::DATA; } + +void QMP6988Component::update() { + this->calculate_pressure_(); + float pressurehectopascals = this->qmp6988_data_.pressure / 100; + float temperature = this->qmp6988_data_.temperature; + + ESP_LOGD(TAG, "Temperature=%.2f°C, Pressure=%.2fhPa", temperature, pressurehectopascals); + if (this->temperature_sensor_ != nullptr) + this->temperature_sensor_->publish_state(temperature); + if (this->pressure_sensor_ != nullptr) + this->pressure_sensor_->publish_state(pressurehectopascals); +} + +} // namespace qmp6988 +} // namespace esphome diff --git a/esphome/components/qmp6988/qmp6988.h b/esphome/components/qmp6988/qmp6988.h new file mode 100644 index 0000000000..ef944ba4ff --- /dev/null +++ b/esphome/components/qmp6988/qmp6988.h @@ -0,0 +1,116 @@ +#pragma once + +#include "esphome/core/log.h" +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace qmp6988 { + +#define QMP6988_U16_t unsigned short +#define QMP6988_S16_t short +#define QMP6988_U32_t unsigned int +#define QMP6988_S32_t int +#define QMP6988_U64_t unsigned long long +#define QMP6988_S64_t long long + +/* oversampling */ +enum QMP6988Oversampling { + QMP6988_OVERSAMPLING_SKIPPED = 0x00, + QMP6988_OVERSAMPLING_1X = 0x01, + QMP6988_OVERSAMPLING_2X = 0x02, + QMP6988_OVERSAMPLING_4X = 0x03, + QMP6988_OVERSAMPLING_8X = 0x04, + QMP6988_OVERSAMPLING_16X = 0x05, + QMP6988_OVERSAMPLING_32X = 0x06, + QMP6988_OVERSAMPLING_64X = 0x07, +}; + +/* filter */ +enum QMP6988IIRFilter { + QMP6988_IIR_FILTER_OFF = 0x00, + QMP6988_IIR_FILTER_2X = 0x01, + QMP6988_IIR_FILTER_4X = 0x02, + QMP6988_IIR_FILTER_8X = 0x03, + QMP6988_IIR_FILTER_16X = 0x04, + QMP6988_IIR_FILTER_32X = 0x05, +}; + +using qmp6988_cali_data_t = struct Qmp6988CaliData { + QMP6988_S32_t COE_a0; + QMP6988_S16_t COE_a1; + QMP6988_S16_t COE_a2; + QMP6988_S32_t COE_b00; + QMP6988_S16_t COE_bt1; + QMP6988_S16_t COE_bt2; + QMP6988_S16_t COE_bp1; + QMP6988_S16_t COE_b11; + QMP6988_S16_t COE_bp2; + QMP6988_S16_t COE_b12; + QMP6988_S16_t COE_b21; + QMP6988_S16_t COE_bp3; +}; + +using qmp6988_fk_data_t = struct Qmp6988FkData { + float a0, b00; + float a1, a2, bt1, bt2, bp1, b11, bp2, b12, b21, bp3; +}; + +using qmp6988_ik_data_t = struct Qmp6988IkData { + QMP6988_S32_t a0, b00; + QMP6988_S32_t a1, a2; + QMP6988_S64_t bt1, bt2, bp1, b11, bp2, b12, b21, bp3; +}; + +using qmp6988_data_t = struct Qmp6988Data { + uint8_t chip_id; + uint8_t power_mode; + float temperature; + float pressure; + float altitude; + qmp6988_cali_data_t qmp6988_cali; + qmp6988_ik_data_t ik; +}; + +class QMP6988Component : public PollingComponent, public i2c::I2CDevice { + public: + void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } + void set_pressure_sensor(sensor::Sensor *pressure_sensor) { pressure_sensor_ = pressure_sensor; } + + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + void update() override; + + void set_iir_filter(QMP6988IIRFilter iirfilter); + void set_temperature_oversampling(QMP6988Oversampling oversampling_t); + void set_pressure_oversampling(QMP6988Oversampling oversampling_p); + + protected: + qmp6988_data_t qmp6988_data_; + sensor::Sensor *temperature_sensor_; + sensor::Sensor *pressure_sensor_; + + QMP6988Oversampling temperature_oversampling_{QMP6988_OVERSAMPLING_16X}; + QMP6988Oversampling pressure_oversampling_{QMP6988_OVERSAMPLING_16X}; + QMP6988IIRFilter iir_filter_{QMP6988_IIR_FILTER_OFF}; + + void software_reset_(); + bool get_calibration_data_(); + bool device_check_(); + void set_power_mode_(uint8_t power_mode); + void write_oversampling_temperature_(unsigned char oversampling_t); + void write_oversampling_pressure_(unsigned char oversampling_p); + void write_filter_(unsigned char filter); + void calculate_pressure_(); + void calculate_altitude_(float pressure, float temp); + + QMP6988_S32_t get_compensated_pressure_(qmp6988_ik_data_t *ik, QMP6988_S32_t dp, QMP6988_S16_t tx); + QMP6988_S16_t get_compensated_temperature_(qmp6988_ik_data_t *ik, QMP6988_S32_t dt); +}; + +} // namespace qmp6988 +} // namespace esphome diff --git a/esphome/components/qmp6988/sensor.py b/esphome/components/qmp6988/sensor.py new file mode 100644 index 0000000000..fdcfd4e66b --- /dev/null +++ b/esphome/components/qmp6988/sensor.py @@ -0,0 +1,101 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_ID, + CONF_PRESSURE, + CONF_TEMPERATURE, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_HECTOPASCAL, + CONF_IIR_FILTER, + CONF_OVERSAMPLING, +) + +DEPENDENCIES = ["i2c"] + +qmp6988_ns = cg.esphome_ns.namespace("qmp6988") +QMP6988Component = qmp6988_ns.class_( + "QMP6988Component", cg.PollingComponent, i2c.I2CDevice +) + +QMP6988Oversampling = qmp6988_ns.enum("QMP6988Oversampling") +OVERSAMPLING_OPTIONS = { + "NONE": QMP6988Oversampling.QMP6988_OVERSAMPLING_SKIPPED, + "1X": QMP6988Oversampling.QMP6988_OVERSAMPLING_1X, + "2X": QMP6988Oversampling.QMP6988_OVERSAMPLING_2X, + "4X": QMP6988Oversampling.QMP6988_OVERSAMPLING_4X, + "8X": QMP6988Oversampling.QMP6988_OVERSAMPLING_8X, + "16X": QMP6988Oversampling.QMP6988_OVERSAMPLING_16X, + "32X": QMP6988Oversampling.QMP6988_OVERSAMPLING_32X, + "64X": QMP6988Oversampling.QMP6988_OVERSAMPLING_64X, +} + +QMP6988IIRFilter = qmp6988_ns.enum("QMP6988IIRFilter") +IIR_FILTER_OPTIONS = { + "OFF": QMP6988IIRFilter.QMP6988_IIR_FILTER_OFF, + "2X": QMP6988IIRFilter.QMP6988_IIR_FILTER_2X, + "4X": QMP6988IIRFilter.QMP6988_IIR_FILTER_4X, + "8X": QMP6988IIRFilter.QMP6988_IIR_FILTER_8X, + "16X": QMP6988IIRFilter.QMP6988_IIR_FILTER_16X, + "32X": QMP6988IIRFilter.QMP6988_IIR_FILTER_32X, +} + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(QMP6988Component), + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ).extend( + { + cv.Optional(CONF_OVERSAMPLING, default="8X"): cv.enum( + OVERSAMPLING_OPTIONS, upper=True + ), + } + ), + cv.Optional(CONF_PRESSURE): sensor.sensor_schema( + unit_of_measurement=UNIT_HECTOPASCAL, + accuracy_decimals=1, + device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, + ).extend( + { + cv.Optional(CONF_OVERSAMPLING, default="8X"): cv.enum( + OVERSAMPLING_OPTIONS, upper=True + ), + } + ), + cv.Optional(CONF_IIR_FILTER, default="OFF"): cv.enum( + IIR_FILTER_OPTIONS, upper=True + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x70)) +) + + +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) + + if CONF_TEMPERATURE in config: + conf = config[CONF_TEMPERATURE] + sens = await sensor.new_sensor(conf) + cg.add(var.set_temperature_sensor(sens)) + cg.add(var.set_temperature_oversampling(conf[CONF_OVERSAMPLING])) + + if CONF_PRESSURE in config: + conf = config[CONF_PRESSURE] + sens = await sensor.new_sensor(conf) + cg.add(var.set_pressure_sensor(sens)) + cg.add(var.set_pressure_oversampling(conf[CONF_OVERSAMPLING])) + + cg.add(var.set_iir_filter(config[CONF_IIR_FILTER])) diff --git a/tests/test1.yaml b/tests/test1.yaml index 3763fd3fa5..bd4c919e67 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -798,6 +798,17 @@ sensor: value: 12345 total: name: "Pulse Meter Total" + - platform: qmp6988 + temperature: + name: "Living Temperature QMP" + oversampling: 32x + pressure: + name: "Living Pressure QMP" + oversampling: 2x + address: 0x70 + update_interval: 30s + iir_filter: 16x + i2c_id: i2c_bus - platform: rotary_encoder name: "Rotary Encoder" id: rotary_encoder1 From 68e957c14792ea6874b284db1f52f3d0731165f5 Mon Sep 17 00:00:00 2001 From: rbaron Date: Tue, 15 Mar 2022 23:05:29 +0100 Subject: [PATCH 0320/1729] Adds support for b-parasite's v2 BLE protocol (#3290) --- esphome/components/b_parasite/b_parasite.cpp | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/esphome/components/b_parasite/b_parasite.cpp b/esphome/components/b_parasite/b_parasite.cpp index ee12226977..2e548a8072 100644 --- a/esphome/components/b_parasite/b_parasite.cpp +++ b/esphome/components/b_parasite/b_parasite.cpp @@ -38,7 +38,7 @@ bool BParasite::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { const auto &data = service_data.data; const uint8_t protocol_version = data[0] >> 4; - if (protocol_version != 1) { + if (protocol_version != 1 && protocol_version != 2) { ESP_LOGE(TAG, "Unsupported protocol version: %u", protocol_version); return false; } @@ -57,9 +57,15 @@ bool BParasite::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { uint16_t battery_millivolt = data[2] << 8 | data[3]; float battery_voltage = battery_millivolt / 1000.0f; - // Temperature in 1000 * Celsius. - uint16_t temp_millicelcius = data[4] << 8 | data[5]; - float temp_celcius = temp_millicelcius / 1000.0f; + // Temperature in 1000 * Celsius (protocol v1) or 100 * Celsius (protocol v2). + float temp_celsius; + if (protocol_version == 1) { + uint16_t temp_millicelsius = data[4] << 8 | data[5]; + temp_celsius = temp_millicelsius / 1000.0f; + } else { + int16_t temp_centicelsius = data[4] << 8 | data[5]; + temp_celsius = temp_centicelsius / 100.0f; + } // Relative air humidity in the range [0, 2^16). uint16_t humidity = data[6] << 8 | data[7]; @@ -76,7 +82,7 @@ bool BParasite::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { battery_voltage_->publish_state(battery_voltage); } if (temperature_ != nullptr) { - temperature_->publish_state(temp_celcius); + temperature_->publish_state(temp_celsius); } if (humidity_ != nullptr) { humidity_->publish_state(humidity_percent); From 4525588116492bca019758f64dddc1a14bf52eb1 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 16 Mar 2022 13:35:37 +1300 Subject: [PATCH 0321/1729] Add helper overloads for hex print 16-bit (#3297) --- esphome/core/helpers.cpp | 19 +++++++++++++++++++ esphome/core/helpers.h | 4 ++++ 2 files changed, 23 insertions(+) diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index a346cd7e0b..b03d890ad8 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -213,6 +213,25 @@ std::string format_hex_pretty(const uint8_t *data, size_t length) { } std::string format_hex_pretty(const std::vector &data) { return format_hex_pretty(data.data(), data.size()); } +std::string format_hex_pretty(const uint16_t *data, size_t length) { + if (length == 0) + return ""; + std::string ret; + ret.resize(5 * length - 1); + for (size_t i = 0; i < length; i++) { + ret[5 * i] = format_hex_pretty_char((data[i] & 0xF000) >> 12); + ret[5 * i + 1] = format_hex_pretty_char((data[i] & 0x0F00) >> 8); + ret[5 * i + 2] = format_hex_pretty_char((data[i] & 0x00F0) >> 4); + ret[5 * i + 3] = format_hex_pretty_char(data[i] & 0x000F); + if (i != length - 1) + ret[5 * i + 2] = '.'; + } + if (length > 4) + return ret + " (" + to_string(length) + ")"; + return ret; +} +std::string format_hex_pretty(const std::vector &data) { return format_hex_pretty(data.data(), data.size()); } + ParseOnOffState parse_on_off(const char *str, const char *on, const char *off) { if (on == nullptr && strcasecmp(str, "on") == 0) return PARSE_ON; diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index e0763d2c71..074bea6fd1 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -386,8 +386,12 @@ template::value, int> = 0> std::stri /// Format the byte array \p data of length \p len in pretty-printed, human-readable hex. std::string format_hex_pretty(const uint8_t *data, size_t length); +/// Format the word array \p data of length \p len in pretty-printed, human-readable hex. +std::string format_hex_pretty(const uint16_t *data, size_t length); /// Format the vector \p data in pretty-printed, human-readable hex. std::string format_hex_pretty(const std::vector &data); +/// Format the vector \p data in pretty-printed, human-readable hex. +std::string format_hex_pretty(const std::vector &data); /// Format an unsigned integer in pretty-printed, human-readable hex, starting with the most significant byte. template::value, int> = 0> std::string format_hex_pretty(T val) { val = convert_big_endian(val); From 0372d17a116a17b3da0048f927ee67cdb35f3c55 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 16 Mar 2022 13:43:05 +1300 Subject: [PATCH 0322/1729] Allow custom register type for modbus number (#3202) --- esphome/components/modbus_controller/number/__init__.py | 6 ++++++ esphome/components/modbus_controller/number/modbus_number.h | 6 +++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/esphome/components/modbus_controller/number/__init__.py b/esphome/components/modbus_controller/number/__init__.py index 56ec734315..37a39ff334 100644 --- a/esphome/components/modbus_controller/number/__init__.py +++ b/esphome/components/modbus_controller/number/__init__.py @@ -11,6 +11,7 @@ from esphome.const import ( ) from .. import ( + MODBUS_WRITE_REGISTER_TYPE, add_modbus_base_properties, modbus_controller_ns, modbus_calc_properties, @@ -24,6 +25,7 @@ from ..const import ( CONF_CUSTOM_COMMAND, CONF_FORCE_NEW_RANGE, CONF_MODBUS_CONTROLLER_ID, + CONF_REGISTER_TYPE, CONF_SKIP_UPDATES, CONF_USE_WRITE_MULTIPLE, CONF_VALUE_TYPE, @@ -61,6 +63,9 @@ CONFIG_SCHEMA = cv.All( number.NUMBER_SCHEMA.extend(ModbusItemBaseSchema).extend( { cv.GenerateID(): cv.declare_id(ModbusNumber), + cv.Optional(CONF_REGISTER_TYPE, default="holding"): cv.enum( + MODBUS_WRITE_REGISTER_TYPE + ), cv.Optional(CONF_VALUE_TYPE, default="U_WORD"): cv.enum(SENSOR_VALUE_TYPE), cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda, # 24 bits are the maximum value for fp32 before precison is lost @@ -81,6 +86,7 @@ async def to_code(config): byte_offset, reg_count = modbus_calc_properties(config) var = cg.new_Pvariable( config[CONF_ID], + config[CONF_REGISTER_TYPE], config[CONF_ADDRESS], byte_offset, config[CONF_BITMASK], diff --git a/esphome/components/modbus_controller/number/modbus_number.h b/esphome/components/modbus_controller/number/modbus_number.h index 0c525d9c89..aa5c8d1500 100644 --- a/esphome/components/modbus_controller/number/modbus_number.h +++ b/esphome/components/modbus_controller/number/modbus_number.h @@ -11,9 +11,9 @@ using value_to_data_t = std::function(float); class ModbusNumber : public number::Number, public Component, public SensorItem { public: - ModbusNumber(uint16_t start_address, uint8_t offset, uint32_t bitmask, SensorValueType value_type, int register_count, - uint8_t skip_updates, bool force_new_range) { - this->register_type = ModbusRegisterType::HOLDING; + ModbusNumber(ModbusRegisterType register_type, uint16_t start_address, uint8_t offset, uint32_t bitmask, + SensorValueType value_type, int register_count, uint8_t skip_updates, bool force_new_range) { + this->register_type = register_type; this->start_address = start_address; this->offset = offset; this->bitmask = bitmask; From 06a35056989ad441b94c35d0240a5fbf97ad6f67 Mon Sep 17 00:00:00 2001 From: stegm Date: Wed, 9 Mar 2022 20:40:43 +0100 Subject: [PATCH 0323/1729] Add optimistic config flag to modbus select. (#3267) --- esphome/components/modbus_controller/select/__init__.py | 4 +++- esphome/components/modbus_controller/select/modbus_select.cpp | 3 +++ esphome/components/modbus_controller/select/modbus_select.h | 2 ++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/esphome/components/modbus_controller/select/__init__.py b/esphome/components/modbus_controller/select/__init__.py index 7d03064fa5..6f194ef2a3 100644 --- a/esphome/components/modbus_controller/select/__init__.py +++ b/esphome/components/modbus_controller/select/__init__.py @@ -1,7 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import select -from esphome.const import CONF_ADDRESS, CONF_ID, CONF_LAMBDA +from esphome.const import CONF_ADDRESS, CONF_ID, CONF_LAMBDA, CONF_OPTIMISTIC from esphome.jsonschema import jschema_composite from .. import ( @@ -79,6 +79,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_FORCE_NEW_RANGE, default=False): cv.boolean, cv.Required(CONF_OPTIONSMAP): ensure_option_map(), cv.Optional(CONF_USE_WRITE_MULTIPLE, default=False): cv.boolean, + cv.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, cv.Optional(CONF_LAMBDA): cv.returning_lambda, cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda, }, @@ -112,6 +113,7 @@ async def to_code(config): cg.add(parent.add_sensor_item(var)) cg.add(var.set_parent(parent)) cg.add(var.set_use_write_mutiple(config[CONF_USE_WRITE_MULTIPLE])) + cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) if CONF_LAMBDA in config: template_ = await cg.process_lambda( diff --git a/esphome/components/modbus_controller/select/modbus_select.cpp b/esphome/components/modbus_controller/select/modbus_select.cpp index 2c6b32f545..33cef39a18 100644 --- a/esphome/components/modbus_controller/select/modbus_select.cpp +++ b/esphome/components/modbus_controller/select/modbus_select.cpp @@ -80,6 +80,9 @@ void ModbusSelect::control(const std::string &value) { } parent_->queue_command(write_cmd); + + if (this->optimistic_) + this->publish_state(value); } } // namespace modbus_controller diff --git a/esphome/components/modbus_controller/select/modbus_select.h b/esphome/components/modbus_controller/select/modbus_select.h index 0875194768..2a31dfd7cc 100644 --- a/esphome/components/modbus_controller/select/modbus_select.h +++ b/esphome/components/modbus_controller/select/modbus_select.h @@ -32,6 +32,7 @@ class ModbusSelect : public Component, public select::Select, public SensorItem void set_parent(ModbusController *const parent) { this->parent_ = parent; } void set_use_write_mutiple(bool use_write_multiple) { this->use_write_multiple_ = use_write_multiple; } + void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } void set_template(transform_func_t &&f) { this->transform_func_ = f; } void set_write_template(write_transform_func_t &&f) { this->write_transform_func_ = f; } @@ -43,6 +44,7 @@ class ModbusSelect : public Component, public select::Select, public SensorItem std::vector mapping_; ModbusController *parent_; bool use_write_multiple_{false}; + bool optimistic_{false}; optional transform_func_; optional write_transform_func_; }; From b7535693fa4628f8f831e2c87fdbccc3c050f05f Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 16 Mar 2022 13:35:37 +1300 Subject: [PATCH 0324/1729] Add helper overloads for hex print 16-bit (#3297) --- esphome/core/helpers.cpp | 19 +++++++++++++++++++ esphome/core/helpers.h | 4 ++++ 2 files changed, 23 insertions(+) diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index a346cd7e0b..b03d890ad8 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -213,6 +213,25 @@ std::string format_hex_pretty(const uint8_t *data, size_t length) { } std::string format_hex_pretty(const std::vector &data) { return format_hex_pretty(data.data(), data.size()); } +std::string format_hex_pretty(const uint16_t *data, size_t length) { + if (length == 0) + return ""; + std::string ret; + ret.resize(5 * length - 1); + for (size_t i = 0; i < length; i++) { + ret[5 * i] = format_hex_pretty_char((data[i] & 0xF000) >> 12); + ret[5 * i + 1] = format_hex_pretty_char((data[i] & 0x0F00) >> 8); + ret[5 * i + 2] = format_hex_pretty_char((data[i] & 0x00F0) >> 4); + ret[5 * i + 3] = format_hex_pretty_char(data[i] & 0x000F); + if (i != length - 1) + ret[5 * i + 2] = '.'; + } + if (length > 4) + return ret + " (" + to_string(length) + ")"; + return ret; +} +std::string format_hex_pretty(const std::vector &data) { return format_hex_pretty(data.data(), data.size()); } + ParseOnOffState parse_on_off(const char *str, const char *on, const char *off) { if (on == nullptr && strcasecmp(str, "on") == 0) return PARSE_ON; diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index e0763d2c71..074bea6fd1 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -386,8 +386,12 @@ template::value, int> = 0> std::stri /// Format the byte array \p data of length \p len in pretty-printed, human-readable hex. std::string format_hex_pretty(const uint8_t *data, size_t length); +/// Format the word array \p data of length \p len in pretty-printed, human-readable hex. +std::string format_hex_pretty(const uint16_t *data, size_t length); /// Format the vector \p data in pretty-printed, human-readable hex. std::string format_hex_pretty(const std::vector &data); +/// Format the vector \p data in pretty-printed, human-readable hex. +std::string format_hex_pretty(const std::vector &data); /// Format an unsigned integer in pretty-printed, human-readable hex, starting with the most significant byte. template::value, int> = 0> std::string format_hex_pretty(T val) { val = convert_big_endian(val); From 756f71c3822a2f457b42c5b55f8af1151c69c444 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 16 Mar 2022 13:43:05 +1300 Subject: [PATCH 0325/1729] Allow custom register type for modbus number (#3202) --- esphome/components/modbus_controller/number/__init__.py | 6 ++++++ esphome/components/modbus_controller/number/modbus_number.h | 6 +++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/esphome/components/modbus_controller/number/__init__.py b/esphome/components/modbus_controller/number/__init__.py index 56ec734315..37a39ff334 100644 --- a/esphome/components/modbus_controller/number/__init__.py +++ b/esphome/components/modbus_controller/number/__init__.py @@ -11,6 +11,7 @@ from esphome.const import ( ) from .. import ( + MODBUS_WRITE_REGISTER_TYPE, add_modbus_base_properties, modbus_controller_ns, modbus_calc_properties, @@ -24,6 +25,7 @@ from ..const import ( CONF_CUSTOM_COMMAND, CONF_FORCE_NEW_RANGE, CONF_MODBUS_CONTROLLER_ID, + CONF_REGISTER_TYPE, CONF_SKIP_UPDATES, CONF_USE_WRITE_MULTIPLE, CONF_VALUE_TYPE, @@ -61,6 +63,9 @@ CONFIG_SCHEMA = cv.All( number.NUMBER_SCHEMA.extend(ModbusItemBaseSchema).extend( { cv.GenerateID(): cv.declare_id(ModbusNumber), + cv.Optional(CONF_REGISTER_TYPE, default="holding"): cv.enum( + MODBUS_WRITE_REGISTER_TYPE + ), cv.Optional(CONF_VALUE_TYPE, default="U_WORD"): cv.enum(SENSOR_VALUE_TYPE), cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda, # 24 bits are the maximum value for fp32 before precison is lost @@ -81,6 +86,7 @@ async def to_code(config): byte_offset, reg_count = modbus_calc_properties(config) var = cg.new_Pvariable( config[CONF_ID], + config[CONF_REGISTER_TYPE], config[CONF_ADDRESS], byte_offset, config[CONF_BITMASK], diff --git a/esphome/components/modbus_controller/number/modbus_number.h b/esphome/components/modbus_controller/number/modbus_number.h index 0c525d9c89..aa5c8d1500 100644 --- a/esphome/components/modbus_controller/number/modbus_number.h +++ b/esphome/components/modbus_controller/number/modbus_number.h @@ -11,9 +11,9 @@ using value_to_data_t = std::function(float); class ModbusNumber : public number::Number, public Component, public SensorItem { public: - ModbusNumber(uint16_t start_address, uint8_t offset, uint32_t bitmask, SensorValueType value_type, int register_count, - uint8_t skip_updates, bool force_new_range) { - this->register_type = ModbusRegisterType::HOLDING; + ModbusNumber(ModbusRegisterType register_type, uint16_t start_address, uint8_t offset, uint32_t bitmask, + SensorValueType value_type, int register_count, uint8_t skip_updates, bool force_new_range) { + this->register_type = register_type; this->start_address = start_address; this->offset = offset; this->bitmask = bitmask; From e5c2dbc7ec1f7e06c043a095957a6945f4f7448d Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 16 Mar 2022 14:06:00 +1300 Subject: [PATCH 0326/1729] Bump version to 2022.3.0b2 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 26444baffd..579cefd507 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2022.3.0b1" +__version__ = "2022.3.0b2" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From 792a24f38d05f87a106096f90be7cd9cbe959b6f Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 16 Mar 2022 22:32:24 +1300 Subject: [PATCH 0327/1729] Bump version to 2022.3.0 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 579cefd507..e9e3809b2c 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2022.3.0b2" +__version__ = "2022.3.0" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From e621b938e36b0b6ede82d14419a4830529789659 Mon Sep 17 00:00:00 2001 From: wysiwyng <4764286+wysiwyng@users.noreply.github.com> Date: Wed, 16 Mar 2022 20:33:05 +0100 Subject: [PATCH 0328/1729] Fix WDT reset during dallas search algorithm (#3293) --- esphome/components/dallas/esp_one_wire.cpp | 4 ---- esphome/components/dallas/esp_one_wire.h | 1 - 2 files changed, 5 deletions(-) diff --git a/esphome/components/dallas/esp_one_wire.cpp b/esphome/components/dallas/esp_one_wire.cpp index 885846e5e5..5bd0f42855 100644 --- a/esphome/components/dallas/esp_one_wire.cpp +++ b/esphome/components/dallas/esp_one_wire.cpp @@ -142,7 +142,6 @@ void IRAM_ATTR ESPOneWire::select(uint64_t address) { void IRAM_ATTR ESPOneWire::reset_search() { this->last_discrepancy_ = 0; this->last_device_flag_ = false; - this->last_family_discrepancy_ = 0; this->rom_number_ = 0; } uint64_t IRAM_ATTR ESPOneWire::search() { @@ -195,9 +194,6 @@ uint64_t IRAM_ATTR ESPOneWire::search() { if (!branch) { last_zero = id_bit_number; - if (last_zero < 9) { - this->last_discrepancy_ = last_zero; - } } } diff --git a/esphome/components/dallas/esp_one_wire.h b/esphome/components/dallas/esp_one_wire.h index ef6f079f02..7544a6fe98 100644 --- a/esphome/components/dallas/esp_one_wire.h +++ b/esphome/components/dallas/esp_one_wire.h @@ -60,7 +60,6 @@ class ESPOneWire { ISRInternalGPIOPin pin_; uint8_t last_discrepancy_{0}; - uint8_t last_family_discrepancy_{0}; bool last_device_flag_{false}; uint64_t rom_number_{0}; }; From bfbf88b2ea0d521564f3937111dcc6fc2a6cfbbb Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 23 Mar 2022 09:45:05 +1300 Subject: [PATCH 0329/1729] Webserver utilize Component Iterator to not overload eventstream (#3310) --- esphome/components/api/api_connection.cpp | 2 +- esphome/components/api/api_server.h | 1 - esphome/components/api/list_entities.cpp | 3 +- esphome/components/api/list_entities.h | 6 +- esphome/components/api/proto.cpp | 1 - esphome/components/api/subscribe_state.cpp | 3 +- esphome/components/api/subscribe_state.h | 6 +- .../components/web_server/list_entities.cpp | 97 +++++++++++++++++++ esphome/components/web_server/list_entities.h | 60 ++++++++++++ esphome/components/web_server/web_server.cpp | 86 +--------------- esphome/components/web_server/web_server.h | 7 +- .../util.cpp => core/component_iterator.cpp} | 56 ++++++----- .../api/util.h => core/component_iterator.h} | 23 +++-- 13 files changed, 217 insertions(+), 134 deletions(-) create mode 100644 esphome/components/web_server/list_entities.cpp create mode 100644 esphome/components/web_server/list_entities.h rename esphome/{components/api/util.cpp => core/component_iterator.cpp} (80%) rename esphome/{components/api/util.h => core/component_iterator.h} (91%) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index b998ef5929..81f2465b74 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -23,7 +23,7 @@ static const char *const TAG = "api.connection"; static const int ESP32_CAMERA_STOP_STREAM = 5000; APIConnection::APIConnection(std::unique_ptr sock, APIServer *parent) - : parent_(parent), initial_state_iterator_(parent, this), list_entities_iterator_(parent, this) { + : parent_(parent), initial_state_iterator_(this), list_entities_iterator_(this) { this->proto_write_buffer_.reserve(64); #if defined(USE_API_PLAINTEXT) diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 3214da5b3d..fdc46922ad 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -7,7 +7,6 @@ #include "esphome/components/socket/socket.h" #include "api_pb2.h" #include "api_pb2_service.h" -#include "util.h" #include "list_entities.h" #include "subscribe_state.h" #include "user_services.h" diff --git a/esphome/components/api/list_entities.cpp b/esphome/components/api/list_entities.cpp index fb0dfa3d05..9f55fda617 100644 --- a/esphome/components/api/list_entities.cpp +++ b/esphome/components/api/list_entities.cpp @@ -40,8 +40,7 @@ bool ListEntitiesIterator::on_lock(lock::Lock *a_lock) { return this->client_->s #endif bool ListEntitiesIterator::on_end() { return this->client_->send_list_info_done(); } -ListEntitiesIterator::ListEntitiesIterator(APIServer *server, APIConnection *client) - : ComponentIterator(server), client_(client) {} +ListEntitiesIterator::ListEntitiesIterator(APIConnection *client) : client_(client) {} bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) { auto resp = service->encode_list_service_response(); return this->client_->send_list_entities_services_response(resp); diff --git a/esphome/components/api/list_entities.h b/esphome/components/api/list_entities.h index bfceb39ebf..51c343eb03 100644 --- a/esphome/components/api/list_entities.h +++ b/esphome/components/api/list_entities.h @@ -1,8 +1,8 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/component_iterator.h" #include "esphome/core/defines.h" -#include "util.h" namespace esphome { namespace api { @@ -11,7 +11,7 @@ class APIConnection; class ListEntitiesIterator : public ComponentIterator { public: - ListEntitiesIterator(APIServer *server, APIConnection *client); + ListEntitiesIterator(APIConnection *client); #ifdef USE_BINARY_SENSOR bool on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) override; #endif @@ -60,5 +60,3 @@ class ListEntitiesIterator : public ComponentIterator { } // namespace api } // namespace esphome - -#include "api_server.h" diff --git a/esphome/components/api/proto.cpp b/esphome/components/api/proto.cpp index 0ba277d90a..ca7a4c0887 100644 --- a/esphome/components/api/proto.cpp +++ b/esphome/components/api/proto.cpp @@ -1,5 +1,4 @@ #include "proto.h" -#include "util.h" #include "esphome/core/log.h" namespace esphome { diff --git a/esphome/components/api/subscribe_state.cpp b/esphome/components/api/subscribe_state.cpp index 10416ecc5c..ba277502c8 100644 --- a/esphome/components/api/subscribe_state.cpp +++ b/esphome/components/api/subscribe_state.cpp @@ -50,8 +50,7 @@ bool InitialStateIterator::on_select(select::Select *select) { #ifdef USE_LOCK bool InitialStateIterator::on_lock(lock::Lock *a_lock) { return this->client_->send_lock_state(a_lock, a_lock->state); } #endif -InitialStateIterator::InitialStateIterator(APIServer *server, APIConnection *client) - : ComponentIterator(server), client_(client) {} +InitialStateIterator::InitialStateIterator(APIConnection *client) : client_(client) {} } // namespace api } // namespace esphome diff --git a/esphome/components/api/subscribe_state.h b/esphome/components/api/subscribe_state.h index caea013f84..515e1a2d07 100644 --- a/esphome/components/api/subscribe_state.h +++ b/esphome/components/api/subscribe_state.h @@ -1,9 +1,9 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/component_iterator.h" #include "esphome/core/controller.h" #include "esphome/core/defines.h" -#include "util.h" namespace esphome { namespace api { @@ -12,7 +12,7 @@ class APIConnection; class InitialStateIterator : public ComponentIterator { public: - InitialStateIterator(APIServer *server, APIConnection *client); + InitialStateIterator(APIConnection *client); #ifdef USE_BINARY_SENSOR bool on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) override; #endif @@ -55,5 +55,3 @@ class InitialStateIterator : public ComponentIterator { } // namespace api } // namespace esphome - -#include "api_server.h" diff --git a/esphome/components/web_server/list_entities.cpp b/esphome/components/web_server/list_entities.cpp new file mode 100644 index 0000000000..6f833a5c83 --- /dev/null +++ b/esphome/components/web_server/list_entities.cpp @@ -0,0 +1,97 @@ +#ifdef USE_ARDUINO + +#include "list_entities.h" +#include "esphome/core/application.h" +#include "esphome/core/log.h" +#include "esphome/core/util.h" + +#include "web_server.h" + +namespace esphome { +namespace web_server { + +ListEntitiesIterator::ListEntitiesIterator(WebServer *web_server) : web_server_(web_server) {} + +#ifdef USE_BINARY_SENSOR +bool ListEntitiesIterator::on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) { + this->web_server_->events_.send( + this->web_server_->binary_sensor_json(binary_sensor, binary_sensor->state, DETAIL_ALL).c_str(), "state"); + return true; +} +#endif +#ifdef USE_COVER +bool ListEntitiesIterator::on_cover(cover::Cover *cover) { + this->web_server_->events_.send(this->web_server_->cover_json(cover, DETAIL_ALL).c_str(), "state"); + return true; +} +#endif +#ifdef USE_FAN +bool ListEntitiesIterator::on_fan(fan::Fan *fan) { + this->web_server_->events_.send(this->web_server_->fan_json(fan, DETAIL_ALL).c_str(), "state"); + return true; +} +#endif +#ifdef USE_LIGHT +bool ListEntitiesIterator::on_light(light::LightState *light) { + this->web_server_->events_.send(this->web_server_->light_json(light, DETAIL_ALL).c_str(), "state"); + return true; +} +#endif +#ifdef USE_SENSOR +bool ListEntitiesIterator::on_sensor(sensor::Sensor *sensor) { + this->web_server_->events_.send(this->web_server_->sensor_json(sensor, sensor->state, DETAIL_ALL).c_str(), "state"); + return true; +} +#endif +#ifdef USE_SWITCH +bool ListEntitiesIterator::on_switch(switch_::Switch *a_switch) { + this->web_server_->events_.send(this->web_server_->switch_json(a_switch, a_switch->state, DETAIL_ALL).c_str(), + "state"); + return true; +} +#endif +#ifdef USE_BUTTON +bool ListEntitiesIterator::on_button(button::Button *button) { + this->web_server_->events_.send(this->web_server_->button_json(button, DETAIL_ALL).c_str(), "state"); + return true; +} +#endif +#ifdef USE_TEXT_SENSOR +bool ListEntitiesIterator::on_text_sensor(text_sensor::TextSensor *text_sensor) { + this->web_server_->events_.send( + this->web_server_->text_sensor_json(text_sensor, text_sensor->state, DETAIL_ALL).c_str(), "state"); + return true; +} +#endif +#ifdef USE_LOCK +bool ListEntitiesIterator::on_lock(lock::Lock *a_lock) { + this->web_server_->events_.send(this->web_server_->lock_json(a_lock, a_lock->state, DETAIL_ALL).c_str(), "state"); + return true; +} +#endif + +#ifdef USE_CLIMATE +bool ListEntitiesIterator::on_climate(climate::Climate *climate) { + this->web_server_->events_.send(this->web_server_->climate_json(climate, DETAIL_ALL).c_str(), "state"); + return true; +} +#endif + +#ifdef USE_NUMBER +bool ListEntitiesIterator::on_number(number::Number *number) { + this->web_server_->events_.send(this->web_server_->number_json(number, number->state, DETAIL_ALL).c_str(), "state"); + return true; +} +#endif + +#ifdef USE_SELECT +bool ListEntitiesIterator::on_select(select::Select *select) { + this->web_server_->events_.send(this->web_server_->select_json(select, select->state, DETAIL_ALL).c_str(), "state"); + return true; +} +#endif + +} // namespace web_server +} // namespace esphome + +#endif // USE_ARDUINO diff --git a/esphome/components/web_server/list_entities.h b/esphome/components/web_server/list_entities.h new file mode 100644 index 0000000000..85868caff8 --- /dev/null +++ b/esphome/components/web_server/list_entities.h @@ -0,0 +1,60 @@ +#pragma once + +#ifdef USE_ARDUINO + +#include "esphome/core/component.h" +#include "esphome/core/component_iterator.h" +#include "esphome/core/defines.h" +namespace esphome { +namespace web_server { + +class WebServer; + +class ListEntitiesIterator : public ComponentIterator { + public: + ListEntitiesIterator(WebServer *web_server); +#ifdef USE_BINARY_SENSOR + bool on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) override; +#endif +#ifdef USE_COVER + bool on_cover(cover::Cover *cover) override; +#endif +#ifdef USE_FAN + bool on_fan(fan::Fan *fan) override; +#endif +#ifdef USE_LIGHT + bool on_light(light::LightState *light) override; +#endif +#ifdef USE_SENSOR + bool on_sensor(sensor::Sensor *sensor) override; +#endif +#ifdef USE_SWITCH + bool on_switch(switch_::Switch *a_switch) override; +#endif +#ifdef USE_BUTTON + bool on_button(button::Button *button) override; +#endif +#ifdef USE_TEXT_SENSOR + bool on_text_sensor(text_sensor::TextSensor *text_sensor) override; +#endif +#ifdef USE_CLIMATE + bool on_climate(climate::Climate *climate) override; +#endif +#ifdef USE_NUMBER + bool on_number(number::Number *number) override; +#endif +#ifdef USE_SELECT + bool on_select(select::Select *select) override; +#endif +#ifdef USE_LOCK + bool on_lock(lock::Lock *a_lock) override; +#endif + + protected: + WebServer *web_server_; +}; + +} // namespace web_server +} // namespace esphome + +#endif // USE_ARDUINO diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 278aeab937..0dfd608661 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -1,6 +1,7 @@ #ifdef USE_ARDUINO #include "web_server.h" + #include "esphome/core/log.h" #include "esphome/core/application.h" #include "esphome/core/entity_base.h" @@ -17,7 +18,7 @@ #endif #ifdef USE_LOGGER -#include +#include "esphome/components/logger/logger.h" #endif #ifdef USE_FAN @@ -106,87 +107,7 @@ void WebServer::setup() { }).c_str(), "ping", millis(), 30000); -#ifdef USE_SENSOR - for (auto *obj : App.get_sensors()) { - if (this->include_internal_ || !obj->is_internal()) - client->send(this->sensor_json(obj, obj->state, DETAIL_ALL).c_str(), "state"); - } -#endif - -#ifdef USE_SWITCH - for (auto *obj : App.get_switches()) { - if (this->include_internal_ || !obj->is_internal()) - client->send(this->switch_json(obj, obj->state, DETAIL_ALL).c_str(), "state"); - } -#endif - -#ifdef USE_BUTTON - for (auto *obj : App.get_buttons()) - client->send(this->button_json(obj, DETAIL_ALL).c_str(), "state"); -#endif - -#ifdef USE_BINARY_SENSOR - for (auto *obj : App.get_binary_sensors()) { - if (this->include_internal_ || !obj->is_internal()) - client->send(this->binary_sensor_json(obj, obj->state, DETAIL_ALL).c_str(), "state"); - } -#endif - -#ifdef USE_FAN - for (auto *obj : App.get_fans()) { - if (this->include_internal_ || !obj->is_internal()) - client->send(this->fan_json(obj, DETAIL_ALL).c_str(), "state"); - } -#endif - -#ifdef USE_LIGHT - for (auto *obj : App.get_lights()) { - if (this->include_internal_ || !obj->is_internal()) - client->send(this->light_json(obj, DETAIL_ALL).c_str(), "state"); - } -#endif - -#ifdef USE_TEXT_SENSOR - for (auto *obj : App.get_text_sensors()) { - if (this->include_internal_ || !obj->is_internal()) - client->send(this->text_sensor_json(obj, obj->state, DETAIL_ALL).c_str(), "state"); - } -#endif - -#ifdef USE_COVER - for (auto *obj : App.get_covers()) { - if (this->include_internal_ || !obj->is_internal()) - client->send(this->cover_json(obj, DETAIL_ALL).c_str(), "state"); - } -#endif - -#ifdef USE_NUMBER - for (auto *obj : App.get_numbers()) { - if (this->include_internal_ || !obj->is_internal()) - client->send(this->number_json(obj, obj->state, DETAIL_ALL).c_str(), "state"); - } -#endif - -#ifdef USE_SELECT - for (auto *obj : App.get_selects()) { - if (this->include_internal_ || !obj->is_internal()) - client->send(this->select_json(obj, obj->state, DETAIL_ALL).c_str(), "state"); - } -#endif - -#ifdef USE_CLIMATE - for (auto *obj : App.get_climates()) { - if (this->include_internal_ || !obj->is_internal()) - client->send(this->climate_json(obj, DETAIL_ALL).c_str(), "state"); - } -#endif - -#ifdef USE_LOCK - for (auto *obj : App.get_locks()) { - if (this->include_internal_ || !obj->is_internal()) - client->send(this->lock_json(obj, obj->state, DETAIL_ALL).c_str(), "state"); - } -#endif + this->entities_iterator_.begin(this->include_internal_); }); #ifdef USE_LOGGER @@ -203,6 +124,7 @@ void WebServer::setup() { this->set_interval(10000, [this]() { this->events_.send("", "ping", millis(), 30000); }); } +void WebServer::loop() { this->entities_iterator_.advance(); } void WebServer::dump_config() { ESP_LOGCONFIG(TAG, "Web Server:"); ESP_LOGCONFIG(TAG, " Address: %s:%u", network::get_use_address().c_str(), this->base_->get_port()); diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 2717997f60..73813ecfa1 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -2,6 +2,8 @@ #ifdef USE_ARDUINO +#include "list_entities.h" + #include "esphome/components/web_server_base/web_server_base.h" #include "esphome/core/component.h" #include "esphome/core/controller.h" @@ -32,7 +34,7 @@ enum JsonDetail { DETAIL_ALL, DETAIL_STATE }; */ class WebServer : public Controller, public Component, public AsyncWebHandler { public: - WebServer(web_server_base::WebServerBase *base) : base_(base) {} + WebServer(web_server_base::WebServerBase *base) : base_(base), entities_iterator_(ListEntitiesIterator(this)) {} /** Set the URL to the CSS that's sent to each client. Defaults to * https://esphome.io/_static/webserver-v1.min.css @@ -76,6 +78,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { // (In most use cases you won't need these) /// Setup the internal web server and register handlers. void setup() override; + void loop() override; void dump_config() override; @@ -217,8 +220,10 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { bool isRequestHandlerTrivial() override; protected: + friend ListEntitiesIterator; web_server_base::WebServerBase *base_; AsyncEventSource events_{"/events"}; + ListEntitiesIterator entities_iterator_; const char *css_url_{nullptr}; const char *css_include_{nullptr}; const char *js_url_{nullptr}; diff --git a/esphome/components/api/util.cpp b/esphome/core/component_iterator.cpp similarity index 80% rename from esphome/components/api/util.cpp rename to esphome/core/component_iterator.cpp index fd55f89f9b..4781607a2d 100644 --- a/esphome/components/api/util.cpp +++ b/esphome/core/component_iterator.cpp @@ -1,16 +1,18 @@ -#include "util.h" -#include "api_server.h" -#include "user_services.h" -#include "esphome/core/log.h" +#include "component_iterator.h" + #include "esphome/core/application.h" -namespace esphome { -namespace api { +#ifdef USE_API +#include "esphome/components/api/api_server.h" +#include "esphome/components/api/user_services.h" +#endif -ComponentIterator::ComponentIterator(APIServer *server) : server_(server) {} -void ComponentIterator::begin() { +namespace esphome { + +void ComponentIterator::begin(bool include_internal) { this->state_ = IteratorState::BEGIN; this->at_ = 0; + this->include_internal_ = include_internal; } void ComponentIterator::advance() { bool advance_platform = false; @@ -32,7 +34,7 @@ void ComponentIterator::advance() { advance_platform = true; } else { auto *binary_sensor = App.get_binary_sensors()[this->at_]; - if (binary_sensor->is_internal()) { + if (binary_sensor->is_internal() && !this->include_internal_) { success = true; break; } else { @@ -47,7 +49,7 @@ void ComponentIterator::advance() { advance_platform = true; } else { auto *cover = App.get_covers()[this->at_]; - if (cover->is_internal()) { + if (cover->is_internal() && !this->include_internal_) { success = true; break; } else { @@ -62,7 +64,7 @@ void ComponentIterator::advance() { advance_platform = true; } else { auto *fan = App.get_fans()[this->at_]; - if (fan->is_internal()) { + if (fan->is_internal() && !this->include_internal_) { success = true; break; } else { @@ -77,7 +79,7 @@ void ComponentIterator::advance() { advance_platform = true; } else { auto *light = App.get_lights()[this->at_]; - if (light->is_internal()) { + if (light->is_internal() && !this->include_internal_) { success = true; break; } else { @@ -92,7 +94,7 @@ void ComponentIterator::advance() { advance_platform = true; } else { auto *sensor = App.get_sensors()[this->at_]; - if (sensor->is_internal()) { + if (sensor->is_internal() && !this->include_internal_) { success = true; break; } else { @@ -107,7 +109,7 @@ void ComponentIterator::advance() { advance_platform = true; } else { auto *a_switch = App.get_switches()[this->at_]; - if (a_switch->is_internal()) { + if (a_switch->is_internal() && !this->include_internal_) { success = true; break; } else { @@ -122,7 +124,7 @@ void ComponentIterator::advance() { advance_platform = true; } else { auto *button = App.get_buttons()[this->at_]; - if (button->is_internal()) { + if (button->is_internal() && !this->include_internal_) { success = true; break; } else { @@ -137,7 +139,7 @@ void ComponentIterator::advance() { advance_platform = true; } else { auto *text_sensor = App.get_text_sensors()[this->at_]; - if (text_sensor->is_internal()) { + if (text_sensor->is_internal() && !this->include_internal_) { success = true; break; } else { @@ -146,20 +148,22 @@ void ComponentIterator::advance() { } break; #endif +#ifdef USE_API case IteratorState ::SERVICE: - if (this->at_ >= this->server_->get_user_services().size()) { + if (this->at_ >= api::global_api_server->get_user_services().size()) { advance_platform = true; } else { - auto *service = this->server_->get_user_services()[this->at_]; + auto *service = api::global_api_server->get_user_services()[this->at_]; success = this->on_service(service); } break; +#endif #ifdef USE_ESP32_CAMERA case IteratorState::CAMERA: if (esp32_camera::global_esp32_camera == nullptr) { advance_platform = true; } else { - if (esp32_camera::global_esp32_camera->is_internal()) { + if (esp32_camera::global_esp32_camera->is_internal() && !this->include_internal_) { advance_platform = success = true; break; } else { @@ -174,7 +178,7 @@ void ComponentIterator::advance() { advance_platform = true; } else { auto *climate = App.get_climates()[this->at_]; - if (climate->is_internal()) { + if (climate->is_internal() && !this->include_internal_) { success = true; break; } else { @@ -189,7 +193,7 @@ void ComponentIterator::advance() { advance_platform = true; } else { auto *number = App.get_numbers()[this->at_]; - if (number->is_internal()) { + if (number->is_internal() && !this->include_internal_) { success = true; break; } else { @@ -204,7 +208,7 @@ void ComponentIterator::advance() { advance_platform = true; } else { auto *select = App.get_selects()[this->at_]; - if (select->is_internal()) { + if (select->is_internal() && !this->include_internal_) { success = true; break; } else { @@ -219,7 +223,7 @@ void ComponentIterator::advance() { advance_platform = true; } else { auto *a_lock = App.get_locks()[this->at_]; - if (a_lock->is_internal()) { + if (a_lock->is_internal() && !this->include_internal_) { success = true; break; } else { @@ -244,10 +248,10 @@ void ComponentIterator::advance() { } bool ComponentIterator::on_end() { return true; } bool ComponentIterator::on_begin() { return true; } -bool ComponentIterator::on_service(UserServiceDescriptor *service) { return true; } +#ifdef USE_API +bool ComponentIterator::on_service(api::UserServiceDescriptor *service) { return true; } +#endif #ifdef USE_ESP32_CAMERA bool ComponentIterator::on_camera(esp32_camera::ESP32Camera *camera) { return true; } #endif - -} // namespace api } // namespace esphome diff --git a/esphome/components/api/util.h b/esphome/core/component_iterator.h similarity index 91% rename from esphome/components/api/util.h rename to esphome/core/component_iterator.h index 9204b0829e..bd95fe95e1 100644 --- a/esphome/components/api/util.h +++ b/esphome/core/component_iterator.h @@ -1,23 +1,24 @@ #pragma once -#include "esphome/core/helpers.h" #include "esphome/core/component.h" #include "esphome/core/controller.h" +#include "esphome/core/helpers.h" + #ifdef USE_ESP32_CAMERA #include "esphome/components/esp32_camera/esp32_camera.h" #endif namespace esphome { -namespace api { -class APIServer; +#ifdef USE_API +namespace api { class UserServiceDescriptor; +} // namespace api +#endif class ComponentIterator { public: - ComponentIterator(APIServer *server); - - void begin(); + void begin(bool include_internal = false); void advance(); virtual bool on_begin(); #ifdef USE_BINARY_SENSOR @@ -44,7 +45,9 @@ class ComponentIterator { #ifdef USE_TEXT_SENSOR virtual bool on_text_sensor(text_sensor::TextSensor *text_sensor) = 0; #endif - virtual bool on_service(UserServiceDescriptor *service); +#ifdef USE_API + virtual bool on_service(api::UserServiceDescriptor *service); +#endif #ifdef USE_ESP32_CAMERA virtual bool on_camera(esp32_camera::ESP32Camera *camera); #endif @@ -90,7 +93,9 @@ class ComponentIterator { #ifdef USE_TEXT_SENSOR TEXT_SENSOR, #endif +#ifdef USE_API SERVICE, +#endif #ifdef USE_ESP32_CAMERA CAMERA, #endif @@ -109,9 +114,7 @@ class ComponentIterator { MAX, } state_{IteratorState::NONE}; size_t at_{0}; - - APIServer *server_; + bool include_internal_{false}; }; -} // namespace api } // namespace esphome From 1496bc1b0729cfaef625df8b2d0d5e732b20c2a1 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 23 Mar 2022 09:46:25 +1300 Subject: [PATCH 0330/1729] Reserve less memory for json (#3289) --- esphome/components/json/json_util.cpp | 60 +++++++++++++++++++-------- 1 file changed, 43 insertions(+), 17 deletions(-) diff --git a/esphome/components/json/json_util.cpp b/esphome/components/json/json_util.cpp index 9acba76597..2070b312e8 100644 --- a/esphome/components/json/json_util.cpp +++ b/esphome/components/json/json_util.cpp @@ -16,16 +16,24 @@ static const char *const TAG = "json"; static std::vector global_json_build_buffer; // NOLINT std::string build_json(const json_build_t &f) { - // Here we are allocating as much heap memory as available minus 2kb to be safe + // Here we are allocating up to 5kb of memory, + // with the heap size minus 2kb to be safe if less than 5kb // as we can not have a true dynamic sized document. // The excess memory is freed below with `shrinkToFit()` #ifdef USE_ESP8266 - const size_t free_heap = ESP.getMaxFreeBlockSize() - 2048; // NOLINT(readability-static-accessed-through-instance) + const size_t free_heap = ESP.getMaxFreeBlockSize(); // NOLINT(readability-static-accessed-through-instance) #elif defined(USE_ESP32) - const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL) - 2048; + const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL); #endif - DynamicJsonDocument json_document(free_heap); + const size_t request_size = std::min(free_heap - 2048, (size_t) 5120); + + DynamicJsonDocument json_document(request_size); + if (json_document.memoryPool().buffer() == nullptr) { + ESP_LOGE(TAG, "Could not allocate memory for JSON document! Requested %u bytes, largest free heap block: %u bytes", + request_size, free_heap); + return "{}"; + } JsonObject root = json_document.to(); f(root); json_document.shrinkToFit(); @@ -36,27 +44,45 @@ std::string build_json(const json_build_t &f) { } void parse_json(const std::string &data, const json_parse_t &f) { - // Here we are allocating as much heap memory as available minus 2kb to be safe + // Here we are allocating 1.5 times the data size, + // with the heap size minus 2kb to be safe if less than that // as we can not have a true dynamic sized document. // The excess memory is freed below with `shrinkToFit()` #ifdef USE_ESP8266 - const size_t free_heap = ESP.getMaxFreeBlockSize() - 2048; // NOLINT(readability-static-accessed-through-instance) + const size_t free_heap = ESP.getMaxFreeBlockSize(); // NOLINT(readability-static-accessed-through-instance) #elif defined(USE_ESP32) - const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL) - 2048; + const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL); #endif + bool pass = false; + do { + const size_t request_size = std::min(free_heap - 2048, (size_t)(data.size() * 1.5)); - DynamicJsonDocument json_document(free_heap); - DeserializationError err = deserializeJson(json_document, data); - json_document.shrinkToFit(); + DynamicJsonDocument json_document(request_size); + if (json_document.memoryPool().buffer() == nullptr) { + ESP_LOGE(TAG, "Could not allocate memory for JSON document! Requested %u bytes, free heap: %u", request_size, + free_heap); + return; + } + DeserializationError err = deserializeJson(json_document, data); + json_document.shrinkToFit(); - JsonObject root = json_document.as(); + JsonObject root = json_document.as(); - if (err) { - ESP_LOGW(TAG, "Parsing JSON failed."); - return; - } - - f(root); + if (err == DeserializationError::Ok) { + pass = true; + f(root); + } else if (err == DeserializationError::NoMemory) { + if (request_size * 2 >= free_heap) { + ESP_LOGE(TAG, "Can not allocate more memory for deserialization. Consider making source string smaller"); + return; + } + ESP_LOGW(TAG, "Increasing memory allocation."); + continue; + } else { + ESP_LOGE(TAG, "JSON parse error: %s", err.c_str()); + return; + } + } while (!pass); } } // namespace json From 58b70b42dd5fc91e7e0eca7db20ea114b5b12672 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 23 Mar 2022 11:12:22 +1300 Subject: [PATCH 0331/1729] Add small delay before setting up app in safe mode (#3323) --- esphome/components/ota/ota_component.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/ota/ota_component.cpp b/esphome/components/ota/ota_component.cpp index 37da3bdc44..3138c495da 100644 --- a/esphome/components/ota/ota_component.cpp +++ b/esphome/components/ota/ota_component.cpp @@ -473,6 +473,8 @@ bool OTAComponent::should_enter_safe_mode(uint8_t num_attempts, uint32_t enable_ App.reboot(); }); + // Delay here to allow power to stabilise before Wi-Fi/Ethernet is initialised. + delay(100); // NOLINT App.setup(); ESP_LOGI(TAG, "Waiting for OTA attempt."); From 24b75b7ed64ee9e239e27511c91e33a66519e9a2 Mon Sep 17 00:00:00 2001 From: wysiwyng <4764286+wysiwyng@users.noreply.github.com> Date: Wed, 16 Mar 2022 20:33:05 +0100 Subject: [PATCH 0332/1729] Fix WDT reset during dallas search algorithm (#3293) --- esphome/components/dallas/esp_one_wire.cpp | 4 ---- esphome/components/dallas/esp_one_wire.h | 1 - 2 files changed, 5 deletions(-) diff --git a/esphome/components/dallas/esp_one_wire.cpp b/esphome/components/dallas/esp_one_wire.cpp index 885846e5e5..5bd0f42855 100644 --- a/esphome/components/dallas/esp_one_wire.cpp +++ b/esphome/components/dallas/esp_one_wire.cpp @@ -142,7 +142,6 @@ void IRAM_ATTR ESPOneWire::select(uint64_t address) { void IRAM_ATTR ESPOneWire::reset_search() { this->last_discrepancy_ = 0; this->last_device_flag_ = false; - this->last_family_discrepancy_ = 0; this->rom_number_ = 0; } uint64_t IRAM_ATTR ESPOneWire::search() { @@ -195,9 +194,6 @@ uint64_t IRAM_ATTR ESPOneWire::search() { if (!branch) { last_zero = id_bit_number; - if (last_zero < 9) { - this->last_discrepancy_ = last_zero; - } } } diff --git a/esphome/components/dallas/esp_one_wire.h b/esphome/components/dallas/esp_one_wire.h index ef6f079f02..7544a6fe98 100644 --- a/esphome/components/dallas/esp_one_wire.h +++ b/esphome/components/dallas/esp_one_wire.h @@ -60,7 +60,6 @@ class ESPOneWire { ISRInternalGPIOPin pin_; uint8_t last_discrepancy_{0}; - uint8_t last_family_discrepancy_{0}; bool last_device_flag_{false}; uint64_t rom_number_{0}; }; From 0729ed538e6b13b5ad5d4f6d3051e79012430cf5 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 23 Mar 2022 09:45:05 +1300 Subject: [PATCH 0333/1729] Webserver utilize Component Iterator to not overload eventstream (#3310) --- esphome/components/api/api_connection.cpp | 2 +- esphome/components/api/api_server.h | 1 - esphome/components/api/list_entities.cpp | 3 +- esphome/components/api/list_entities.h | 6 +- esphome/components/api/proto.cpp | 1 - esphome/components/api/subscribe_state.cpp | 3 +- esphome/components/api/subscribe_state.h | 6 +- .../components/web_server/list_entities.cpp | 97 +++++++++++++++++++ esphome/components/web_server/list_entities.h | 60 ++++++++++++ esphome/components/web_server/web_server.cpp | 86 +--------------- esphome/components/web_server/web_server.h | 7 +- .../util.cpp => core/component_iterator.cpp} | 56 ++++++----- .../api/util.h => core/component_iterator.h} | 23 +++-- 13 files changed, 217 insertions(+), 134 deletions(-) create mode 100644 esphome/components/web_server/list_entities.cpp create mode 100644 esphome/components/web_server/list_entities.h rename esphome/{components/api/util.cpp => core/component_iterator.cpp} (80%) rename esphome/{components/api/util.h => core/component_iterator.h} (91%) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index b998ef5929..81f2465b74 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -23,7 +23,7 @@ static const char *const TAG = "api.connection"; static const int ESP32_CAMERA_STOP_STREAM = 5000; APIConnection::APIConnection(std::unique_ptr sock, APIServer *parent) - : parent_(parent), initial_state_iterator_(parent, this), list_entities_iterator_(parent, this) { + : parent_(parent), initial_state_iterator_(this), list_entities_iterator_(this) { this->proto_write_buffer_.reserve(64); #if defined(USE_API_PLAINTEXT) diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 3214da5b3d..fdc46922ad 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -7,7 +7,6 @@ #include "esphome/components/socket/socket.h" #include "api_pb2.h" #include "api_pb2_service.h" -#include "util.h" #include "list_entities.h" #include "subscribe_state.h" #include "user_services.h" diff --git a/esphome/components/api/list_entities.cpp b/esphome/components/api/list_entities.cpp index fb0dfa3d05..9f55fda617 100644 --- a/esphome/components/api/list_entities.cpp +++ b/esphome/components/api/list_entities.cpp @@ -40,8 +40,7 @@ bool ListEntitiesIterator::on_lock(lock::Lock *a_lock) { return this->client_->s #endif bool ListEntitiesIterator::on_end() { return this->client_->send_list_info_done(); } -ListEntitiesIterator::ListEntitiesIterator(APIServer *server, APIConnection *client) - : ComponentIterator(server), client_(client) {} +ListEntitiesIterator::ListEntitiesIterator(APIConnection *client) : client_(client) {} bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) { auto resp = service->encode_list_service_response(); return this->client_->send_list_entities_services_response(resp); diff --git a/esphome/components/api/list_entities.h b/esphome/components/api/list_entities.h index bfceb39ebf..51c343eb03 100644 --- a/esphome/components/api/list_entities.h +++ b/esphome/components/api/list_entities.h @@ -1,8 +1,8 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/component_iterator.h" #include "esphome/core/defines.h" -#include "util.h" namespace esphome { namespace api { @@ -11,7 +11,7 @@ class APIConnection; class ListEntitiesIterator : public ComponentIterator { public: - ListEntitiesIterator(APIServer *server, APIConnection *client); + ListEntitiesIterator(APIConnection *client); #ifdef USE_BINARY_SENSOR bool on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) override; #endif @@ -60,5 +60,3 @@ class ListEntitiesIterator : public ComponentIterator { } // namespace api } // namespace esphome - -#include "api_server.h" diff --git a/esphome/components/api/proto.cpp b/esphome/components/api/proto.cpp index 0ba277d90a..ca7a4c0887 100644 --- a/esphome/components/api/proto.cpp +++ b/esphome/components/api/proto.cpp @@ -1,5 +1,4 @@ #include "proto.h" -#include "util.h" #include "esphome/core/log.h" namespace esphome { diff --git a/esphome/components/api/subscribe_state.cpp b/esphome/components/api/subscribe_state.cpp index 10416ecc5c..ba277502c8 100644 --- a/esphome/components/api/subscribe_state.cpp +++ b/esphome/components/api/subscribe_state.cpp @@ -50,8 +50,7 @@ bool InitialStateIterator::on_select(select::Select *select) { #ifdef USE_LOCK bool InitialStateIterator::on_lock(lock::Lock *a_lock) { return this->client_->send_lock_state(a_lock, a_lock->state); } #endif -InitialStateIterator::InitialStateIterator(APIServer *server, APIConnection *client) - : ComponentIterator(server), client_(client) {} +InitialStateIterator::InitialStateIterator(APIConnection *client) : client_(client) {} } // namespace api } // namespace esphome diff --git a/esphome/components/api/subscribe_state.h b/esphome/components/api/subscribe_state.h index caea013f84..515e1a2d07 100644 --- a/esphome/components/api/subscribe_state.h +++ b/esphome/components/api/subscribe_state.h @@ -1,9 +1,9 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/component_iterator.h" #include "esphome/core/controller.h" #include "esphome/core/defines.h" -#include "util.h" namespace esphome { namespace api { @@ -12,7 +12,7 @@ class APIConnection; class InitialStateIterator : public ComponentIterator { public: - InitialStateIterator(APIServer *server, APIConnection *client); + InitialStateIterator(APIConnection *client); #ifdef USE_BINARY_SENSOR bool on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) override; #endif @@ -55,5 +55,3 @@ class InitialStateIterator : public ComponentIterator { } // namespace api } // namespace esphome - -#include "api_server.h" diff --git a/esphome/components/web_server/list_entities.cpp b/esphome/components/web_server/list_entities.cpp new file mode 100644 index 0000000000..6f833a5c83 --- /dev/null +++ b/esphome/components/web_server/list_entities.cpp @@ -0,0 +1,97 @@ +#ifdef USE_ARDUINO + +#include "list_entities.h" +#include "esphome/core/application.h" +#include "esphome/core/log.h" +#include "esphome/core/util.h" + +#include "web_server.h" + +namespace esphome { +namespace web_server { + +ListEntitiesIterator::ListEntitiesIterator(WebServer *web_server) : web_server_(web_server) {} + +#ifdef USE_BINARY_SENSOR +bool ListEntitiesIterator::on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) { + this->web_server_->events_.send( + this->web_server_->binary_sensor_json(binary_sensor, binary_sensor->state, DETAIL_ALL).c_str(), "state"); + return true; +} +#endif +#ifdef USE_COVER +bool ListEntitiesIterator::on_cover(cover::Cover *cover) { + this->web_server_->events_.send(this->web_server_->cover_json(cover, DETAIL_ALL).c_str(), "state"); + return true; +} +#endif +#ifdef USE_FAN +bool ListEntitiesIterator::on_fan(fan::Fan *fan) { + this->web_server_->events_.send(this->web_server_->fan_json(fan, DETAIL_ALL).c_str(), "state"); + return true; +} +#endif +#ifdef USE_LIGHT +bool ListEntitiesIterator::on_light(light::LightState *light) { + this->web_server_->events_.send(this->web_server_->light_json(light, DETAIL_ALL).c_str(), "state"); + return true; +} +#endif +#ifdef USE_SENSOR +bool ListEntitiesIterator::on_sensor(sensor::Sensor *sensor) { + this->web_server_->events_.send(this->web_server_->sensor_json(sensor, sensor->state, DETAIL_ALL).c_str(), "state"); + return true; +} +#endif +#ifdef USE_SWITCH +bool ListEntitiesIterator::on_switch(switch_::Switch *a_switch) { + this->web_server_->events_.send(this->web_server_->switch_json(a_switch, a_switch->state, DETAIL_ALL).c_str(), + "state"); + return true; +} +#endif +#ifdef USE_BUTTON +bool ListEntitiesIterator::on_button(button::Button *button) { + this->web_server_->events_.send(this->web_server_->button_json(button, DETAIL_ALL).c_str(), "state"); + return true; +} +#endif +#ifdef USE_TEXT_SENSOR +bool ListEntitiesIterator::on_text_sensor(text_sensor::TextSensor *text_sensor) { + this->web_server_->events_.send( + this->web_server_->text_sensor_json(text_sensor, text_sensor->state, DETAIL_ALL).c_str(), "state"); + return true; +} +#endif +#ifdef USE_LOCK +bool ListEntitiesIterator::on_lock(lock::Lock *a_lock) { + this->web_server_->events_.send(this->web_server_->lock_json(a_lock, a_lock->state, DETAIL_ALL).c_str(), "state"); + return true; +} +#endif + +#ifdef USE_CLIMATE +bool ListEntitiesIterator::on_climate(climate::Climate *climate) { + this->web_server_->events_.send(this->web_server_->climate_json(climate, DETAIL_ALL).c_str(), "state"); + return true; +} +#endif + +#ifdef USE_NUMBER +bool ListEntitiesIterator::on_number(number::Number *number) { + this->web_server_->events_.send(this->web_server_->number_json(number, number->state, DETAIL_ALL).c_str(), "state"); + return true; +} +#endif + +#ifdef USE_SELECT +bool ListEntitiesIterator::on_select(select::Select *select) { + this->web_server_->events_.send(this->web_server_->select_json(select, select->state, DETAIL_ALL).c_str(), "state"); + return true; +} +#endif + +} // namespace web_server +} // namespace esphome + +#endif // USE_ARDUINO diff --git a/esphome/components/web_server/list_entities.h b/esphome/components/web_server/list_entities.h new file mode 100644 index 0000000000..85868caff8 --- /dev/null +++ b/esphome/components/web_server/list_entities.h @@ -0,0 +1,60 @@ +#pragma once + +#ifdef USE_ARDUINO + +#include "esphome/core/component.h" +#include "esphome/core/component_iterator.h" +#include "esphome/core/defines.h" +namespace esphome { +namespace web_server { + +class WebServer; + +class ListEntitiesIterator : public ComponentIterator { + public: + ListEntitiesIterator(WebServer *web_server); +#ifdef USE_BINARY_SENSOR + bool on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) override; +#endif +#ifdef USE_COVER + bool on_cover(cover::Cover *cover) override; +#endif +#ifdef USE_FAN + bool on_fan(fan::Fan *fan) override; +#endif +#ifdef USE_LIGHT + bool on_light(light::LightState *light) override; +#endif +#ifdef USE_SENSOR + bool on_sensor(sensor::Sensor *sensor) override; +#endif +#ifdef USE_SWITCH + bool on_switch(switch_::Switch *a_switch) override; +#endif +#ifdef USE_BUTTON + bool on_button(button::Button *button) override; +#endif +#ifdef USE_TEXT_SENSOR + bool on_text_sensor(text_sensor::TextSensor *text_sensor) override; +#endif +#ifdef USE_CLIMATE + bool on_climate(climate::Climate *climate) override; +#endif +#ifdef USE_NUMBER + bool on_number(number::Number *number) override; +#endif +#ifdef USE_SELECT + bool on_select(select::Select *select) override; +#endif +#ifdef USE_LOCK + bool on_lock(lock::Lock *a_lock) override; +#endif + + protected: + WebServer *web_server_; +}; + +} // namespace web_server +} // namespace esphome + +#endif // USE_ARDUINO diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 278aeab937..0dfd608661 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -1,6 +1,7 @@ #ifdef USE_ARDUINO #include "web_server.h" + #include "esphome/core/log.h" #include "esphome/core/application.h" #include "esphome/core/entity_base.h" @@ -17,7 +18,7 @@ #endif #ifdef USE_LOGGER -#include +#include "esphome/components/logger/logger.h" #endif #ifdef USE_FAN @@ -106,87 +107,7 @@ void WebServer::setup() { }).c_str(), "ping", millis(), 30000); -#ifdef USE_SENSOR - for (auto *obj : App.get_sensors()) { - if (this->include_internal_ || !obj->is_internal()) - client->send(this->sensor_json(obj, obj->state, DETAIL_ALL).c_str(), "state"); - } -#endif - -#ifdef USE_SWITCH - for (auto *obj : App.get_switches()) { - if (this->include_internal_ || !obj->is_internal()) - client->send(this->switch_json(obj, obj->state, DETAIL_ALL).c_str(), "state"); - } -#endif - -#ifdef USE_BUTTON - for (auto *obj : App.get_buttons()) - client->send(this->button_json(obj, DETAIL_ALL).c_str(), "state"); -#endif - -#ifdef USE_BINARY_SENSOR - for (auto *obj : App.get_binary_sensors()) { - if (this->include_internal_ || !obj->is_internal()) - client->send(this->binary_sensor_json(obj, obj->state, DETAIL_ALL).c_str(), "state"); - } -#endif - -#ifdef USE_FAN - for (auto *obj : App.get_fans()) { - if (this->include_internal_ || !obj->is_internal()) - client->send(this->fan_json(obj, DETAIL_ALL).c_str(), "state"); - } -#endif - -#ifdef USE_LIGHT - for (auto *obj : App.get_lights()) { - if (this->include_internal_ || !obj->is_internal()) - client->send(this->light_json(obj, DETAIL_ALL).c_str(), "state"); - } -#endif - -#ifdef USE_TEXT_SENSOR - for (auto *obj : App.get_text_sensors()) { - if (this->include_internal_ || !obj->is_internal()) - client->send(this->text_sensor_json(obj, obj->state, DETAIL_ALL).c_str(), "state"); - } -#endif - -#ifdef USE_COVER - for (auto *obj : App.get_covers()) { - if (this->include_internal_ || !obj->is_internal()) - client->send(this->cover_json(obj, DETAIL_ALL).c_str(), "state"); - } -#endif - -#ifdef USE_NUMBER - for (auto *obj : App.get_numbers()) { - if (this->include_internal_ || !obj->is_internal()) - client->send(this->number_json(obj, obj->state, DETAIL_ALL).c_str(), "state"); - } -#endif - -#ifdef USE_SELECT - for (auto *obj : App.get_selects()) { - if (this->include_internal_ || !obj->is_internal()) - client->send(this->select_json(obj, obj->state, DETAIL_ALL).c_str(), "state"); - } -#endif - -#ifdef USE_CLIMATE - for (auto *obj : App.get_climates()) { - if (this->include_internal_ || !obj->is_internal()) - client->send(this->climate_json(obj, DETAIL_ALL).c_str(), "state"); - } -#endif - -#ifdef USE_LOCK - for (auto *obj : App.get_locks()) { - if (this->include_internal_ || !obj->is_internal()) - client->send(this->lock_json(obj, obj->state, DETAIL_ALL).c_str(), "state"); - } -#endif + this->entities_iterator_.begin(this->include_internal_); }); #ifdef USE_LOGGER @@ -203,6 +124,7 @@ void WebServer::setup() { this->set_interval(10000, [this]() { this->events_.send("", "ping", millis(), 30000); }); } +void WebServer::loop() { this->entities_iterator_.advance(); } void WebServer::dump_config() { ESP_LOGCONFIG(TAG, "Web Server:"); ESP_LOGCONFIG(TAG, " Address: %s:%u", network::get_use_address().c_str(), this->base_->get_port()); diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 2717997f60..73813ecfa1 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -2,6 +2,8 @@ #ifdef USE_ARDUINO +#include "list_entities.h" + #include "esphome/components/web_server_base/web_server_base.h" #include "esphome/core/component.h" #include "esphome/core/controller.h" @@ -32,7 +34,7 @@ enum JsonDetail { DETAIL_ALL, DETAIL_STATE }; */ class WebServer : public Controller, public Component, public AsyncWebHandler { public: - WebServer(web_server_base::WebServerBase *base) : base_(base) {} + WebServer(web_server_base::WebServerBase *base) : base_(base), entities_iterator_(ListEntitiesIterator(this)) {} /** Set the URL to the CSS that's sent to each client. Defaults to * https://esphome.io/_static/webserver-v1.min.css @@ -76,6 +78,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { // (In most use cases you won't need these) /// Setup the internal web server and register handlers. void setup() override; + void loop() override; void dump_config() override; @@ -217,8 +220,10 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { bool isRequestHandlerTrivial() override; protected: + friend ListEntitiesIterator; web_server_base::WebServerBase *base_; AsyncEventSource events_{"/events"}; + ListEntitiesIterator entities_iterator_; const char *css_url_{nullptr}; const char *css_include_{nullptr}; const char *js_url_{nullptr}; diff --git a/esphome/components/api/util.cpp b/esphome/core/component_iterator.cpp similarity index 80% rename from esphome/components/api/util.cpp rename to esphome/core/component_iterator.cpp index fd55f89f9b..4781607a2d 100644 --- a/esphome/components/api/util.cpp +++ b/esphome/core/component_iterator.cpp @@ -1,16 +1,18 @@ -#include "util.h" -#include "api_server.h" -#include "user_services.h" -#include "esphome/core/log.h" +#include "component_iterator.h" + #include "esphome/core/application.h" -namespace esphome { -namespace api { +#ifdef USE_API +#include "esphome/components/api/api_server.h" +#include "esphome/components/api/user_services.h" +#endif -ComponentIterator::ComponentIterator(APIServer *server) : server_(server) {} -void ComponentIterator::begin() { +namespace esphome { + +void ComponentIterator::begin(bool include_internal) { this->state_ = IteratorState::BEGIN; this->at_ = 0; + this->include_internal_ = include_internal; } void ComponentIterator::advance() { bool advance_platform = false; @@ -32,7 +34,7 @@ void ComponentIterator::advance() { advance_platform = true; } else { auto *binary_sensor = App.get_binary_sensors()[this->at_]; - if (binary_sensor->is_internal()) { + if (binary_sensor->is_internal() && !this->include_internal_) { success = true; break; } else { @@ -47,7 +49,7 @@ void ComponentIterator::advance() { advance_platform = true; } else { auto *cover = App.get_covers()[this->at_]; - if (cover->is_internal()) { + if (cover->is_internal() && !this->include_internal_) { success = true; break; } else { @@ -62,7 +64,7 @@ void ComponentIterator::advance() { advance_platform = true; } else { auto *fan = App.get_fans()[this->at_]; - if (fan->is_internal()) { + if (fan->is_internal() && !this->include_internal_) { success = true; break; } else { @@ -77,7 +79,7 @@ void ComponentIterator::advance() { advance_platform = true; } else { auto *light = App.get_lights()[this->at_]; - if (light->is_internal()) { + if (light->is_internal() && !this->include_internal_) { success = true; break; } else { @@ -92,7 +94,7 @@ void ComponentIterator::advance() { advance_platform = true; } else { auto *sensor = App.get_sensors()[this->at_]; - if (sensor->is_internal()) { + if (sensor->is_internal() && !this->include_internal_) { success = true; break; } else { @@ -107,7 +109,7 @@ void ComponentIterator::advance() { advance_platform = true; } else { auto *a_switch = App.get_switches()[this->at_]; - if (a_switch->is_internal()) { + if (a_switch->is_internal() && !this->include_internal_) { success = true; break; } else { @@ -122,7 +124,7 @@ void ComponentIterator::advance() { advance_platform = true; } else { auto *button = App.get_buttons()[this->at_]; - if (button->is_internal()) { + if (button->is_internal() && !this->include_internal_) { success = true; break; } else { @@ -137,7 +139,7 @@ void ComponentIterator::advance() { advance_platform = true; } else { auto *text_sensor = App.get_text_sensors()[this->at_]; - if (text_sensor->is_internal()) { + if (text_sensor->is_internal() && !this->include_internal_) { success = true; break; } else { @@ -146,20 +148,22 @@ void ComponentIterator::advance() { } break; #endif +#ifdef USE_API case IteratorState ::SERVICE: - if (this->at_ >= this->server_->get_user_services().size()) { + if (this->at_ >= api::global_api_server->get_user_services().size()) { advance_platform = true; } else { - auto *service = this->server_->get_user_services()[this->at_]; + auto *service = api::global_api_server->get_user_services()[this->at_]; success = this->on_service(service); } break; +#endif #ifdef USE_ESP32_CAMERA case IteratorState::CAMERA: if (esp32_camera::global_esp32_camera == nullptr) { advance_platform = true; } else { - if (esp32_camera::global_esp32_camera->is_internal()) { + if (esp32_camera::global_esp32_camera->is_internal() && !this->include_internal_) { advance_platform = success = true; break; } else { @@ -174,7 +178,7 @@ void ComponentIterator::advance() { advance_platform = true; } else { auto *climate = App.get_climates()[this->at_]; - if (climate->is_internal()) { + if (climate->is_internal() && !this->include_internal_) { success = true; break; } else { @@ -189,7 +193,7 @@ void ComponentIterator::advance() { advance_platform = true; } else { auto *number = App.get_numbers()[this->at_]; - if (number->is_internal()) { + if (number->is_internal() && !this->include_internal_) { success = true; break; } else { @@ -204,7 +208,7 @@ void ComponentIterator::advance() { advance_platform = true; } else { auto *select = App.get_selects()[this->at_]; - if (select->is_internal()) { + if (select->is_internal() && !this->include_internal_) { success = true; break; } else { @@ -219,7 +223,7 @@ void ComponentIterator::advance() { advance_platform = true; } else { auto *a_lock = App.get_locks()[this->at_]; - if (a_lock->is_internal()) { + if (a_lock->is_internal() && !this->include_internal_) { success = true; break; } else { @@ -244,10 +248,10 @@ void ComponentIterator::advance() { } bool ComponentIterator::on_end() { return true; } bool ComponentIterator::on_begin() { return true; } -bool ComponentIterator::on_service(UserServiceDescriptor *service) { return true; } +#ifdef USE_API +bool ComponentIterator::on_service(api::UserServiceDescriptor *service) { return true; } +#endif #ifdef USE_ESP32_CAMERA bool ComponentIterator::on_camera(esp32_camera::ESP32Camera *camera) { return true; } #endif - -} // namespace api } // namespace esphome diff --git a/esphome/components/api/util.h b/esphome/core/component_iterator.h similarity index 91% rename from esphome/components/api/util.h rename to esphome/core/component_iterator.h index 9204b0829e..bd95fe95e1 100644 --- a/esphome/components/api/util.h +++ b/esphome/core/component_iterator.h @@ -1,23 +1,24 @@ #pragma once -#include "esphome/core/helpers.h" #include "esphome/core/component.h" #include "esphome/core/controller.h" +#include "esphome/core/helpers.h" + #ifdef USE_ESP32_CAMERA #include "esphome/components/esp32_camera/esp32_camera.h" #endif namespace esphome { -namespace api { -class APIServer; +#ifdef USE_API +namespace api { class UserServiceDescriptor; +} // namespace api +#endif class ComponentIterator { public: - ComponentIterator(APIServer *server); - - void begin(); + void begin(bool include_internal = false); void advance(); virtual bool on_begin(); #ifdef USE_BINARY_SENSOR @@ -44,7 +45,9 @@ class ComponentIterator { #ifdef USE_TEXT_SENSOR virtual bool on_text_sensor(text_sensor::TextSensor *text_sensor) = 0; #endif - virtual bool on_service(UserServiceDescriptor *service); +#ifdef USE_API + virtual bool on_service(api::UserServiceDescriptor *service); +#endif #ifdef USE_ESP32_CAMERA virtual bool on_camera(esp32_camera::ESP32Camera *camera); #endif @@ -90,7 +93,9 @@ class ComponentIterator { #ifdef USE_TEXT_SENSOR TEXT_SENSOR, #endif +#ifdef USE_API SERVICE, +#endif #ifdef USE_ESP32_CAMERA CAMERA, #endif @@ -109,9 +114,7 @@ class ComponentIterator { MAX, } state_{IteratorState::NONE}; size_t at_{0}; - - APIServer *server_; + bool include_internal_{false}; }; -} // namespace api } // namespace esphome From 4e4a512107f714bac29fab43a2fd6ddb7dcea308 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 23 Mar 2022 09:46:25 +1300 Subject: [PATCH 0334/1729] Reserve less memory for json (#3289) --- esphome/components/json/json_util.cpp | 60 +++++++++++++++++++-------- 1 file changed, 43 insertions(+), 17 deletions(-) diff --git a/esphome/components/json/json_util.cpp b/esphome/components/json/json_util.cpp index 9acba76597..2070b312e8 100644 --- a/esphome/components/json/json_util.cpp +++ b/esphome/components/json/json_util.cpp @@ -16,16 +16,24 @@ static const char *const TAG = "json"; static std::vector global_json_build_buffer; // NOLINT std::string build_json(const json_build_t &f) { - // Here we are allocating as much heap memory as available minus 2kb to be safe + // Here we are allocating up to 5kb of memory, + // with the heap size minus 2kb to be safe if less than 5kb // as we can not have a true dynamic sized document. // The excess memory is freed below with `shrinkToFit()` #ifdef USE_ESP8266 - const size_t free_heap = ESP.getMaxFreeBlockSize() - 2048; // NOLINT(readability-static-accessed-through-instance) + const size_t free_heap = ESP.getMaxFreeBlockSize(); // NOLINT(readability-static-accessed-through-instance) #elif defined(USE_ESP32) - const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL) - 2048; + const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL); #endif - DynamicJsonDocument json_document(free_heap); + const size_t request_size = std::min(free_heap - 2048, (size_t) 5120); + + DynamicJsonDocument json_document(request_size); + if (json_document.memoryPool().buffer() == nullptr) { + ESP_LOGE(TAG, "Could not allocate memory for JSON document! Requested %u bytes, largest free heap block: %u bytes", + request_size, free_heap); + return "{}"; + } JsonObject root = json_document.to(); f(root); json_document.shrinkToFit(); @@ -36,27 +44,45 @@ std::string build_json(const json_build_t &f) { } void parse_json(const std::string &data, const json_parse_t &f) { - // Here we are allocating as much heap memory as available minus 2kb to be safe + // Here we are allocating 1.5 times the data size, + // with the heap size minus 2kb to be safe if less than that // as we can not have a true dynamic sized document. // The excess memory is freed below with `shrinkToFit()` #ifdef USE_ESP8266 - const size_t free_heap = ESP.getMaxFreeBlockSize() - 2048; // NOLINT(readability-static-accessed-through-instance) + const size_t free_heap = ESP.getMaxFreeBlockSize(); // NOLINT(readability-static-accessed-through-instance) #elif defined(USE_ESP32) - const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL) - 2048; + const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL); #endif + bool pass = false; + do { + const size_t request_size = std::min(free_heap - 2048, (size_t)(data.size() * 1.5)); - DynamicJsonDocument json_document(free_heap); - DeserializationError err = deserializeJson(json_document, data); - json_document.shrinkToFit(); + DynamicJsonDocument json_document(request_size); + if (json_document.memoryPool().buffer() == nullptr) { + ESP_LOGE(TAG, "Could not allocate memory for JSON document! Requested %u bytes, free heap: %u", request_size, + free_heap); + return; + } + DeserializationError err = deserializeJson(json_document, data); + json_document.shrinkToFit(); - JsonObject root = json_document.as(); + JsonObject root = json_document.as(); - if (err) { - ESP_LOGW(TAG, "Parsing JSON failed."); - return; - } - - f(root); + if (err == DeserializationError::Ok) { + pass = true; + f(root); + } else if (err == DeserializationError::NoMemory) { + if (request_size * 2 >= free_heap) { + ESP_LOGE(TAG, "Can not allocate more memory for deserialization. Consider making source string smaller"); + return; + } + ESP_LOGW(TAG, "Increasing memory allocation."); + continue; + } else { + ESP_LOGE(TAG, "JSON parse error: %s", err.c_str()); + return; + } + } while (!pass); } } // namespace json From 9a9d5964eeda7fda76900ec50109f4465f092871 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 23 Mar 2022 11:12:22 +1300 Subject: [PATCH 0335/1729] Add small delay before setting up app in safe mode (#3323) --- esphome/components/ota/ota_component.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/ota/ota_component.cpp b/esphome/components/ota/ota_component.cpp index 37da3bdc44..3138c495da 100644 --- a/esphome/components/ota/ota_component.cpp +++ b/esphome/components/ota/ota_component.cpp @@ -473,6 +473,8 @@ bool OTAComponent::should_enter_safe_mode(uint8_t num_attempts, uint32_t enable_ App.reboot(); }); + // Delay here to allow power to stabilise before Wi-Fi/Ethernet is initialised. + delay(100); // NOLINT App.setup(); ESP_LOGI(TAG, "Waiting for OTA attempt."); From 24029cc918ea6f9bfbdcfb2f0f4a932dd5c460c8 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 23 Mar 2022 12:26:02 +1300 Subject: [PATCH 0336/1729] Bump version to 2022.3.1 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index e9e3809b2c..8bf303db09 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2022.3.0" +__version__ = "2022.3.1" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From 2034ab4f6c88dba9e14615dc4ced56a86ecc2372 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=2E=20=C3=81rkosi=20R=C3=B3bert?= Date: Thu, 24 Mar 2022 02:28:21 +0100 Subject: [PATCH 0337/1729] increase delay for Ethernet module warm up (#3326) --- esphome/components/ota/ota_component.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/ota/ota_component.cpp b/esphome/components/ota/ota_component.cpp index 3138c495da..fa2605d589 100644 --- a/esphome/components/ota/ota_component.cpp +++ b/esphome/components/ota/ota_component.cpp @@ -474,7 +474,7 @@ bool OTAComponent::should_enter_safe_mode(uint8_t num_attempts, uint32_t enable_ }); // Delay here to allow power to stabilise before Wi-Fi/Ethernet is initialised. - delay(100); // NOLINT + delay(300); // NOLINT App.setup(); ESP_LOGI(TAG, "Waiting for OTA attempt."); From d8024a59288d0db176dc18eed0ba58b74eafda2e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 24 Mar 2022 14:29:43 +1300 Subject: [PATCH 0338/1729] Bump esptool from 3.2 to 3.3 (#3327) Bumps [esptool](https://github.com/espressif/esptool) from 3.2 to 3.3. - [Release notes](https://github.com/espressif/esptool/releases) - [Commits](https://github.com/espressif/esptool/compare/v3.2...v3.3) --- updated-dependencies: - dependency-name: esptool dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c23cdcd33f..e934eb2c6f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ tzlocal==4.1 # from time tzdata>=2021.1 # from time pyserial==3.5 platformio==5.2.5 # When updating platformio, also update Dockerfile -esptool==3.2 +esptool==3.3 click==8.0.4 esphome-dashboard==20220309.0 aioesphomeapi==10.8.2 From 48584e94c4cc14d878bfac1fa53cd05cb821f540 Mon Sep 17 00:00:00 2001 From: Stanislav Meduna Date: Thu, 24 Mar 2022 07:37:48 +0100 Subject: [PATCH 0339/1729] Allow to set user defined characters on LCD (#3322) --- esphome/components/lcd_base/__init__.py | 33 ++++++++++++++++++++- esphome/components/lcd_base/lcd_display.cpp | 7 +++++ esphome/components/lcd_base/lcd_display.h | 5 ++++ tests/test1.yaml | 11 +++++++ 4 files changed, 55 insertions(+), 1 deletion(-) diff --git a/esphome/components/lcd_base/__init__.py b/esphome/components/lcd_base/__init__.py index 0ed2036c55..92fd0b5563 100644 --- a/esphome/components/lcd_base/__init__.py +++ b/esphome/components/lcd_base/__init__.py @@ -1,7 +1,9 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import display -from esphome.const import CONF_DIMENSIONS +from esphome.const import CONF_DIMENSIONS, CONF_POSITION, CONF_DATA + +CONF_USER_CHARACTERS = "user_characters" lcd_base_ns = cg.esphome_ns.namespace("lcd_base") LCDDisplay = lcd_base_ns.class_("LCDDisplay", cg.PollingComponent) @@ -16,9 +18,35 @@ def validate_lcd_dimensions(value): return value +def validate_user_characters(value): + positions = set() + for conf in value: + if conf[CONF_POSITION] in positions: + raise cv.Invalid( + f"Duplicate user defined character at position {conf[CONF_POSITION]}" + ) + positions.add(conf[CONF_POSITION]) + return value + + LCD_SCHEMA = display.BASIC_DISPLAY_SCHEMA.extend( { cv.Required(CONF_DIMENSIONS): validate_lcd_dimensions, + cv.Optional(CONF_USER_CHARACTERS): cv.All( + cv.ensure_list( + cv.Schema( + { + cv.Required(CONF_POSITION): cv.int_range(min=0, max=7), + cv.Required(CONF_DATA): cv.All( + cv.ensure_list(cv.int_range(min=0, max=31)), + cv.Length(min=8, max=8), + ), + } + ), + ), + cv.Length(max=8), + validate_user_characters, + ), } ).extend(cv.polling_component_schema("1s")) @@ -27,3 +55,6 @@ async def setup_lcd_display(var, config): await cg.register_component(var, config) await display.register_display(var, config) cg.add(var.set_dimensions(config[CONF_DIMENSIONS][0], config[CONF_DIMENSIONS][1])) + if CONF_USER_CHARACTERS in config: + for usr in config[CONF_USER_CHARACTERS]: + cg.add(var.set_user_defined_char(usr[CONF_POSITION], usr[CONF_DATA])) diff --git a/esphome/components/lcd_base/lcd_display.cpp b/esphome/components/lcd_base/lcd_display.cpp index ddd7d6a6b3..b937e36c6c 100644 --- a/esphome/components/lcd_base/lcd_display.cpp +++ b/esphome/components/lcd_base/lcd_display.cpp @@ -65,6 +65,13 @@ void LCDDisplay::setup() { this->command_(LCD_DISPLAY_COMMAND_FUNCTION_SET | display_function); } + // store user defined characters + for (auto &user_defined_char : this->user_defined_chars_) { + this->command_(LCD_DISPLAY_COMMAND_SET_CGRAM_ADDR | (user_defined_char.first << 3)); + for (auto data : user_defined_char.second) + this->send(data, true); + } + this->command_(LCD_DISPLAY_COMMAND_FUNCTION_SET | display_function); uint8_t display_control = LCD_DISPLAY_DISPLAY_ON; this->command_(LCD_DISPLAY_COMMAND_DISPLAY_CONTROL | display_control); diff --git a/esphome/components/lcd_base/lcd_display.h b/esphome/components/lcd_base/lcd_display.h index ee150059c6..0c9e59758c 100644 --- a/esphome/components/lcd_base/lcd_display.h +++ b/esphome/components/lcd_base/lcd_display.h @@ -7,6 +7,8 @@ #include "esphome/components/time/real_time_clock.h" #endif +#include + namespace esphome { namespace lcd_base { @@ -19,6 +21,8 @@ class LCDDisplay : public PollingComponent { this->rows_ = rows; } + void set_user_defined_char(uint8_t pos, const std::vector &data) { this->user_defined_chars_[pos] = data; } + void setup() override; float get_setup_priority() const override; void update() override; @@ -58,6 +62,7 @@ class LCDDisplay : public PollingComponent { uint8_t columns_; uint8_t rows_; uint8_t *buffer_{nullptr}; + std::map > user_defined_chars_; }; } // namespace lcd_base diff --git a/tests/test1.yaml b/tests/test1.yaml index bd4c919e67..181f62d3f4 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -2218,6 +2218,17 @@ display: - platform: lcd_pcf8574 dimensions: 18x4 address: 0x3F + user_characters: + - position: 0 + data: + - 0b00000 + - 0b01010 + - 0b00000 + - 0b00100 + - 0b00100 + - 0b10001 + - 0b01110 + - 0b00000 lambda: |- it.print("Hello World!"); i2c_id: i2c_bus From 9a82057303627d6ee656032df962c87a2456afde Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Mon, 28 Mar 2022 01:07:48 +0200 Subject: [PATCH 0340/1729] Font allow using google fonts directly (#3243) --- esphome/components/font/__init__.py | 151 +++++++++++++++++++++++++++- 1 file changed, 147 insertions(+), 4 deletions(-) diff --git a/esphome/components/font/__init__.py b/esphome/components/font/__init__.py index 6af5be45d4..9317b2ec94 100644 --- a/esphome/components/font/__init__.py +++ b/esphome/components/font/__init__.py @@ -1,12 +1,29 @@ import functools +from pathlib import Path +import hashlib +import re + +import requests from esphome import core from esphome.components import display import esphome.config_validation as cv import esphome.codegen as cg -from esphome.const import CONF_FILE, CONF_GLYPHS, CONF_ID, CONF_RAW_DATA_ID, CONF_SIZE +from esphome.const import ( + CONF_FAMILY, + CONF_FILE, + CONF_GLYPHS, + CONF_ID, + CONF_RAW_DATA_ID, + CONF_TYPE, + CONF_SIZE, + CONF_PATH, + CONF_WEIGHT, +) from esphome.core import CORE, HexInt + +DOMAIN = "font" DEPENDENCIES = ["display"] MULTI_CONF = True @@ -71,6 +88,128 @@ def validate_truetype_file(value): return cv.file_(value) +def _compute_gfonts_local_path(value) -> Path: + name = f"{value[CONF_FAMILY]}@{value[CONF_WEIGHT]}@{value[CONF_ITALIC]}@v1" + base_dir = Path(CORE.config_dir) / ".esphome" / DOMAIN + h = hashlib.new("sha256") + h.update(name.encode()) + return base_dir / h.hexdigest()[:8] / "font.ttf" + + +TYPE_LOCAL = "local" +TYPE_GFONTS = "gfonts" +LOCAL_SCHEMA = cv.Schema( + { + cv.Required(CONF_PATH): validate_truetype_file, + } +) +CONF_ITALIC = "italic" +FONT_WEIGHTS = { + "thin": 100, + "extra-light": 200, + "light": 300, + "regular": 400, + "medium": 500, + "semi-bold": 600, + "bold": 700, + "extra-bold": 800, + "black": 900, +} + + +def validate_weight_name(value): + return FONT_WEIGHTS[cv.one_of(*FONT_WEIGHTS, lower=True, space="-")(value)] + + +def download_gfonts(value): + wght = value[CONF_WEIGHT] + if value[CONF_ITALIC]: + wght = f"1,{wght}" + name = f"{value[CONF_FAMILY]}@{value[CONF_WEIGHT]}" + url = f"https://fonts.googleapis.com/css2?family={value[CONF_FAMILY]}:wght@{wght}" + + path = _compute_gfonts_local_path(value) + if path.is_file(): + return value + try: + req = requests.get(url) + req.raise_for_status() + except requests.exceptions.RequestException as e: + raise cv.Invalid( + f"Could not download font for {name}, please check the fonts exists " + f"at google fonts ({e})" + ) + match = re.search(r"src:\s+url\((.+)\)\s+format\('truetype'\);", req.text) + if match is None: + raise cv.Invalid( + f"Could not extract ttf file from gfonts response for {name}, " + f"please report this." + ) + + ttf_url = match.group(1) + try: + req = requests.get(ttf_url) + req.raise_for_status() + except requests.exceptions.RequestException as e: + raise cv.Invalid(f"Could not download ttf file for {name} ({ttf_url}): {e}") + + path.parent.mkdir(exist_ok=True, parents=True) + path.write_bytes(req.content) + return value + + +GFONTS_SCHEMA = cv.All( + { + cv.Required(CONF_FAMILY): cv.string_strict, + cv.Optional(CONF_WEIGHT, default="regular"): cv.Any( + cv.int_, validate_weight_name + ), + cv.Optional(CONF_ITALIC, default=False): cv.boolean, + }, + download_gfonts, +) + + +def validate_file_shorthand(value): + value = cv.string_strict(value) + if value.startswith("gfonts://"): + match = re.match(r"^gfonts://([^@]+)(@.+)?$", value) + if match is None: + raise cv.Invalid("Could not parse gfonts shorthand syntax, please check it") + family = match.group(1) + weight = match.group(2) + data = { + CONF_TYPE: TYPE_GFONTS, + CONF_FAMILY: family, + } + if weight is not None: + data[CONF_WEIGHT] = weight[1:] + return FILE_SCHEMA(data) + return FILE_SCHEMA( + { + CONF_TYPE: TYPE_LOCAL, + CONF_PATH: value, + } + ) + + +TYPED_FILE_SCHEMA = cv.typed_schema( + { + TYPE_LOCAL: LOCAL_SCHEMA, + TYPE_GFONTS: GFONTS_SCHEMA, + } +) + + +def _file_schema(value): + if isinstance(value, str): + return validate_file_shorthand(value) + return TYPED_FILE_SCHEMA(value) + + +FILE_SCHEMA = cv.Schema(_file_schema) + + DEFAULT_GLYPHS = ( ' !"%()+=,-.:/0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz°' ) @@ -79,7 +218,7 @@ CONF_RAW_GLYPH_ID = "raw_glyph_id" FONT_SCHEMA = cv.Schema( { cv.Required(CONF_ID): cv.declare_id(Font), - cv.Required(CONF_FILE): validate_truetype_file, + cv.Required(CONF_FILE): FILE_SCHEMA, cv.Optional(CONF_GLYPHS, default=DEFAULT_GLYPHS): validate_glyphs, cv.Optional(CONF_SIZE, default=20): cv.int_range(min=1), cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), @@ -93,9 +232,13 @@ CONFIG_SCHEMA = cv.All(validate_pillow_installed, FONT_SCHEMA) async def to_code(config): from PIL import ImageFont - path = CORE.relative_config_path(config[CONF_FILE]) + conf = config[CONF_FILE] + if conf[CONF_TYPE] == TYPE_LOCAL: + path = CORE.relative_config_path(conf[CONF_PATH]) + elif conf[CONF_TYPE] == TYPE_GFONTS: + path = _compute_gfonts_local_path(conf) try: - font = ImageFont.truetype(path, config[CONF_SIZE]) + font = ImageFont.truetype(str(path), config[CONF_SIZE]) except Exception as e: raise core.EsphomeError(f"Could not load truetype file {path}: {e}") From 6b9371d1053f4e47c290af22900a8fd1ffcae23a Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 28 Mar 2022 17:04:25 +1300 Subject: [PATCH 0341/1729] Actually increase request memory for json parsing (#3331) --- esphome/components/json/json_util.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/json/json_util.cpp b/esphome/components/json/json_util.cpp index 2070b312e8..10179c9954 100644 --- a/esphome/components/json/json_util.cpp +++ b/esphome/components/json/json_util.cpp @@ -54,9 +54,8 @@ void parse_json(const std::string &data, const json_parse_t &f) { const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL); #endif bool pass = false; + size_t request_size = std::min(free_heap - 2048, (size_t)(data.size() * 1.5)); do { - const size_t request_size = std::min(free_heap - 2048, (size_t)(data.size() * 1.5)); - DynamicJsonDocument json_document(request_size); if (json_document.memoryPool().buffer() == nullptr) { ESP_LOGE(TAG, "Could not allocate memory for JSON document! Requested %u bytes, free heap: %u", request_size, @@ -76,7 +75,8 @@ void parse_json(const std::string &data, const json_parse_t &f) { ESP_LOGE(TAG, "Can not allocate more memory for deserialization. Consider making source string smaller"); return; } - ESP_LOGW(TAG, "Increasing memory allocation."); + ESP_LOGV(TAG, "Increasing memory allocation."); + request_size *= 2; continue; } else { ESP_LOGE(TAG, "JSON parse error: %s", err.c_str()); From cf5c640ae4ed59bd98460dbfeb3dd528f67d23e2 Mon Sep 17 00:00:00 2001 From: Dan Jackson Date: Tue, 29 Mar 2022 02:05:38 -0700 Subject: [PATCH 0342/1729] Change beginning of file comments to avoid creating doxygen tag for `esphome` namespace (#3314) --- esphome/components/rc522_spi/rc522_spi.h | 15 +++++++-------- esphome/components/sx1509/sx1509_registers.h | 13 +++++++------ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/esphome/components/rc522_spi/rc522_spi.h b/esphome/components/rc522_spi/rc522_spi.h index 58edbbed4f..0ccbcd7588 100644 --- a/esphome/components/rc522_spi/rc522_spi.h +++ b/esphome/components/rc522_spi/rc522_spi.h @@ -1,3 +1,10 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/rc522/rc522.h" +#include "esphome/components/spi/spi.h" + +namespace esphome { /** * Library based on https://github.com/miguelbalboa/rfid * and adapted to ESPHome by @glmnet @@ -6,14 +13,6 @@ * * */ - -#pragma once - -#include "esphome/core/component.h" -#include "esphome/components/rc522/rc522.h" -#include "esphome/components/spi/spi.h" - -namespace esphome { namespace rc522_spi { class RC522Spi : public rc522::RC522, diff --git a/esphome/components/sx1509/sx1509_registers.h b/esphome/components/sx1509/sx1509_registers.h index b97b85993f..9712cacf9b 100644 --- a/esphome/components/sx1509/sx1509_registers.h +++ b/esphome/components/sx1509/sx1509_registers.h @@ -1,11 +1,15 @@ -/****************************************************************************** +/* sx1509_registers.h Register definitions for SX1509. Jim Lindblom @ SparkFun Electronics Original Creation Date: September 21, 2015 https://github.com/sparkfun/SparkFun_SX1509_Arduino_Library +*/ +#pragma once -Here you'll find the Arduino code used to interface with the SX1509 I2C +namespace esphome { +/** + Here you'll find the Arduino code used to interface with the SX1509 I2C 16 I/O expander. There are functions to take advantage of everything the SX1509 provides - input/output setting, writing pins high/low, reading the input value of pins, LED driver utilities (blink, breath, pwm), and @@ -20,10 +24,7 @@ This code is beerware; if you see me (or any other SparkFun employee) at the local, and you've found our code helpful, please buy us a round! Distributed as-is; no warranty is given. -******************************************************************************/ -#pragma once - -namespace esphome { +*/ namespace sx1509 { const uint8_t REG_INPUT_DISABLE_B = From 7f7175b1840c2ad38104159e9881cf2ad24d071c Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 29 Mar 2022 22:22:11 +1300 Subject: [PATCH 0343/1729] Publish custom data when modbus number lambda fills vector (#3295) --- .../modbus_controller/modbus_controller.cpp | 22 ++++++++ .../modbus_controller/modbus_controller.h | 20 +++++-- .../number/modbus_number.cpp | 55 +++++++++++-------- 3 files changed, 69 insertions(+), 28 deletions(-) diff --git a/esphome/components/modbus_controller/modbus_controller.cpp b/esphome/components/modbus_controller/modbus_controller.cpp index 64046b9578..91e0dcc45f 100644 --- a/esphome/components/modbus_controller/modbus_controller.cpp +++ b/esphome/components/modbus_controller/modbus_controller.cpp @@ -455,6 +455,28 @@ ModbusCommandItem ModbusCommandItem::create_custom_command( return cmd; } +ModbusCommandItem ModbusCommandItem::create_custom_command( + ModbusController *modbusdevice, const std::vector &values, + std::function &data)> + &&handler) { + ModbusCommandItem cmd = {}; + cmd.modbusdevice = modbusdevice; + cmd.function_code = ModbusFunctionCode::CUSTOM; + if (handler == nullptr) { + cmd.on_data_func = [](ModbusRegisterType register_type, uint16_t start_address, const std::vector &data) { + ESP_LOGI(TAG, "Custom Command sent"); + }; + } else { + cmd.on_data_func = handler; + } + for (auto v : values) { + cmd.payload.push_back((v >> 8) & 0xFF); + cmd.payload.push_back(v & 0xFF); + } + + return cmd; +} + bool ModbusCommandItem::send() { if (this->function_code != ModbusFunctionCode::CUSTOM) { modbusdevice->send(uint8_t(this->function_code), this->register_address, this->register_count, this->payload.size(), diff --git a/esphome/components/modbus_controller/modbus_controller.h b/esphome/components/modbus_controller/modbus_controller.h index 09395f29b3..6aecf7f8a4 100644 --- a/esphome/components/modbus_controller/modbus_controller.h +++ b/esphome/components/modbus_controller/modbus_controller.h @@ -2,12 +2,12 @@ #include "esphome/core/component.h" -#include "esphome/core/automation.h" #include "esphome/components/modbus/modbus.h" +#include "esphome/core/automation.h" #include -#include #include +#include #include namespace esphome { @@ -374,8 +374,8 @@ class ModbusCommandItem { const std::vector &values); /** Create custom modbus command * @param modbusdevice pointer to the device to execute the command - * @param values byte vector of data to be sent to the device. The compplete payload must be provided with the - * exception of the crc codess + * @param values byte vector of data to be sent to the device. The complete payload must be provided with the + * exception of the crc codes * @param handler function called when the response is received. Default is just logging a response * @return ModbusCommandItem with the prepared command */ @@ -383,6 +383,18 @@ class ModbusCommandItem { ModbusController *modbusdevice, const std::vector &values, std::function &data)> &&handler = nullptr); + + /** Create custom modbus command + * @param modbusdevice pointer to the device to execute the command + * @param values word vector of data to be sent to the device. The complete payload must be provided with the + * exception of the crc codes + * @param handler function called when the response is received. Default is just logging a response + * @return ModbusCommandItem with the prepared command + */ + static ModbusCommandItem create_custom_command( + ModbusController *modbusdevice, const std::vector &values, + std::function &data)> + &&handler = nullptr); }; /** Modbus controller class. diff --git a/esphome/components/modbus_controller/number/modbus_number.cpp b/esphome/components/modbus_controller/number/modbus_number.cpp index a0e990d272..001cfb5787 100644 --- a/esphome/components/modbus_controller/number/modbus_number.cpp +++ b/esphome/components/modbus_controller/number/modbus_number.cpp @@ -26,6 +26,7 @@ void ModbusNumber::parse_and_publish(const std::vector &data) { } void ModbusNumber::control(float value) { + ModbusCommandItem write_cmd; std::vector data; float write_value = value; // Is there are lambda configured? @@ -45,33 +46,39 @@ void ModbusNumber::control(float value) { write_value = multiply_by_ * write_value; } - // lambda didn't set payload - if (data.empty()) { - data = float_to_payload(write_value, this->sensor_value_type); - } - - ESP_LOGD(TAG, - "Updating register: connected Sensor=%s start address=0x%X register count=%d new value=%.02f (val=%.02f)", - this->get_name().c_str(), this->start_address, this->register_count, value, write_value); - - // Create and send the write command - ModbusCommandItem write_cmd; - if (this->register_count == 1 && !this->use_write_multiple_) { - // since offset is in bytes and a register is 16 bits we get the start by adding offset/2 - write_cmd = - ModbusCommandItem::create_write_single_command(parent_, this->start_address + this->offset / 2, data[0]); + if (!data.empty()) { + ESP_LOGV(TAG, "Modbus Number write raw: %s", format_hex_pretty(data).c_str()); + write_cmd = ModbusCommandItem::create_custom_command( + this->parent_, data, + [this, write_cmd](ModbusRegisterType register_type, uint16_t start_address, const std::vector &data) { + this->parent_->on_write_register_response(write_cmd.register_type, this->start_address, data); + }); } else { - write_cmd = ModbusCommandItem::create_write_multiple_command(parent_, this->start_address + this->offset / 2, - this->register_count, data); + data = float_to_payload(write_value, this->sensor_value_type); + + ESP_LOGD(TAG, + "Updating register: connected Sensor=%s start address=0x%X register count=%d new value=%.02f (val=%.02f)", + this->get_name().c_str(), this->start_address, this->register_count, value, write_value); + + // Create and send the write command + if (this->register_count == 1 && !this->use_write_multiple_) { + // since offset is in bytes and a register is 16 bits we get the start by adding offset/2 + write_cmd = + ModbusCommandItem::create_write_single_command(parent_, this->start_address + this->offset / 2, data[0]); + } else { + write_cmd = ModbusCommandItem::create_write_multiple_command(parent_, this->start_address + this->offset / 2, + this->register_count, data); + } + // publish new value + write_cmd.on_data_func = [this, write_cmd, value](ModbusRegisterType register_type, uint16_t start_address, + const std::vector &data) { + // gets called when the write command is ack'd from the device + parent_->on_write_register_response(write_cmd.register_type, start_address, data); + this->publish_state(value); + }; } - // publish new value - write_cmd.on_data_func = [this, write_cmd, value](ModbusRegisterType register_type, uint16_t start_address, - const std::vector &data) { - // gets called when the write command is ack'd from the device - parent_->on_write_register_response(write_cmd.register_type, start_address, data); - this->publish_state(value); - }; parent_->queue_command(write_cmd); + this->publish_state(value); } void ModbusNumber::dump_config() { LOG_NUMBER(TAG, "Modbus Number", this); } From bb5f7249a6575751b95edf91eafc0b572c215dea Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 28 Mar 2022 17:04:25 +1300 Subject: [PATCH 0344/1729] Actually increase request memory for json parsing (#3331) --- esphome/components/json/json_util.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/json/json_util.cpp b/esphome/components/json/json_util.cpp index 2070b312e8..10179c9954 100644 --- a/esphome/components/json/json_util.cpp +++ b/esphome/components/json/json_util.cpp @@ -54,9 +54,8 @@ void parse_json(const std::string &data, const json_parse_t &f) { const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL); #endif bool pass = false; + size_t request_size = std::min(free_heap - 2048, (size_t)(data.size() * 1.5)); do { - const size_t request_size = std::min(free_heap - 2048, (size_t)(data.size() * 1.5)); - DynamicJsonDocument json_document(request_size); if (json_document.memoryPool().buffer() == nullptr) { ESP_LOGE(TAG, "Could not allocate memory for JSON document! Requested %u bytes, free heap: %u", request_size, @@ -76,7 +75,8 @@ void parse_json(const std::string &data, const json_parse_t &f) { ESP_LOGE(TAG, "Can not allocate more memory for deserialization. Consider making source string smaller"); return; } - ESP_LOGW(TAG, "Increasing memory allocation."); + ESP_LOGV(TAG, "Increasing memory allocation."); + request_size *= 2; continue; } else { ESP_LOGE(TAG, "JSON parse error: %s", err.c_str()); From 8be2456c7e8facb0bab7eaaed6a0e8a4f8472350 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 30 Mar 2022 08:15:50 +1300 Subject: [PATCH 0345/1729] Bump version to 2022.3.2 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 8bf303db09..7eeed11271 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2022.3.1" +__version__ = "2022.3.2" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From 9de61fcf587514f203f8215ffaa186b280975da8 Mon Sep 17 00:00:00 2001 From: Ian Reinhart Geiser Date: Thu, 31 Mar 2022 23:46:39 -0400 Subject: [PATCH 0346/1729] Define touchscreen support when in use. (#3296) --- esphome/components/touchscreen/__init__.py | 7 +++++++ esphome/core/defines.h | 1 + 2 files changed, 8 insertions(+) diff --git a/esphome/components/touchscreen/__init__.py b/esphome/components/touchscreen/__init__.py index 125103e2b8..a4bdc8cafd 100644 --- a/esphome/components/touchscreen/__init__.py +++ b/esphome/components/touchscreen/__init__.py @@ -4,6 +4,7 @@ import esphome.codegen as cg from esphome.components import display from esphome import automation from esphome.const import CONF_ON_TOUCH +from esphome.core import coroutine_with_priority CODEOWNERS = ["@jesserockz"] DEPENDENCIES = ["display"] @@ -39,3 +40,9 @@ async def register_touchscreen(var, config): [(TouchPoint, "touch")], config[CONF_ON_TOUCH], ) + + +@coroutine_with_priority(100.0) +async def to_code(config): + cg.add_global(touchscreen_ns.using) + cg.add_define("USE_TOUCHSCREEN") diff --git a/esphome/core/defines.h b/esphome/core/defines.h index aabb5510f4..b5c82338b3 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -40,6 +40,7 @@ #define USE_SWITCH #define USE_TEXT_SENSOR #define USE_TIME +#define USE_TOUCHSCREEN #define USE_UART_DEBUGGER #define USE_WIFI From 05dc97099a4efe6810ca682939f322106deb0f2d Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Sun, 3 Apr 2022 04:30:22 -0300 Subject: [PATCH 0347/1729] New vscode schema gen (#3336) --- esphome/automation.py | 21 +- esphome/components/logger/__init__.py | 18 +- .../components/modbus_controller/__init__.py | 8 +- .../modbus_controller/select/__init__.py | 2 - esphome/config_validation.py | 10 +- esphome/jsonschema.py | 14 +- script/build_jsonschema.py | 59 +- script/build_language_schema.py | 813 ++++++++++++++++++ 8 files changed, 887 insertions(+), 58 deletions(-) create mode 100644 script/build_language_schema.py diff --git a/esphome/automation.py b/esphome/automation.py index fab998527f..4007dc4c51 100644 --- a/esphome/automation.py +++ b/esphome/automation.py @@ -262,21 +262,16 @@ async def repeat_action_to_code(config, action_id, template_arg, args): return var -def validate_wait_until(value): - schema = cv.Schema( - { - cv.Required(CONF_CONDITION): validate_potentially_and_condition, - cv.Optional(CONF_TIMEOUT): cv.templatable( - cv.positive_time_period_milliseconds - ), - } - ) - if isinstance(value, dict) and CONF_CONDITION in value: - return schema(value) - return validate_wait_until({CONF_CONDITION: value}) +_validate_wait_until = cv.maybe_simple_value( + { + cv.Required(CONF_CONDITION): validate_potentially_and_condition, + cv.Optional(CONF_TIMEOUT): cv.templatable(cv.positive_time_period_milliseconds), + }, + key=CONF_CONDITION, +) -@register_action("wait_until", WaitUntilAction, validate_wait_until) +@register_action("wait_until", WaitUntilAction, _validate_wait_until) async def wait_until_action_to_code(config, action_id, template_arg, args): conditions = await build_condition(config[CONF_CONDITION], template_arg, args) var = cg.new_Pvariable(action_id, template_arg, conditions) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 20a0b0f792..d11b00405d 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -203,15 +203,6 @@ async def to_code(config): ) -def maybe_simple_message(schema): - def validator(value): - if isinstance(value, dict): - return cv.Schema(schema)(value) - return cv.Schema(schema)({CONF_FORMAT: value}) - - return validator - - def validate_printf(value): # https://stackoverflow.com/questions/30011379/how-can-i-parse-a-c-format-string-in-python cfmt = r""" @@ -234,7 +225,7 @@ def validate_printf(value): CONF_LOGGER_LOG = "logger.log" LOGGER_LOG_ACTION_SCHEMA = cv.All( - maybe_simple_message( + cv.maybe_simple_value( { cv.Required(CONF_FORMAT): cv.string, cv.Optional(CONF_ARGS, default=list): cv.ensure_list(cv.lambda_), @@ -242,9 +233,10 @@ LOGGER_LOG_ACTION_SCHEMA = cv.All( *LOG_LEVEL_TO_ESP_LOG, upper=True ), cv.Optional(CONF_TAG, default="main"): cv.string, - } - ), - validate_printf, + }, + validate_printf, + key=CONF_FORMAT, + ) ) diff --git a/esphome/components/modbus_controller/__init__.py b/esphome/components/modbus_controller/__init__.py index f919cb0678..41beb67e1c 100644 --- a/esphome/components/modbus_controller/__init__.py +++ b/esphome/components/modbus_controller/__init__.py @@ -71,9 +71,9 @@ SENSOR_VALUE_TYPE = { "S_DWORD": SensorValueType.S_DWORD, "S_DWORD_R": SensorValueType.S_DWORD_R, "U_QWORD": SensorValueType.U_QWORD, - "U_QWORDU_R": SensorValueType.U_QWORD_R, + "U_QWORD_R": SensorValueType.U_QWORD_R, "S_QWORD": SensorValueType.S_QWORD, - "U_QWORD_R": SensorValueType.S_QWORD_R, + "S_QWORD_R": SensorValueType.S_QWORD_R, "FP32": SensorValueType.FP32, "FP32_R": SensorValueType.FP32_R, } @@ -87,9 +87,9 @@ TYPE_REGISTER_MAP = { "S_DWORD": 2, "S_DWORD_R": 2, "U_QWORD": 4, - "U_QWORDU_R": 4, - "S_QWORD": 4, "U_QWORD_R": 4, + "S_QWORD": 4, + "S_QWORD_R": 4, "FP32": 2, "FP32_R": 2, } diff --git a/esphome/components/modbus_controller/select/__init__.py b/esphome/components/modbus_controller/select/__init__.py index 6f194ef2a3..f8ef61ddc4 100644 --- a/esphome/components/modbus_controller/select/__init__.py +++ b/esphome/components/modbus_controller/select/__init__.py @@ -2,7 +2,6 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import select from esphome.const import CONF_ADDRESS, CONF_ID, CONF_LAMBDA, CONF_OPTIMISTIC -from esphome.jsonschema import jschema_composite from .. import ( SENSOR_VALUE_TYPE, @@ -30,7 +29,6 @@ ModbusSelect = modbus_controller_ns.class_( ) -@jschema_composite def ensure_option_map(): def validator(value): cv.check_not_templatable(value) diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 8e1c63a54e..3dc8011a87 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -58,7 +58,7 @@ from esphome.core import ( ) from esphome.helpers import list_starts_with, add_class_to_obj from esphome.jsonschema import ( - jschema_composite, + jschema_list, jschema_extractor, jschema_registry, jschema_typed, @@ -327,7 +327,7 @@ def boolean(value): ) -@jschema_composite +@jschema_list def ensure_list(*validators): """Validate this configuration option to be a list. @@ -494,7 +494,11 @@ def templatable(other_validators): """ schema = Schema(other_validators) + @jschema_extractor("templatable") def validator(value): + # pylint: disable=comparison-with-callable + if value == jschema_extractor: + return other_validators if isinstance(value, Lambda): return returning_lambda(value) if isinstance(other_validators, dict): @@ -1546,7 +1550,7 @@ def validate_registry(name, registry): return ensure_list(validate_registry_entry(name, registry)) -@jschema_composite +@jschema_list def maybe_simple_value(*validators, **kwargs): key = kwargs.pop("key", CONF_VALUE) validator = All(*validators) diff --git a/esphome/jsonschema.py b/esphome/jsonschema.py index 12929dc602..94325f4abc 100644 --- a/esphome/jsonschema.py +++ b/esphome/jsonschema.py @@ -1,7 +1,7 @@ """Helpers to retrieve schema from voluptuous validators. These are a helper decorators to help get schema from some -components which uses volutuous in a way where validation +components which uses voluptuous in a way where validation is hidden in local functions These decorators should not modify at all what the functions originally do. @@ -24,7 +24,7 @@ def jschema_extractor(validator_name): if EnableJsonSchemaCollect: def decorator(func): - hidden_schemas[str(func)] = validator_name + hidden_schemas[repr(func)] = validator_name return func return decorator @@ -41,7 +41,7 @@ def jschema_extended(func): def decorate(*args, **kwargs): ret = func(*args, **kwargs) assert len(args) == 2 - extended_schemas[str(ret)] = args + extended_schemas[repr(ret)] = args return ret return decorate @@ -49,13 +49,13 @@ def jschema_extended(func): return func -def jschema_composite(func): +def jschema_list(func): if EnableJsonSchemaCollect: def decorate(*args, **kwargs): ret = func(*args, **kwargs) # args length might be 2, but 2nd is always validator - list_schemas[str(ret)] = args + list_schemas[repr(ret)] = args return ret return decorate @@ -67,7 +67,7 @@ def jschema_registry(registry): if EnableJsonSchemaCollect: def decorator(func): - registry_schemas[str(func)] = registry + registry_schemas[repr(func)] = registry return func return decorator @@ -83,7 +83,7 @@ def jschema_typed(func): def decorate(*args, **kwargs): ret = func(*args, **kwargs) - typed_schemas[str(ret)] = (args, kwargs) + typed_schemas[repr(ret)] = (args, kwargs) return ret return decorate diff --git a/script/build_jsonschema.py b/script/build_jsonschema.py index 7673519916..5373d404a7 100644 --- a/script/build_jsonschema.py +++ b/script/build_jsonschema.py @@ -70,7 +70,7 @@ def add_definition_array_or_single_object(ref): def add_core(): from esphome.core.config import CONFIG_SCHEMA - base_props["esphome"] = get_jschema("esphome", CONFIG_SCHEMA.schema) + base_props["esphome"] = get_jschema("esphome", CONFIG_SCHEMA) def add_buses(): @@ -216,7 +216,7 @@ def add_components(): add_module_registries(domain, c.module) add_module_schemas(domain, c.module) - # need first to iterate all platforms then iteate components + # need first to iterate all platforms then iterate components # a platform component can have other components as properties, # e.g. climate components usually have a temperature sensor @@ -325,7 +325,9 @@ def get_entry(parent_key, vschema): if DUMP_COMMENTS: entry[JSC_COMMENT] = "entry: " + parent_key + "/" + str(vschema) - if isinstance(vschema, list): + if isinstance(vschema, dict): + entry = {"what": "is_this"} + elif isinstance(vschema, list): ref = get_jschema(parent_key + "[]", vschema[0]) entry = {"type": "array", "items": ref} elif isinstance(vschema, schema_type) and hasattr(vschema, "schema"): @@ -387,8 +389,10 @@ def get_entry(parent_key, vschema): v = vschema(None) if isinstance(v, ID): - if v.type.base != "script::Script" and ( - v.type.inherits_from(Trigger) or v.type == Automation + if ( + v.type.base != "script::Script" + and v.type.base != "switch_::Switch" + and (v.type.inherits_from(Trigger) or v.type == Automation) ): return None entry = {"type": "string", "id_type": v.type.base} @@ -410,6 +414,8 @@ def default_schema(): def is_default_schema(jschema): + if jschema is None: + return False if is_ref(jschema): jschema = unref(jschema) if not jschema: @@ -425,6 +431,9 @@ def get_jschema(path, vschema, create_return_ref=True): jschema = convert_schema(path, vschema) + if jschema is None: + return None + if is_ref(jschema): # this can happen when returned extended # schemas where all properties found in previous extended schema @@ -450,6 +459,9 @@ def get_schema_str(vschema): def create_ref(name, vschema, jschema): + if jschema is None: + raise ValueError("Cannot create a ref with null jschema for " + name) + if name in schema_names: raise ValueError("Not supported") @@ -523,6 +535,15 @@ def convert_schema(path, vschema, un_extend=True): extended = ejs.extended_schemas.get(str(vschema)) if extended: lhs = get_jschema(path, extended[0], False) + + # The midea actions are extending an empty schema (resulted in the templatize not templatizing anything) + # this causes a recursion in that this extended looks the same in extended schema as the extended[1] + if ejs.extended_schemas.get(str(vschema)) == ejs.extended_schemas.get( + str(extended[1]) + ): + assert path.startswith("midea_ac") + return convert_schema(path, extended[1], False) + rhs = get_jschema(path, extended[1], False) # check if we are not merging properties which are already in base component @@ -567,6 +588,8 @@ def convert_schema(path, vschema, un_extend=True): # we should take the valid schema, # commonly all is used to validate a schema, and then a function which # is not a schema es also given, get_schema will then return a default_schema() + if v == dict: + continue # this is a dict in the SCHEMA of packages val_schema = get_jschema(path, v, False) if is_default_schema(val_schema): if not output: @@ -673,6 +696,11 @@ def add_pin_registry(): for mode in ("INPUT", "OUTPUT"): schema_name = f"PIN.GPIO_FULL_{mode}_PIN_SCHEMA" + + # TODO: get pin definitions properly + if schema_name not in definitions: + definitions[schema_name] = {"type": ["object", "null"], JSC_PROPERTIES: {}} + internal = definitions[schema_name] definitions[schema_name]["additionalItems"] = False definitions[f"PIN.{mode}_INTERNAL"] = internal @@ -683,12 +711,11 @@ def add_pin_registry(): definitions[schema_name] = {"oneOf": schemas, "type": ["string", "object"]} for k, v in pin_registry.items(): - pin_jschema = get_jschema( - f"PIN.{mode}_" + k, v[1][0 if mode == "OUTPUT" else 1] - ) - if unref(pin_jschema): - pin_jschema["required"] = [k] - schemas.append(pin_jschema) + if isinstance(v[1], vol.validators.All): + pin_jschema = get_jschema(f"PIN.{mode}_" + k, v[1]) + if unref(pin_jschema): + pin_jschema["required"] = [k] + schemas.append(pin_jschema) def dump_schema(): @@ -730,9 +757,9 @@ def dump_schema(): cv.valid_name, cv.hex_int, cv.hex_int_range, - pins.output_pin, - pins.input_pin, - pins.input_pullup_pin, + pins.gpio_output_pin_schema, + pins.gpio_input_pin_schema, + pins.gpio_input_pullup_pin_schema, cv.float_with_unit, cv.subscribe_topic, cv.publish_topic, @@ -753,12 +780,12 @@ def dump_schema(): for v in [pins.gpio_input_pin_schema, pins.gpio_input_pullup_pin_schema]: schema_registry[v] = get_ref("PIN.GPIO_FULL_INPUT_PIN_SCHEMA") - for v in [pins.internal_gpio_input_pin_schema, pins.input_pin]: + for v in [pins.internal_gpio_input_pin_schema, pins.gpio_input_pin_schema]: schema_registry[v] = get_ref("PIN.INPUT_INTERNAL") for v in [pins.gpio_output_pin_schema, pins.internal_gpio_output_pin_schema]: schema_registry[v] = get_ref("PIN.GPIO_FULL_OUTPUT_PIN_SCHEMA") - for v in [pins.internal_gpio_output_pin_schema, pins.output_pin]: + for v in [pins.internal_gpio_output_pin_schema, pins.gpio_output_pin_schema]: schema_registry[v] = get_ref("PIN.OUTPUT_INTERNAL") add_module_schemas("CONFIG", cv) diff --git a/script/build_language_schema.py b/script/build_language_schema.py new file mode 100644 index 0000000000..ec0912e9e6 --- /dev/null +++ b/script/build_language_schema.py @@ -0,0 +1,813 @@ +import inspect +import json +import argparse +from operator import truediv +import os +import voluptuous as vol + +# NOTE: Cannot import other esphome components globally as a modification in jsonschema +# is needed before modules are loaded +import esphome.jsonschema as ejs + +ejs.EnableJsonSchemaCollect = True + +# schema format: +# Schemas are splitted in several files in json format, one for core stuff, one for each platform (sensor, binary_sensor, etc) and +# one for each component (dallas, sim800l, etc.) component can have schema for root component/hub and also for platform component, +# e.g. dallas has hub component which has pin and then has the sensor platform which has sensor name, index, etc. +# When files are loaded they are merged in a single object. +# The root format is + +S_CONFIG_VAR = "config_var" +S_CONFIG_VARS = "config_vars" +S_CONFIG_SCHEMA = "CONFIG_SCHEMA" +S_COMPONENT = "component" +S_COMPONENTS = "components" +S_PLATFORMS = "platforms" +S_SCHEMA = "schema" +S_SCHEMAS = "schemas" +S_EXTENDS = "extends" +S_TYPE = "type" +S_NAME = "name" + +parser = argparse.ArgumentParser() +parser.add_argument( + "--output-path", default=".", help="Output path", type=os.path.abspath +) + +args = parser.parse_args() + +DUMP_RAW = False +DUMP_UNKNOWN = False +DUMP_PATH = False +JSON_DUMP_PRETTY = True + +# store here dynamic load of esphome components +components = {} + +schema_core = {} + +# output is where all is built +output = {"core": schema_core} +# The full generated output is here here +schema_full = {"components": output} + +# A string, string map, key is the str(schema) and value is +# a tuple, first element is the schema reference and second is the schema path given, the schema reference is needed to test as different schemas have same key +known_schemas = {} + +solve_registry = [] + + +def get_component_names(): + # return [ + # "esphome", + # "esp32", + # "esp8266", + # "logger", + # "sensor", + # "remote_receiver", + # "binary_sensor", + # ] + from esphome.loader import CORE_COMPONENTS_PATH + + component_names = ["esphome", "sensor"] + + for d in os.listdir(CORE_COMPONENTS_PATH): + if not d.startswith("__") and os.path.isdir( + os.path.join(CORE_COMPONENTS_PATH, d) + ): + if d not in component_names: + component_names.append(d) + + return component_names + + +def load_components(): + from esphome.config import get_component + + for domain in get_component_names(): + components[domain] = get_component(domain) + + +load_components() + +# Import esphome after loading components (so schema is tracked) +# pylint: disable=wrong-import-position +import esphome.core as esphome_core +import esphome.config_validation as cv +from esphome import automation +from esphome import pins +from esphome.components import remote_base +from esphome.const import CONF_TYPE +from esphome.loader import get_platform +from esphome.helpers import write_file_if_changed +from esphome.util import Registry + +# pylint: enable=wrong-import-position + + +def write_file(name, obj): + full_path = os.path.join(args.output_path, name + ".json") + if JSON_DUMP_PRETTY: + json_str = json.dumps(obj, indent=2) + else: + json_str = json.dumps(obj, separators=(",", ":")) + write_file_if_changed(full_path, json_str) + print(f"Wrote {full_path}") + + +def register_module_schemas(key, module, manifest=None): + for name, schema in module_schemas(module): + register_known_schema(key, name, schema) + if ( + manifest and manifest.multi_conf and S_CONFIG_SCHEMA in output[key][S_SCHEMAS] + ): # not sure about 2nd part of the if, might be useless config (e.g. as3935) + output[key][S_SCHEMAS][S_CONFIG_SCHEMA]["is_list"] = True + + +def register_known_schema(module, name, schema): + if module not in output: + output[module] = {S_SCHEMAS: {}} + config = convert_config(schema, f"{module}/{name}") + if S_TYPE not in config: + print(f"Config var without type: {module}.{name}") + + output[module][S_SCHEMAS][name] = config + repr_schema = repr(schema) + if repr_schema in known_schemas: + schema_info = known_schemas[repr_schema] + schema_info.append((schema, f"{module}.{name}")) + else: + known_schemas[repr_schema] = [(schema, f"{module}.{name}")] + + +def module_schemas(module): + # This should yield elements in order so extended schemas are resolved properly + # To do this we check on the source code where the symbol is seen first. Seems to work. + try: + module_str = inspect.getsource(module) + except TypeError: + # improv + module_str = "" + except OSError: + # some empty __init__ files + module_str = "" + schemas = {} + for m_attr_name in dir(module): + m_attr_obj = getattr(module, m_attr_name) + if isConvertibleSchema(m_attr_obj): + schemas[module_str.find(m_attr_name)] = [m_attr_name, m_attr_obj] + + for pos in sorted(schemas.keys()): + yield schemas[pos] + + +found_registries = {} + +# Pin validators keys are the functions in pin which validate the pins +pin_validators = {} + + +def add_pin_validators(): + for m_attr_name in dir(pins): + if "gpio" in m_attr_name: + s = pin_validators[repr(getattr(pins, m_attr_name))] = {} + if "schema" in m_attr_name: + s["schema"] = True # else is just number + if "internal" in m_attr_name: + s["internal"] = True + if "input" in m_attr_name: + s["modes"] = ["input"] + elif "output" in m_attr_name: + s["modes"] = ["output"] + else: + s["modes"] = [] + if "pullup" in m_attr_name: + s["modes"].append("pullup") + from esphome.components.adc import sensor as adc_sensor + + pin_validators[repr(adc_sensor.validate_adc_pin)] = { + "internal": True, + "modes": ["input"], + } + + +def add_module_registries(domain, module): + for attr_name in dir(module): + attr_obj = getattr(module, attr_name) + if isinstance(attr_obj, Registry): + if attr_obj == automation.ACTION_REGISTRY: + reg_type = "action" + reg_domain = "core" + found_registries[repr(attr_obj)] = reg_type + elif attr_obj == automation.CONDITION_REGISTRY: + reg_type = "condition" + reg_domain = "core" + found_registries[repr(attr_obj)] = reg_type + else: # attr_name == "FILTER_REGISTRY": + reg_domain = domain + reg_type = attr_name.partition("_")[0].lower() + found_registries[repr(attr_obj)] = f"{domain}.{reg_type}" + + for name in attr_obj.keys(): + if "." not in name: + reg_entry_name = name + else: + parts = name.split(".") + if len(parts) == 2: + reg_domain = parts[0] + reg_entry_name = parts[1] + else: + reg_domain = ".".join([parts[1], parts[0]]) + reg_entry_name = parts[2] + + if reg_domain not in output: + output[reg_domain] = {} + if reg_type not in output[reg_domain]: + output[reg_domain][reg_type] = {} + output[reg_domain][reg_type][reg_entry_name] = convert_config( + attr_obj[name].schema, f"{reg_domain}/{reg_type}/{reg_entry_name}" + ) + + # print(f"{domain} - {attr_name} - {name}") + + +def do_pins(): + # do pin registries + pins_providers = schema_core["pins"] = [] + for pin_registry in pins.PIN_SCHEMA_REGISTRY: + s = convert_config( + pins.PIN_SCHEMA_REGISTRY[pin_registry][1], f"pins/{pin_registry}" + ) + if pin_registry not in output: + output[pin_registry] = {} # mcp23xxx does not create a component yet + output[pin_registry]["pin"] = s + pins_providers.append(pin_registry) + + +def do_esp32(): + import esphome.components.esp32.boards as esp32_boards + + setEnum( + output["esp32"]["schemas"]["CONFIG_SCHEMA"]["schema"]["config_vars"]["board"], + list(esp32_boards.BOARD_TO_VARIANT.keys()), + ) + + +def do_esp8266(): + import esphome.components.esp8266.boards as esp8266_boards + + setEnum( + output["esp8266"]["schemas"]["CONFIG_SCHEMA"]["schema"]["config_vars"]["board"], + list(esp8266_boards.ESP8266_BOARD_PINS.keys()), + ) + + +def fix_remote_receiver(): + output["remote_receiver.binary_sensor"]["schemas"]["CONFIG_SCHEMA"] = { + "type": "schema", + "schema": { + "extends": ["binary_sensor.BINARY_SENSOR_SCHEMA", "core.COMPONENT_SCHEMA"], + "config_vars": output["remote_base"]["binary"], + }, + } + + +def add_referenced_recursive(referenced_schemas, config_var, path, eat_schema=False): + assert ( + S_CONFIG_VARS not in config_var and S_EXTENDS not in config_var + ) # S_TYPE in cv or "key" in cv or len(cv) == 0 + if ( + config_var.get(S_TYPE) in ["schema", "trigger", "maybe"] + and S_SCHEMA in config_var + ): + schema = config_var[S_SCHEMA] + for k, v in schema.get(S_CONFIG_VARS, {}).items(): + if eat_schema: + new_path = path + [S_CONFIG_VARS, k] + else: + new_path = path + ["schema", S_CONFIG_VARS, k] + add_referenced_recursive(referenced_schemas, v, new_path) + for k in schema.get(S_EXTENDS, []): + if k not in referenced_schemas: + referenced_schemas[k] = [path] + else: + if path not in referenced_schemas[k]: + referenced_schemas[k].append(path) + + s1 = get_str_path_schema(k) + p = k.split(".") + if len(p) == 3 and path[0] == f"{p[0]}.{p[1]}": + # special case for schema inside platforms + add_referenced_recursive( + referenced_schemas, s1, [path[0], "schemas", p[2]] + ) + else: + add_referenced_recursive( + referenced_schemas, s1, [p[0], "schemas", p[1]] + ) + elif config_var.get(S_TYPE) == "typed": + for tk, tv in config_var.get("types").items(): + add_referenced_recursive( + referenced_schemas, + { + S_TYPE: S_SCHEMA, + S_SCHEMA: tv, + }, + path + ["types", tk], + eat_schema=True, + ) + + +def get_str_path_schema(strPath): + parts = strPath.split(".") + if len(parts) > 2: + parts[0] += "." + parts[1] + parts[1] = parts[2] + s1 = output.get(parts[0], {}).get(S_SCHEMAS, {}).get(parts[1], {}) + return s1 + + +def pop_str_path_schema(strPath): + parts = strPath.split(".") + if len(parts) > 2: + parts[0] += "." + parts[1] + parts[1] = parts[2] + output.get(parts[0], {}).get(S_SCHEMAS, {}).pop(parts[1]) + + +def get_arr_path_schema(path): + s = output + for x in path: + s = s[x] + return s + + +def merge(source, destination): + """ + run me with nosetests --with-doctest file.py + + >>> a = { 'first' : { 'all_rows' : { 'pass' : 'dog', 'number' : '1' } } } + >>> b = { 'first' : { 'all_rows' : { 'fail' : 'cat', 'number' : '5' } } } + >>> merge(b, a) == { 'first' : { 'all_rows' : { 'pass' : 'dog', 'fail' : 'cat', 'number' : '5' } } } + True + """ + for key, value in source.items(): + if isinstance(value, dict): + # get node or create one + node = destination.setdefault(key, {}) + merge(value, node) + else: + destination[key] = value + + return destination + + +def shrink(): + """Shrink the extending schemas which has just an end type, e.g. at this point + ota / port is type schema with extended pointing to core.port, this should instead be + type number. core.port is number + + This also fixes enums, as they are another schema and they are instead put in the same cv + """ + + # referenced_schemas contains a dict, keys are all that are shown in extends: [] arrays, values are lists of paths that are pointing to that extend + # e.g. key: core.COMPONENT_SCHEMA has a lot of paths of config vars which extends this schema + + pass_again = True + + while pass_again: + pass_again = False + + referenced_schemas = {} + + for k, v in output.items(): + for kv, vv in v.items(): + if kv != "pin" and isinstance(vv, dict): + for kvv, vvv in vv.items(): + add_referenced_recursive(referenced_schemas, vvv, [k, kv, kvv]) + + for x, paths in referenced_schemas.items(): + if len(paths) == 1: + key_s = get_str_path_schema(x) + arr_s = get_arr_path_schema(paths[0]) + # key_s |= arr_s + # key_s.pop(S_EXTENDS) + pass_again = True + if S_SCHEMA in arr_s: + if S_EXTENDS in arr_s[S_SCHEMA]: + arr_s[S_SCHEMA].pop(S_EXTENDS) + else: + print("expected extends here!" + x) + arr_s = merge(key_s, arr_s) + if arr_s[S_TYPE] == "enum": + arr_s.pop(S_SCHEMA) + else: + arr_s.pop(S_EXTENDS) + arr_s |= key_s[S_SCHEMA] + print(x) + + # simple types should be spread on each component, + # for enums so far these are logger.is_log_level, cover.validate_cover_state and pulse_counter.sensor.COUNT_MODE_SCHEMA + # then for some reasons sensor filter registry falls here + # then are all simple types, integer and strings + for x, paths in referenced_schemas.items(): + key_s = get_str_path_schema(x) + if key_s and key_s[S_TYPE] in ["enum", "registry", "integer", "string"]: + if key_s[S_TYPE] == "registry": + print("Spreading registry: " + x) + for target in paths: + target_s = get_arr_path_schema(target) + assert target_s[S_SCHEMA][S_EXTENDS] == [x] + target_s.pop(S_SCHEMA) + target_s |= key_s + if key_s[S_TYPE] in ["integer", "string"]: + target_s["data_type"] = x.split(".")[1] + # remove this dangling again + pop_str_path_schema(x) + elif not key_s: + for target in paths: + target_s = get_arr_path_schema(target) + assert target_s[S_SCHEMA][S_EXTENDS] == [x] + target_s.pop(S_SCHEMA) + target_s.pop(S_TYPE) # undefined + target_s["data_type"] = x.split(".")[1] + # remove this dangling again + pop_str_path_schema(x) + + # remove dangling items (unreachable schemas) + for domain, domain_schemas in output.items(): + for schema_name in list(domain_schemas.get(S_SCHEMAS, {}).keys()): + s = f"{domain}.{schema_name}" + if ( + not s.endswith("." + S_CONFIG_SCHEMA) + and s not in referenced_schemas.keys() + ): + print(f"Removing {s}") + output[domain][S_SCHEMAS].pop(schema_name) + + +def build_schema(): + print("Building schema") + + # check esphome was not loaded globally (IDE auto imports) + if len(ejs.extended_schemas) == 0: + raise Exception( + "no data collected. Did you globally import an ESPHome component?" + ) + + # Core schema + schema_core[S_SCHEMAS] = {} + register_module_schemas("core", cv) + + platforms = {} + schema_core[S_PLATFORMS] = platforms + core_components = {} + schema_core[S_COMPONENTS] = core_components + + add_pin_validators() + + # Load a preview of each component + for domain, manifest in components.items(): + if manifest.is_platform_component: + # e.g. sensor, binary sensor, add S_COMPONENTS + # note: S_COMPONENTS is not filled until loaded, e.g. + # if lock: is not used, then we don't need to know about their + # platforms yet. + output[domain] = {S_COMPONENTS: {}, S_SCHEMAS: {}} + platforms[domain] = {} + elif manifest.config_schema is not None: + # e.g. dallas + output[domain] = {S_SCHEMAS: {S_CONFIG_SCHEMA: {}}} + + # Generate platforms (e.g. sensor, binary_sensor, climate ) + for domain in platforms: + c = components[domain] + register_module_schemas(domain, c.module) + + # Generate components + for domain, manifest in components.items(): + if domain not in platforms: + if manifest.config_schema is not None: + core_components[domain] = {} + register_module_schemas(domain, manifest.module, manifest) + + for platform in platforms: + platform_manifest = get_platform(domain=platform, platform=domain) + if platform_manifest is not None: + output[platform][S_COMPONENTS][domain] = {} + register_module_schemas( + f"{domain}.{platform}", platform_manifest.module + ) + + # Do registries + add_module_registries("core", automation) + for domain, manifest in components.items(): + add_module_registries(domain, manifest.module) + add_module_registries("remote_base", remote_base) + + # update props pointing to registries + for reg_config_var in solve_registry: + (registry, config_var) = reg_config_var + config_var[S_TYPE] = "registry" + config_var["registry"] = found_registries[repr(registry)] + + do_pins() + do_esp8266() + do_esp32() + fix_remote_receiver() + shrink() + + # aggregate components, so all component info is in same file, otherwise we have dallas.json, dallas.sensor.json, etc. + data = {} + for component, component_schemas in output.items(): + if "." in component: + key = component.partition(".")[0] + if key not in data: + data[key] = {} + data[key][component] = component_schemas + else: + if component not in data: + data[component] = {} + data[component] |= {component: component_schemas} + + # bundle core inside esphome + data["esphome"]["core"] = data.pop("core")["core"] + + for c, s in data.items(): + write_file(c, s) + + +def setEnum(obj, items): + obj[S_TYPE] = "enum" + obj["values"] = items + + +def isConvertibleSchema(schema): + if schema is None: + return False + if isinstance(schema, (cv.Schema, cv.All)): + return True + if repr(schema) in ejs.hidden_schemas: + return True + if repr(schema) in ejs.typed_schemas: + return True + if repr(schema) in ejs.list_schemas: + return True + if repr(schema) in ejs.registry_schemas: + return True + if isinstance(schema, dict): + for k in schema.keys(): + if isinstance(k, (cv.Required, cv.Optional)): + return True + return False + + +def convert_config(schema, path): + converted = {} + convert_1(schema, converted, path) + return converted + + +def convert_1(schema, config_var, path): + """config_var can be a config_var or a schema: both are dicts + config_var has a S_TYPE property, if this is S_SCHEMA, then it has a S_SCHEMA property + schema does not have a type property, schema can have optionally both S_CONFIG_VARS and S_EXTENDS + """ + repr_schema = repr(schema) + + if repr_schema in known_schemas: + schema_info = known_schemas[(repr_schema)] + for (schema_instance, name) in schema_info: + if schema_instance is schema: + assert S_CONFIG_VARS not in config_var + assert S_EXTENDS not in config_var + if not S_TYPE in config_var: + config_var[S_TYPE] = S_SCHEMA + assert config_var[S_TYPE] == S_SCHEMA + + if S_SCHEMA not in config_var: + config_var[S_SCHEMA] = {} + if S_EXTENDS not in config_var[S_SCHEMA]: + config_var[S_SCHEMA][S_EXTENDS] = [name] + else: + config_var[S_SCHEMA][S_EXTENDS].append(name) + return + + # Extended schemas are tracked when the .extend() is used in a schema + if repr_schema in ejs.extended_schemas: + extended = ejs.extended_schemas.get(repr_schema) + # The midea actions are extending an empty schema (resulted in the templatize not templatizing anything) + # this causes a recursion in that this extended looks the same in extended schema as the extended[1] + if repr_schema == repr(extended[1]): + assert path.startswith("midea_ac/") + return + + assert len(extended) == 2 + convert_1(extended[0], config_var, path + "/extL") + convert_1(extended[1], config_var, path + "/extR") + return + + if isinstance(schema, cv.All): + i = 0 + for inner in schema.validators: + i = i + 1 + convert_1(inner, config_var, path + f"/val {i}") + return + + if hasattr(schema, "validators"): + i = 0 + for inner in schema.validators: + i = i + 1 + convert_1(inner, config_var, path + f"/val {i}") + + if isinstance(schema, cv.Schema): + convert_1(schema.schema, config_var, path + "/all") + return + + if isinstance(schema, dict): + convert_keys(config_var, schema, path) + return + + if repr_schema in ejs.list_schemas: + config_var["is_list"] = True + items_schema = ejs.list_schemas[repr_schema][0] + convert_1(items_schema, config_var, path + "/list") + return + + if DUMP_RAW: + config_var["raw"] = repr_schema + + # pylint: disable=comparison-with-callable + if schema == cv.boolean: + config_var[S_TYPE] = "boolean" + elif schema == automation.validate_potentially_and_condition: + config_var[S_TYPE] = "registry" + config_var["registry"] = "condition" + elif schema == cv.int_ or schema == cv.int_range: + config_var[S_TYPE] = "integer" + elif schema == cv.string or schema == cv.string_strict or schema == cv.valid_name: + config_var[S_TYPE] = "string" + + elif isinstance(schema, vol.Schema): + # test: esphome/project + config_var[S_TYPE] = "schema" + config_var["schema"] = convert_config(schema.schema, path + "/s")["schema"] + + elif repr_schema in pin_validators: + config_var |= pin_validators[repr_schema] + config_var[S_TYPE] = "pin" + + elif repr_schema in ejs.hidden_schemas: + schema_type = ejs.hidden_schemas[repr_schema] + + data = schema(ejs.jschema_extractor) + + # enums, e.g. esp32/variant + if schema_type == "one_of": + config_var[S_TYPE] = "enum" + config_var["values"] = list(data) + elif schema_type == "enum": + config_var[S_TYPE] = "enum" + config_var["values"] = list(data.keys()) + elif schema_type == "maybe": + config_var[S_TYPE] = "maybe" + config_var["schema"] = convert_config(data, path + "/maybe")["schema"] + # esphome/on_boot + elif schema_type == "automation": + extra_schema = None + config_var[S_TYPE] = "trigger" + if automation.AUTOMATION_SCHEMA == ejs.extended_schemas[repr(data)][0]: + extra_schema = ejs.extended_schemas[repr(data)][1] + if ( + extra_schema is not None and len(extra_schema) > 1 + ): # usually only trigger_id here + config = convert_config(extra_schema, path + "/extra") + if "schema" in config: + automation_schema = config["schema"] + if not ( + len(automation_schema["config_vars"]) == 1 + and "trigger_id" in automation_schema["config_vars"] + ): + automation_schema["config_vars"]["then"] = {S_TYPE: "trigger"} + if "trigger_id" in automation_schema["config_vars"]: + automation_schema["config_vars"].pop("trigger_id") + + config_var[S_TYPE] = "trigger" + config_var["schema"] = automation_schema + # some triggers can have a list of actions directly, while others needs to have some other configuration, + # e.g. sensor.on_value_rang, and the list of actions is only accepted under "then" property. + try: + schema({"delay": "1s"}) + except cv.Invalid: + config_var["has_required_var"] = True + else: + print("figure out " + path) + elif schema_type == "effects": + config_var[S_TYPE] = "registry" + config_var["registry"] = "light.effects" + config_var["filter"] = data[0] + elif schema_type == "templatable": + config_var["templatable"] = True + convert_1(data, config_var, path + "/templat") + elif schema_type == "triggers": + # remote base + convert_1(data, config_var, path + "/trigger") + elif schema_type == "sensor": + schema = data + convert_1(data, config_var, path + "/trigger") + else: + raise Exception("Unknown extracted schema type") + + elif repr_schema in ejs.registry_schemas: + solve_registry.append((ejs.registry_schemas[repr_schema], config_var)) + + elif repr_schema in ejs.typed_schemas: + config_var[S_TYPE] = "typed" + types = config_var["types"] = {} + typed_schema = ejs.typed_schemas[repr_schema] + if len(typed_schema) > 1: + config_var["typed_key"] = typed_schema[1].get("key", CONF_TYPE) + for schema_key, schema_type in typed_schema[0][0].items(): + config = convert_config(schema_type, path + "/type_" + schema_key) + types[schema_key] = config["schema"] + + elif DUMP_UNKNOWN: + if S_TYPE not in config_var: + config_var["unknown"] = repr_schema + + if DUMP_PATH: + config_var["path"] = path + + +def get_overridden_config(key, converted): + # check if the key is in any extended schema in this converted schema, i.e. + # if we see a on_value_range in a dallas sensor, then this is overridden because + # it is already defined in sensor + assert S_CONFIG_VARS not in converted and S_EXTENDS not in converted + config = converted.get(S_SCHEMA, {}) + + return get_overridden_key_inner(key, config, {}) + + +def get_overridden_key_inner(key, config, ret): + if S_EXTENDS not in config: + return ret + for s in config[S_EXTENDS]: + p = s.partition(".") + s1 = output.get(p[0], {}).get(S_SCHEMAS, {}).get(p[2], {}).get(S_SCHEMA) + if s1: + if key in s1.get(S_CONFIG_VARS, {}): + for k, v in s1.get(S_CONFIG_VARS)[key].items(): + if k not in ret: # keep most overridden + ret[k] = v + get_overridden_key_inner(key, s1, ret) + + return ret + + +def convert_keys(converted, schema, path): + for k, v in schema.items(): + # deprecated stuff + if repr(v).startswith(" Date: Sun, 3 Apr 2022 15:38:44 -0500 Subject: [PATCH 0348/1729] protobuf: fix incomplete 64 bits implementation (#3341) --- esphome/components/api/proto.h | 23 +++++++++++++++++++++++ script/api_protobuf/api_protobuf.py | 16 ++++++++-------- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index 38fd98b489..32f525990d 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -195,6 +195,20 @@ class ProtoWriteBuffer { this->write((value >> 16) & 0xFF); this->write((value >> 24) & 0xFF); } + void encode_fixed64(uint32_t field_id, uint64_t value, bool force = false) { + if (value == 0 && !force) + return; + + this->encode_field_raw(field_id, 5); + this->write((value >> 0) & 0xFF); + this->write((value >> 8) & 0xFF); + this->write((value >> 16) & 0xFF); + this->write((value >> 24) & 0xFF); + this->write((value >> 32) & 0xFF); + this->write((value >> 40) & 0xFF); + this->write((value >> 48) & 0xFF); + this->write((value >> 56) & 0xFF); + } template void encode_enum(uint32_t field_id, T value, bool force = false) { this->encode_uint32(field_id, static_cast(value), force); } @@ -229,6 +243,15 @@ class ProtoWriteBuffer { } this->encode_uint32(field_id, uvalue, force); } + void encode_sint64(uint32_t field_id, int64_t value, bool force = false) { + uint64_t uvalue; + if (value < 0) { + uvalue = ~(value << 1); + } else { + uvalue = value << 1; + } + this->encode_uint64(field_id, uvalue, force); + } template void encode_message(uint32_t field_id, const C &value, bool force = false) { this->encode_field_raw(field_id, 2); size_t begin = this->buffer_->size(); diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 016a0995b9..26bf8647af 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -236,7 +236,7 @@ class Int64Type(TypeInfo): encode_func = "encode_int64" def dump(self, name): - o = f'sprintf(buffer, "%ll", {name});\n' + o = f'sprintf(buffer, "%lld", {name});\n' o += f"out.append(buffer);" return o @@ -249,7 +249,7 @@ class UInt64Type(TypeInfo): encode_func = "encode_uint64" def dump(self, name): - o = f'sprintf(buffer, "%ull", {name});\n' + o = f'sprintf(buffer, "%llu", {name});\n' o += f"out.append(buffer);" return o @@ -275,7 +275,7 @@ class Fixed64Type(TypeInfo): encode_func = "encode_fixed64" def dump(self, name): - o = f'sprintf(buffer, "%ull", {name});\n' + o = f'sprintf(buffer, "%llu", {name});\n' o += f"out.append(buffer);" return o @@ -417,7 +417,7 @@ class SFixed64Type(TypeInfo): encode_func = "encode_sfixed64" def dump(self, name): - o = f'sprintf(buffer, "%ll", {name});\n' + o = f'sprintf(buffer, "%lld", {name});\n' o += f"out.append(buffer);" return o @@ -440,10 +440,10 @@ class SInt64Type(TypeInfo): cpp_type = "int64_t" default_value = "0" decode_varint = "value.as_sint64()" - encode_func = "encode_sin64" + encode_func = "encode_sint64" def dump(self, name): - o = f'sprintf(buffer, "%ll", {name});\n' + o = f'sprintf(buffer, "%lld", {name});\n' o += f"out.append(buffer);" return o @@ -622,13 +622,13 @@ def build_message_type(desc): protected_content.insert(0, prot) if decode_64bit: decode_64bit.append("default:\n return false;") - o = f"bool {desc.name}::decode_64bit(uint32_t field_id, Proto64bit value) {{\n" + o = f"bool {desc.name}::decode_64bit(uint32_t field_id, Proto64Bit value) {{\n" o += " switch (field_id) {\n" o += indent("\n".join(decode_64bit), " ") + "\n" o += " }\n" o += "}\n" cpp += o - prot = "bool decode_64bit(uint32_t field_id, Proto64bit value) override;" + prot = "bool decode_64bit(uint32_t field_id, Proto64Bit value) override;" protected_content.insert(0, prot) o = f"void {desc.name}::encode(ProtoWriteBuffer buffer) const {{" From fa1b5117fd1b4e9a3ea5b88fe5f2b54acd436aac Mon Sep 17 00:00:00 2001 From: Branden Cash <203336+ammmze@users.noreply.github.com> Date: Sun, 3 Apr 2022 14:35:48 -0700 Subject: [PATCH 0349/1729] feat: support ble_client that use security w/o pin codes (#3320) --- esphome/components/ble_client/ble_client.cpp | 41 ++++++++++++++++--- esphome/components/ble_client/ble_client.h | 2 + .../esp32_ble_tracker/esp32_ble_tracker.cpp | 3 ++ .../esp32_ble_tracker/esp32_ble_tracker.h | 1 + 4 files changed, 42 insertions(+), 5 deletions(-) diff --git a/esphome/components/ble_client/ble_client.cpp b/esphome/components/ble_client/ble_client.cpp index 5b2701d77a..7bef0d652c 100644 --- a/esphome/components/ble_client/ble_client.cpp +++ b/esphome/components/ble_client/ble_client.cpp @@ -118,16 +118,21 @@ void BLEClient::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t es this->set_states_(espbt::ClientState::IDLE); break; } - this->conn_id = param->open.conn_id; - auto ret = esp_ble_gattc_send_mtu_req(this->gattc_if, param->open.conn_id); + break; + } + case ESP_GATTC_CONNECT_EVT: { + ESP_LOGV(TAG, "[%s] ESP_GATTC_CONNECT_EVT", this->address_str().c_str()); + this->conn_id = param->connect.conn_id; + auto ret = esp_ble_gattc_send_mtu_req(this->gattc_if, param->connect.conn_id); if (ret) { - ESP_LOGW(TAG, "esp_ble_gattc_send_mtu_req failed, status=%d", ret); + ESP_LOGW(TAG, "esp_ble_gattc_send_mtu_req failed, status=%x", ret); } break; } case ESP_GATTC_CFG_MTU_EVT: { if (param->cfg_mtu.status != ESP_GATT_OK) { - ESP_LOGW(TAG, "cfg_mtu to %s failed, status %d", this->address_str().c_str(), param->cfg_mtu.status); + ESP_LOGW(TAG, "cfg_mtu to %s failed, mtu %d, status %d", this->address_str().c_str(), param->cfg_mtu.mtu, + param->cfg_mtu.status); this->set_states_(espbt::ClientState::IDLE); break; } @@ -139,7 +144,7 @@ void BLEClient::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t es if (memcmp(param->disconnect.remote_bda, this->remote_bda, 6) != 0) { return; } - ESP_LOGV(TAG, "[%s] ESP_GATTC_DISCONNECT_EVT", this->address_str().c_str()); + ESP_LOGV(TAG, "[%s] ESP_GATTC_DISCONNECT_EVT, reason %d", this->address_str().c_str(), param->disconnect.reason); for (auto &svc : this->services_) delete svc; // NOLINT(cppcoreguidelines-owning-memory) this->services_.clear(); @@ -201,6 +206,32 @@ void BLEClient::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t es } } +void BLEClient::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { + switch (event) { + // This event is sent by the server when it requests security + case ESP_GAP_BLE_SEC_REQ_EVT: + ESP_LOGV(TAG, "ESP_GAP_BLE_SEC_REQ_EVT %x", event); + esp_ble_gap_security_rsp(param->ble_security.ble_req.bd_addr, true); + break; + // This event is sent once authentication has completed + case ESP_GAP_BLE_AUTH_CMPL_EVT: + esp_bd_addr_t bd_addr; + memcpy(bd_addr, param->ble_security.auth_cmpl.bd_addr, sizeof(esp_bd_addr_t)); + ESP_LOGI(TAG, "auth complete. remote BD_ADDR: %s", format_hex(bd_addr, 6).c_str()); + if (!param->ble_security.auth_cmpl.success) { + ESP_LOGE(TAG, "auth fail reason = 0x%x", param->ble_security.auth_cmpl.fail_reason); + } else { + ESP_LOGV(TAG, "auth success. address type = %d auth mode = %d", param->ble_security.auth_cmpl.addr_type, + param->ble_security.auth_cmpl.auth_mode); + } + break; + // There are other events we'll want to implement at some point to support things like pass key + // https://github.com/espressif/esp-idf/blob/cba69dd088344ed9d26739f04736ae7a37541b3a/examples/bluetooth/bluedroid/ble/gatt_security_client/tutorial/Gatt_Security_Client_Example_Walkthrough.md + default: + break; + } +} + // Parse GATT values into a float for a sensor. // Ref: https://www.bluetooth.com/specifications/assigned-numbers/format-types/ float BLEClient::parse_char_value(uint8_t *value, uint16_t length) { diff --git a/esphome/components/ble_client/ble_client.h b/esphome/components/ble_client/ble_client.h index e0a1bf61b9..b122bfd11e 100644 --- a/esphome/components/ble_client/ble_client.h +++ b/esphome/components/ble_client/ble_client.h @@ -11,6 +11,7 @@ #include #include #include +#include namespace esphome { namespace ble_client { @@ -86,6 +87,7 @@ class BLEClient : public espbt::ESPBTClient, public Component { void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override; + void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override; bool parse_device(const espbt::ESPBTDevice &device) override; void on_scan_end() override {} void connect() override; diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index 7614e33979..9722104e25 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -262,6 +262,9 @@ void ESP32BLETracker::real_gap_event_handler_(esp_gap_ble_cb_event_t event, esp_ default: break; } + for (auto *client : global_esp32_ble_tracker->clients_) { + client->gap_event_handler(event, param); + } } void ESP32BLETracker::gap_scan_set_param_complete_(const esp_ble_gap_cb_param_t::ble_scan_param_cmpl_evt_param ¶m) { diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index 9ff2a5a861..62fff30a20 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -155,6 +155,7 @@ class ESPBTClient : public ESPBTDeviceListener { public: virtual void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) = 0; + virtual void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) = 0; virtual void connect() = 0; void set_state(ClientState st) { this->state_ = st; } ClientState state() const { return state_; } From 792108686cb6b0a90b2ee7dbf6ddb24cb5487ba7 Mon Sep 17 00:00:00 2001 From: Martin <25747549+martgras@users.noreply.github.com> Date: Mon, 4 Apr 2022 01:07:20 +0200 Subject: [PATCH 0350/1729] Add mqtt for idf (#2930) Co-authored-by: Flaviu Tamas Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Co-authored-by: Oxan van Leeuwen --- esphome/components/mqtt/__init__.py | 37 ++++- esphome/components/mqtt/mqtt_backend.h | 69 ++++++++ .../components/mqtt/mqtt_backend_arduino.h | 74 +++++++++ esphome/components/mqtt/mqtt_backend_idf.cpp | 149 ++++++++++++++++++ esphome/components/mqtt/mqtt_backend_idf.h | 143 +++++++++++++++++ esphome/components/mqtt/mqtt_client.cpp | 117 +++++++------- esphome/components/mqtt/mqtt_client.h | 31 ++-- esphome/core/defines.h | 7 +- tests/test5.yaml | 13 ++ 9 files changed, 567 insertions(+), 73 deletions(-) create mode 100644 esphome/components/mqtt/mqtt_backend.h create mode 100644 esphome/components/mqtt/mqtt_backend_arduino.h create mode 100644 esphome/components/mqtt/mqtt_backend_idf.cpp create mode 100644 esphome/components/mqtt/mqtt_backend_idf.h diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index 901b77474d..b2548d6081 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -9,6 +9,7 @@ from esphome.const import ( CONF_AVAILABILITY, CONF_BIRTH_MESSAGE, CONF_BROKER, + CONF_CERTIFICATE_AUTHORITY, CONF_CLIENT_ID, CONF_COMMAND_TOPIC, CONF_COMMAND_RETAIN, @@ -42,9 +43,14 @@ from esphome.const import ( CONF_WILL_MESSAGE, ) from esphome.core import coroutine_with_priority, CORE +from esphome.components.esp32 import add_idf_sdkconfig_option DEPENDENCIES = ["network"] -AUTO_LOAD = ["json", "async_tcp"] + +AUTO_LOAD = ["json"] + +CONF_IDF_SEND_ASYNC = "idf_send_async" +CONF_SKIP_CERT_CN_CHECK = "skip_cert_cn_check" def validate_message_just_topic(value): @@ -163,6 +169,15 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_USERNAME, default=""): cv.string, cv.Optional(CONF_PASSWORD, default=""): cv.string, cv.Optional(CONF_CLIENT_ID): cv.string, + cv.SplitDefault(CONF_IDF_SEND_ASYNC, esp32_idf=False): cv.All( + cv.boolean, cv.only_with_esp_idf + ), + cv.Optional(CONF_CERTIFICATE_AUTHORITY): cv.All( + cv.string, cv.only_with_esp_idf + ), + cv.SplitDefault(CONF_SKIP_CERT_CN_CHECK, esp32_idf=False): cv.All( + cv.boolean, cv.only_with_esp_idf + ), cv.Optional(CONF_DISCOVERY, default=True): cv.Any( cv.boolean, cv.one_of("CLEAN", upper=True) ), @@ -217,7 +232,6 @@ CONFIG_SCHEMA = cv.All( } ), validate_config, - cv.only_with_arduino, ) @@ -238,9 +252,11 @@ def exp_mqtt_message(config): async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) + # Add required libraries for arduino + if CORE.using_arduino: + # https://github.com/OttoWinter/async-mqtt-client/blob/master/library.json + cg.add_library("ottowinter/AsyncMqttClient-esphome", "0.8.6") - # https://github.com/OttoWinter/async-mqtt-client/blob/master/library.json - cg.add_library("ottowinter/AsyncMqttClient-esphome", "0.8.6") cg.add_define("USE_MQTT") cg.add_global(mqtt_ns.using) @@ -321,6 +337,19 @@ async def to_code(config): cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT])) + # esp-idf only + if CONF_CERTIFICATE_AUTHORITY in config: + cg.add(var.set_ca_certificate(config[CONF_CERTIFICATE_AUTHORITY])) + cg.add(var.set_skip_cert_cn_check(config[CONF_SKIP_CERT_CN_CHECK])) + + # prevent error -0x428e + # See https://github.com/espressif/esp-idf/issues/139 + add_idf_sdkconfig_option("CONFIG_MBEDTLS_HARDWARE_MPI", False) + + if CONF_IDF_SEND_ASYNC in config and config[CONF_IDF_SEND_ASYNC]: + cg.add_define("USE_MQTT_IDF_ENQUEUE") + # end esp-idf + for conf in config.get(CONF_ON_MESSAGE, []): trig = cg.new_Pvariable(conf[CONF_TRIGGER_ID], conf[CONF_TOPIC]) cg.add(trig.set_qos(conf[CONF_QOS])) diff --git a/esphome/components/mqtt/mqtt_backend.h b/esphome/components/mqtt/mqtt_backend.h new file mode 100644 index 0000000000..d23cda578d --- /dev/null +++ b/esphome/components/mqtt/mqtt_backend.h @@ -0,0 +1,69 @@ +#pragma once + +#include +#include +#include "esphome/components/network/ip_address.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace mqtt { + +enum class MQTTClientDisconnectReason : int8_t { + TCP_DISCONNECTED = 0, + MQTT_UNACCEPTABLE_PROTOCOL_VERSION = 1, + MQTT_IDENTIFIER_REJECTED = 2, + MQTT_SERVER_UNAVAILABLE = 3, + MQTT_MALFORMED_CREDENTIALS = 4, + MQTT_NOT_AUTHORIZED = 5, + ESP8266_NOT_ENOUGH_SPACE = 6, + TLS_BAD_FINGERPRINT = 7 +}; + +/// internal struct for MQTT messages. +struct MQTTMessage { + std::string topic; + std::string payload; + uint8_t qos; ///< QoS. Only for last will testaments. + bool retain; +}; + +class MQTTBackend { + public: + using on_connect_callback_t = void(bool session_present); + using on_disconnect_callback_t = void(MQTTClientDisconnectReason reason); + using on_subscribe_callback_t = void(uint16_t packet_id, uint8_t qos); + using on_unsubscribe_callback_t = void(uint16_t packet_id); + using on_message_callback_t = void(const char *topic, const char *payload, size_t len, size_t index, size_t total); + using on_publish_user_callback_t = void(uint16_t packet_id); + + virtual void set_keep_alive(uint16_t keep_alive) = 0; + virtual void set_client_id(const char *client_id) = 0; + virtual void set_clean_session(bool clean_session) = 0; + virtual void set_credentials(const char *username, const char *password) = 0; + virtual void set_will(const char *topic, uint8_t qos, bool retain, const char *payload) = 0; + virtual void set_server(network::IPAddress ip, uint16_t port) = 0; + virtual void set_server(const char *host, uint16_t port) = 0; + virtual void set_on_connect(std::function &&callback) = 0; + virtual void set_on_disconnect(std::function &&callback) = 0; + virtual void set_on_subscribe(std::function &&callback) = 0; + virtual void set_on_unsubscribe(std::function &&callback) = 0; + virtual void set_on_message(std::function &&callback) = 0; + virtual void set_on_publish(std::function &&callback) = 0; + virtual bool connected() const = 0; + virtual void connect() = 0; + virtual void disconnect() = 0; + virtual bool subscribe(const char *topic, uint8_t qos) = 0; + virtual bool unsubscribe(const char *topic) = 0; + virtual bool publish(const char *topic, const char *payload, size_t length, uint8_t qos, bool retain) = 0; + + virtual bool publish(const MQTTMessage &message) { + return publish(message.topic.c_str(), message.payload.c_str(), message.payload.length(), message.qos, + message.retain); + } + + // called from MQTTClient::loop() + virtual void loop() {} +}; + +} // namespace mqtt +} // namespace esphome diff --git a/esphome/components/mqtt/mqtt_backend_arduino.h b/esphome/components/mqtt/mqtt_backend_arduino.h new file mode 100644 index 0000000000..6399ec88e0 --- /dev/null +++ b/esphome/components/mqtt/mqtt_backend_arduino.h @@ -0,0 +1,74 @@ +#pragma once + +#ifdef USE_ARDUINO + +#include "mqtt_backend.h" +#include + +namespace esphome { +namespace mqtt { + +class MQTTBackendArduino final : public MQTTBackend { + public: + void set_keep_alive(uint16_t keep_alive) final { mqtt_client_.setKeepAlive(keep_alive); } + void set_client_id(const char *client_id) final { mqtt_client_.setClientId(client_id); } + void set_clean_session(bool clean_session) final { mqtt_client_.setCleanSession(clean_session); } + void set_credentials(const char *username, const char *password) final { + mqtt_client_.setCredentials(username, password); + } + void set_will(const char *topic, uint8_t qos, bool retain, const char *payload) final { + mqtt_client_.setWill(topic, qos, retain, payload); + } + void set_server(network::IPAddress ip, uint16_t port) final { + mqtt_client_.setServer(IPAddress(static_cast(ip)), port); + } + void set_server(const char *host, uint16_t port) final { mqtt_client_.setServer(host, port); } +#if ASYNC_TCP_SSL_ENABLED + void set_secure(bool secure) { mqtt_client.setSecure(secure); } + void add_server_fingerprint(const uint8_t *fingerprint) { mqtt_client.addServerFingerprint(fingerprint); } +#endif + + void set_on_connect(std::function &&callback) final { + this->mqtt_client_.onConnect(std::move(callback)); + } + void set_on_disconnect(std::function &&callback) final { + auto async_callback = [callback](AsyncMqttClientDisconnectReason reason) { + // int based enum so casting isn't a problem + callback(static_cast(reason)); + }; + this->mqtt_client_.onDisconnect(std::move(async_callback)); + } + void set_on_subscribe(std::function &&callback) final { + this->mqtt_client_.onSubscribe(std::move(callback)); + } + void set_on_unsubscribe(std::function &&callback) final { + this->mqtt_client_.onUnsubscribe(std::move(callback)); + } + void set_on_message(std::function &&callback) final { + auto async_callback = [callback](const char *topic, const char *payload, + AsyncMqttClientMessageProperties async_properties, size_t len, size_t index, + size_t total) { callback(topic, payload, len, index, total); }; + mqtt_client_.onMessage(std::move(async_callback)); + } + void set_on_publish(std::function &&callback) final { + this->mqtt_client_.onPublish(std::move(callback)); + } + + bool connected() const final { return mqtt_client_.connected(); } + void connect() final { mqtt_client_.connect(); } + void disconnect() final { mqtt_client_.disconnect(true); } + bool subscribe(const char *topic, uint8_t qos) final { return mqtt_client_.subscribe(topic, qos) != 0; } + bool unsubscribe(const char *topic) final { return mqtt_client_.unsubscribe(topic) != 0; } + bool publish(const char *topic, const char *payload, size_t length, uint8_t qos, bool retain) final { + return mqtt_client_.publish(topic, qos, retain, payload, length, false, 0) != 0; + } + using MQTTBackend::publish; + + protected: + AsyncMqttClient mqtt_client_; +}; + +} // namespace mqtt +} // namespace esphome + +#endif // defined(USE_ARDUINO) diff --git a/esphome/components/mqtt/mqtt_backend_idf.cpp b/esphome/components/mqtt/mqtt_backend_idf.cpp new file mode 100644 index 0000000000..0726f72567 --- /dev/null +++ b/esphome/components/mqtt/mqtt_backend_idf.cpp @@ -0,0 +1,149 @@ +#ifdef USE_ESP_IDF + +#include +#include "mqtt_backend_idf.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace mqtt { + +static const char *const TAG = "mqtt.idf"; + +bool MQTTBackendIDF::initialize_() { + mqtt_cfg_.user_context = (void *) this; + mqtt_cfg_.buffer_size = MQTT_BUFFER_SIZE; + + mqtt_cfg_.host = this->host_.c_str(); + mqtt_cfg_.port = this->port_; + mqtt_cfg_.keepalive = this->keep_alive_; + mqtt_cfg_.disable_clean_session = !this->clean_session_; + + if (!this->username_.empty()) { + mqtt_cfg_.username = this->username_.c_str(); + if (!this->password_.empty()) { + mqtt_cfg_.password = this->password_.c_str(); + } + } + + if (!this->lwt_topic_.empty()) { + mqtt_cfg_.lwt_topic = this->lwt_topic_.c_str(); + this->mqtt_cfg_.lwt_qos = this->lwt_qos_; + this->mqtt_cfg_.lwt_retain = this->lwt_retain_; + + if (!this->lwt_message_.empty()) { + mqtt_cfg_.lwt_msg = this->lwt_message_.c_str(); + mqtt_cfg_.lwt_msg_len = this->lwt_message_.size(); + } + } + + if (!this->client_id_.empty()) { + mqtt_cfg_.client_id = this->client_id_.c_str(); + } + if (ca_certificate_.has_value()) { + mqtt_cfg_.cert_pem = ca_certificate_.value().c_str(); + mqtt_cfg_.skip_cert_common_name_check = skip_cert_cn_check_; + mqtt_cfg_.transport = MQTT_TRANSPORT_OVER_SSL; + } else { + mqtt_cfg_.transport = MQTT_TRANSPORT_OVER_TCP; + } + auto *mqtt_client = esp_mqtt_client_init(&mqtt_cfg_); + if (mqtt_client) { + handler_.reset(mqtt_client); + is_initalized_ = true; + esp_mqtt_client_register_event(mqtt_client, MQTT_EVENT_ANY, mqtt_event_handler, this); + return true; + } else { + ESP_LOGE(TAG, "Failed to initialize IDF-MQTT"); + return false; + } +} + +void MQTTBackendIDF::loop() { + // process new events + // handle only 1 message per loop iteration + if (!mqtt_events_.empty()) { + auto &event = mqtt_events_.front(); + mqtt_event_handler_(event); + mqtt_events_.pop(); + } +} + +void MQTTBackendIDF::mqtt_event_handler_(const esp_mqtt_event_t &event) { + ESP_LOGV(TAG, "Event dispatched from event loop event_id=%d", event.event_id); + switch (event.event_id) { + case MQTT_EVENT_BEFORE_CONNECT: + ESP_LOGV(TAG, "MQTT_EVENT_BEFORE_CONNECT"); + break; + + case MQTT_EVENT_CONNECTED: + ESP_LOGV(TAG, "MQTT_EVENT_CONNECTED"); + // TODO session present check + this->is_connected_ = true; + this->on_connect_.call(!mqtt_cfg_.disable_clean_session); + break; + case MQTT_EVENT_DISCONNECTED: + ESP_LOGV(TAG, "MQTT_EVENT_DISCONNECTED"); + // TODO is there a way to get the disconnect reason? + this->is_connected_ = false; + this->on_disconnect_.call(MQTTClientDisconnectReason::TCP_DISCONNECTED); + break; + + case MQTT_EVENT_SUBSCRIBED: + ESP_LOGV(TAG, "MQTT_EVENT_SUBSCRIBED, msg_id=%d", event.msg_id); + // hardcode QoS to 0. QoS is not used in this context but required to mirror the AsyncMqtt interface + this->on_subscribe_.call((int) event.msg_id, 0); + break; + case MQTT_EVENT_UNSUBSCRIBED: + ESP_LOGV(TAG, "MQTT_EVENT_UNSUBSCRIBED, msg_id=%d", event.msg_id); + this->on_unsubscribe_.call((int) event.msg_id); + break; + case MQTT_EVENT_PUBLISHED: + ESP_LOGV(TAG, "MQTT_EVENT_PUBLISHED, msg_id=%d", event.msg_id); + this->on_publish_.call((int) event.msg_id); + break; + case MQTT_EVENT_DATA: { + static std::string topic; + if (event.topic) { + // not 0 terminated - create a string from it + topic = std::string(event.topic, event.topic_len); + } + ESP_LOGV(TAG, "MQTT_EVENT_DATA %s", topic.c_str()); + auto data_len = event.data_len; + if (data_len == 0) + data_len = strlen(event.data); + this->on_message_.call(event.topic ? const_cast(topic.c_str()) : nullptr, event.data, data_len, + event.current_data_offset, event.total_data_len); + } break; + case MQTT_EVENT_ERROR: + ESP_LOGE(TAG, "MQTT_EVENT_ERROR"); + if (event.error_handle->error_type == MQTT_ERROR_TYPE_TCP_TRANSPORT) { + ESP_LOGE(TAG, "Last error code reported from esp-tls: 0x%x", event.error_handle->esp_tls_last_esp_err); + ESP_LOGE(TAG, "Last tls stack error number: 0x%x", event.error_handle->esp_tls_stack_err); + ESP_LOGE(TAG, "Last captured errno : %d (%s)", event.error_handle->esp_transport_sock_errno, + strerror(event.error_handle->esp_transport_sock_errno)); + } else if (event.error_handle->error_type == MQTT_ERROR_TYPE_CONNECTION_REFUSED) { + ESP_LOGE(TAG, "Connection refused error: 0x%x", event.error_handle->connect_return_code); + } else { + ESP_LOGE(TAG, "Unknown error type: 0x%x", event.error_handle->error_type); + } + break; + default: + ESP_LOGV(TAG, "Other event id:%d", event.event_id); + break; + } +} + +/// static - Dispatch event to instance method +void MQTTBackendIDF::mqtt_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data) { + MQTTBackendIDF *instance = static_cast(handler_args); + // queue event to decouple processing + if (instance) { + auto event = *static_cast(event_data); + instance->mqtt_events_.push(event); + } +} + +} // namespace mqtt +} // namespace esphome +#endif // USE_ESP_IDF diff --git a/esphome/components/mqtt/mqtt_backend_idf.h b/esphome/components/mqtt/mqtt_backend_idf.h new file mode 100644 index 0000000000..77b5592d72 --- /dev/null +++ b/esphome/components/mqtt/mqtt_backend_idf.h @@ -0,0 +1,143 @@ +#pragma once + +#ifdef USE_ESP_IDF + +#include +#include +#include +#include "esphome/components/network/ip_address.h" +#include "esphome/core/helpers.h" +#include "mqtt_backend.h" + +namespace esphome { +namespace mqtt { + +class MQTTBackendIDF final : public MQTTBackend { + public: + static const size_t MQTT_BUFFER_SIZE = 4096; + + void set_keep_alive(uint16_t keep_alive) final { this->keep_alive_ = keep_alive; } + void set_client_id(const char *client_id) final { this->client_id_ = client_id; } + void set_clean_session(bool clean_session) final { this->clean_session_ = clean_session; } + + void set_credentials(const char *username, const char *password) final { + if (username) + this->username_ = username; + if (password) + this->password_ = password; + } + void set_will(const char *topic, uint8_t qos, bool retain, const char *payload) final { + if (topic) + this->lwt_topic_ = topic; + this->lwt_qos_ = qos; + if (payload) + this->lwt_message_ = payload; + this->lwt_retain_ = retain; + } + void set_server(network::IPAddress ip, uint16_t port) final { + this->host_ = ip.str(); + this->port_ = port; + } + void set_server(const char *host, uint16_t port) final { + this->host_ = host; + this->port_ = port; + } + void set_on_connect(std::function &&callback) final { + this->on_connect_.add(std::move(callback)); + } + void set_on_disconnect(std::function &&callback) final { + this->on_disconnect_.add(std::move(callback)); + } + void set_on_subscribe(std::function &&callback) final { + this->on_subscribe_.add(std::move(callback)); + } + void set_on_unsubscribe(std::function &&callback) final { + this->on_unsubscribe_.add(std::move(callback)); + } + void set_on_message(std::function &&callback) final { + this->on_message_.add(std::move(callback)); + } + void set_on_publish(std::function &&callback) final { + this->on_publish_.add(std::move(callback)); + } + bool connected() const final { return this->is_connected_; } + + void connect() final { + if (!is_initalized_) { + if (initialize_()) { + esp_mqtt_client_start(handler_.get()); + } + } + } + void disconnect() final { + if (is_initalized_) + esp_mqtt_client_disconnect(handler_.get()); + } + + bool subscribe(const char *topic, uint8_t qos) final { + return esp_mqtt_client_subscribe(handler_.get(), topic, qos) != -1; + } + bool unsubscribe(const char *topic) final { return esp_mqtt_client_unsubscribe(handler_.get(), topic) != -1; } + + bool publish(const char *topic, const char *payload, size_t length, uint8_t qos, bool retain) final { +#if defined(USE_MQTT_IDF_ENQUEUE) + // use the non-blocking version + // it can delay sending a couple of seconds but won't block + return esp_mqtt_client_enqueue(handler_.get(), topic, payload, length, qos, retain, true) != -1; +#else + // might block for several seconds, either due to network timeout (10s) + // or if publishing payloads longer than internal buffer (due to message fragmentation) + return esp_mqtt_client_publish(handler_.get(), topic, payload, length, qos, retain) != -1; +#endif + } + using MQTTBackend::publish; + + void loop() final; + + void set_ca_certificate(const std::string &cert) { ca_certificate_ = cert; } + void set_skip_cert_cn_check(bool skip_check) { skip_cert_cn_check_ = skip_check; } + + protected: + bool initialize_(); + void mqtt_event_handler_(const esp_mqtt_event_t &event); + static void mqtt_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data); + + struct MqttClientDeleter { + void operator()(esp_mqtt_client *client_handler) { esp_mqtt_client_destroy(client_handler); } + }; + using ClientHandler_ = std::unique_ptr; + ClientHandler_ handler_; + + bool is_connected_{false}; + bool is_initalized_{false}; + + esp_mqtt_client_config_t mqtt_cfg_{}; + + std::string host_; + uint16_t port_; + std::string username_; + std::string password_; + std::string lwt_topic_; + std::string lwt_message_; + uint8_t lwt_qos_; + bool lwt_retain_; + std::string client_id_; + uint16_t keep_alive_; + bool clean_session_; + optional ca_certificate_; + bool skip_cert_cn_check_{false}; + + // callbacks + CallbackManager on_connect_; + CallbackManager on_disconnect_; + CallbackManager on_subscribe_; + CallbackManager on_unsubscribe_; + CallbackManager on_message_; + CallbackManager on_publish_; + std::queue mqtt_events_; +}; + +} // namespace mqtt +} // namespace esphome + +#endif diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index 1fea0c80cc..3c6ce7cdfc 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -27,21 +27,21 @@ MQTTClientComponent::MQTTClientComponent() { // Connection void MQTTClientComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up MQTT..."); - this->mqtt_client_.onMessage([this](char const *topic, char *payload, AsyncMqttClientMessageProperties properties, - size_t len, size_t index, size_t total) { - if (index == 0) - this->payload_buffer_.reserve(total); + this->mqtt_backend_.set_on_message( + [this](const char *topic, const char *payload, size_t len, size_t index, size_t total) { + if (index == 0) + this->payload_buffer_.reserve(total); - // append new payload, may contain incomplete MQTT message - this->payload_buffer_.append(payload, len); + // append new payload, may contain incomplete MQTT message + this->payload_buffer_.append(payload, len); - // MQTT fully received - if (len + index == total) { - this->on_message(topic, this->payload_buffer_); - this->payload_buffer_.clear(); - } - }); - this->mqtt_client_.onDisconnect([this](AsyncMqttClientDisconnectReason reason) { + // MQTT fully received + if (len + index == total) { + this->on_message(topic, this->payload_buffer_); + this->payload_buffer_.clear(); + } + }); + this->mqtt_backend_.set_on_disconnect([this](MQTTClientDisconnectReason reason) { this->state_ = MQTT_CLIENT_DISCONNECTED; this->disconnect_reason_ = reason; }); @@ -49,8 +49,10 @@ void MQTTClientComponent::setup() { if (this->is_log_message_enabled() && logger::global_logger != nullptr) { logger::global_logger->add_on_log_callback([this](int level, const char *tag, const char *message) { if (level <= this->log_level_ && this->is_connected()) { - this->publish(this->log_message_.topic, message, strlen(message), this->log_message_.qos, - this->log_message_.retain); + this->publish({.topic = this->log_message_.topic, + .payload = message, + .qos = this->log_message_.qos, + .retain = this->log_message_.retain}); } }); } @@ -173,9 +175,9 @@ void MQTTClientComponent::start_connect_() { ESP_LOGI(TAG, "Connecting to MQTT..."); // Force disconnect first - this->mqtt_client_.disconnect(true); + this->mqtt_backend_.disconnect(); - this->mqtt_client_.setClientId(this->credentials_.client_id.c_str()); + this->mqtt_backend_.set_client_id(this->credentials_.client_id.c_str()); const char *username = nullptr; if (!this->credentials_.username.empty()) username = this->credentials_.username.c_str(); @@ -183,24 +185,24 @@ void MQTTClientComponent::start_connect_() { if (!this->credentials_.password.empty()) password = this->credentials_.password.c_str(); - this->mqtt_client_.setCredentials(username, password); + this->mqtt_backend_.set_credentials(username, password); - this->mqtt_client_.setServer((uint32_t) this->ip_, this->credentials_.port); + this->mqtt_backend_.set_server((uint32_t) this->ip_, this->credentials_.port); if (!this->last_will_.topic.empty()) { - this->mqtt_client_.setWill(this->last_will_.topic.c_str(), this->last_will_.qos, this->last_will_.retain, - this->last_will_.payload.c_str(), this->last_will_.payload.length()); + this->mqtt_backend_.set_will(this->last_will_.topic.c_str(), this->last_will_.qos, this->last_will_.retain, + this->last_will_.payload.c_str()); } - this->mqtt_client_.connect(); + this->mqtt_backend_.connect(); this->state_ = MQTT_CLIENT_CONNECTING; this->connect_begin_ = millis(); } bool MQTTClientComponent::is_connected() { - return this->state_ == MQTT_CLIENT_CONNECTED && this->mqtt_client_.connected(); + return this->state_ == MQTT_CLIENT_CONNECTED && this->mqtt_backend_.connected(); } void MQTTClientComponent::check_connected() { - if (!this->mqtt_client_.connected()) { + if (!this->mqtt_backend_.connected()) { if (millis() - this->connect_begin_ > 60000) { this->state_ = MQTT_CLIENT_DISCONNECTED; this->start_dnslookup_(); @@ -222,31 +224,34 @@ void MQTTClientComponent::check_connected() { } void MQTTClientComponent::loop() { + // Call the backend loop first + mqtt_backend_.loop(); + if (this->disconnect_reason_.has_value()) { const LogString *reason_s; switch (*this->disconnect_reason_) { - case AsyncMqttClientDisconnectReason::TCP_DISCONNECTED: + case MQTTClientDisconnectReason::TCP_DISCONNECTED: reason_s = LOG_STR("TCP disconnected"); break; - case AsyncMqttClientDisconnectReason::MQTT_UNACCEPTABLE_PROTOCOL_VERSION: + case MQTTClientDisconnectReason::MQTT_UNACCEPTABLE_PROTOCOL_VERSION: reason_s = LOG_STR("Unacceptable Protocol Version"); break; - case AsyncMqttClientDisconnectReason::MQTT_IDENTIFIER_REJECTED: + case MQTTClientDisconnectReason::MQTT_IDENTIFIER_REJECTED: reason_s = LOG_STR("Identifier Rejected"); break; - case AsyncMqttClientDisconnectReason::MQTT_SERVER_UNAVAILABLE: + case MQTTClientDisconnectReason::MQTT_SERVER_UNAVAILABLE: reason_s = LOG_STR("Server Unavailable"); break; - case AsyncMqttClientDisconnectReason::MQTT_MALFORMED_CREDENTIALS: + case MQTTClientDisconnectReason::MQTT_MALFORMED_CREDENTIALS: reason_s = LOG_STR("Malformed Credentials"); break; - case AsyncMqttClientDisconnectReason::MQTT_NOT_AUTHORIZED: + case MQTTClientDisconnectReason::MQTT_NOT_AUTHORIZED: reason_s = LOG_STR("Not Authorized"); break; - case AsyncMqttClientDisconnectReason::ESP8266_NOT_ENOUGH_SPACE: + case MQTTClientDisconnectReason::ESP8266_NOT_ENOUGH_SPACE: reason_s = LOG_STR("Not Enough Space"); break; - case AsyncMqttClientDisconnectReason::TLS_BAD_FINGERPRINT: + case MQTTClientDisconnectReason::TLS_BAD_FINGERPRINT: reason_s = LOG_STR("TLS Bad Fingerprint"); break; default: @@ -275,7 +280,7 @@ void MQTTClientComponent::loop() { this->check_connected(); break; case MQTT_CLIENT_CONNECTED: - if (!this->mqtt_client_.connected()) { + if (!this->mqtt_backend_.connected()) { this->state_ = MQTT_CLIENT_DISCONNECTED; ESP_LOGW(TAG, "Lost MQTT Client connection!"); this->start_dnslookup_(); @@ -302,10 +307,10 @@ bool MQTTClientComponent::subscribe_(const char *topic, uint8_t qos) { if (!this->is_connected()) return false; - uint16_t ret = this->mqtt_client_.subscribe(topic, qos); + bool ret = this->mqtt_backend_.subscribe(topic, qos); yield(); - if (ret != 0) { + if (ret) { ESP_LOGV(TAG, "subscribe(topic='%s')", topic); } else { delay(5); @@ -360,9 +365,9 @@ void MQTTClientComponent::subscribe_json(const std::string &topic, const mqtt_js } void MQTTClientComponent::unsubscribe(const std::string &topic) { - uint16_t ret = this->mqtt_client_.unsubscribe(topic.c_str()); + bool ret = this->mqtt_backend_.unsubscribe(topic.c_str()); yield(); - if (ret != 0) { + if (ret) { ESP_LOGV(TAG, "unsubscribe(topic='%s')", topic.c_str()); } else { delay(5); @@ -387,34 +392,35 @@ bool MQTTClientComponent::publish(const std::string &topic, const std::string &p bool MQTTClientComponent::publish(const std::string &topic, const char *payload, size_t payload_length, uint8_t qos, bool retain) { + return publish({.topic = topic, .payload = payload, .qos = qos, .retain = retain}); +} + +bool MQTTClientComponent::publish(const MQTTMessage &message) { if (!this->is_connected()) { // critical components will re-transmit their messages return false; } - bool logging_topic = topic == this->log_message_.topic; - uint16_t ret = this->mqtt_client_.publish(topic.c_str(), qos, retain, payload, payload_length); + bool logging_topic = this->log_message_.topic == message.topic; + bool ret = this->mqtt_backend_.publish(message); delay(0); - if (ret == 0 && !logging_topic && this->is_connected()) { + if (!ret && !logging_topic && this->is_connected()) { delay(0); - ret = this->mqtt_client_.publish(topic.c_str(), qos, retain, payload, payload_length); + ret = this->mqtt_backend_.publish(message); delay(0); } if (!logging_topic) { - if (ret != 0) { - ESP_LOGV(TAG, "Publish(topic='%s' payload='%s' retain=%d)", topic.c_str(), payload, retain); + if (ret) { + ESP_LOGV(TAG, "Publish(topic='%s' payload='%s' retain=%d)", message.topic.c_str(), message.payload.c_str(), + message.retain); } else { - ESP_LOGV(TAG, "Publish failed for topic='%s' (len=%u). will retry later..", topic.c_str(), - payload_length); // NOLINT + ESP_LOGV(TAG, "Publish failed for topic='%s' (len=%u). will retry later..", message.topic.c_str(), + message.payload.length()); this->status_momentary_warning("publish", 1000); } } return ret != 0; } - -bool MQTTClientComponent::publish(const MQTTMessage &message) { - return this->publish(message.topic, message.payload, message.qos, message.retain); -} bool MQTTClientComponent::publish_json(const std::string &topic, const json::json_build_t &f, uint8_t qos, bool retain) { std::string message = json::build_json(f); @@ -499,10 +505,10 @@ bool MQTTClientComponent::is_log_message_enabled() const { return !this->log_mes void MQTTClientComponent::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; } void MQTTClientComponent::register_mqtt_component(MQTTComponent *component) { this->children_.push_back(component); } void MQTTClientComponent::set_log_level(int level) { this->log_level_ = level; } -void MQTTClientComponent::set_keep_alive(uint16_t keep_alive_s) { this->mqtt_client_.setKeepAlive(keep_alive_s); } +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(std::string topic_prefix) { this->topic_prefix_ = std::move(topic_prefix); } +void MQTTClientComponent::set_topic_prefix(const std::string &topic_prefix) { this->topic_prefix_ = topic_prefix; } const std::string &MQTTClientComponent::get_topic_prefix() const { return this->topic_prefix_; } void MQTTClientComponent::disable_birth_message() { this->birth_message_.topic = ""; @@ -549,7 +555,8 @@ void MQTTClientComponent::set_discovery_info(std::string &&prefix, MQTTDiscovery void MQTTClientComponent::disable_last_will() { this->last_will_.topic = ""; } void MQTTClientComponent::disable_discovery() { - this->discovery_info_ = MQTTDiscoveryInfo{.prefix = "", .retain = false}; + this->discovery_info_ = MQTTDiscoveryInfo{ + .prefix = "", .retain = false, .clean = false, .unique_id_generator = MQTT_LEGACY_UNIQUE_ID_GENERATOR}; } void MQTTClientComponent::on_shutdown() { if (!this->shutdown_message_.topic.empty()) { @@ -557,13 +564,13 @@ void MQTTClientComponent::on_shutdown() { this->publish(this->shutdown_message_); yield(); } - this->mqtt_client_.disconnect(true); + this->mqtt_backend_.disconnect(); } #if ASYNC_TCP_SSL_ENABLED void MQTTClientComponent::add_ssl_fingerprint(const std::array &fingerprint) { - this->mqtt_client_.setSecure(true); - this->mqtt_client_.addServerFingerprint(fingerprint.data()); + this->mqtt_backend_.setSecure(true); + this->mqtt_backend_.addServerFingerprint(fingerprint.data()); } #endif diff --git a/esphome/components/mqtt/mqtt_client.h b/esphome/components/mqtt/mqtt_client.h index 58a4fbe166..4880bbaa5b 100644 --- a/esphome/components/mqtt/mqtt_client.h +++ b/esphome/components/mqtt/mqtt_client.h @@ -9,7 +9,11 @@ #include "esphome/core/log.h" #include "esphome/components/json/json_util.h" #include "esphome/components/network/ip_address.h" -#include +#if defined(USE_ESP_IDF) +#include "mqtt_backend_idf.h" +#elif defined(USE_ARDUINO) +#include "mqtt_backend_arduino.h" +#endif #include "lwip/ip_addr.h" namespace esphome { @@ -22,14 +26,6 @@ namespace mqtt { using mqtt_callback_t = std::function; using mqtt_json_callback_t = std::function; -/// internal struct for MQTT messages. -struct MQTTMessage { - std::string topic; - std::string payload; - uint8_t qos; ///< QoS. Only for last will testaments. - bool retain; -}; - /// internal struct for MQTT subscriptions. struct MQTTSubscription { std::string topic; @@ -139,7 +135,10 @@ class MQTTClientComponent : public Component { */ void add_ssl_fingerprint(const std::array &fingerprint); #endif - +#ifdef USE_ESP_IDF + void set_ca_certificate(const char *cert) { this->mqtt_backend_.set_ca_certificate(cert); } + void set_skip_cert_cn_check(bool skip_check) { this->mqtt_backend_.set_skip_cert_cn_check(skip_check); } +#endif const Availability &get_availability(); /** Set the topic prefix that will be prepended to all topics together with "/". This will, in most cases, @@ -150,7 +149,7 @@ class MQTTClientComponent : public Component { * * @param topic_prefix The topic prefix. The last "/" is appended automatically. */ - void set_topic_prefix(std::string topic_prefix); + void set_topic_prefix(const std::string &topic_prefix); /// Get the topic prefix of this device, using default if necessary const std::string &get_topic_prefix() const; @@ -277,6 +276,7 @@ class MQTTClientComponent : public Component { .prefix = "homeassistant", .retain = true, .clean = false, + .unique_id_generator = MQTT_LEGACY_UNIQUE_ID_GENERATOR, }; std::string topic_prefix_{}; MQTTMessage log_message_; @@ -284,7 +284,12 @@ class MQTTClientComponent : public Component { int log_level_{ESPHOME_LOG_LEVEL}; std::vector subscriptions_; - AsyncMqttClient mqtt_client_; +#if defined(USE_ESP_IDF) + MQTTBackendIDF mqtt_backend_; +#elif defined(USE_ARDUINO) + MQTTBackendArduino mqtt_backend_; +#endif + MQTTClientState state_{MQTT_CLIENT_DISCONNECTED}; network::IPAddress ip_; bool dns_resolved_{false}; @@ -293,7 +298,7 @@ class MQTTClientComponent : public Component { uint32_t reboot_timeout_{300000}; uint32_t connect_begin_; uint32_t last_connected_{0}; - optional disconnect_reason_{}; + optional disconnect_reason_{}; }; extern MQTTClientComponent *global_mqtt_client; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/core/defines.h b/esphome/core/defines.h index b5c82338b3..f304f847a5 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -29,6 +29,7 @@ #define USE_LOCK #define USE_LOGGER #define USE_MDNS +#define USE_MQTT #define USE_NUMBER #define USE_OTA_PASSWORD #define USE_OTA_STATE_CALLBACK @@ -49,13 +50,17 @@ #define USE_CAPTIVE_PORTAL #define USE_JSON #define USE_NEXTION_TFT_UPLOAD -#define USE_MQTT #define USE_PROMETHEUS #define USE_WEBSERVER #define USE_WEBSERVER_PORT 80 // NOLINT #define USE_WIFI_WPA2_EAP #endif +// IDF-specific feature flags +#ifdef USE_ESP_IDF +#define USE_MQTT_IDF_ENQUEUE +#endif + // ESP32-specific feature flags #ifdef USE_ESP32 #define USE_ESP32_BLE_CLIENT diff --git a/tests/test5.yaml b/tests/test5.yaml index 9bfd395538..ee90cc1149 100644 --- a/tests/test5.yaml +++ b/tests/test5.yaml @@ -49,6 +49,19 @@ modbus_controller: address: 0x2 modbus_id: mod_bus1 +mqtt: + broker: test.mosquitto.org + port: 1883 + discovery: true + discovery_prefix: homeassistant + idf_send_async: false + on_message: + topic: testing/sensor/testing_sensor/state + qos: 0 + then: + - lambda: |- + ESP_LOGD("Mqtt Test","testing/sensor/testing_sensor/state=[%s]",x.c_str()); + binary_sensor: - platform: gpio pin: GPIO0 From fd7e861ff5395d360f894de344ecc2df741cf522 Mon Sep 17 00:00:00 2001 From: "Andrew J.Swan" Date: Mon, 4 Apr 2022 02:13:59 +0300 Subject: [PATCH 0351/1729] Added a function to load custom characters in LCD display (#3279) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/lcd_base/lcd_display.cpp | 7 +++++++ esphome/components/lcd_base/lcd_display.h | 3 +++ 2 files changed, 10 insertions(+) diff --git a/esphome/components/lcd_base/lcd_display.cpp b/esphome/components/lcd_base/lcd_display.cpp index b937e36c6c..180d5e93ac 100644 --- a/esphome/components/lcd_base/lcd_display.cpp +++ b/esphome/components/lcd_base/lcd_display.cpp @@ -167,6 +167,13 @@ void LCDDisplay::strftime(uint8_t column, uint8_t row, const char *format, time: } void LCDDisplay::strftime(const char *format, time::ESPTime time) { this->strftime(0, 0, format, time); } #endif +void LCDDisplay::loadchar(uint8_t location, uint8_t charmap[]) { + location &= 0x7; // we only have 8 locations 0-7 + this->command_(LCD_DISPLAY_COMMAND_SET_CGRAM_ADDR | (location << 3)); + for (int i = 0; i < 8; i++) { + this->send(charmap[i], true); + } +} } // namespace lcd_base } // namespace esphome diff --git a/esphome/components/lcd_base/lcd_display.h b/esphome/components/lcd_base/lcd_display.h index 0c9e59758c..c8ba39f0d4 100644 --- a/esphome/components/lcd_base/lcd_display.h +++ b/esphome/components/lcd_base/lcd_display.h @@ -51,6 +51,9 @@ class LCDDisplay : public PollingComponent { void strftime(const char *format, time::ESPTime time) __attribute__((format(strftime, 2, 0))); #endif + /// Load custom char to given location + void loadchar(uint8_t location, uint8_t charmap[]); + protected: virtual bool is_four_bit_mode() = 0; virtual void write_n_bits(uint8_t value, uint8_t n) = 0; From 2e436eae6bd89289588f5ddab2127d1aa20fb8c7 Mon Sep 17 00:00:00 2001 From: Felix Storm Date: Mon, 4 Apr 2022 01:15:51 +0200 Subject: [PATCH 0352/1729] CAN bus: support remote transmission request flag for canbus.send (#3193) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/canbus/__init__.py | 7 +++++++ esphome/components/canbus/canbus.cpp | 8 +++++--- esphome/components/canbus/canbus.h | 16 +++++++++++++--- tests/test1.yaml | 10 ++++++++++ 4 files changed, 35 insertions(+), 6 deletions(-) diff --git a/esphome/components/canbus/__init__.py b/esphome/components/canbus/__init__.py index 5f614eb0a4..20f2642144 100644 --- a/esphome/components/canbus/__init__.py +++ b/esphome/components/canbus/__init__.py @@ -10,6 +10,7 @@ IS_PLATFORM_COMPONENT = True CONF_CAN_ID = "can_id" CONF_CAN_ID_MASK = "can_id_mask" CONF_USE_EXTENDED_ID = "use_extended_id" +CONF_REMOTE_TRANSMISSION_REQUEST = "remote_transmission_request" CONF_CANBUS_ID = "canbus_id" CONF_BIT_RATE = "bit_rate" CONF_ON_FRAME = "on_frame" @@ -122,6 +123,7 @@ async def register_canbus(var, config): cv.GenerateID(CONF_CANBUS_ID): cv.use_id(CanbusComponent), cv.Optional(CONF_CAN_ID): cv.int_range(min=0, max=0x1FFFFFFF), cv.Optional(CONF_USE_EXTENDED_ID, default=False): cv.boolean, + cv.Optional(CONF_REMOTE_TRANSMISSION_REQUEST, default=False): cv.boolean, cv.Required(CONF_DATA): cv.templatable(validate_raw_data), }, validate_id, @@ -140,6 +142,11 @@ async def canbus_action_to_code(config, action_id, template_arg, args): ) cg.add(var.set_use_extended_id(use_extended_id)) + remote_transmission_request = await cg.templatable( + config[CONF_REMOTE_TRANSMISSION_REQUEST], args, bool + ) + cg.add(var.set_remote_transmission_request(remote_transmission_request)) + data = config[CONF_DATA] if isinstance(data, bytes): data = [int(x) for x in data] diff --git a/esphome/components/canbus/canbus.cpp b/esphome/components/canbus/canbus.cpp index 14dc1544cf..5d9084706b 100644 --- a/esphome/components/canbus/canbus.cpp +++ b/esphome/components/canbus/canbus.cpp @@ -22,20 +22,22 @@ void Canbus::dump_config() { } } -void Canbus::send_data(uint32_t can_id, bool use_extended_id, const std::vector &data) { +void Canbus::send_data(uint32_t can_id, bool use_extended_id, bool remote_transmission_request, + const std::vector &data) { struct CanFrame can_message; uint8_t size = static_cast(data.size()); if (use_extended_id) { - ESP_LOGD(TAG, "send extended id=0x%08x size=%d", can_id, size); + ESP_LOGD(TAG, "send extended id=0x%08x rtr=%s size=%d", can_id, TRUEFALSE(remote_transmission_request), size); } else { - ESP_LOGD(TAG, "send extended id=0x%03x size=%d", can_id, size); + ESP_LOGD(TAG, "send extended id=0x%03x rtr=%s size=%d", can_id, TRUEFALSE(remote_transmission_request), size); } if (size > CAN_MAX_DATA_LENGTH) size = CAN_MAX_DATA_LENGTH; can_message.can_data_length_code = size; can_message.can_id = can_id; can_message.use_extended_id = use_extended_id; + can_message.remote_transmission_request = remote_transmission_request; for (int i = 0; i < size; i++) { can_message.data[i] = data[i]; diff --git a/esphome/components/canbus/canbus.h b/esphome/components/canbus/canbus.h index 0491e8d3c1..20c490c083 100644 --- a/esphome/components/canbus/canbus.h +++ b/esphome/components/canbus/canbus.h @@ -62,7 +62,12 @@ class Canbus : public Component { float get_setup_priority() const override { return setup_priority::HARDWARE; } void loop() override; - void send_data(uint32_t can_id, bool use_extended_id, const std::vector &data); + void send_data(uint32_t can_id, bool use_extended_id, bool remote_transmission_request, + const std::vector &data); + void send_data(uint32_t can_id, bool use_extended_id, const std::vector &data) { + // for backwards compatibility only + this->send_data(can_id, use_extended_id, false, data); + } void set_can_id(uint32_t can_id) { this->can_id_ = can_id; } void set_use_extended_id(bool use_extended_id) { this->use_extended_id_ = use_extended_id; } void set_bitrate(CanSpeed bit_rate) { this->bit_rate_ = bit_rate; } @@ -96,21 +101,26 @@ template class CanbusSendAction : public Action, public P void set_use_extended_id(bool use_extended_id) { this->use_extended_id_ = use_extended_id; } + void set_remote_transmission_request(bool remote_transmission_request) { + this->remote_transmission_request_ = remote_transmission_request; + } + void play(Ts... x) override { auto can_id = this->can_id_.has_value() ? *this->can_id_ : this->parent_->can_id_; auto use_extended_id = this->use_extended_id_.has_value() ? *this->use_extended_id_ : this->parent_->use_extended_id_; if (this->static_) { - this->parent_->send_data(can_id, use_extended_id, this->data_static_); + this->parent_->send_data(can_id, use_extended_id, this->remote_transmission_request_, this->data_static_); } else { auto val = this->data_func_(x...); - this->parent_->send_data(can_id, use_extended_id, val); + this->parent_->send_data(can_id, use_extended_id, this->remote_transmission_request_, val); } } protected: optional can_id_{}; optional use_extended_id_{}; + bool remote_transmission_request_{false}; bool static_{false}; std::function(Ts...)> data_func_{}; std::vector data_static_{}; diff --git a/tests/test1.yaml b/tests/test1.yaml index 181f62d3f4..77c4a76bda 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -2616,6 +2616,16 @@ text_sensor: canbus_id: esp32_internal_can can_id: 23 data: [0x10, 0x20, 0x30] + - canbus.send: + canbus_id: mcp2515_can + can_id: 24 + remote_transmission_request: true + data: [] + - canbus.send: + canbus_id: esp32_internal_can + can_id: 24 + remote_transmission_request: true + data: [] - platform: template name: Template Text Sensor id: ${textname}_text From 70fafa473bed82bacac20e25649ba6087f817ca5 Mon Sep 17 00:00:00 2001 From: Michiel van Turnhout Date: Mon, 4 Apr 2022 01:42:10 +0200 Subject: [PATCH 0353/1729] Tm1637 binarysensor (#2792) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/tm1637/binary_sensor.py | 26 ++++++ esphome/components/tm1637/tm1637.cpp | 92 ++++++++++++++++++---- esphome/components/tm1637/tm1637.h | 31 +++++++- 3 files changed, 132 insertions(+), 17 deletions(-) create mode 100644 esphome/components/tm1637/binary_sensor.py diff --git a/esphome/components/tm1637/binary_sensor.py b/esphome/components/tm1637/binary_sensor.py new file mode 100644 index 0000000000..66b5172358 --- /dev/null +++ b/esphome/components/tm1637/binary_sensor.py @@ -0,0 +1,26 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import binary_sensor +from esphome.const import CONF_ID, CONF_KEY + +CONF_TM1637_ID = "tm1637_id" + +tm1637_ns = cg.esphome_ns.namespace("tm1637") +TM1637Display = tm1637_ns.class_("TM1637Display", cg.PollingComponent) +TM1637Key = tm1637_ns.class_("TM1637Key", binary_sensor.BinarySensor) + +CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(TM1637Key), + cv.GenerateID(CONF_TM1637_ID): cv.use_id(TM1637Display), + cv.Required(CONF_KEY): cv.int_range(min=0, max=15), + } +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await binary_sensor.register_binary_sensor(var, config) + cg.add(var.set_keycode(config[CONF_KEY])) + hub = await cg.get_variable(config[CONF_TM1637_ID]) + cg.add(hub.add_tm1637_key(var)) diff --git a/esphome/components/tm1637/tm1637.cpp b/esphome/components/tm1637/tm1637.cpp index 44f0a841b8..be2192ea22 100644 --- a/esphome/components/tm1637/tm1637.cpp +++ b/esphome/components/tm1637/tm1637.cpp @@ -7,11 +7,17 @@ namespace esphome { namespace tm1637 { static const char *const TAG = "display.tm1637"; -const uint8_t TM1637_I2C_COMM1 = 0x40; -const uint8_t TM1637_I2C_COMM2 = 0xC0; -const uint8_t TM1637_I2C_COMM3 = 0x80; +const uint8_t TM1637_CMD_DATA = 0x40; //!< Display data command +const uint8_t TM1637_CMD_CTRL = 0x80; //!< Display control command +const uint8_t TM1637_CMD_ADDR = 0xc0; //!< Display address command const uint8_t TM1637_UNKNOWN_CHAR = 0b11111111; +// Data command bits +const uint8_t TM1637_DATA_WRITE = 0x00; //!< Write data +const uint8_t TM1637_DATA_READ_KEYS = 0x02; //!< Read keys +const uint8_t TM1637_DATA_AUTO_INC_ADDR = 0x00; //!< Auto increment address +const uint8_t TM1637_DATA_FIXED_ADDR = 0x04; //!< Fixed address + // // A // --- @@ -138,6 +144,36 @@ void TM1637Display::dump_config() { LOG_UPDATE_INTERVAL(this); } +#ifdef USE_BINARY_SENSOR +void TM1637Display::loop() { + uint8_t val = this->get_keys(); + for (auto *tm1637_key : this->tm1637_keys_) + tm1637_key->process(val); +} + +uint8_t TM1637Display::get_keys() { + this->start_(); + this->send_byte_(TM1637_CMD_DATA | TM1637_DATA_READ_KEYS); + this->start_(); + uint8_t key_code = read_byte_(); + this->stop_(); + if (key_code != 0xFF) { + // Invert key_code: + // Bit | 7 6 5 4 3 2 1 0 + // ------+------------------------- + // From | S0 S1 S2 K1 K2 1 1 1 + // To | S0 S1 S2 K1 K2 0 0 0 + key_code = ~key_code; + // Shift bits to: + // Bit | 7 6 5 4 3 2 1 0 + // ------+------------------------ + // To | 0 0 0 0 K2 S2 S1 S0 + key_code = (uint8_t)((key_code & 0x80) >> 7 | (key_code & 0x40) >> 5 | (key_code & 0x20) >> 3 | (key_code & 0x08)); + } + return key_code; +} +#endif + void TM1637Display::update() { for (uint8_t &i : this->buffer_) i = 0; @@ -165,14 +201,14 @@ void TM1637Display::stop_() { void TM1637Display::display() { ESP_LOGVV(TAG, "Display %02X%02X%02X%02X", buffer_[0], buffer_[1], buffer_[2], buffer_[3]); - // Write COMM1 + // Write DATA CMND this->start_(); - this->send_byte_(TM1637_I2C_COMM1); + this->send_byte_(TM1637_CMD_DATA); this->stop_(); - // Write COMM2 + first digit address + // Write ADDR CMD + first digit address this->start_(); - this->send_byte_(TM1637_I2C_COMM2); + this->send_byte_(TM1637_CMD_ADDR); // Write the data bytes if (this->inverted_) { @@ -187,20 +223,17 @@ void TM1637Display::display() { this->stop_(); - // Write COMM3 + brightness + // Write display CTRL CMND + brightness this->start_(); - this->send_byte_(TM1637_I2C_COMM3 + ((this->intensity_ & 0x7) | 0x08)); + this->send_byte_(TM1637_CMD_CTRL + ((this->intensity_ & 0x7) | 0x08)); this->stop_(); } bool TM1637Display::send_byte_(uint8_t b) { uint8_t data = b; - - // 8 Data Bits for (uint8_t i = 0; i < 8; i++) { // CLK low this->clk_pin_->pin_mode(gpio::FLAG_OUTPUT); this->bit_delay_(); - // Set data bit if (data & 0x01) { this->dio_pin_->pin_mode(gpio::FLAG_INPUT); @@ -209,19 +242,16 @@ bool TM1637Display::send_byte_(uint8_t b) { } this->bit_delay_(); - // CLK high this->clk_pin_->pin_mode(gpio::FLAG_INPUT); this->bit_delay_(); data = data >> 1; } - // Wait for acknowledge // CLK to zero this->clk_pin_->pin_mode(gpio::FLAG_OUTPUT); this->dio_pin_->pin_mode(gpio::FLAG_INPUT); this->bit_delay_(); - // CLK to high this->clk_pin_->pin_mode(gpio::FLAG_INPUT); this->bit_delay_(); @@ -237,8 +267,38 @@ bool TM1637Display::send_byte_(uint8_t b) { return ack; } +uint8_t TM1637Display::read_byte_() { + uint8_t retval = 0; + // Prepare DIO to read data + this->dio_pin_->pin_mode(gpio::FLAG_INPUT); + this->bit_delay_(); + // Data is shifted out by the TM1637 on the CLK falling edge + for (uint8_t bit = 0; bit < 8; bit++) { + this->clk_pin_->pin_mode(gpio::FLAG_INPUT); + this->bit_delay_(); + // Read next bit + retval <<= 1; + if (this->dio_pin_->digital_read()) { + retval |= 0x01; + } + this->clk_pin_->pin_mode(gpio::FLAG_OUTPUT); + this->bit_delay_(); + } + // Return DIO to output mode + // Dummy ACK + this->dio_pin_->pin_mode(gpio::FLAG_OUTPUT); + this->bit_delay_(); + this->clk_pin_->pin_mode(gpio::FLAG_INPUT); + this->bit_delay_(); + this->clk_pin_->pin_mode(gpio::FLAG_OUTPUT); + this->bit_delay_(); + this->dio_pin_->pin_mode(gpio::FLAG_INPUT); + this->bit_delay_(); + return retval; +} + uint8_t TM1637Display::print(uint8_t start_pos, const char *str) { - ESP_LOGV(TAG, "Print at %d: %s", start_pos, str); + // ESP_LOGV(TAG, "Print at %d: %s", start_pos, str); uint8_t pos = start_pos; for (; *str != '\0'; str++) { uint8_t data = TM1637_UNKNOWN_CHAR; diff --git a/esphome/components/tm1637/tm1637.h b/esphome/components/tm1637/tm1637.h index 9b2f014ff9..0a77acaabe 100644 --- a/esphome/components/tm1637/tm1637.h +++ b/esphome/components/tm1637/tm1637.h @@ -8,10 +8,17 @@ #include "esphome/components/time/real_time_clock.h" #endif +#ifdef USE_BINARY_SENSOR +#include "esphome/components/binary_sensor/binary_sensor.h" +#endif + namespace esphome { namespace tm1637 { class TM1637Display; +#ifdef USE_BINARY_SENSOR +class TM1637Key; +#endif using tm1637_writer_t = std::function; @@ -46,10 +53,15 @@ class TM1637Display : public PollingComponent { void display(); +#ifdef USE_BINARY_SENSOR + void loop() override; + uint8_t get_keys(); + void add_tm1637_key(TM1637Key *tm1637_key) { this->tm1637_keys_.push_back(tm1637_key); } +#endif + #ifdef USE_TIME /// Evaluate the strftime-format and print the result at the given position. uint8_t strftime(uint8_t pos, const char *format, time::ESPTime time) __attribute__((format(strftime, 3, 0))); - /// Evaluate the strftime-format and print the result at position 0. uint8_t strftime(const char *format, time::ESPTime time) __attribute__((format(strftime, 2, 0))); #endif @@ -58,6 +70,7 @@ class TM1637Display : public PollingComponent { void bit_delay_(); void setup_pins_(); bool send_byte_(uint8_t b); + uint8_t read_byte_(); void start_(); void stop_(); @@ -68,7 +81,23 @@ class TM1637Display : public PollingComponent { bool inverted_; optional writer_{}; uint8_t buffer_[6] = {0}; +#ifdef USE_BINARY_SENSOR + std::vector tm1637_keys_{}; +#endif }; +#ifdef USE_BINARY_SENSOR +class TM1637Key : public binary_sensor::BinarySensor { + friend class TM1637Display; + + public: + void set_keycode(uint8_t key_code) { key_code_ = key_code; } + void process(uint8_t data) { this->publish_state(static_cast(data == this->key_code_)); } + + protected: + uint8_t key_code_{0}; +}; +#endif + } // namespace tm1637 } // namespace esphome From c54c20ab3cbcd4963db2f969d3c9357cebea4f2e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Apr 2022 19:10:53 +0200 Subject: [PATCH 0354/1729] Bump click from 8.0.4 to 8.1.2 (#3351) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e934eb2c6f..8b67068505 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ tzdata>=2021.1 # from time pyserial==3.5 platformio==5.2.5 # When updating platformio, also update Dockerfile esptool==3.3 -click==8.0.4 +click==8.1.2 esphome-dashboard==20220309.0 aioesphomeapi==10.8.2 zeroconf==0.38.4 From de96376565a50f318f396c0765c2abca6f8f903a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Apr 2022 19:11:04 +0200 Subject: [PATCH 0355/1729] Bump pylint from 2.12.2 to 2.13.4 (#3348) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 8da8945c8b..a9dafcf063 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,4 +1,4 @@ -pylint==2.12.2 +pylint==2.13.4 flake8==4.0.1 black==22.1.0 pyupgrade==2.31.0 From a39d87460075e520fafe1bf2bbbf2689e0919c07 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Apr 2022 19:13:06 +0200 Subject: [PATCH 0356/1729] Bump pytest-asyncio from 0.18.2 to 0.18.3 (#3335) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index a9dafcf063..f7bfac4c16 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -8,6 +8,6 @@ pre-commit pytest==7.0.1 pytest-cov==3.0.0 pytest-mock==3.7.0 -pytest-asyncio==0.18.2 +pytest-asyncio==0.18.3 asyncmock==0.4.2 hypothesis==5.49.0 From 061e1a471dbf69fac34766cd08afa64861480713 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Apr 2022 19:13:11 +0200 Subject: [PATCH 0357/1729] Bump pytest from 7.0.1 to 7.1.1 (#3313) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index f7bfac4c16..066b1ae32f 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -5,7 +5,7 @@ pyupgrade==2.31.0 pre-commit # Unit tests -pytest==7.0.1 +pytest==7.1.1 pytest-cov==3.0.0 pytest-mock==3.7.0 pytest-asyncio==0.18.3 From 0b1161f7ef24eeb5e791a6a4b12ec9af2b5baab8 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Mon, 4 Apr 2022 19:21:43 +0200 Subject: [PATCH 0358/1729] Bump docker dependencies (#3354) --- docker/Dockerfile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index ad4891d62e..610b689298 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -10,9 +10,9 @@ FROM ghcr.io/hassio-addons/debian-base/amd64:5.3.0 AS base-hassio-amd64 FROM ghcr.io/hassio-addons/debian-base/aarch64:5.3.0 AS base-hassio-arm64 FROM ghcr.io/hassio-addons/debian-base/armv7:5.3.0 AS base-hassio-armv7 # https://hub.docker.com/_/debian?tab=tags&page=1&name=bullseye -FROM debian:bullseye-20220228-slim AS base-docker-amd64 -FROM debian:bullseye-20220228-slim AS base-docker-arm64 -FROM debian:bullseye-20220228-slim AS base-docker-armv7 +FROM debian:bullseye-20220328-slim AS base-docker-amd64 +FROM debian:bullseye-20220328-slim AS base-docker-arm64 +FROM debian:bullseye-20220328-slim AS base-docker-armv7 # Use TARGETARCH/TARGETVARIANT defined by docker # https://docs.docker.com/engine/reference/builder/#automatic-platform-args-in-the-global-scope @@ -23,7 +23,7 @@ RUN \ # Use pinned versions so that we get updates with build caching && apt-get install -y --no-install-recommends \ python3=3.9.2-3 \ - python3-pip=20.3.4-4 \ + python3-pip=20.3.4-4+deb11u1 \ python3-setuptools=52.0.0-4 \ python3-pil=8.1.2+dfsg-0.3+deb11u1 \ python3-cryptography=3.3.2-1 \ From d97c3a7e017474a74db90637052a592135688a8b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Apr 2022 19:37:35 +0200 Subject: [PATCH 0359/1729] Bump voluptuous from 0.12.2 to 0.13.0 (#3355) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 8b67068505..410cdb6b1d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -voluptuous==0.12.2 +voluptuous==0.13.0 PyYAML==6.0 paho-mqtt==1.6.1 colorama==0.4.4 From d48ffa2913edf6727f84229ec65ec49f9f562391 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Apr 2022 19:39:45 +0200 Subject: [PATCH 0360/1729] Bump tzlocal from 4.1 to 4.2 (#3356) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 410cdb6b1d..4386099e2b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ PyYAML==6.0 paho-mqtt==1.6.1 colorama==0.4.4 tornado==6.1 -tzlocal==4.1 # from time +tzlocal==4.2 # from time tzdata>=2021.1 # from time pyserial==3.5 platformio==5.2.5 # When updating platformio, also update Dockerfile From bff06e448b5aed849e1054d63e980f24c24d8218 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Apr 2022 20:06:42 +0200 Subject: [PATCH 0361/1729] Bump pyupgrade from 2.31.0 to 2.31.1 (#3292) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Otto Winter --- .pre-commit-config.yaml | 2 +- requirements_test.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9549d5cedc..e94d8aa236 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -26,7 +26,7 @@ repos: - --branch=release - --branch=beta - repo: https://github.com/asottile/pyupgrade - rev: v2.31.0 + rev: v2.31.1 hooks: - id: pyupgrade args: [--py38-plus] diff --git a/requirements_test.txt b/requirements_test.txt index 066b1ae32f..28a8e1db72 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,7 +1,7 @@ pylint==2.13.4 flake8==4.0.1 black==22.1.0 -pyupgrade==2.31.0 +pyupgrade==2.31.1 pre-commit # Unit tests From 06f4ad922c7fe3cf7c0bb99f17c8d1e621d6a4c3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 Apr 2022 11:50:51 +0200 Subject: [PATCH 0362/1729] Bump black from 22.1.0 to 22.3.0 (#3357) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Otto Winter --- .pre-commit-config.yaml | 2 +- requirements_test.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e94d8aa236..1e666f20af 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/ambv/black - rev: 22.1.0 + rev: 22.3.0 hooks: - id: black args: diff --git a/requirements_test.txt b/requirements_test.txt index 28a8e1db72..bc5316ca71 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==2.13.4 flake8==4.0.1 -black==22.1.0 +black==22.3.0 pyupgrade==2.31.1 pre-commit From ba8d255cb4f09e35e94253c8bb3e3992d21448b4 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 5 Apr 2022 22:06:36 +1200 Subject: [PATCH 0363/1729] Allow on_value_range for sensor and number to be templated (#3359) --- esphome/components/number/__init__.py | 4 ++-- esphome/components/sensor/__init__.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/number/__init__.py b/esphome/components/number/__init__.py index 71e288a4cc..89788f1e98 100644 --- a/esphome/components/number/__init__.py +++ b/esphome/components/number/__init__.py @@ -63,8 +63,8 @@ NUMBER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).e cv.Optional(CONF_ON_VALUE_RANGE): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ValueRangeTrigger), - cv.Optional(CONF_ABOVE): cv.float_, - cv.Optional(CONF_BELOW): cv.float_, + cv.Optional(CONF_ABOVE): cv.templatable(cv.float_), + cv.Optional(CONF_BELOW): cv.templatable(cv.float_), }, cv.has_at_least_one_key(CONF_ABOVE, CONF_BELOW), ), diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 65ae7b2168..0c38ceeb37 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -212,8 +212,8 @@ SENSOR_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).extend( cv.Optional(CONF_ON_VALUE_RANGE): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ValueRangeTrigger), - cv.Optional(CONF_ABOVE): cv.float_, - cv.Optional(CONF_BELOW): cv.float_, + cv.Optional(CONF_ABOVE): cv.templatable(cv.float_), + cv.Optional(CONF_BELOW): cv.templatable(cv.float_), }, cv.has_at_least_one_key(CONF_ABOVE, CONF_BELOW), ), From d4ff98680abffbb6d1f52db61c1c18e215bde113 Mon Sep 17 00:00:00 2001 From: Tim Smeets Date: Thu, 7 Apr 2022 22:04:00 +0200 Subject: [PATCH 0364/1729] Add support for Electrolux heatpump and bump arduino-heatpumpir version (#3353) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/heatpumpir/climate.py | 5 ++--- esphome/components/heatpumpir/heatpumpir.cpp | 1 + esphome/components/heatpumpir/heatpumpir.h | 1 + platformio.ini | 4 +--- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/esphome/components/heatpumpir/climate.py b/esphome/components/heatpumpir/climate.py index 744ef5e527..a253a778de 100644 --- a/esphome/components/heatpumpir/climate.py +++ b/esphome/components/heatpumpir/climate.py @@ -25,6 +25,7 @@ PROTOCOLS = { "daikin_arc417": Protocol.PROTOCOL_DAIKIN_ARC417, "daikin_arc480": Protocol.PROTOCOL_DAIKIN_ARC480, "daikin": Protocol.PROTOCOL_DAIKIN, + "electroluxyal": Protocol.PROTOCOL_ELECTROLUXYAL, "fuego": Protocol.PROTOCOL_FUEGO, "fujitsu_awyz": Protocol.PROTOCOL_FUJITSU_AWYZ, "gree": Protocol.PROTOCOL_GREE, @@ -112,6 +113,4 @@ def to_code(config): cg.add(var.set_max_temperature(config[CONF_MAX_TEMPERATURE])) cg.add(var.set_min_temperature(config[CONF_MIN_TEMPERATURE])) - # PIO isn't updating releases, so referencing the release tag directly. See: - # https://github.com/ToniA/arduino-heatpumpir/commit/0948c619d86407a4e50e8db2f3c193e0576c86fd - cg.add_library("", "", "https://github.com/ToniA/arduino-heatpumpir.git#1.0.18") + cg.add_library("tonia/HeatpumpIR", "1.0.20") diff --git a/esphome/components/heatpumpir/heatpumpir.cpp b/esphome/components/heatpumpir/heatpumpir.cpp index ad3731b955..cd24411763 100644 --- a/esphome/components/heatpumpir/heatpumpir.cpp +++ b/esphome/components/heatpumpir/heatpumpir.cpp @@ -20,6 +20,7 @@ const std::map> PROTOCOL_CONSTRUCTOR_MAP {PROTOCOL_DAIKIN_ARC417, []() { return new DaikinHeatpumpARC417IR(); }}, // NOLINT {PROTOCOL_DAIKIN_ARC480, []() { return new DaikinHeatpumpARC480A14IR(); }}, // NOLINT {PROTOCOL_DAIKIN, []() { return new DaikinHeatpumpIR(); }}, // NOLINT + {PROTOCOL_ELECTROLUXYAL, []() { return new ElectroluxYALHeatpumpIR(); }}, // NOLINT {PROTOCOL_FUEGO, []() { return new FuegoHeatpumpIR(); }}, // NOLINT {PROTOCOL_FUJITSU_AWYZ, []() { return new FujitsuHeatpumpIR(); }}, // NOLINT {PROTOCOL_GREE, []() { return new GreeGenericHeatpumpIR(); }}, // NOLINT diff --git a/esphome/components/heatpumpir/heatpumpir.h b/esphome/components/heatpumpir/heatpumpir.h index 18d9b5040f..decf1eae07 100644 --- a/esphome/components/heatpumpir/heatpumpir.h +++ b/esphome/components/heatpumpir/heatpumpir.h @@ -20,6 +20,7 @@ enum Protocol { PROTOCOL_DAIKIN_ARC417, PROTOCOL_DAIKIN_ARC480, PROTOCOL_DAIKIN, + PROTOCOL_ELECTROLUXYAL, PROTOCOL_FUEGO, PROTOCOL_FUJITSU_AWYZ, PROTOCOL_GREE, diff --git a/platformio.ini b/platformio.ini index 8775b28156..bc2cddb9f7 100644 --- a/platformio.ini +++ b/platformio.ini @@ -62,9 +62,7 @@ lib_deps = glmnet/Dsmr@0.5 ; dsmr rweather/Crypto@0.2.0 ; dsmr dudanov/MideaUART@1.1.8 ; midea - ; PIO isn't update releases correctly, see: - ; https://github.com/ToniA/arduino-heatpumpir/commit/0948c619d86407a4e50e8db2f3c193e0576c86fd - https://github.com/ToniA/arduino-heatpumpir.git#1.0.18 ; heatpumpir + tonia/HeatpumpIR@1.0.20 ; heatpumpir build_flags = ${common.build_flags} -DUSE_ARDUINO From 5e79a1f500b91a5706044b5645af6acde5a607dc Mon Sep 17 00:00:00 2001 From: djwlindenaar <32413299+djwlindenaar@users.noreply.github.com> Date: Sun, 10 Apr 2022 22:06:11 +0200 Subject: [PATCH 0365/1729] Implement newer RTU protocol of Growatt inverters (#3315) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Co-authored-by: Daniel Lindenaar --- .../growatt_solar/growatt_solar.cpp | 89 ++++++++++++++----- .../components/growatt_solar/growatt_solar.h | 8 ++ esphome/components/growatt_solar/sensor.py | 14 ++- 3 files changed, 86 insertions(+), 25 deletions(-) diff --git a/esphome/components/growatt_solar/growatt_solar.cpp b/esphome/components/growatt_solar/growatt_solar.cpp index ed7240ab6c..ed753c4d3f 100644 --- a/esphome/components/growatt_solar/growatt_solar.cpp +++ b/esphome/components/growatt_solar/growatt_solar.cpp @@ -7,9 +7,11 @@ namespace growatt_solar { static const char *const TAG = "growatt_solar"; static const uint8_t MODBUS_CMD_READ_IN_REGISTERS = 0x04; -static const uint8_t MODBUS_REGISTER_COUNT = 33; +static const uint8_t MODBUS_REGISTER_COUNT[] = {33, 95}; // indexed with enum GrowattProtocolVersion -void GrowattSolar::update() { this->send(MODBUS_CMD_READ_IN_REGISTERS, 0, MODBUS_REGISTER_COUNT); } +void GrowattSolar::update() { + this->send(MODBUS_CMD_READ_IN_REGISTERS, 0, MODBUS_REGISTER_COUNT[this->protocol_version_]); +} void GrowattSolar::on_modbus_data(const std::vector &data) { auto publish_1_reg_sensor_state = [&](sensor::Sensor *sensor, size_t i, float unit) -> void { @@ -27,37 +29,76 @@ void GrowattSolar::on_modbus_data(const std::vector &data) { sensor->publish_state(value); }; - publish_1_reg_sensor_state(this->inverter_status_, 0, 1); + switch (this->protocol_version_) { + case RTU: { + publish_1_reg_sensor_state(this->inverter_status_, 0, 1); - publish_2_reg_sensor_state(this->pv_active_power_sensor_, 1, 2, ONE_DEC_UNIT); + publish_2_reg_sensor_state(this->pv_active_power_sensor_, 1, 2, ONE_DEC_UNIT); - publish_1_reg_sensor_state(this->pvs_[0].voltage_sensor_, 3, ONE_DEC_UNIT); - publish_1_reg_sensor_state(this->pvs_[0].current_sensor_, 4, ONE_DEC_UNIT); - publish_2_reg_sensor_state(this->pvs_[0].active_power_sensor_, 5, 6, ONE_DEC_UNIT); + publish_1_reg_sensor_state(this->pvs_[0].voltage_sensor_, 3, ONE_DEC_UNIT); + publish_1_reg_sensor_state(this->pvs_[0].current_sensor_, 4, ONE_DEC_UNIT); + publish_2_reg_sensor_state(this->pvs_[0].active_power_sensor_, 5, 6, ONE_DEC_UNIT); - publish_1_reg_sensor_state(this->pvs_[1].voltage_sensor_, 7, ONE_DEC_UNIT); - publish_1_reg_sensor_state(this->pvs_[1].current_sensor_, 8, ONE_DEC_UNIT); - publish_2_reg_sensor_state(this->pvs_[1].active_power_sensor_, 9, 10, ONE_DEC_UNIT); + publish_1_reg_sensor_state(this->pvs_[1].voltage_sensor_, 7, ONE_DEC_UNIT); + publish_1_reg_sensor_state(this->pvs_[1].current_sensor_, 8, ONE_DEC_UNIT); + publish_2_reg_sensor_state(this->pvs_[1].active_power_sensor_, 9, 10, ONE_DEC_UNIT); - publish_2_reg_sensor_state(this->grid_active_power_sensor_, 11, 12, ONE_DEC_UNIT); - publish_1_reg_sensor_state(this->grid_frequency_sensor_, 13, TWO_DEC_UNIT); + publish_2_reg_sensor_state(this->grid_active_power_sensor_, 11, 12, ONE_DEC_UNIT); + publish_1_reg_sensor_state(this->grid_frequency_sensor_, 13, TWO_DEC_UNIT); - publish_1_reg_sensor_state(this->phases_[0].voltage_sensor_, 14, ONE_DEC_UNIT); - publish_1_reg_sensor_state(this->phases_[0].current_sensor_, 15, ONE_DEC_UNIT); - publish_2_reg_sensor_state(this->phases_[0].active_power_sensor_, 16, 17, ONE_DEC_UNIT); + publish_1_reg_sensor_state(this->phases_[0].voltage_sensor_, 14, ONE_DEC_UNIT); + publish_1_reg_sensor_state(this->phases_[0].current_sensor_, 15, ONE_DEC_UNIT); + publish_2_reg_sensor_state(this->phases_[0].active_power_sensor_, 16, 17, ONE_DEC_UNIT); - publish_1_reg_sensor_state(this->phases_[1].voltage_sensor_, 18, ONE_DEC_UNIT); - publish_1_reg_sensor_state(this->phases_[1].current_sensor_, 19, ONE_DEC_UNIT); - publish_2_reg_sensor_state(this->phases_[1].active_power_sensor_, 20, 21, ONE_DEC_UNIT); + publish_1_reg_sensor_state(this->phases_[1].voltage_sensor_, 18, ONE_DEC_UNIT); + publish_1_reg_sensor_state(this->phases_[1].current_sensor_, 19, ONE_DEC_UNIT); + publish_2_reg_sensor_state(this->phases_[1].active_power_sensor_, 20, 21, ONE_DEC_UNIT); - publish_1_reg_sensor_state(this->phases_[2].voltage_sensor_, 22, ONE_DEC_UNIT); - publish_1_reg_sensor_state(this->phases_[2].current_sensor_, 23, ONE_DEC_UNIT); - publish_2_reg_sensor_state(this->phases_[2].active_power_sensor_, 24, 25, ONE_DEC_UNIT); + publish_1_reg_sensor_state(this->phases_[2].voltage_sensor_, 22, ONE_DEC_UNIT); + publish_1_reg_sensor_state(this->phases_[2].current_sensor_, 23, ONE_DEC_UNIT); + publish_2_reg_sensor_state(this->phases_[2].active_power_sensor_, 24, 25, ONE_DEC_UNIT); - publish_2_reg_sensor_state(this->today_production_, 26, 27, ONE_DEC_UNIT); - publish_2_reg_sensor_state(this->total_energy_production_, 28, 29, ONE_DEC_UNIT); + publish_2_reg_sensor_state(this->today_production_, 26, 27, ONE_DEC_UNIT); + publish_2_reg_sensor_state(this->total_energy_production_, 28, 29, ONE_DEC_UNIT); - publish_1_reg_sensor_state(this->inverter_module_temp_, 32, ONE_DEC_UNIT); + publish_1_reg_sensor_state(this->inverter_module_temp_, 32, ONE_DEC_UNIT); + break; + } + case RTU2: { + publish_1_reg_sensor_state(this->inverter_status_, 0, 1); + + publish_2_reg_sensor_state(this->pv_active_power_sensor_, 1, 2, ONE_DEC_UNIT); + + publish_1_reg_sensor_state(this->pvs_[0].voltage_sensor_, 3, ONE_DEC_UNIT); + publish_1_reg_sensor_state(this->pvs_[0].current_sensor_, 4, ONE_DEC_UNIT); + publish_2_reg_sensor_state(this->pvs_[0].active_power_sensor_, 5, 6, ONE_DEC_UNIT); + + publish_1_reg_sensor_state(this->pvs_[1].voltage_sensor_, 7, ONE_DEC_UNIT); + publish_1_reg_sensor_state(this->pvs_[1].current_sensor_, 8, ONE_DEC_UNIT); + publish_2_reg_sensor_state(this->pvs_[1].active_power_sensor_, 9, 10, ONE_DEC_UNIT); + + publish_2_reg_sensor_state(this->grid_active_power_sensor_, 35, 36, ONE_DEC_UNIT); + publish_1_reg_sensor_state(this->grid_frequency_sensor_, 37, TWO_DEC_UNIT); + + publish_1_reg_sensor_state(this->phases_[0].voltage_sensor_, 38, ONE_DEC_UNIT); + publish_1_reg_sensor_state(this->phases_[0].current_sensor_, 39, ONE_DEC_UNIT); + publish_2_reg_sensor_state(this->phases_[0].active_power_sensor_, 40, 41, ONE_DEC_UNIT); + + publish_1_reg_sensor_state(this->phases_[1].voltage_sensor_, 42, ONE_DEC_UNIT); + publish_1_reg_sensor_state(this->phases_[1].current_sensor_, 43, ONE_DEC_UNIT); + publish_2_reg_sensor_state(this->phases_[1].active_power_sensor_, 44, 45, ONE_DEC_UNIT); + + publish_1_reg_sensor_state(this->phases_[2].voltage_sensor_, 46, ONE_DEC_UNIT); + publish_1_reg_sensor_state(this->phases_[2].current_sensor_, 47, ONE_DEC_UNIT); + publish_2_reg_sensor_state(this->phases_[2].active_power_sensor_, 48, 49, ONE_DEC_UNIT); + + publish_2_reg_sensor_state(this->today_production_, 53, 54, ONE_DEC_UNIT); + publish_2_reg_sensor_state(this->total_energy_production_, 55, 56, ONE_DEC_UNIT); + + publish_1_reg_sensor_state(this->inverter_module_temp_, 93, ONE_DEC_UNIT); + break; + } + } } void GrowattSolar::dump_config() { diff --git a/esphome/components/growatt_solar/growatt_solar.h b/esphome/components/growatt_solar/growatt_solar.h index 5356ac907a..0067998133 100644 --- a/esphome/components/growatt_solar/growatt_solar.h +++ b/esphome/components/growatt_solar/growatt_solar.h @@ -10,12 +10,19 @@ namespace growatt_solar { static const float TWO_DEC_UNIT = 0.01; static const float ONE_DEC_UNIT = 0.1; +enum GrowattProtocolVersion { + RTU = 0, + RTU2, +}; + class GrowattSolar : public PollingComponent, public modbus::ModbusDevice { public: void update() override; void on_modbus_data(const std::vector &data) override; void dump_config() override; + void set_protocol_version(GrowattProtocolVersion protocol_version) { this->protocol_version_ = protocol_version; } + void set_inverter_status_sensor(sensor::Sensor *sensor) { this->inverter_status_ = sensor; } void set_grid_frequency_sensor(sensor::Sensor *sensor) { this->grid_frequency_sensor_ = sensor; } @@ -67,6 +74,7 @@ class GrowattSolar : public PollingComponent, public modbus::ModbusDevice { sensor::Sensor *today_production_{nullptr}; sensor::Sensor *total_energy_production_{nullptr}; sensor::Sensor *inverter_module_temp_{nullptr}; + GrowattProtocolVersion protocol_version_; }; } // namespace growatt_solar diff --git a/esphome/components/growatt_solar/sensor.py b/esphome/components/growatt_solar/sensor.py index 99936c33ee..4961595505 100644 --- a/esphome/components/growatt_solar/sensor.py +++ b/esphome/components/growatt_solar/sensor.py @@ -39,7 +39,7 @@ UNIT_MILLIAMPERE = "mA" CONF_INVERTER_STATUS = "inverter_status" CONF_PV_ACTIVE_POWER = "pv_active_power" CONF_INVERTER_MODULE_TEMP = "inverter_module_temp" - +CONF_PROTOCOL_VERSION = "protocol_version" AUTO_LOAD = ["modbus"] CODEOWNERS = ["@leeuwte"] @@ -95,10 +95,20 @@ PV_SCHEMA = cv.Schema( {cv.Optional(sensor): schema for sensor, schema in PV_SENSORS.items()} ) +GrowattProtocolVersion = growatt_solar_ns.enum("GrowattProtocolVersion") +PROTOCOL_VERSIONS = { + "RTU": GrowattProtocolVersion.RTU, + "RTU2": GrowattProtocolVersion.RTU2, +} + + CONFIG_SCHEMA = ( cv.Schema( { cv.GenerateID(): cv.declare_id(GrowattSolar), + cv.Optional(CONF_PROTOCOL_VERSION, default="RTU"): cv.enum( + PROTOCOL_VERSIONS, upper=True + ), cv.Optional(CONF_PHASE_A): PHASE_SCHEMA, cv.Optional(CONF_PHASE_B): PHASE_SCHEMA, cv.Optional(CONF_PHASE_C): PHASE_SCHEMA, @@ -152,6 +162,8 @@ async def to_code(config): await cg.register_component(var, config) await modbus.register_modbus_device(var, config) + cg.add(var.set_protocol_version(config[CONF_PROTOCOL_VERSION])) + if CONF_INVERTER_STATUS in config: sens = await sensor.new_sensor(config[CONF_INVERTER_STATUS]) cg.add(var.set_inverter_status_sensor(sens)) From a9e653724c53fcd79b2c8745f5b2c8ec33f0b09f Mon Sep 17 00:00:00 2001 From: Keilin Bickar Date: Sun, 10 Apr 2022 16:38:29 -0400 Subject: [PATCH 0366/1729] Add parameter to control i2c stop signal at endTransmission (#3370) --- esphome/components/i2c/i2c.h | 22 +++++++++++++--------- esphome/components/i2c/i2c_bus.h | 10 ++++++++-- esphome/components/i2c/i2c_bus_arduino.cpp | 4 ++-- esphome/components/i2c/i2c_bus_arduino.h | 2 +- esphome/components/i2c/i2c_bus_esp_idf.cpp | 2 +- esphome/components/i2c/i2c_bus_esp_idf.h | 2 +- esphome/components/tca9548a/tca9548a.cpp | 4 ++-- esphome/components/tca9548a/tca9548a.h | 2 +- 8 files changed, 29 insertions(+), 19 deletions(-) diff --git a/esphome/components/i2c/i2c.h b/esphome/components/i2c/i2c.h index 50a0b3ae50..ffc0dadf81 100644 --- a/esphome/components/i2c/i2c.h +++ b/esphome/components/i2c/i2c.h @@ -46,21 +46,21 @@ class I2CDevice { I2CRegister reg(uint8_t a_register) { return {this, a_register}; } ErrorCode read(uint8_t *data, size_t len) { return bus_->read(address_, data, len); } - ErrorCode read_register(uint8_t a_register, uint8_t *data, size_t len) { - ErrorCode err = this->write(&a_register, 1); + ErrorCode read_register(uint8_t a_register, uint8_t *data, size_t len, bool stop = true) { + ErrorCode err = this->write(&a_register, 1, stop); if (err != ERROR_OK) return err; return this->read(data, len); } - ErrorCode write(const uint8_t *data, uint8_t len) { return bus_->write(address_, data, len); } - ErrorCode write_register(uint8_t a_register, const uint8_t *data, size_t len) { + ErrorCode write(const uint8_t *data, uint8_t len, bool stop = true) { return bus_->write(address_, data, len, stop); } + ErrorCode write_register(uint8_t a_register, const uint8_t *data, size_t len, bool stop = true) { WriteBuffer buffers[2]; buffers[0].data = &a_register; buffers[0].len = 1; buffers[1].data = data; buffers[1].len = len; - return bus_->writev(address_, buffers, 2); + return bus_->writev(address_, buffers, 2, stop); } // Compat APIs @@ -93,7 +93,9 @@ class I2CDevice { return true; } - bool read_byte(uint8_t a_register, uint8_t *data) { return read_register(a_register, data, 1) == ERROR_OK; } + bool read_byte(uint8_t a_register, uint8_t *data, bool stop = true) { + return read_register(a_register, data, 1, stop) == ERROR_OK; + } optional read_byte(uint8_t a_register) { uint8_t data; @@ -104,8 +106,8 @@ class I2CDevice { bool read_byte_16(uint8_t a_register, uint16_t *data) { return read_bytes_16(a_register, data, 1); } - bool write_bytes(uint8_t a_register, const uint8_t *data, uint8_t len) { - return write_register(a_register, data, len) == ERROR_OK; + bool write_bytes(uint8_t a_register, const uint8_t *data, uint8_t len, bool stop = true) { + return write_register(a_register, data, len, stop) == ERROR_OK; } bool write_bytes(uint8_t a_register, const std::vector &data) { @@ -118,7 +120,9 @@ class I2CDevice { bool write_bytes_16(uint8_t a_register, const uint16_t *data, uint8_t len); - bool write_byte(uint8_t a_register, uint8_t data) { return write_bytes(a_register, &data, 1); } + bool write_byte(uint8_t a_register, uint8_t data, bool stop = true) { + return write_bytes(a_register, &data, 1, stop); + } bool write_byte_16(uint8_t a_register, uint16_t data) { return write_bytes_16(a_register, &data, 1); } diff --git a/esphome/components/i2c/i2c_bus.h b/esphome/components/i2c/i2c_bus.h index 71f6b1d15b..c07a2dd1dd 100644 --- a/esphome/components/i2c/i2c_bus.h +++ b/esphome/components/i2c/i2c_bus.h @@ -36,12 +36,18 @@ class I2CBus { } virtual ErrorCode readv(uint8_t address, ReadBuffer *buffers, size_t cnt) = 0; virtual ErrorCode write(uint8_t address, const uint8_t *buffer, size_t len) { + return write(address, buffer, len, true); + } + virtual ErrorCode write(uint8_t address, const uint8_t *buffer, size_t len, bool stop) { WriteBuffer buf; buf.data = buffer; buf.len = len; - return writev(address, &buf, 1); + return writev(address, &buf, 1, stop); } - virtual ErrorCode writev(uint8_t address, WriteBuffer *buffers, size_t cnt) = 0; + virtual ErrorCode writev(uint8_t address, WriteBuffer *buffers, size_t cnt) { + return writev(address, buffers, cnt, true); + } + virtual ErrorCode writev(uint8_t address, WriteBuffer *buffers, size_t cnt, bool stop) = 0; protected: void i2c_scan_() { diff --git a/esphome/components/i2c/i2c_bus_arduino.cpp b/esphome/components/i2c/i2c_bus_arduino.cpp index 693b869bf7..cfdf818112 100644 --- a/esphome/components/i2c/i2c_bus_arduino.cpp +++ b/esphome/components/i2c/i2c_bus_arduino.cpp @@ -104,7 +104,7 @@ ErrorCode ArduinoI2CBus::readv(uint8_t address, ReadBuffer *buffers, size_t cnt) return ERROR_OK; } -ErrorCode ArduinoI2CBus::writev(uint8_t address, WriteBuffer *buffers, size_t cnt) { +ErrorCode ArduinoI2CBus::writev(uint8_t address, WriteBuffer *buffers, size_t cnt, bool stop) { // logging is only enabled with vv level, if warnings are shown the caller // should log them if (!initialized_) { @@ -139,7 +139,7 @@ ErrorCode ArduinoI2CBus::writev(uint8_t address, WriteBuffer *buffers, size_t cn return ERROR_UNKNOWN; } } - uint8_t status = wire_->endTransmission(true); + uint8_t status = wire_->endTransmission(stop); if (status == 0) { return ERROR_OK; } else if (status == 1) { diff --git a/esphome/components/i2c/i2c_bus_arduino.h b/esphome/components/i2c/i2c_bus_arduino.h index f4151e4f37..7298c3a1c9 100644 --- a/esphome/components/i2c/i2c_bus_arduino.h +++ b/esphome/components/i2c/i2c_bus_arduino.h @@ -20,7 +20,7 @@ class ArduinoI2CBus : public I2CBus, public Component { void setup() override; void dump_config() override; ErrorCode readv(uint8_t address, ReadBuffer *buffers, size_t cnt) override; - ErrorCode writev(uint8_t address, WriteBuffer *buffers, size_t cnt) override; + ErrorCode writev(uint8_t address, WriteBuffer *buffers, size_t cnt, bool stop) override; float get_setup_priority() const override { return setup_priority::BUS; } void set_scan(bool scan) { scan_ = scan; } diff --git a/esphome/components/i2c/i2c_bus_esp_idf.cpp b/esphome/components/i2c/i2c_bus_esp_idf.cpp index 606583fd7c..160b1b96d8 100644 --- a/esphome/components/i2c/i2c_bus_esp_idf.cpp +++ b/esphome/components/i2c/i2c_bus_esp_idf.cpp @@ -142,7 +142,7 @@ ErrorCode IDFI2CBus::readv(uint8_t address, ReadBuffer *buffers, size_t cnt) { return ERROR_OK; } -ErrorCode IDFI2CBus::writev(uint8_t address, WriteBuffer *buffers, size_t cnt) { +ErrorCode IDFI2CBus::writev(uint8_t address, WriteBuffer *buffers, size_t cnt, bool stop) { // logging is only enabled with vv level, if warnings are shown the caller // should log them if (!initialized_) { diff --git a/esphome/components/i2c/i2c_bus_esp_idf.h b/esphome/components/i2c/i2c_bus_esp_idf.h index d4b0626467..c80ea8c99d 100644 --- a/esphome/components/i2c/i2c_bus_esp_idf.h +++ b/esphome/components/i2c/i2c_bus_esp_idf.h @@ -20,7 +20,7 @@ class IDFI2CBus : public I2CBus, public Component { void setup() override; void dump_config() override; ErrorCode readv(uint8_t address, ReadBuffer *buffers, size_t cnt) override; - ErrorCode writev(uint8_t address, WriteBuffer *buffers, size_t cnt) override; + ErrorCode writev(uint8_t address, WriteBuffer *buffers, size_t cnt, bool stop) override; float get_setup_priority() const override { return setup_priority::BUS; } void set_scan(bool scan) { scan_ = scan; } diff --git a/esphome/components/tca9548a/tca9548a.cpp b/esphome/components/tca9548a/tca9548a.cpp index f3f8685287..de0d21b968 100644 --- a/esphome/components/tca9548a/tca9548a.cpp +++ b/esphome/components/tca9548a/tca9548a.cpp @@ -12,11 +12,11 @@ i2c::ErrorCode TCA9548AChannel::readv(uint8_t address, i2c::ReadBuffer *buffers, return err; return parent_->bus_->readv(address, buffers, cnt); } -i2c::ErrorCode TCA9548AChannel::writev(uint8_t address, i2c::WriteBuffer *buffers, size_t cnt) { +i2c::ErrorCode TCA9548AChannel::writev(uint8_t address, i2c::WriteBuffer *buffers, size_t cnt, bool stop) { auto err = parent_->switch_to_channel(channel_); if (err != i2c::ERROR_OK) return err; - return parent_->bus_->writev(address, buffers, cnt); + return parent_->bus_->writev(address, buffers, cnt, stop); } void TCA9548AComponent::setup() { diff --git a/esphome/components/tca9548a/tca9548a.h b/esphome/components/tca9548a/tca9548a.h index 39d07c2eb4..02553f8cd0 100644 --- a/esphome/components/tca9548a/tca9548a.h +++ b/esphome/components/tca9548a/tca9548a.h @@ -13,7 +13,7 @@ class TCA9548AChannel : public i2c::I2CBus { void set_parent(TCA9548AComponent *parent) { parent_ = parent; } i2c::ErrorCode readv(uint8_t address, i2c::ReadBuffer *buffers, size_t cnt) override; - i2c::ErrorCode writev(uint8_t address, i2c::WriteBuffer *buffers, size_t cnt) override; + i2c::ErrorCode writev(uint8_t address, i2c::WriteBuffer *buffers, size_t cnt, bool stop) override; protected: uint8_t channel_; From 3297267a16c96fea48abff308a4d5421d727b4de Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Sun, 10 Apr 2022 13:42:31 -0700 Subject: [PATCH 0367/1729] Fix SHTC3 sensor detection (#3365) Co-authored-by: Samuel Sieb --- esphome/components/shtcx/shtcx.cpp | 33 +++++++++++++++++++----------- esphome/components/shtcx/shtcx.h | 1 + 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/esphome/components/shtcx/shtcx.cpp b/esphome/components/shtcx/shtcx.cpp index 867c26df1d..4112270c02 100644 --- a/esphome/components/shtcx/shtcx.cpp +++ b/esphome/components/shtcx/shtcx.cpp @@ -35,15 +35,17 @@ void SHTCXComponent::setup() { return; } - uint16_t device_id_register[1]; - if (!this->read_data_(device_id_register, 1)) { + uint16_t device_id_register; + if (!this->read_data_(&device_id_register, 1)) { ESP_LOGE(TAG, "Error reading Device ID"); this->mark_failed(); return; } - if (((device_id_register[0] << 2) & 0x1C) == 0x1C) { - if ((device_id_register[0] & 0x847) == 0x847) { + this->sensor_id_ = device_id_register; + + if ((device_id_register & 0x3F) == 0x07) { + if (device_id_register & 0x800) { this->type_ = SHTCX_TYPE_SHTC3; } else { this->type_ = SHTCX_TYPE_SHTC1; @@ -51,11 +53,11 @@ void SHTCXComponent::setup() { } else { this->type_ = SHTCX_TYPE_UNKNOWN; } - ESP_LOGCONFIG(TAG, " Device identified: %s", to_string(this->type_)); + ESP_LOGCONFIG(TAG, " Device identified: %s (%04x)", to_string(this->type_), device_id_register); } void SHTCXComponent::dump_config() { ESP_LOGCONFIG(TAG, "SHTCx:"); - ESP_LOGCONFIG(TAG, " Model: %s", to_string(this->type_)); + ESP_LOGCONFIG(TAG, " Model: %s (%04x)", to_string(this->type_), this->sensor_id_); LOG_I2C_DEVICE(this); if (this->is_failed()) { ESP_LOGE(TAG, "Communication with SHTCx failed!"); @@ -75,21 +77,28 @@ void SHTCXComponent::update() { this->wake_up(); } if (!this->write_command_(SHTCX_COMMAND_POLLING_H)) { + ESP_LOGE(TAG, "sensor polling failed"); + if (this->temperature_sensor_ != nullptr) + this->temperature_sensor_->publish_state(NAN); + if (this->humidity_sensor_ != nullptr) + this->humidity_sensor_->publish_state(NAN); this->status_set_warning(); return; } this->set_timeout(50, [this]() { + float temperature = NAN; + float humidity = NAN; uint16_t raw_data[2]; if (!this->read_data_(raw_data, 2)) { + ESP_LOGE(TAG, "sensor read failed"); this->status_set_warning(); - return; + } else { + temperature = 175.0f * float(raw_data[0]) / 65536.0f - 45.0f; + humidity = 100.0f * float(raw_data[1]) / 65536.0f; + + ESP_LOGD(TAG, "Got temperature=%.2f°C humidity=%.2f%%", temperature, humidity); } - - float temperature = 175.0f * float(raw_data[0]) / 65536.0f - 45.0f; - float humidity = 100.0f * float(raw_data[1]) / 65536.0f; - - ESP_LOGD(TAG, "Got temperature=%.2f°C humidity=%.2f%%", temperature, humidity); if (this->temperature_sensor_ != nullptr) this->temperature_sensor_->publish_state(temperature); if (this->humidity_sensor_ != nullptr) diff --git a/esphome/components/shtcx/shtcx.h b/esphome/components/shtcx/shtcx.h index ccc6533bfa..cb2b46d348 100644 --- a/esphome/components/shtcx/shtcx.h +++ b/esphome/components/shtcx/shtcx.h @@ -27,6 +27,7 @@ class SHTCXComponent : public PollingComponent, public i2c::I2CDevice { bool write_command_(uint16_t command); bool read_data_(uint16_t *data, uint8_t len); SHTCXType type_; + uint16_t sensor_id_; sensor::Sensor *temperature_sensor_; sensor::Sensor *humidity_sensor_; }; From 2b91c23bf3c3fcd6ec86ff86fe4a3c0ac4909bc0 Mon Sep 17 00:00:00 2001 From: RadekHvizdos <10856567+RadekHvizdos@users.noreply.github.com> Date: Sun, 10 Apr 2022 22:44:11 +0200 Subject: [PATCH 0368/1729] Extend mcp3204 to support 8 channels (mcp3208 variant) (#3332) --- esphome/components/mcp3204/mcp3204.cpp | 2 +- esphome/components/mcp3204/sensor/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/mcp3204/mcp3204.cpp b/esphome/components/mcp3204/mcp3204.cpp index 44044349a3..283df4ccdc 100644 --- a/esphome/components/mcp3204/mcp3204.cpp +++ b/esphome/components/mcp3204/mcp3204.cpp @@ -20,7 +20,7 @@ void MCP3204::dump_config() { } float MCP3204::read_data(uint8_t pin) { - uint8_t adc_primary_config = 0b00000110 & 0b00000111; + uint8_t adc_primary_config = 0b00000110 | (pin >> 2); uint8_t adc_secondary_config = pin << 6; this->enable(); this->transfer_byte(adc_primary_config); diff --git a/esphome/components/mcp3204/sensor/__init__.py b/esphome/components/mcp3204/sensor/__init__.py index 1d8701a91e..404880d405 100644 --- a/esphome/components/mcp3204/sensor/__init__.py +++ b/esphome/components/mcp3204/sensor/__init__.py @@ -17,7 +17,7 @@ CONFIG_SCHEMA = sensor.SENSOR_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(MCP3204Sensor), cv.GenerateID(CONF_MCP3204_ID): cv.use_id(MCP3204), - cv.Required(CONF_NUMBER): cv.int_range(min=0, max=3), + cv.Required(CONF_NUMBER): cv.int_range(min=0, max=7), } ).extend(cv.polling_component_schema("60s")) From 84666b54b96e0af8841fc43d9ed3c4b1d8cfaf85 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Apr 2022 09:22:54 +1200 Subject: [PATCH 0369/1729] Bump pyupgrade from 2.31.1 to 2.32.0 (#3366) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index bc5316ca71..ece84cd53d 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,7 +1,7 @@ pylint==2.13.4 flake8==4.0.1 black==22.3.0 -pyupgrade==2.31.1 +pyupgrade==2.32.0 pre-commit # Unit tests From c2cacb3478f4cff7fb41b7404d3949f7268ef320 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Apr 2022 09:23:13 +1200 Subject: [PATCH 0370/1729] Bump voluptuous from 0.13.0 to 0.13.1 (#3364) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 4386099e2b..465d961cb6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -voluptuous==0.13.0 +voluptuous==0.13.1 PyYAML==6.0 paho-mqtt==1.6.1 colorama==0.4.4 From 7663716ae839ea8ec4a6a0be3782f6ba87b33e79 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Apr 2022 09:23:47 +1200 Subject: [PATCH 0371/1729] Bump pylint from 2.13.4 to 2.13.5 (#3363) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index ece84cd53d..083050252d 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,4 +1,4 @@ -pylint==2.13.4 +pylint==2.13.5 flake8==4.0.1 black==22.3.0 pyupgrade==2.32.0 From a2d0c1bf18258f60709fae36fbae4796f499f5d6 Mon Sep 17 00:00:00 2001 From: calco88 Date: Sun, 10 Apr 2022 15:14:53 -0700 Subject: [PATCH 0372/1729] Fix HM3301 AQI int8 overflow (#3361) --- esphome/components/hm3301/abstract_aqi_calculator.h | 2 +- esphome/components/hm3301/aqi_calculator.h | 2 +- esphome/components/hm3301/caqi_calculator.h | 2 +- esphome/components/hm3301/hm3301.cpp | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/hm3301/abstract_aqi_calculator.h b/esphome/components/hm3301/abstract_aqi_calculator.h index 42d900a262..038828e9de 100644 --- a/esphome/components/hm3301/abstract_aqi_calculator.h +++ b/esphome/components/hm3301/abstract_aqi_calculator.h @@ -7,7 +7,7 @@ namespace hm3301 { class AbstractAQICalculator { public: - virtual uint8_t get_aqi(uint16_t pm2_5_value, uint16_t pm10_0_value) = 0; + virtual uint16_t get_aqi(uint16_t pm2_5_value, uint16_t pm10_0_value) = 0; }; } // namespace hm3301 diff --git a/esphome/components/hm3301/aqi_calculator.h b/esphome/components/hm3301/aqi_calculator.h index 08d1dc2921..6c830f9bad 100644 --- a/esphome/components/hm3301/aqi_calculator.h +++ b/esphome/components/hm3301/aqi_calculator.h @@ -7,7 +7,7 @@ namespace hm3301 { class AQICalculator : public AbstractAQICalculator { public: - uint8_t get_aqi(uint16_t pm2_5_value, uint16_t pm10_0_value) override { + uint16_t get_aqi(uint16_t pm2_5_value, uint16_t pm10_0_value) override { int pm2_5_index = calculate_index_(pm2_5_value, pm2_5_calculation_grid_); int pm10_0_index = calculate_index_(pm10_0_value, pm10_0_calculation_grid_); diff --git a/esphome/components/hm3301/caqi_calculator.h b/esphome/components/hm3301/caqi_calculator.h index 1ec61f2416..3f338776d8 100644 --- a/esphome/components/hm3301/caqi_calculator.h +++ b/esphome/components/hm3301/caqi_calculator.h @@ -8,7 +8,7 @@ namespace hm3301 { class CAQICalculator : public AbstractAQICalculator { public: - uint8_t get_aqi(uint16_t pm2_5_value, uint16_t pm10_0_value) override { + uint16_t get_aqi(uint16_t pm2_5_value, uint16_t pm10_0_value) override { int pm2_5_index = calculate_index_(pm2_5_value, pm2_5_calculation_grid_); int pm10_0_index = calculate_index_(pm10_0_value, pm10_0_calculation_grid_); diff --git a/esphome/components/hm3301/hm3301.cpp b/esphome/components/hm3301/hm3301.cpp index a2bef2a01d..379c4dbc5a 100644 --- a/esphome/components/hm3301/hm3301.cpp +++ b/esphome/components/hm3301/hm3301.cpp @@ -62,7 +62,7 @@ void HM3301Component::update() { pm_10_0_value = get_sensor_value_(data_buffer_, PM_10_0_VALUE_INDEX); } - int8_t aqi_value = -1; + int16_t aqi_value = -1; if (this->aqi_sensor_ != nullptr && pm_2_5_value != -1 && pm_10_0_value != -1) { AbstractAQICalculator *calculator = this->aqi_calculator_factory_.get_calculator(this->aqi_calc_type_); aqi_value = calculator->get_aqi(pm_2_5_value, pm_10_0_value); From 9e3e34acf5b01515e43d1ecf289576f97df77222 Mon Sep 17 00:00:00 2001 From: rrooggiieerr Date: Mon, 11 Apr 2022 00:55:45 +0200 Subject: [PATCH 0373/1729] Add cover toggle support to endstop cover (#3358) --- esphome/components/endstop/endstop_cover.cpp | 17 +++++++++++++++++ esphome/components/endstop/endstop_cover.h | 1 + tests/test3.yaml | 2 ++ 3 files changed, 20 insertions(+) diff --git a/esphome/components/endstop/endstop_cover.cpp b/esphome/components/endstop/endstop_cover.cpp index 67c6a4ebd3..f468d13492 100644 --- a/esphome/components/endstop/endstop_cover.cpp +++ b/esphome/components/endstop/endstop_cover.cpp @@ -12,6 +12,7 @@ using namespace esphome::cover; CoverTraits EndstopCover::get_traits() { auto traits = CoverTraits(); traits.set_supports_position(true); + traits.set_supports_toggle(true); traits.set_is_assumed_state(false); return traits; } @@ -20,6 +21,20 @@ void EndstopCover::control(const CoverCall &call) { this->start_direction_(COVER_OPERATION_IDLE); this->publish_state(); } + if (call.get_toggle().has_value()) { + if (this->current_operation != COVER_OPERATION_IDLE) { + this->start_direction_(COVER_OPERATION_IDLE); + this->publish_state(); + } else { + if (this->position == COVER_CLOSED || this->last_operation_ == COVER_OPERATION_CLOSING) { + this->target_position_ = COVER_OPEN; + this->start_direction_(COVER_OPERATION_OPENING); + } else { + this->target_position_ = COVER_CLOSED; + this->start_direction_(COVER_OPERATION_CLOSING); + } + } + } if (call.get_position().has_value()) { auto pos = *call.get_position(); if (pos == this->position) { @@ -125,9 +140,11 @@ void EndstopCover::start_direction_(CoverOperation dir) { trig = this->stop_trigger_; break; case COVER_OPERATION_OPENING: + this->last_operation_ = dir; trig = this->open_trigger_; break; case COVER_OPERATION_CLOSING: + this->last_operation_ = dir; trig = this->close_trigger_; break; default: diff --git a/esphome/components/endstop/endstop_cover.h b/esphome/components/endstop/endstop_cover.h index f8d2746234..6ae15de8c1 100644 --- a/esphome/components/endstop/endstop_cover.h +++ b/esphome/components/endstop/endstop_cover.h @@ -51,6 +51,7 @@ class EndstopCover : public cover::Cover, public Component { uint32_t start_dir_time_{0}; uint32_t last_publish_time_{0}; float target_position_{0}; + cover::CoverOperation last_operation_{cover::COVER_OPERATION_OPENING}; }; } // namespace endstop diff --git a/tests/test3.yaml b/tests/test3.yaml index 853d7bd389..f0975f9918 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -795,6 +795,7 @@ binary_sensor: on_press: then: - cover.toggle: time_based_cover + - cover.toggle: endstop_cover - platform: template id: 'pzemac_reset_energy' on_press: @@ -1060,6 +1061,7 @@ climate: cover: - platform: endstop name: Endstop Cover + id: endstop_cover stop_action: - switch.turn_on: gpio_switch1 open_endstop: my_binary_sensor From efa6fd03e5694d2856c877a02d9e1b714cbccf64 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 11 Apr 2022 12:45:15 +1200 Subject: [PATCH 0374/1729] Make home_assistant imported sensors internal by default (#3372) --- esphome/components/homeassistant/__init__.py | 16 ++++++++++++ .../homeassistant/binary_sensor/__init__.py | 26 +++++++------------ .../homeassistant/sensor/__init__.py | 26 +++++++------------ .../homeassistant/text_sensor/__init__.py | 21 ++++++--------- 4 files changed, 43 insertions(+), 46 deletions(-) diff --git a/esphome/components/homeassistant/__init__.py b/esphome/components/homeassistant/__init__.py index c151abc250..776aa7fd7b 100644 --- a/esphome/components/homeassistant/__init__.py +++ b/esphome/components/homeassistant/__init__.py @@ -1,4 +1,20 @@ import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_ATTRIBUTE, CONF_ENTITY_ID, CONF_INTERNAL CODEOWNERS = ["@OttoWinter"] homeassistant_ns = cg.esphome_ns.namespace("homeassistant") + +HOME_ASSISTANT_IMPORT_SCHEMA = cv.Schema( + { + cv.Required(CONF_ENTITY_ID): cv.entity_id, + cv.Optional(CONF_ATTRIBUTE): cv.string, + cv.Optional(CONF_INTERNAL, default=True): cv.boolean, + } +) + + +def setup_home_assistant_entity(var, config): + cg.add(var.set_entity_id(config[CONF_ENTITY_ID])) + if CONF_ATTRIBUTE in config: + cg.add(var.set_attribute(config[CONF_ATTRIBUTE])) diff --git a/esphome/components/homeassistant/binary_sensor/__init__.py b/esphome/components/homeassistant/binary_sensor/__init__.py index a4f854c16e..a943368dd7 100644 --- a/esphome/components/homeassistant/binary_sensor/__init__.py +++ b/esphome/components/homeassistant/binary_sensor/__init__.py @@ -1,30 +1,24 @@ import esphome.codegen as cg -import esphome.config_validation as cv from esphome.components import binary_sensor -from esphome.const import CONF_ATTRIBUTE, CONF_ENTITY_ID -from .. import homeassistant_ns + +from .. import ( + HOME_ASSISTANT_IMPORT_SCHEMA, + homeassistant_ns, + setup_home_assistant_entity, +) DEPENDENCIES = ["api"] + HomeassistantBinarySensor = homeassistant_ns.class_( "HomeassistantBinarySensor", binary_sensor.BinarySensor, cg.Component ) -CONFIG_SCHEMA = ( - binary_sensor.binary_sensor_schema(HomeassistantBinarySensor) - .extend( - { - cv.Required(CONF_ENTITY_ID): cv.entity_id, - cv.Optional(CONF_ATTRIBUTE): cv.string, - } - ) - .extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(HomeassistantBinarySensor).extend( + HOME_ASSISTANT_IMPORT_SCHEMA ) async def to_code(config): var = await binary_sensor.new_binary_sensor(config) await cg.register_component(var, config) - - cg.add(var.set_entity_id(config[CONF_ENTITY_ID])) - if CONF_ATTRIBUTE in config: - cg.add(var.set_attribute(config[CONF_ATTRIBUTE])) + setup_home_assistant_entity(var, config) diff --git a/esphome/components/homeassistant/sensor/__init__.py b/esphome/components/homeassistant/sensor/__init__.py index 28fee9f7f6..6437476827 100644 --- a/esphome/components/homeassistant/sensor/__init__.py +++ b/esphome/components/homeassistant/sensor/__init__.py @@ -1,12 +1,11 @@ import esphome.codegen as cg -import esphome.config_validation as cv from esphome.components import sensor -from esphome.const import ( - CONF_ATTRIBUTE, - CONF_ENTITY_ID, - CONF_ID, + +from .. import ( + HOME_ASSISTANT_IMPORT_SCHEMA, + homeassistant_ns, + setup_home_assistant_entity, ) -from .. import homeassistant_ns DEPENDENCIES = ["api"] @@ -14,19 +13,12 @@ HomeassistantSensor = homeassistant_ns.class_( "HomeassistantSensor", sensor.Sensor, cg.Component ) -CONFIG_SCHEMA = sensor.sensor_schema(HomeassistantSensor, accuracy_decimals=1,).extend( - { - cv.Required(CONF_ENTITY_ID): cv.entity_id, - cv.Optional(CONF_ATTRIBUTE): cv.string, - } +CONFIG_SCHEMA = sensor.sensor_schema(HomeassistantSensor, accuracy_decimals=1).extend( + HOME_ASSISTANT_IMPORT_SCHEMA ) async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = await sensor.new_sensor(config) await cg.register_component(var, config) - await sensor.register_sensor(var, config) - - cg.add(var.set_entity_id(config[CONF_ENTITY_ID])) - if CONF_ATTRIBUTE in config: - cg.add(var.set_attribute(config[CONF_ATTRIBUTE])) + setup_home_assistant_entity(var, config) diff --git a/esphome/components/homeassistant/text_sensor/__init__.py b/esphome/components/homeassistant/text_sensor/__init__.py index be59bab676..b59f9d23df 100644 --- a/esphome/components/homeassistant/text_sensor/__init__.py +++ b/esphome/components/homeassistant/text_sensor/__init__.py @@ -1,9 +1,11 @@ import esphome.codegen as cg -import esphome.config_validation as cv from esphome.components import text_sensor -from esphome.const import CONF_ATTRIBUTE, CONF_ENTITY_ID -from .. import homeassistant_ns +from .. import ( + HOME_ASSISTANT_IMPORT_SCHEMA, + homeassistant_ns, + setup_home_assistant_entity, +) DEPENDENCIES = ["api"] @@ -11,19 +13,12 @@ HomeassistantTextSensor = homeassistant_ns.class_( "HomeassistantTextSensor", text_sensor.TextSensor, cg.Component ) -CONFIG_SCHEMA = text_sensor.text_sensor_schema().extend( - { - cv.GenerateID(): cv.declare_id(HomeassistantTextSensor), - cv.Required(CONF_ENTITY_ID): cv.entity_id, - cv.Optional(CONF_ATTRIBUTE): cv.string, - } +CONFIG_SCHEMA = text_sensor.text_sensor_schema(HomeassistantTextSensor).extend( + HOME_ASSISTANT_IMPORT_SCHEMA ) async def to_code(config): var = await text_sensor.new_text_sensor(config) await cg.register_component(var, config) - - cg.add(var.set_entity_id(config[CONF_ENTITY_ID])) - if CONF_ATTRIBUTE in config: - cg.add(var.set_attribute(config[CONF_ATTRIBUTE])) + setup_home_assistant_entity(var, config) From fdda47db6e3103a44b9a20028e67b7e7a4934a36 Mon Sep 17 00:00:00 2001 From: functionpointer Date: Mon, 11 Apr 2022 04:50:56 +0200 Subject: [PATCH 0375/1729] Add integration hydreon_rgxx for rain sensors by Hydreon (#2711) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/hydreon_rgxx/__init__.py | 11 + .../components/hydreon_rgxx/binary_sensor.py | 36 +++ .../components/hydreon_rgxx/hydreon_rgxx.cpp | 211 ++++++++++++++++++ .../components/hydreon_rgxx/hydreon_rgxx.h | 76 +++++++ esphome/components/hydreon_rgxx/sensor.py | 119 ++++++++++ tests/test3.yaml | 22 ++ 7 files changed, 476 insertions(+) create mode 100644 esphome/components/hydreon_rgxx/__init__.py create mode 100644 esphome/components/hydreon_rgxx/binary_sensor.py create mode 100644 esphome/components/hydreon_rgxx/hydreon_rgxx.cpp create mode 100644 esphome/components/hydreon_rgxx/hydreon_rgxx.h create mode 100644 esphome/components/hydreon_rgxx/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 8958aa2928..309f6f4d51 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -82,6 +82,7 @@ esphome/components/hitachi_ac424/* @sourabhjaiswal esphome/components/homeassistant/* @OttoWinter esphome/components/honeywellabp/* @RubyBailey esphome/components/hrxl_maxsonar_wr/* @netmikey +esphome/components/hydreon_rgxx/* @functionpointer esphome/components/i2c/* @esphome/core esphome/components/improv_serial/* @esphome/core esphome/components/ina260/* @MrEditor97 diff --git a/esphome/components/hydreon_rgxx/__init__.py b/esphome/components/hydreon_rgxx/__init__.py new file mode 100644 index 0000000000..5fe050edf2 --- /dev/null +++ b/esphome/components/hydreon_rgxx/__init__.py @@ -0,0 +1,11 @@ +import esphome.codegen as cg +from esphome.components import uart + +CODEOWNERS = ["@functionpointer"] +DEPENDENCIES = ["uart"] + +hydreon_rgxx_ns = cg.esphome_ns.namespace("hydreon_rgxx") +RGModel = hydreon_rgxx_ns.enum("RGModel") +HydreonRGxxComponent = hydreon_rgxx_ns.class_( + "HydreonRGxxComponent", cg.PollingComponent, uart.UARTDevice +) diff --git a/esphome/components/hydreon_rgxx/binary_sensor.py b/esphome/components/hydreon_rgxx/binary_sensor.py new file mode 100644 index 0000000000..0d489ebcb7 --- /dev/null +++ b/esphome/components/hydreon_rgxx/binary_sensor.py @@ -0,0 +1,36 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import binary_sensor +from esphome.const import ( + CONF_ID, + DEVICE_CLASS_COLD, +) + +from . import hydreon_rgxx_ns, HydreonRGxxComponent + +CONF_HYDREON_RGXX_ID = "hydreon_rgxx_id" +CONF_TOO_COLD = "too_cold" + +HydreonRGxxBinarySensor = hydreon_rgxx_ns.class_( + "HydreonRGxxBinaryComponent", cg.Component +) + + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(HydreonRGxxBinarySensor), + cv.GenerateID(CONF_HYDREON_RGXX_ID): cv.use_id(HydreonRGxxComponent), + cv.Optional(CONF_TOO_COLD): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_COLD + ), + } +) + + +async def to_code(config): + main_sensor = await cg.get_variable(config[CONF_HYDREON_RGXX_ID]) + bin_component = cg.new_Pvariable(config[CONF_ID], main_sensor) + await cg.register_component(bin_component, config) + if CONF_TOO_COLD in config: + tc = await binary_sensor.new_binary_sensor(config[CONF_TOO_COLD]) + cg.add(main_sensor.set_too_cold_sensor(tc)) diff --git a/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp b/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp new file mode 100644 index 0000000000..3ed65831ae --- /dev/null +++ b/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp @@ -0,0 +1,211 @@ +#include "hydreon_rgxx.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace hydreon_rgxx { + +static const char *const TAG = "hydreon_rgxx.sensor"; +static const int MAX_DATA_LENGTH_BYTES = 80; +static const uint8_t ASCII_LF = 0x0A; +#define HYDREON_RGXX_COMMA , +static const char *const PROTOCOL_NAMES[] = {HYDREON_RGXX_PROTOCOL_LIST(, HYDREON_RGXX_COMMA)}; + +void HydreonRGxxComponent::dump_config() { + this->check_uart_settings(9600, 1, esphome::uart::UART_CONFIG_PARITY_NONE, 8); + ESP_LOGCONFIG(TAG, "hydreon_rgxx:"); + if (this->is_failed()) { + ESP_LOGE(TAG, "Connection with hydreon_rgxx failed!"); + } + LOG_UPDATE_INTERVAL(this); + + int i = 0; +#define HYDREON_RGXX_LOG_SENSOR(s) \ + if (this->sensors_[i++] != nullptr) { \ + LOG_SENSOR(" ", #s, this->sensors_[i - 1]); \ + } + HYDREON_RGXX_PROTOCOL_LIST(HYDREON_RGXX_LOG_SENSOR, ); +} + +void HydreonRGxxComponent::setup() { + ESP_LOGCONFIG(TAG, "Setting up hydreon_rgxx..."); + while (this->available() != 0) { + this->read(); + } + this->schedule_reboot_(); +} + +bool HydreonRGxxComponent::sensor_missing_() { + if (this->sensors_received_ == -1) { + // no request sent yet, don't check + return false; + } else { + if (this->sensors_received_ == 0) { + ESP_LOGW(TAG, "No data at all"); + return true; + } + for (int i = 0; i < NUM_SENSORS; i++) { + if (this->sensors_[i] == nullptr) { + continue; + } + if ((this->sensors_received_ >> i & 1) == 0) { + ESP_LOGW(TAG, "Missing %s", PROTOCOL_NAMES[i]); + return true; + } + } + return false; + } +} + +void HydreonRGxxComponent::update() { + if (this->boot_count_ > 0) { + if (this->sensor_missing_()) { + this->no_response_count_++; + ESP_LOGE(TAG, "data missing %d times", this->no_response_count_); + if (this->no_response_count_ > 15) { + ESP_LOGE(TAG, "asking sensor to reboot"); + for (auto &sensor : this->sensors_) { + if (sensor != nullptr) { + sensor->publish_state(NAN); + } + } + this->schedule_reboot_(); + return; + } + } else { + this->no_response_count_ = 0; + } + this->write_str("R\n"); +#ifdef USE_BINARY_SENSOR + if (this->too_cold_sensor_ != nullptr) { + this->too_cold_sensor_->publish_state(this->too_cold_); + } +#endif + this->too_cold_ = false; + this->sensors_received_ = 0; + } +} + +void HydreonRGxxComponent::loop() { + uint8_t data; + while (this->available() > 0) { + if (this->read_byte(&data)) { + buffer_ += (char) data; + if (this->buffer_.back() == static_cast(ASCII_LF) || this->buffer_.length() >= MAX_DATA_LENGTH_BYTES) { + // complete line received + this->process_line_(); + this->buffer_.clear(); + } + } + } +} + +/** + * Communication with the sensor is asynchronous. + * We send requests and let esphome continue doing its thing. + * Once we have received a complete line, we process it. + * + * Catching communication failures is done in two layers: + * + * 1. We check if all requested data has been received + * before we send out the next request. If data keeps + * missing, we escalate. + * 2. Request the sensor to reboot. We retry based on + * a timeout. If the sensor does not respond after + * several boot attempts, we give up. + */ +void HydreonRGxxComponent::schedule_reboot_() { + this->boot_count_ = 0; + this->set_interval("reboot", 5000, [this]() { + if (this->boot_count_ < 0) { + ESP_LOGW(TAG, "hydreon_rgxx failed to boot %d times", -this->boot_count_); + } + this->boot_count_--; + this->write_str("K\n"); + if (this->boot_count_ < -5) { + ESP_LOGE(TAG, "hydreon_rgxx can't boot, giving up"); + for (auto &sensor : this->sensors_) { + if (sensor != nullptr) { + sensor->publish_state(NAN); + } + } + this->mark_failed(); + } + }); +} + +bool HydreonRGxxComponent::buffer_starts_with_(const std::string &prefix) { + return this->buffer_starts_with_(prefix.c_str()); +} + +bool HydreonRGxxComponent::buffer_starts_with_(const char *prefix) { return buffer_.rfind(prefix, 0) == 0; } + +void HydreonRGxxComponent::process_line_() { + ESP_LOGV(TAG, "Read from serial: %s", this->buffer_.substr(0, this->buffer_.size() - 2).c_str()); + + if (buffer_[0] == ';') { + ESP_LOGI(TAG, "Comment: %s", this->buffer_.substr(0, this->buffer_.size() - 2).c_str()); + return; + } + if (this->buffer_starts_with_("PwrDays")) { + if (this->boot_count_ <= 0) { + this->boot_count_ = 1; + } else { + this->boot_count_++; + } + this->cancel_interval("reboot"); + this->no_response_count_ = 0; + ESP_LOGI(TAG, "Boot detected: %s", this->buffer_.substr(0, this->buffer_.size() - 2).c_str()); + this->write_str("P\nH\nM\n"); // set sensor to polling mode, high res mode, metric mode + return; + } + if (this->buffer_starts_with_("SW")) { + std::string::size_type majend = this->buffer_.find('.'); + std::string::size_type endversion = this->buffer_.find(' ', 3); + if (majend == std::string::npos || endversion == std::string::npos || majend > endversion) { + ESP_LOGW(TAG, "invalid version string: %s", this->buffer_.substr(0, this->buffer_.size() - 2).c_str()); + } + int major = strtol(this->buffer_.substr(3, majend - 3).c_str(), nullptr, 10); + int minor = strtol(this->buffer_.substr(majend + 1, endversion - (majend + 1)).c_str(), nullptr, 10); + + if (major > 10 || minor >= 1000 || minor < 0 || major < 0) { + ESP_LOGW(TAG, "invalid version: %s", this->buffer_.substr(0, this->buffer_.size() - 2).c_str()); + } + this->sw_version_ = major * 1000 + minor; + ESP_LOGI(TAG, "detected sw version %i", this->sw_version_); + return; + } + bool is_data_line = false; + for (int i = 0; i < NUM_SENSORS; i++) { + if (this->sensors_[i] != nullptr && this->buffer_starts_with_(PROTOCOL_NAMES[i])) { + is_data_line = true; + break; + } + } + if (is_data_line) { + std::string::size_type tc = this->buffer_.find("TooCold"); + this->too_cold_ |= tc != std::string::npos; + if (this->too_cold_) { + ESP_LOGD(TAG, "Received TooCold"); + } + for (int i = 0; i < NUM_SENSORS; i++) { + if (this->sensors_[i] == nullptr) { + continue; + } + std::string::size_type n = this->buffer_.find(PROTOCOL_NAMES[i]); + if (n == std::string::npos) { + continue; + } + int data = strtol(this->buffer_.substr(n + strlen(PROTOCOL_NAMES[i])).c_str(), nullptr, 10); + this->sensors_[i]->publish_state(data); + ESP_LOGD(TAG, "Received %s: %f", PROTOCOL_NAMES[i], this->sensors_[i]->get_raw_state()); + this->sensors_received_ |= (1 << i); + } + } else { + ESP_LOGI(TAG, "Got unknown line: %s", this->buffer_.c_str()); + } +} + +float HydreonRGxxComponent::get_setup_priority() const { return setup_priority::DATA; } + +} // namespace hydreon_rgxx +} // namespace esphome diff --git a/esphome/components/hydreon_rgxx/hydreon_rgxx.h b/esphome/components/hydreon_rgxx/hydreon_rgxx.h new file mode 100644 index 0000000000..ebe4a35b19 --- /dev/null +++ b/esphome/components/hydreon_rgxx/hydreon_rgxx.h @@ -0,0 +1,76 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/defines.h" +#include "esphome/components/sensor/sensor.h" +#ifdef USE_BINARY_SENSOR +#include "esphome/components/binary_sensor/binary_sensor.h" +#endif +#include "esphome/components/uart/uart.h" + +namespace esphome { +namespace hydreon_rgxx { + +enum RGModel { + RG9 = 1, + RG15 = 2, +}; + +#ifdef HYDREON_RGXX_NUM_SENSORS +static const uint8_t NUM_SENSORS = HYDREON_RGXX_NUM_SENSORS; +#else +static const uint8_t NUM_SENSORS = 1; +#endif + +#ifndef HYDREON_RGXX_PROTOCOL_LIST +#define HYDREON_RGXX_PROTOCOL_LIST(F, SEP) F("") +#endif + +class HydreonRGxxComponent : public PollingComponent, public uart::UARTDevice { + public: + void set_sensor(sensor::Sensor *sensor, int index) { this->sensors_[index] = sensor; } +#ifdef USE_BINARY_SENSOR + void set_too_cold_sensor(binary_sensor::BinarySensor *sensor) { this->too_cold_sensor_ = sensor; } +#endif + void set_model(RGModel model) { model_ = model; } + + /// Schedule data readings. + void update() override; + /// Read data once available + void loop() override; + /// Setup the sensor and test for a connection. + void setup() override; + void dump_config() override; + + float get_setup_priority() const override; + + protected: + void process_line_(); + void schedule_reboot_(); + bool buffer_starts_with_(const std::string &prefix); + bool buffer_starts_with_(const char *prefix); + bool sensor_missing_(); + + sensor::Sensor *sensors_[NUM_SENSORS] = {nullptr}; +#ifdef USE_BINARY_SENSOR + binary_sensor::BinarySensor *too_cold_sensor_ = nullptr; +#endif + + int16_t boot_count_ = 0; + int16_t no_response_count_ = 0; + std::string buffer_; + RGModel model_ = RG9; + int sw_version_ = 0; + bool too_cold_ = false; + + // bit field showing which sensors we have received data for + int sensors_received_ = -1; +}; + +class HydreonRGxxBinaryComponent : public Component { + public: + HydreonRGxxBinaryComponent(HydreonRGxxComponent *parent) {} +}; + +} // namespace hydreon_rgxx +} // namespace esphome diff --git a/esphome/components/hydreon_rgxx/sensor.py b/esphome/components/hydreon_rgxx/sensor.py new file mode 100644 index 0000000000..409500305a --- /dev/null +++ b/esphome/components/hydreon_rgxx/sensor.py @@ -0,0 +1,119 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import uart, sensor +from esphome.const import ( + CONF_ID, + CONF_MODEL, + CONF_MOISTURE, + DEVICE_CLASS_HUMIDITY, + STATE_CLASS_MEASUREMENT, +) + +from . import RGModel, HydreonRGxxComponent + +UNIT_INTENSITY = "intensity" +UNIT_MILLIMETERS = "mm" +UNIT_MILLIMETERS_PER_HOUR = "mm/h" + +CONF_ACC = "acc" +CONF_EVENT_ACC = "event_acc" +CONF_TOTAL_ACC = "total_acc" +CONF_R_INT = "r_int" + +RG_MODELS = { + "RG_9": RGModel.RG9, + "RG_15": RGModel.RG15, + # https://rainsensors.com/wp-content/uploads/sites/3/2020/07/rg-15_instructions_sw_1.000.pdf + # https://rainsensors.com/wp-content/uploads/sites/3/2021/03/2020.08.25-rg-9_instructions.pdf + # https://rainsensors.com/wp-content/uploads/sites/3/2021/03/2021.03.11-rg-9_instructions.pdf +} +SUPPORTED_SENSORS = { + CONF_ACC: ["RG_15"], + CONF_EVENT_ACC: ["RG_15"], + CONF_TOTAL_ACC: ["RG_15"], + CONF_R_INT: ["RG_15"], + CONF_MOISTURE: ["RG_9"], +} +PROTOCOL_NAMES = { + CONF_MOISTURE: "R", + CONF_ACC: "Acc", + CONF_R_INT: "Rint", + CONF_EVENT_ACC: "EventAcc", + CONF_TOTAL_ACC: "TotalAcc", +} + + +def _validate(config): + for conf, models in SUPPORTED_SENSORS.items(): + if conf in config: + if config[CONF_MODEL] not in models: + raise cv.Invalid( + f"{conf} is only available on {' and '.join(models)}, not {config[CONF_MODEL]}" + ) + return config + + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(HydreonRGxxComponent), + cv.Required(CONF_MODEL): cv.enum( + RG_MODELS, + upper=True, + space="_", + ), + cv.Optional(CONF_ACC): sensor.sensor_schema( + unit_of_measurement=UNIT_MILLIMETERS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_EVENT_ACC): sensor.sensor_schema( + unit_of_measurement=UNIT_MILLIMETERS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TOTAL_ACC): sensor.sensor_schema( + unit_of_measurement=UNIT_MILLIMETERS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_R_INT): sensor.sensor_schema( + unit_of_measurement=UNIT_MILLIMETERS_PER_HOUR, + accuracy_decimals=2, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_MOISTURE): sensor.sensor_schema( + unit_of_measurement=UNIT_INTENSITY, + accuracy_decimals=0, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(uart.UART_DEVICE_SCHEMA), + _validate, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) + + cg.add_define( + "HYDREON_RGXX_PROTOCOL_LIST(F, sep)", + cg.RawExpression( + " sep ".join([f'F("{name}")' for name in PROTOCOL_NAMES.values()]) + ), + ) + cg.add_define("HYDREON_RGXX_NUM_SENSORS", len(PROTOCOL_NAMES)) + + for i, conf in enumerate(PROTOCOL_NAMES): + if conf in config: + sens = await sensor.new_sensor(config[conf]) + cg.add(var.set_sensor(sens, i)) diff --git a/tests/test3.yaml b/tests/test3.yaml index f0975f9918..a6ad6b9e92 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -349,6 +349,24 @@ sensor: name: 'Temperature' humidity: name: 'Humidity' + - platform: hydreon_rgxx + model: "RG 9" + uart_id: uart6 + id: "hydreon_rg9" + moisture: + name: "hydreon_rain" + id: hydreon_rain + - platform: hydreon_rgxx + model: "RG_15" + uart_id: uart6 + acc: + name: "hydreon_acc" + event_acc: + name: "hydreon_event_acc" + total_acc: + name: "hydreon_total_acc" + r_int: + name: "hydreon_r_int" - platform: adc pin: VCC id: my_sensor @@ -796,6 +814,10 @@ binary_sensor: then: - cover.toggle: time_based_cover - cover.toggle: endstop_cover + - platform: hydreon_rgxx + hydreon_rgxx_id: "hydreon_rg9" + too_cold: + name: "rg9_toocold" - platform: template id: 'pzemac_reset_energy' on_press: From dabd27d4be62e450932aa7090f8af836fe09c39c Mon Sep 17 00:00:00 2001 From: andrewpc Date: Tue, 12 Apr 2022 10:45:54 +1000 Subject: [PATCH 0376/1729] Addition of Deep Sleep RTC pin definition for ESP32-S2 (#3303) --- esphome/components/deep_sleep/__init__.py | 25 +++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/esphome/components/deep_sleep/__init__.py b/esphome/components/deep_sleep/__init__.py index 2a74d0c1bb..24fc0cabb0 100644 --- a/esphome/components/deep_sleep/__init__.py +++ b/esphome/components/deep_sleep/__init__.py @@ -15,6 +15,7 @@ from esphome.components.esp32 import get_esp32_variant from esphome.components.esp32.const import ( VARIANT_ESP32, VARIANT_ESP32C3, + VARIANT_ESP32S2, ) WAKEUP_PINS = { @@ -39,6 +40,30 @@ WAKEUP_PINS = { 39, ], VARIANT_ESP32C3: [0, 1, 2, 3, 4, 5], + VARIANT_ESP32S2: [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + ], } From da336247eb853c02a5d977bbcfb0914d8f87df68 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 12 Apr 2022 16:19:16 +1200 Subject: [PATCH 0377/1729] Add Xiaomi RTCGQ02LM - Mi Motion Sensor 2 (#3186) --- CODEOWNERS | 1 + esphome/components/xiaomi_ble/xiaomi_ble.cpp | 97 +++++++++++-------- esphome/components/xiaomi_ble/xiaomi_ble.h | 8 +- .../components/xiaomi_rtcgq02lm/__init__.py | 36 +++++++ .../xiaomi_rtcgq02lm/binary_sensor.py | 64 ++++++++++++ esphome/components/xiaomi_rtcgq02lm/sensor.py | 37 +++++++ .../xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.cpp | 91 +++++++++++++++++ .../xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.h | 61 ++++++++++++ esphome/core/helpers.h | 4 + tests/test2.yaml | 17 ++++ 10 files changed, 373 insertions(+), 43 deletions(-) create mode 100644 esphome/components/xiaomi_rtcgq02lm/__init__.py create mode 100644 esphome/components/xiaomi_rtcgq02lm/binary_sensor.py create mode 100644 esphome/components/xiaomi_rtcgq02lm/sensor.py create mode 100644 esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.cpp create mode 100644 esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.h diff --git a/CODEOWNERS b/CODEOWNERS index 309f6f4d51..5a1220354a 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -224,4 +224,5 @@ esphome/components/whirlpool/* @glmnet esphome/components/xiaomi_lywsd03mmc/* @ahpohl esphome/components/xiaomi_mhoc303/* @drug123 esphome/components/xiaomi_mhoc401/* @vevsvevs +esphome/components/xiaomi_rtcgq02lm/* @jesserockz esphome/components/xpt2046/* @numo68 diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.cpp b/esphome/components/xiaomi_ble/xiaomi_ble.cpp index bdd745b859..95d97defe2 100644 --- a/esphome/components/xiaomi_ble/xiaomi_ble.cpp +++ b/esphome/components/xiaomi_ble/xiaomi_ble.cpp @@ -1,6 +1,6 @@ #include "xiaomi_ble.h" -#include "esphome/core/log.h" #include "esphome/core/helpers.h" +#include "esphome/core/log.h" #ifdef USE_ESP32 @@ -12,67 +12,74 @@ namespace xiaomi_ble { static const char *const TAG = "xiaomi_ble"; -bool parse_xiaomi_value(uint8_t value_type, const uint8_t *data, uint8_t value_length, XiaomiParseResult &result) { +bool parse_xiaomi_value(uint16_t value_type, const uint8_t *data, uint8_t value_length, XiaomiParseResult &result) { + // button pressed, 3 bytes, only byte 3 is used for supported devices so far + if ((value_type == 0x1001) && (value_length == 3)) { + result.button_press = data[2] == 0; + return true; + } // motion detection, 1 byte, 8-bit unsigned integer - if ((value_type == 0x03) && (value_length == 1)) { + else if ((value_type == 0x0003) && (value_length == 1)) { result.has_motion = data[0]; } // temperature, 2 bytes, 16-bit signed integer (LE), 0.1 °C - else if ((value_type == 0x04) && (value_length == 2)) { - const int16_t temperature = uint16_t(data[0]) | (uint16_t(data[1]) << 8); + else if ((value_type == 0x1004) && (value_length == 2)) { + const int16_t temperature = encode_uint16(data[1], data[0]); result.temperature = temperature / 10.0f; } // humidity, 2 bytes, 16-bit signed integer (LE), 0.1 % - else if ((value_type == 0x06) && (value_length == 2)) { - const int16_t humidity = uint16_t(data[0]) | (uint16_t(data[1]) << 8); + else if ((value_type == 0x1006) && (value_length == 2)) { + const int16_t humidity = encode_uint16(data[1], data[0]); result.humidity = humidity / 10.0f; } // illuminance (+ motion), 3 bytes, 24-bit unsigned integer (LE), 1 lx - else if (((value_type == 0x07) || (value_type == 0x0F)) && (value_length == 3)) { - const uint32_t illuminance = uint32_t(data[0]) | (uint32_t(data[1]) << 8) | (uint32_t(data[2]) << 16); + else if (((value_type == 0x1007) || (value_type == 0x000F)) && (value_length == 3)) { + const uint32_t illuminance = encode_uint24(data[2], data[1], data[0]); result.illuminance = illuminance; - result.is_light = illuminance == 100; + result.is_light = illuminance >= 100; if (value_type == 0x0F) result.has_motion = true; } // soil moisture, 1 byte, 8-bit unsigned integer, 1 % - else if ((value_type == 0x08) && (value_length == 1)) { + else if ((value_type == 0x1008) && (value_length == 1)) { result.moisture = data[0]; } // conductivity, 2 bytes, 16-bit unsigned integer (LE), 1 µS/cm - else if ((value_type == 0x09) && (value_length == 2)) { - const uint16_t conductivity = uint16_t(data[0]) | (uint16_t(data[1]) << 8); + else if ((value_type == 0x1009) && (value_length == 2)) { + const uint16_t conductivity = encode_uint16(data[1], data[0]); result.conductivity = conductivity; } // battery, 1 byte, 8-bit unsigned integer, 1 % - else if ((value_type == 0x0A) && (value_length == 1)) { + else if ((value_type == 0x100A) && (value_length == 1)) { result.battery_level = data[0]; } // temperature + humidity, 4 bytes, 16-bit signed integer (LE) each, 0.1 °C, 0.1 % - else if ((value_type == 0x0D) && (value_length == 4)) { - const int16_t temperature = uint16_t(data[0]) | (uint16_t(data[1]) << 8); - const int16_t humidity = uint16_t(data[2]) | (uint16_t(data[3]) << 8); + else if ((value_type == 0x100D) && (value_length == 4)) { + const int16_t temperature = encode_uint16(data[1], data[0]); + const int16_t humidity = encode_uint16(data[3], data[2]); result.temperature = temperature / 10.0f; result.humidity = humidity / 10.0f; } // formaldehyde, 2 bytes, 16-bit unsigned integer (LE), 0.01 mg / m3 - else if ((value_type == 0x10) && (value_length == 2)) { - const uint16_t formaldehyde = uint16_t(data[0]) | (uint16_t(data[1]) << 8); + else if ((value_type == 0x1010) && (value_length == 2)) { + const uint16_t formaldehyde = encode_uint16(data[1], data[0]); result.formaldehyde = formaldehyde / 100.0f; } // on/off state, 1 byte, 8-bit unsigned integer - else if ((value_type == 0x12) && (value_length == 1)) { + else if ((value_type == 0x1012) && (value_length == 1)) { result.is_active = data[0]; } // mosquito tablet, 1 byte, 8-bit unsigned integer, 1 % - else if ((value_type == 0x13) && (value_length == 1)) { + else if ((value_type == 0x1013) && (value_length == 1)) { result.tablet = data[0]; } // idle time since last motion, 4 byte, 32-bit unsigned integer, 1 min - else if ((value_type == 0x17) && (value_length == 4)) { + else if ((value_type == 0x1017) && (value_length == 4)) { const uint32_t idle_time = encode_uint32(data[3], data[2], data[1], data[0]); result.idle_time = idle_time / 60.0f; result.has_motion = !idle_time; + } else if ((value_type == 0x1018) && (value_length == 1)) { + result.is_light = data[0]; } else { return false; } @@ -115,7 +122,7 @@ bool parse_xiaomi_message(const std::vector &message, XiaomiParseResult break; } - const uint8_t value_type = payload[payload_offset + 0]; + const uint16_t value_type = encode_uint16(payload[payload_offset + 1], payload[payload_offset + 0]); const uint8_t *data = &payload[payload_offset + 3]; if (parse_xiaomi_value(value_type, data, value_length, result)) @@ -155,60 +162,67 @@ optional parse_xiaomi_header(const esp32_ble_tracker::Service result.is_duplicate = false; result.raw_offset = result.has_capability ? 12 : 11; - if ((raw[2] == 0x98) && (raw[3] == 0x00)) { // MiFlora + const uint16_t device_uuid = encode_uint16(raw[3], raw[2]); + + if (device_uuid == 0x0098) { // MiFlora result.type = XiaomiParseResult::TYPE_HHCCJCY01; result.name = "HHCCJCY01"; - } else if ((raw[2] == 0xaa) && (raw[3] == 0x01)) { // round body, segment LCD + } else if (device_uuid == 0x01aa) { // round body, segment LCD result.type = XiaomiParseResult::TYPE_LYWSDCGQ; result.name = "LYWSDCGQ"; - } else if ((raw[2] == 0x5d) && (raw[3] == 0x01)) { // FlowerPot, RoPot + } else if (device_uuid == 0x015d) { // FlowerPot, RoPot result.type = XiaomiParseResult::TYPE_HHCCPOT002; result.name = "HHCCPOT002"; - } else if ((raw[2] == 0xdf) && (raw[3] == 0x02)) { // Xiaomi (Honeywell) formaldehyde sensor, OLED display + } else if (device_uuid == 0x02df) { // Xiaomi (Honeywell) formaldehyde sensor, OLED display result.type = XiaomiParseResult::TYPE_JQJCY01YM; result.name = "JQJCY01YM"; - } else if ((raw[2] == 0xdd) && (raw[3] == 0x03)) { // Philips/Xiaomi BLE nightlight + } else if (device_uuid == 0x03dd) { // Philips/Xiaomi BLE nightlight result.type = XiaomiParseResult::TYPE_MUE4094RT; result.name = "MUE4094RT"; result.raw_offset -= 6; - } else if ((raw[2] == 0x47 && raw[3] == 0x03) || // ClearGrass-branded, round body, e-ink display - (raw[2] == 0x48 && raw[3] == 0x0B)) { // Qingping-branded, round body, e-ink display — with bindkeys + } else if (device_uuid == 0x0347 || // ClearGrass-branded, round body, e-ink display + device_uuid == 0x0B48) { // Qingping-branded, round body, e-ink display — with bindkeys result.type = XiaomiParseResult::TYPE_CGG1; result.name = "CGG1"; - } else if ((raw[2] == 0xbc) && (raw[3] == 0x03)) { // VegTrug Grow Care Garden + } else if (device_uuid == 0x03bc) { // VegTrug Grow Care Garden result.type = XiaomiParseResult::TYPE_GCLS002; result.name = "GCLS002"; - } else if ((raw[2] == 0x5b) && (raw[3] == 0x04)) { // rectangular body, e-ink display + } else if (device_uuid == 0x045b) { // rectangular body, e-ink display result.type = XiaomiParseResult::TYPE_LYWSD02; result.name = "LYWSD02"; - } else if ((raw[2] == 0x0a) && (raw[3] == 0x04)) { // Mosquito Repellent Smart Version + } else if (device_uuid == 0x040a) { // Mosquito Repellent Smart Version result.type = XiaomiParseResult::TYPE_WX08ZM; result.name = "WX08ZM"; - } else if ((raw[2] == 0x76) && (raw[3] == 0x05)) { // Cleargrass (Qingping) alarm clock, segment LCD + } else if (device_uuid == 0x0576) { // Cleargrass (Qingping) alarm clock, segment LCD result.type = XiaomiParseResult::TYPE_CGD1; result.name = "CGD1"; - } else if ((raw[2] == 0x6F) && (raw[3] == 0x06)) { // Cleargrass (Qingping) Temp & RH Lite + } else if (device_uuid == 0x066F) { // Cleargrass (Qingping) Temp & RH Lite result.type = XiaomiParseResult::TYPE_CGDK2; result.name = "CGDK2"; - } else if ((raw[2] == 0x5b) && (raw[3] == 0x05)) { // small square body, segment LCD, encrypted + } else if (device_uuid == 0x055b) { // small square body, segment LCD, encrypted result.type = XiaomiParseResult::TYPE_LYWSD03MMC; result.name = "LYWSD03MMC"; - } else if ((raw[2] == 0xf6) && (raw[3] == 0x07)) { // Xiaomi-Yeelight BLE nightlight + } else if (device_uuid == 0x07f6) { // Xiaomi-Yeelight BLE nightlight result.type = XiaomiParseResult::TYPE_MJYD02YLA; result.name = "MJYD02YLA"; if (raw.size() == 19) result.raw_offset -= 6; - } else if ((raw[2] == 0xd3) && (raw[3] == 0x06)) { // rectangular body, e-ink display with alarm + } else if (device_uuid == 0x06d3) { // rectangular body, e-ink display with alarm result.type = XiaomiParseResult::TYPE_MHOC303; result.name = "MHOC303"; - } else if ((raw[2] == 0x87) && (raw[3] == 0x03)) { // square body, e-ink display + } else if (device_uuid == 0x0387) { // square body, e-ink display result.type = XiaomiParseResult::TYPE_MHOC401; result.name = "MHOC401"; - } else if ((raw[2] == 0x83) && (raw[3] == 0x0A)) { // Qingping-branded, motion & ambient light sensor + } else if (device_uuid == 0x0A83) { // Qingping-branded, motion & ambient light sensor result.type = XiaomiParseResult::TYPE_CGPR1; result.name = "CGPR1"; if (raw.size() == 19) result.raw_offset -= 6; + } else if (device_uuid == 0x0A8D) { // Xiaomi Mi Motion Sensor 2 + result.type = XiaomiParseResult::TYPE_RTCGQ02LM; + result.name = "RTCGQ02LM"; + if (raw.size() == 19) + result.raw_offset -= 6; } else { ESP_LOGVV(TAG, "parse_xiaomi_header(): unknown device, no magic bytes."); return {}; @@ -343,6 +357,9 @@ bool report_xiaomi_results(const optional &result, const std: if (result->is_light.has_value()) { ESP_LOGD(TAG, " Light: %s", (*result->is_light) ? "on" : "off"); } + if (result->button_press.has_value()) { + ESP_LOGD(TAG, " Button: %s", (*result->button_press) ? "pressed" : ""); + } return true; } diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.h b/esphome/components/xiaomi_ble/xiaomi_ble.h index ee65d7c82f..399bef83b8 100644 --- a/esphome/components/xiaomi_ble/xiaomi_ble.h +++ b/esphome/components/xiaomi_ble/xiaomi_ble.h @@ -1,7 +1,7 @@ #pragma once -#include "esphome/core/component.h" #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/core/component.h" #ifdef USE_ESP32 @@ -25,7 +25,8 @@ struct XiaomiParseResult { TYPE_MJYD02YLA, TYPE_MHOC303, TYPE_MHOC401, - TYPE_CGPR1 + TYPE_CGPR1, + TYPE_RTCGQ02LM, } type; std::string name; optional temperature; @@ -40,6 +41,7 @@ struct XiaomiParseResult { optional is_active; optional has_motion; optional is_light; + optional button_press; bool has_data; // 0x40 bool has_capability; // 0x20 bool has_encryption; // 0x08 @@ -61,7 +63,7 @@ struct XiaomiAESVector { size_t ivsize; }; -bool parse_xiaomi_value(uint8_t value_type, const uint8_t *data, uint8_t value_length, XiaomiParseResult &result); +bool parse_xiaomi_value(uint16_t value_type, const uint8_t *data, uint8_t value_length, XiaomiParseResult &result); bool parse_xiaomi_message(const std::vector &message, XiaomiParseResult &result); optional parse_xiaomi_header(const esp32_ble_tracker::ServiceData &service_data); bool decrypt_xiaomi_payload(std::vector &raw, const uint8_t *bindkey, const uint64_t &address); diff --git a/esphome/components/xiaomi_rtcgq02lm/__init__.py b/esphome/components/xiaomi_rtcgq02lm/__init__.py new file mode 100644 index 0000000000..0c8331db09 --- /dev/null +++ b/esphome/components/xiaomi_rtcgq02lm/__init__.py @@ -0,0 +1,36 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import esp32_ble_tracker +from esphome.const import CONF_MAC_ADDRESS, CONF_ID, CONF_BINDKEY + + +AUTO_LOAD = ["xiaomi_ble"] +CODEOWNERS = ["@jesserockz"] +DEPENDENCIES = ["esp32_ble_tracker"] +MULTI_CONF = True + +xiaomi_rtcgq02lm_ns = cg.esphome_ns.namespace("xiaomi_rtcgq02lm") +XiaomiRTCGQ02LM = xiaomi_rtcgq02lm_ns.class_( + "XiaomiRTCGQ02LM", esp32_ble_tracker.ESPBTDeviceListener, cg.Component +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(XiaomiRTCGQ02LM), + cv.Required(CONF_BINDKEY): cv.bind_key, + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + } + ) + .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await esp32_ble_tracker.register_ble_device(var, config) + + cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + cg.add(var.set_bindkey(config[CONF_BINDKEY])) diff --git a/esphome/components/xiaomi_rtcgq02lm/binary_sensor.py b/esphome/components/xiaomi_rtcgq02lm/binary_sensor.py new file mode 100644 index 0000000000..8eee10685e --- /dev/null +++ b/esphome/components/xiaomi_rtcgq02lm/binary_sensor.py @@ -0,0 +1,64 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import binary_sensor +from esphome.const import ( + CONF_LIGHT, + CONF_MOTION, + CONF_TIMEOUT, + DEVICE_CLASS_LIGHT, + DEVICE_CLASS_MOTION, + CONF_ID, +) +from esphome.core import TimePeriod + +from . import XiaomiRTCGQ02LM + +DEPENDENCIES = ["xiaomi_rtcgq02lm"] + +CONF_BUTTON = "button" + + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(XiaomiRTCGQ02LM), + cv.Optional(CONF_MOTION): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_MOTION + ).extend( + { + cv.Optional(CONF_TIMEOUT, default="5s"): cv.All( + cv.positive_time_period_milliseconds, + cv.Range(max=TimePeriod(milliseconds=65535)), + ), + } + ), + cv.Optional(CONF_LIGHT): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_LIGHT + ), + cv.Optional(CONF_BUTTON): binary_sensor.binary_sensor_schema().extend( + { + cv.Optional(CONF_TIMEOUT, default="200ms"): cv.All( + cv.positive_time_period_milliseconds, + cv.Range(max=TimePeriod(milliseconds=65535)), + ), + } + ), + } +) + + +async def to_code(config): + parent = await cg.get_variable(config[CONF_ID]) + + if CONF_MOTION in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_MOTION]) + cg.add(parent.set_motion(sens)) + cg.add(parent.set_motion_timeout(config[CONF_MOTION][CONF_TIMEOUT])) + + if CONF_LIGHT in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_LIGHT]) + cg.add(parent.set_light(sens)) + + if CONF_BUTTON in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_BUTTON]) + cg.add(parent.set_button(sens)) + cg.add(parent.set_button_timeout(config[CONF_BUTTON][CONF_TIMEOUT])) diff --git a/esphome/components/xiaomi_rtcgq02lm/sensor.py b/esphome/components/xiaomi_rtcgq02lm/sensor.py new file mode 100644 index 0000000000..558e3623e5 --- /dev/null +++ b/esphome/components/xiaomi_rtcgq02lm/sensor.py @@ -0,0 +1,37 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import ( + CONF_BATTERY_LEVEL, + ENTITY_CATEGORY_DIAGNOSTIC, + STATE_CLASS_MEASUREMENT, + UNIT_PERCENT, + CONF_ID, + DEVICE_CLASS_BATTERY, +) + +from . import XiaomiRTCGQ02LM + +DEPENDENCIES = ["xiaomi_rtcgq02lm"] + + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(XiaomiRTCGQ02LM), + cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } +) + + +async def to_code(config): + parent = await cg.get_variable(config[CONF_ID]) + + if CONF_BATTERY_LEVEL in config: + sens = await sensor.new_sensor(config[CONF_BATTERY_LEVEL]) + cg.add(parent.set_battery_level(sens)) diff --git a/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.cpp b/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.cpp new file mode 100644 index 0000000000..498e724368 --- /dev/null +++ b/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.cpp @@ -0,0 +1,91 @@ +#include "xiaomi_rtcgq02lm.h" +#include "esphome/core/log.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace xiaomi_rtcgq02lm { + +static const char *const TAG = "xiaomi_rtcgq02lm"; + +void XiaomiRTCGQ02LM::dump_config() { + ESP_LOGCONFIG(TAG, "Xiaomi RTCGQ02LM"); + ESP_LOGCONFIG(TAG, " Bindkey: %s", format_hex_pretty(this->bindkey_, 16).c_str()); +#ifdef USE_BINARY_SENSOR + LOG_BINARY_SENSOR(" ", "Motion", this->motion_); + LOG_BINARY_SENSOR(" ", "Light", this->light_); + LOG_BINARY_SENSOR(" ", "Button", this->button_); +#endif +#ifdef USE_SENSOR + LOG_SENSOR(" ", "Battery Level", this->battery_level_); +#endif +} + +bool XiaomiRTCGQ02LM::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + if (device.address_uint64() != this->address_) { + ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); + return false; + } + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + + bool success = false; + for (auto &service_data : device.get_service_datas()) { + auto res = xiaomi_ble::parse_xiaomi_header(service_data); + if (!res.has_value()) { + continue; + } + if (res->is_duplicate) { + continue; + } + if (res->has_encryption && + (!(xiaomi_ble::decrypt_xiaomi_payload(const_cast &>(service_data.data), this->bindkey_, + this->address_)))) { + continue; + } + if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) { + continue; + } + + if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) { + continue; + } +#ifdef USE_BINARY_SENSOR + if (res->has_motion.has_value() && this->motion_ != nullptr) { + this->motion_->publish_state(*res->has_motion); + this->set_timeout("motion_timeout", this->motion_timeout_, + [this, res]() { this->motion_->publish_state(false); }); + } + if (res->is_light.has_value() && this->light_ != nullptr) + this->light_->publish_state(*res->is_light); + if (res->button_press.has_value() && this->button_ != nullptr) { + this->button_->publish_state(*res->button_press); + this->set_timeout("button_timeout", this->button_timeout_, + [this, res]() { this->button_->publish_state(false); }); + } +#endif +#ifdef USE_SENSOR + if (res->battery_level.has_value() && this->battery_level_ != nullptr) + this->battery_level_->publish_state(*res->battery_level); +#endif + success = true; + } + + return success; +} + +void XiaomiRTCGQ02LM::set_bindkey(const std::string &bindkey) { + memset(bindkey_, 0, 16); + if (bindkey.size() != 32) { + return; + } + char temp[3] = {0}; + for (int i = 0; i < 16; i++) { + strncpy(temp, &(bindkey.c_str()[i * 2]), 2); + bindkey_[i] = std::strtoul(temp, nullptr, 16); + } +} + +} // namespace xiaomi_rtcgq02lm +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.h b/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.h new file mode 100644 index 0000000000..a16c5209d9 --- /dev/null +++ b/esphome/components/xiaomi_rtcgq02lm/xiaomi_rtcgq02lm.h @@ -0,0 +1,61 @@ +#pragma once + +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.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 +#include "esphome/components/xiaomi_ble/xiaomi_ble.h" +#include "esphome/core/component.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace xiaomi_rtcgq02lm { + +class XiaomiRTCGQ02LM : public Component, public esp32_ble_tracker::ESPBTDeviceListener { + public: + void set_address(uint64_t address) { address_ = address; }; + void set_bindkey(const std::string &bindkey); + + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + +#ifdef USE_BINARY_SENSOR + void set_motion(binary_sensor::BinarySensor *motion) { this->motion_ = motion; } + void set_motion_timeout(uint16_t timeout) { this->motion_timeout_ = timeout; } + + void set_light(binary_sensor::BinarySensor *light) { this->light_ = light; } + void set_button(binary_sensor::BinarySensor *button) { this->button_ = button; } + void set_button_timeout(uint16_t timeout) { this->button_timeout_ = timeout; } +#endif + +#ifdef USE_SENSOR + void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } +#endif + + protected: + uint64_t address_; + uint8_t bindkey_[16]; + +#ifdef USE_BINARY_SENSOR + uint16_t motion_timeout_; + uint16_t button_timeout_; + + binary_sensor::BinarySensor *motion_{nullptr}; + binary_sensor::BinarySensor *light_{nullptr}; + binary_sensor::BinarySensor *button_{nullptr}; +#endif +#ifdef USE_SENSOR + sensor::Sensor *battery_level_{nullptr}; +#endif +}; + +} // namespace xiaomi_rtcgq02lm +} // namespace esphome + +#endif diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 074bea6fd1..0972d6ccd6 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -173,6 +173,10 @@ constexpr uint32_t encode_uint32(uint8_t byte1, uint8_t byte2, uint8_t byte3, ui return (static_cast(byte1) << 24) | (static_cast(byte2) << 16) | (static_cast(byte3) << 8) | (static_cast(byte4)); } +/// Encode a 24-bit value given three bytes in most to least significant byte order. +constexpr uint32_t encode_uint24(uint8_t byte1, uint8_t byte2, uint8_t byte3) { + return ((static_cast(byte1) << 16) | (static_cast(byte2) << 8) | (static_cast(byte3))); +} /// Encode a value from its constituent bytes (from most to least significant) in an array with length sizeof(T). template::value, int> = 0> diff --git a/tests/test2.yaml b/tests/test2.yaml index ec3ccff70c..a7a9ef9661 100644 --- a/tests/test2.yaml +++ b/tests/test2.yaml @@ -263,6 +263,10 @@ sensor: name: 'Inkbird IBS-TH1 Humidity' battery_level: name: 'Inkbird IBS-TH1 Battery Level' + - platform: xiaomi_rtcgq02lm + id: motion_rtcgq02lm + battery_level: + name: 'Mi Motion Sensor 2 Battery level' - platform: ltr390 uv: name: "LTR390 UV" @@ -417,6 +421,14 @@ binary_sensor: name: 'CGPR1 Idle Time' illuminance: name: 'CGPR1 Illuminance' + - platform: xiaomi_rtcgq02lm + id: motion_rtcgq02lm + motion: + name: 'Mi Motion Sensor 2' + light: + name: 'Mi Motion Sensor 2 Light' + button: + name: 'Mi Motion Sensor 2 Button' esp32_ble_tracker: on_ble_advertise: @@ -457,6 +469,11 @@ xiaomi_ble: mopeka_ble: +xiaomi_rtcgq02lm: + - id: motion_rtcgq02lm + mac_address: 01:02:03:04:05:06 + bindkey: '48403ebe2d385db8d0c187f81e62cb64' + #esp32_ble_beacon: # type: iBeacon # uuid: 'c29ce823-e67a-4e71-bff2-abaa32e77a98' From 8b2c032da6a30c049dd89218b3c533cccde63308 Mon Sep 17 00:00:00 2001 From: anatoly-savchenkov <48646998+anatoly-savchenkov@users.noreply.github.com> Date: Tue, 12 Apr 2022 08:03:32 +0300 Subject: [PATCH 0378/1729] Add Sonoff D1 Dimmer support (#2775) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/sonoff_d1/__init__.py | 1 + esphome/components/sonoff_d1/light.py | 43 +++ esphome/components/sonoff_d1/sonoff_d1.cpp | 308 +++++++++++++++++++++ esphome/components/sonoff_d1/sonoff_d1.h | 85 ++++++ tests/test3.yaml | 6 + 6 files changed, 444 insertions(+) create mode 100644 esphome/components/sonoff_d1/__init__.py create mode 100644 esphome/components/sonoff_d1/light.py create mode 100644 esphome/components/sonoff_d1/sonoff_d1.cpp create mode 100644 esphome/components/sonoff_d1/sonoff_d1.h diff --git a/CODEOWNERS b/CODEOWNERS index 5a1220354a..79626c4a38 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -177,6 +177,7 @@ esphome/components/shutdown/* @esphome/core @jsuanet esphome/components/sim800l/* @glmnet esphome/components/sm2135/* @BoukeHaarsma23 esphome/components/socket/* @esphome/core +esphome/components/sonoff_d1/* @anatoly-savchenkov esphome/components/spi/* @esphome/core esphome/components/ssd1322_base/* @kbx81 esphome/components/ssd1322_spi/* @kbx81 diff --git a/esphome/components/sonoff_d1/__init__.py b/esphome/components/sonoff_d1/__init__.py new file mode 100644 index 0000000000..18b4d30d18 --- /dev/null +++ b/esphome/components/sonoff_d1/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@anatoly-savchenkov"] diff --git a/esphome/components/sonoff_d1/light.py b/esphome/components/sonoff_d1/light.py new file mode 100644 index 0000000000..8ffe60224e --- /dev/null +++ b/esphome/components/sonoff_d1/light.py @@ -0,0 +1,43 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import uart, light +from esphome.const import ( + CONF_OUTPUT_ID, + CONF_MIN_VALUE, + CONF_MAX_VALUE, +) + +CONF_USE_RM433_REMOTE = "use_rm433_remote" + +DEPENDENCIES = ["uart", "light"] + +sonoff_d1_ns = cg.esphome_ns.namespace("sonoff_d1") +SonoffD1Output = sonoff_d1_ns.class_( + "SonoffD1Output", cg.Component, uart.UARTDevice, light.LightOutput +) + +CONFIG_SCHEMA = ( + light.BRIGHTNESS_ONLY_LIGHT_SCHEMA.extend( + { + cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(SonoffD1Output), + cv.Optional(CONF_USE_RM433_REMOTE, default=False): cv.boolean, + cv.Optional(CONF_MIN_VALUE, default=0): cv.int_range(min=0, max=100), + cv.Optional(CONF_MAX_VALUE, default=100): cv.int_range(min=0, max=100), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(uart.UART_DEVICE_SCHEMA) +) +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( + "sonoff_d1", baud_rate=9600, require_tx=True, require_rx=True +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) + cg.add(var.set_use_rm433_remote(config[CONF_USE_RM433_REMOTE])) + cg.add(var.set_min_value(config[CONF_MIN_VALUE])) + cg.add(var.set_max_value(config[CONF_MAX_VALUE])) + await light.register_light(var, config) diff --git a/esphome/components/sonoff_d1/sonoff_d1.cpp b/esphome/components/sonoff_d1/sonoff_d1.cpp new file mode 100644 index 0000000000..b4bcbc6760 --- /dev/null +++ b/esphome/components/sonoff_d1/sonoff_d1.cpp @@ -0,0 +1,308 @@ +/* + sonoff_d1.cpp - Sonoff D1 Dimmer support for ESPHome + + Copyright © 2021 Anatoly Savchenkov + Copyright © 2020 Jeff Rescignano + + Permission is hereby granted, free of charge, to any person obtaining a copy of this software + and associated documentation files (the “Software”), to deal in the Software without + restriction, including without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom + the Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all copies or + substantial portions of the Software. + + THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING + BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + ----- + + If modifying this file, in addition to the license above, please ensure to include links back to the original code: + https://jeffresc.dev/blog/2020-10-10 + https://github.com/JeffResc/Sonoff-D1-Dimmer + https://github.com/arendst/Tasmota/blob/2d4a6a29ebc7153dbe2717e3615574ac1c84ba1d/tasmota/xdrv_37_sonoff_d1.ino#L119-L131 + + ----- +*/ + +/*********************************************************************************************\ + * Sonoff D1 dimmer 433 + * Mandatory/Optional + * ^ 0 1 2 3 4 5 6 7 8 9 A B C D E F 10 + * M AA 55 - Header + * M 01 04 - Version? + * M 00 0A - Following data length (10 bytes) + * O 01 - Power state (00 = off, 01 = on, FF = ignore) + * O 64 - Dimmer percentage (01 to 64 = 1 to 100%, 0 - ignore) + * O FF FF FF FF FF FF FF FF - Not used + * M 6C - CRC over bytes 2 to F (Addition) +\*********************************************************************************************/ +#include +#include "sonoff_d1.h" + +namespace esphome { +namespace sonoff_d1 { + +static const char *const TAG = "sonoff_d1"; + +uint8_t SonoffD1Output::calc_checksum_(const uint8_t *cmd, const size_t len) { + uint8_t crc = 0; + for (int i = 2; i < len - 1; i++) { + crc += cmd[i]; + } + return crc; +} + +void SonoffD1Output::populate_checksum_(uint8_t *cmd, const size_t len) { + // Update the checksum + cmd[len - 1] = this->calc_checksum_(cmd, len); +} + +void SonoffD1Output::skip_command_() { + size_t garbage = 0; + // Read out everything from the UART FIFO + while (this->available()) { + uint8_t value = this->read(); + ESP_LOGW(TAG, "[%04d] Skip %02d: 0x%02x from the dimmer", this->write_count_, garbage, value); + garbage++; + } + + // Warn about unexpected bytes in the protocol with UART dimmer + if (garbage) + ESP_LOGW(TAG, "[%04d] Skip %d bytes from the dimmer", this->write_count_, garbage); +} + +// This assumes some data is already available +bool SonoffD1Output::read_command_(uint8_t *cmd, size_t &len) { + // Do consistency check + if (cmd == nullptr || len < 7) { + ESP_LOGW(TAG, "[%04d] Too short command buffer (actual len is %d bytes, minimal is 7)", this->write_count_, len); + return false; + } + + // Read a minimal packet + if (this->read_array(cmd, 6)) { + ESP_LOGV(TAG, "[%04d] Reading from dimmer:", this->write_count_); + ESP_LOGV(TAG, "[%04d] %s", this->write_count_, format_hex_pretty(cmd, 6).c_str()); + + if (cmd[0] != 0xAA || cmd[1] != 0x55) { + ESP_LOGW(TAG, "[%04d] RX: wrong header (%x%x, must be AA55)", this->write_count_, cmd[0], cmd[1]); + this->skip_command_(); + return false; + } + if ((cmd[5] + 7 /*mandatory header + crc suffix length*/) > len) { + ESP_LOGW(TAG, "[%04d] RX: Payload length is unexpected (%d, max expected %d)", this->write_count_, cmd[5], + len - 7); + this->skip_command_(); + return false; + } + if (this->read_array(&cmd[6], cmd[5] + 1 /*checksum suffix*/)) { + ESP_LOGV(TAG, "[%04d] %s", this->write_count_, format_hex_pretty(&cmd[6], cmd[5] + 1).c_str()); + + // Check the checksum + uint8_t valid_checksum = this->calc_checksum_(cmd, cmd[5] + 7); + if (valid_checksum != cmd[cmd[5] + 7 - 1]) { + ESP_LOGW(TAG, "[%04d] RX: checksum mismatch (%d, expected %d)", this->write_count_, cmd[cmd[5] + 7 - 1], + valid_checksum); + this->skip_command_(); + return false; + } + len = cmd[5] + 7 /*mandatory header + suffix length*/; + + // Read remaining gardbled data (just in case, I don't see where this can appear now) + this->skip_command_(); + return true; + } + } else { + ESP_LOGW(TAG, "[%04d] RX: feedback timeout", this->write_count_); + this->skip_command_(); + } + return false; +} + +bool SonoffD1Output::read_ack_(const uint8_t *cmd, const size_t len) { + // Expected acknowledgement from rf chip + uint8_t ref_buffer[7] = {0xAA, 0x55, cmd[2], cmd[3], 0x00, 0x00, 0x00}; + uint8_t buffer[sizeof(ref_buffer)] = {0}; + uint32_t pos = 0, buf_len = sizeof(ref_buffer); + + // Update the reference checksum + this->populate_checksum_(ref_buffer, sizeof(ref_buffer)); + + // Read ack code, this either reads 7 bytes or exits with a timeout + this->read_command_(buffer, buf_len); + + // Compare response with expected response + while (pos < sizeof(ref_buffer) && ref_buffer[pos] == buffer[pos]) { + pos++; + } + if (pos == sizeof(ref_buffer)) { + ESP_LOGD(TAG, "[%04d] Acknowledge received", this->write_count_); + return true; + } else { + ESP_LOGW(TAG, "[%04d] Unexpected acknowledge received (possible clash of RF/HA commands), expected ack was:", + this->write_count_); + ESP_LOGW(TAG, "[%04d] %s", this->write_count_, format_hex_pretty(ref_buffer, sizeof(ref_buffer)).c_str()); + } + return false; +} + +bool SonoffD1Output::write_command_(uint8_t *cmd, const size_t len, bool needs_ack) { + // Do some consistency checks + if (len < 7) { + ESP_LOGW(TAG, "[%04d] Too short command (actual len is %d bytes, minimal is 7)", this->write_count_, len); + return false; + } + if (cmd[0] != 0xAA || cmd[1] != 0x55) { + ESP_LOGW(TAG, "[%04d] Wrong header (%x%x, must be AA55)", this->write_count_, cmd[0], cmd[1]); + return false; + } + if ((cmd[5] + 7 /*mandatory header + suffix length*/) != len) { + ESP_LOGW(TAG, "[%04d] Payload length field does not match packet lenght (%d, expected %d)", this->write_count_, + cmd[5], len - 7); + return false; + } + this->populate_checksum_(cmd, len); + + // Need retries here to handle the following cases: + // 1. On power up companion MCU starts to respond with a delay, so few first commands are ignored + // 2. UART command initiated by this component can clash with a command initiated by RF + uint32_t retries = 10; + do { + ESP_LOGV(TAG, "[%04d] Writing to the dimmer:", this->write_count_); + ESP_LOGV(TAG, "[%04d] %s", this->write_count_, format_hex_pretty(cmd, len).c_str()); + this->write_array(cmd, len); + this->write_count_++; + if (!needs_ack) + return true; + retries--; + } while (!this->read_ack_(cmd, len) && retries > 0); + + if (retries) { + return true; + } else { + ESP_LOGE(TAG, "[%04d] Unable to write to the dimmer", this->write_count_); + } + return false; +} + +bool SonoffD1Output::control_dimmer_(const bool binary, const uint8_t brightness) { + // Include our basic code from the Tasmota project, thank you again! + // 0 1 2 3 4 5 6 7 8 + uint8_t cmd[17] = {0xAA, 0x55, 0x01, 0x04, 0x00, 0x0A, 0x00, 0x00, 0xFF, + // 9 10 11 12 13 14 15 16 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00}; + + cmd[6] = binary; + cmd[7] = remap(brightness, 0, 100, this->min_value_, this->max_value_); + ESP_LOGI(TAG, "[%04d] Setting dimmer state to %s, raw brightness=%d", this->write_count_, ONOFF(binary), cmd[7]); + return this->write_command_(cmd, sizeof(cmd)); +} + +void SonoffD1Output::process_command_(const uint8_t *cmd, const size_t len) { + if (cmd[2] == 0x01 && cmd[3] == 0x04 && cmd[4] == 0x00 && cmd[5] == 0x0A) { + uint8_t ack_buffer[7] = {0xAA, 0x55, cmd[2], cmd[3], 0x00, 0x00, 0x00}; + // Ack a command from RF to ESP to prevent repeating commands + this->write_command_(ack_buffer, sizeof(ack_buffer), false); + ESP_LOGI(TAG, "[%04d] RF sets dimmer state to %s, raw brightness=%d", this->write_count_, ONOFF(cmd[6]), cmd[7]); + const uint8_t new_brightness = remap(cmd[7], this->min_value_, this->max_value_, 0, 100); + const bool new_state = cmd[6]; + + // Got light change state command. In all cases we revert the command immediately + // since we want to rely on ESP controlled transitions + if (new_state != this->last_binary_ || new_brightness != this->last_brightness_) { + this->control_dimmer_(this->last_binary_, this->last_brightness_); + } + + if (!this->use_rm433_remote_) { + // If RF remote is not used, this is a known ghost RF command + ESP_LOGI(TAG, "[%04d] Ghost command from RF detected, reverted", this->write_count_); + } else { + // If remote is used, initiate transition to the new state + this->publish_state_(new_state, new_brightness); + } + } else { + ESP_LOGW(TAG, "[%04d] Unexpected command received", this->write_count_); + } +} + +void SonoffD1Output::publish_state_(const bool is_on, const uint8_t brightness) { + if (light_state_) { + ESP_LOGV(TAG, "Publishing new state: %s, brightness=%d", ONOFF(is_on), brightness); + auto call = light_state_->make_call(); + call.set_state(is_on); + if (brightness != 0) { + // Brightness equal to 0 has a special meaning. + // D1 uses 0 as "previously set brightness". + // Usually zero brightness comes inside light ON command triggered by RF remote. + // Since we unconditionally override commands coming from RF remote in process_command_(), + // here we mimic the original behavior but with LightCall functionality + call.set_brightness((float) brightness / 100.0f); + } + call.perform(); + } +} + +// Set the device's traits +light::LightTraits SonoffD1Output::get_traits() { + auto traits = light::LightTraits(); + traits.set_supported_color_modes({light::ColorMode::BRIGHTNESS}); + return traits; +} + +void SonoffD1Output::write_state(light::LightState *state) { + bool binary; + float brightness; + + // Fill our variables with the device's current state + state->current_values_as_binary(&binary); + state->current_values_as_brightness(&brightness); + + // Convert ESPHome's brightness (0-1) to the device's internal brightness (0-100) + const uint8_t calculated_brightness = std::round(brightness * 100); + + if (calculated_brightness == 0) { + // if(binary) ESP_LOGD(TAG, "current_values_as_binary() returns true for zero brightness"); + binary = false; + } + + // If a new value, write to the dimmer + if (binary != this->last_binary_ || calculated_brightness != this->last_brightness_) { + if (this->control_dimmer_(binary, calculated_brightness)) { + this->last_brightness_ = calculated_brightness; + this->last_binary_ = binary; + } else { + // Return to original value if failed to write to the dimmer + // TODO: Test me, can be tested if high-voltage part is not connected + ESP_LOGW(TAG, "Failed to update the dimmer, publishing the previous state"); + this->publish_state_(this->last_binary_, this->last_brightness_); + } + } +} + +void SonoffD1Output::dump_config() { + ESP_LOGCONFIG(TAG, "Sonoff D1 Dimmer: '%s'", this->light_state_ ? this->light_state_->get_name().c_str() : ""); + ESP_LOGCONFIG(TAG, " Use RM433 Remote: %s", ONOFF(this->use_rm433_remote_)); + ESP_LOGCONFIG(TAG, " Minimal brightness: %d", this->min_value_); + ESP_LOGCONFIG(TAG, " Maximal brightness: %d", this->max_value_); +} + +void SonoffD1Output::loop() { + // Read commands from the dimmer + // RF chip notifies ESP about remotely changed state with the same commands as we send + if (this->available()) { + ESP_LOGV(TAG, "Have some UART data in loop()"); + uint8_t buffer[17] = {0}; + size_t len = sizeof(buffer); + if (this->read_command_(buffer, len)) { + this->process_command_(buffer, len); + } + } +} + +} // namespace sonoff_d1 +} // namespace esphome diff --git a/esphome/components/sonoff_d1/sonoff_d1.h b/esphome/components/sonoff_d1/sonoff_d1.h new file mode 100644 index 0000000000..4df0f5afb2 --- /dev/null +++ b/esphome/components/sonoff_d1/sonoff_d1.h @@ -0,0 +1,85 @@ +#pragma once + +/* + sonoff_d1.h - Sonoff D1 Dimmer support for ESPHome + + Copyright © 2021 Anatoly Savchenkov + Copyright © 2020 Jeff Rescignano + + Permission is hereby granted, free of charge, to any person obtaining a copy of this software + and associated documentation files (the “Software”), to deal in the Software without + restriction, including without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom + the Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all copies or + substantial portions of the Software. + + THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING + BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + ----- + + If modifying this file, in addition to the license above, please ensure to include links back to the original code: + https://jeffresc.dev/blog/2020-10-10 + https://github.com/JeffResc/Sonoff-D1-Dimmer + https://github.com/arendst/Tasmota/blob/2d4a6a29ebc7153dbe2717e3615574ac1c84ba1d/tasmota/xdrv_37_sonoff_d1.ino#L119-L131 + + ----- +*/ + +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" +#include "esphome/core/component.h" +#include "esphome/components/uart/uart.h" +#include "esphome/components/light/light_output.h" +#include "esphome/components/light/light_state.h" +#include "esphome/components/light/light_traits.h" + +namespace esphome { +namespace sonoff_d1 { + +class SonoffD1Output : public light::LightOutput, public uart::UARTDevice, public Component { + public: + // LightOutput methods + light::LightTraits get_traits() override; + void setup_state(light::LightState *state) override { this->light_state_ = state; } + void write_state(light::LightState *state) override; + + // Component methods + void setup() override{}; + void loop() override; + void dump_config() override; + float get_setup_priority() const override { return esphome::setup_priority::DATA; } + + // Custom methods + void set_use_rm433_remote(const bool use_rm433_remote) { this->use_rm433_remote_ = use_rm433_remote; } + void set_min_value(const uint8_t min_value) { this->min_value_ = min_value; } + void set_max_value(const uint8_t max_value) { this->max_value_ = max_value; } + + protected: + uint8_t min_value_{0}; + uint8_t max_value_{100}; + bool use_rm433_remote_{false}; + bool last_binary_{false}; + uint8_t last_brightness_{0}; + int write_count_{0}; + int read_count_{0}; + light::LightState *light_state_{nullptr}; + + uint8_t calc_checksum_(const uint8_t *cmd, size_t len); + void populate_checksum_(uint8_t *cmd, size_t len); + void skip_command_(); + bool read_command_(uint8_t *cmd, size_t &len); + bool read_ack_(const uint8_t *cmd, size_t len); + bool write_command_(uint8_t *cmd, size_t len, bool needs_ack = true); + bool control_dimmer_(bool binary, uint8_t brightness); + void process_command_(const uint8_t *cmd, size_t len); + void publish_state_(bool is_on, uint8_t brightness); +}; + +} // namespace sonoff_d1 +} // namespace esphome diff --git a/tests/test3.yaml b/tests/test3.yaml index a6ad6b9e92..58cb14740f 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -1229,6 +1229,12 @@ light: name: Icicle Lights pin_a: out pin_b: out2 + - platform: sonoff_d1 + uart_id: uart2 + use_rm433_remote: False + name: Sonoff D1 Dimmer + id: d1_light + restore_mode: RESTORE_DEFAULT_OFF servo: id: my_servo From 7895cd92cd8d05d72fdb6631bd17151aff207e2e Mon Sep 17 00:00:00 2001 From: cvwillegen Date: Tue, 12 Apr 2022 21:39:38 +0200 Subject: [PATCH 0379/1729] Remote base pronto receive (#2826) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- .../remote_base/pronto_protocol.cpp | 101 +++++++++++++++++- .../components/remote_base/pronto_protocol.h | 12 ++- 2 files changed, 110 insertions(+), 3 deletions(-) diff --git a/esphome/components/remote_base/pronto_protocol.cpp b/esphome/components/remote_base/pronto_protocol.cpp index 4f6ace720c..a2b1a16e07 100644 --- a/esphome/components/remote_base/pronto_protocol.cpp +++ b/esphome/components/remote_base/pronto_protocol.cpp @@ -40,6 +40,24 @@ namespace remote_base { static const char *const TAG = "remote.pronto"; +bool ProntoData::operator==(const ProntoData &rhs) const { + std::vector data1 = encode_pronto(data); + std::vector data2 = encode_pronto(rhs.data); + + uint32_t total_diff = 0; + // Don't need to check the last one, it's the large gap at the end. + for (std::vector::size_type i = 0; i < data1.size() - 1; ++i) { + int diff = data2[i] - data1[i]; + diff *= diff; + if (diff > 9) + return false; + + total_diff += diff; + } + + return total_diff <= data1.size() * 3; +} + // DO NOT EXPORT from this file static const uint16_t MICROSECONDS_T_MAX = 0xFFFFU; static const uint16_t LEARNED_TOKEN = 0x0000U; @@ -52,6 +70,7 @@ static const uint32_t REFERENCE_FREQUENCY = 4145146UL; static const uint16_t FALLBACK_FREQUENCY = 64767U; // To use with frequency = 0; static const uint32_t MICROSECONDS_IN_SECONDS = 1000000UL; static const uint16_t PRONTO_DEFAULT_GAP = 45000; +static const uint16_t MARK_EXCESS_MICROS = 20; static uint16_t to_frequency_k_hz(uint16_t code) { if (code == 0) @@ -107,7 +126,7 @@ void ProntoProtocol::send_pronto_(RemoteTransmitData *dst, const std::vector encode_pronto(const std::string &str) { size_t len = str.length() / (DIGITS_IN_PRONTO_NUMBER + 1) + 1; std::vector data; const char *p = str.c_str(); @@ -122,12 +141,90 @@ void ProntoProtocol::send_pronto_(RemoteTransmitData *dst, const std::string &st data.push_back(x); // If input is conforming, there can be no overflow! p = *endptr; } + + return data; +} + +void ProntoProtocol::send_pronto_(RemoteTransmitData *dst, const std::string &str) { + std::vector data = encode_pronto(str); send_pronto_(dst, data); } void ProntoProtocol::encode(RemoteTransmitData *dst, const ProntoData &data) { send_pronto_(dst, data.data); } -optional ProntoProtocol::decode(RemoteReceiveData src) { return {}; } +uint16_t ProntoProtocol::effective_frequency_(uint16_t frequency) { + return frequency > 0 ? frequency : FALLBACK_FREQUENCY; +} + +uint16_t ProntoProtocol::to_timebase_(uint16_t frequency) { + return MICROSECONDS_IN_SECONDS / effective_frequency_(frequency); +} + +uint16_t ProntoProtocol::to_frequency_code_(uint16_t frequency) { + return REFERENCE_FREQUENCY / effective_frequency_(frequency); +} + +std::string ProntoProtocol::dump_digit_(uint8_t x) { + return std::string(1, (char) (x <= 9 ? ('0' + x) : ('A' + (x - 10)))); +} + +std::string ProntoProtocol::dump_number_(uint16_t number, bool end /* = false */) { + std::string num; + + for (uint8_t i = 0; i < DIGITS_IN_PRONTO_NUMBER; ++i) { + uint8_t shifts = BITS_IN_HEXADECIMAL * (DIGITS_IN_PRONTO_NUMBER - 1 - i); + num += dump_digit_((number >> shifts) & HEX_MASK); + } + + if (!end) + num += ' '; + + return num; +} + +std::string ProntoProtocol::dump_duration_(uint32_t duration, uint16_t timebase, bool end /* = false */) { + return dump_number_((duration + timebase / 2) / timebase, end); +} + +std::string ProntoProtocol::compensate_and_dump_sequence_(std::vector *data, uint16_t timebase) { + std::string out; + + for (std::vector::size_type i = 0; i < data->size() - 1; i++) { + int32_t t_length = data->at(i); + uint32_t t_duration; + if (t_length > 0) { + // Mark + t_duration = t_length - MARK_EXCESS_MICROS; + } else { + t_duration = -t_length + MARK_EXCESS_MICROS; + } + out += dump_duration_(t_duration, timebase); + } + + // append minimum gap + out += dump_duration_(PRONTO_DEFAULT_GAP, timebase, true); + + return out; +} + +optional ProntoProtocol::decode(RemoteReceiveData src) { + ProntoData out; + + uint16_t frequency = 38000U; + std::vector *data = src.get_raw_data(); + std::string prontodata; + + prontodata += dump_number_(frequency > 0 ? LEARNED_TOKEN : LEARNED_NON_MODULATED_TOKEN); + prontodata += dump_number_(to_frequency_code_(frequency)); + prontodata += dump_number_((data->size() + 1) / 2); + prontodata += dump_number_(0); + uint16_t timebase = to_timebase_(frequency); + prontodata += compensate_and_dump_sequence_(data, timebase); + + out.data = prontodata; + + return out; +} void ProntoProtocol::dump(const ProntoData &data) { ESP_LOGD(TAG, "Received Pronto: data=%s", data.data.c_str()); } diff --git a/esphome/components/remote_base/pronto_protocol.h b/esphome/components/remote_base/pronto_protocol.h index e96511383f..291bb8a99b 100644 --- a/esphome/components/remote_base/pronto_protocol.h +++ b/esphome/components/remote_base/pronto_protocol.h @@ -6,10 +6,12 @@ namespace esphome { namespace remote_base { +std::vector encode_pronto(const std::string &str); + struct ProntoData { std::string data; - bool operator==(const ProntoData &rhs) const { return data == rhs.data; } + bool operator==(const ProntoData &rhs) const; }; class ProntoProtocol : public RemoteProtocol { @@ -17,6 +19,14 @@ class ProntoProtocol : public RemoteProtocol { void send_pronto_(RemoteTransmitData *dst, const std::vector &data); void send_pronto_(RemoteTransmitData *dst, const std::string &str); + uint16_t effective_frequency_(uint16_t frequency); + uint16_t to_timebase_(uint16_t frequency); + uint16_t to_frequency_code_(uint16_t frequency); + std::string dump_digit_(uint8_t x); + std::string dump_number_(uint16_t number, bool end = false); + std::string dump_duration_(uint32_t duration, uint16_t timebase, bool end = false); + std::string compensate_and_dump_sequence_(std::vector *data, uint16_t timebase); + public: void encode(RemoteTransmitData *dst, const ProntoData &data) override; optional decode(RemoteReceiveData src) override; From 99335d986e8e90907639bc1c76038ed06319abf0 Mon Sep 17 00:00:00 2001 From: Janez Troha <239513+dz0ny@users.noreply.github.com> Date: Wed, 13 Apr 2022 00:14:21 +0200 Subject: [PATCH 0380/1729] Use correct http defines (#3378) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/web_server/web_server.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 73813ecfa1..bd7acd91a0 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -88,12 +88,12 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle an index request under '/'. void handle_index_request(AsyncWebServerRequest *request); -#ifdef WEBSERVER_CSS_INCLUDE +#ifdef USE_WEBSERVER_CSS_INCLUDE /// Handle included css request under '/0.css'. void handle_css_request(AsyncWebServerRequest *request); #endif -#ifdef WEBSERVER_JS_INCLUDE +#ifdef USE_WEBSERVER_JS_INCLUDE /// Handle included js request under '/0.js'. void handle_js_request(AsyncWebServerRequest *request); #endif From d620b6dd5e5193ad37abd91b1626c89fa49f0276 Mon Sep 17 00:00:00 2001 From: Martin <25747549+martgras@users.noreply.github.com> Date: Wed, 13 Apr 2022 00:19:48 +0200 Subject: [PATCH 0381/1729] Refactor Sensirion Sensors (#3374) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/i2c/i2c_bus.h | 1 + esphome/components/scd30/scd30.cpp | 91 ++-------- esphome/components/scd30/scd30.h | 8 +- esphome/components/scd30/sensor.py | 6 +- esphome/components/scd4x/scd4x.cpp | 111 ++----------- esphome/components/scd4x/scd4x.h | 8 +- esphome/components/scd4x/sensor.py | 7 +- esphome/components/sdp3x/sdp3x.cpp | 79 +++------ esphome/components/sdp3x/sdp3x.h | 6 +- esphome/components/sdp3x/sensor.py | 6 +- .../components/sensirion_common/__init__.py | 10 ++ .../sensirion_common/i2c_sensirion.cpp | 128 +++++++++++++++ .../sensirion_common/i2c_sensirion.h | 155 ++++++++++++++++++ esphome/components/sgp30/sensor.py | 13 +- esphome/components/sgp30/sgp30.cpp | 79 +-------- esphome/components/sgp30/sgp30.h | 7 +- esphome/components/sgp40/sensor.py | 13 +- esphome/components/sgp40/sgp40.cpp | 116 +++---------- esphome/components/sgp40/sgp40.h | 8 +- esphome/components/sht3xd/sensor.py | 5 +- esphome/components/sht3xd/sht3xd.cpp | 64 +------- esphome/components/sht3xd/sht3xd.h | 7 +- esphome/components/sht4x/sensor.py | 7 +- esphome/components/sht4x/sht4x.cpp | 17 +- esphome/components/sht4x/sht4x.h | 4 +- esphome/components/shtcx/sensor.py | 7 +- esphome/components/shtcx/shtcx.cpp | 65 +------- esphome/components/shtcx/shtcx.h | 6 +- esphome/components/sps30/sensor.py | 7 +- esphome/components/sps30/sps30.cpp | 81 +-------- esphome/components/sps30/sps30.h | 7 +- esphome/components/sts3x/sensor.py | 1 + esphome/components/sts3x/sts3x.cpp | 61 +------ esphome/components/sts3x/sts3x.h | 8 +- esphome/const.py | 2 + 36 files changed, 484 insertions(+), 718 deletions(-) create mode 100644 esphome/components/sensirion_common/__init__.py create mode 100644 esphome/components/sensirion_common/i2c_sensirion.cpp create mode 100644 esphome/components/sensirion_common/i2c_sensirion.h diff --git a/CODEOWNERS b/CODEOWNERS index 79626c4a38..7595fc52e2 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -170,6 +170,7 @@ esphome/components/sdm_meter/* @jesserockz @polyfaces esphome/components/sdp3x/* @Azimath esphome/components/selec_meter/* @sourabhjaiswal esphome/components/select/* @esphome/core +esphome/components/sensirion_common/* @martgras esphome/components/sensor/* @esphome/core esphome/components/sgp40/* @SenexCrenshaw esphome/components/sht4x/* @sjtrny diff --git a/esphome/components/i2c/i2c_bus.h b/esphome/components/i2c/i2c_bus.h index c07a2dd1dd..2633a7adf6 100644 --- a/esphome/components/i2c/i2c_bus.h +++ b/esphome/components/i2c/i2c_bus.h @@ -15,6 +15,7 @@ enum ErrorCode { ERROR_NOT_INITIALIZED = 4, ERROR_TOO_LARGE = 5, ERROR_UNKNOWN = 6, + ERROR_CRC = 7, }; struct ReadBuffer { diff --git a/esphome/components/scd30/scd30.cpp b/esphome/components/scd30/scd30.cpp index 8603072bd5..103b7a255d 100644 --- a/esphome/components/scd30/scd30.cpp +++ b/esphome/components/scd30/scd30.cpp @@ -33,14 +33,8 @@ void SCD30Component::setup() { #endif /// Firmware version identification - if (!this->write_command_(SCD30_CMD_GET_FIRMWARE_VERSION)) { - this->error_code_ = COMMUNICATION_FAILED; - this->mark_failed(); - return; - } uint16_t raw_firmware_version[3]; - - if (!this->read_data_(raw_firmware_version, 3)) { + if (!this->get_register(SCD30_CMD_GET_FIRMWARE_VERSION, raw_firmware_version, 3)) { this->error_code_ = FIRMWARE_IDENTIFICATION_FAILED; this->mark_failed(); return; @@ -49,7 +43,7 @@ void SCD30Component::setup() { uint16_t(raw_firmware_version[0] & 0xFF)); if (this->temperature_offset_ != 0) { - if (!this->write_command_(SCD30_CMD_TEMPERATURE_OFFSET, (uint16_t)(temperature_offset_ * 100.0))) { + if (!this->write_command(SCD30_CMD_TEMPERATURE_OFFSET, (uint16_t)(temperature_offset_ * 100.0))) { ESP_LOGE(TAG, "Sensor SCD30 error setting temperature offset."); this->error_code_ = MEASUREMENT_INIT_FAILED; this->mark_failed(); @@ -69,7 +63,7 @@ void SCD30Component::setup() { delay(30); #endif - if (!this->write_command_(SCD30_CMD_MEASUREMENT_INTERVAL, update_interval_)) { + if (!this->write_command(SCD30_CMD_MEASUREMENT_INTERVAL, update_interval_)) { ESP_LOGE(TAG, "Sensor SCD30 error setting update interval."); this->error_code_ = MEASUREMENT_INIT_FAILED; this->mark_failed(); @@ -81,7 +75,7 @@ void SCD30Component::setup() { // The start measurement command disables the altitude compensation, if any, so we only set it if it's turned on if (this->altitude_compensation_ != 0xFFFF) { - if (!this->write_command_(SCD30_CMD_ALTITUDE_COMPENSATION, altitude_compensation_)) { + if (!this->write_command(SCD30_CMD_ALTITUDE_COMPENSATION, altitude_compensation_)) { ESP_LOGE(TAG, "Sensor SCD30 error setting altitude compensation."); this->error_code_ = MEASUREMENT_INIT_FAILED; this->mark_failed(); @@ -92,7 +86,7 @@ void SCD30Component::setup() { delay(30); #endif - if (!this->write_command_(SCD30_CMD_AUTOMATIC_SELF_CALIBRATION, enable_asc_ ? 1 : 0)) { + if (!this->write_command(SCD30_CMD_AUTOMATIC_SELF_CALIBRATION, enable_asc_ ? 1 : 0)) { ESP_LOGE(TAG, "Sensor SCD30 error setting automatic self calibration."); this->error_code_ = MEASUREMENT_INIT_FAILED; this->mark_failed(); @@ -103,7 +97,7 @@ void SCD30Component::setup() { #endif /// Sensor initialization - if (!this->write_command_(SCD30_CMD_START_CONTINUOUS_MEASUREMENTS, this->ambient_pressure_compensation_)) { + if (!this->write_command(SCD30_CMD_START_CONTINUOUS_MEASUREMENTS, this->ambient_pressure_compensation_)) { ESP_LOGE(TAG, "Sensor SCD30 error starting continuous measurements."); this->error_code_ = MEASUREMENT_INIT_FAILED; this->mark_failed(); @@ -151,14 +145,14 @@ void SCD30Component::dump_config() { } void SCD30Component::update() { - uint16_t raw_read_status[1]; - if (!this->read_data_(raw_read_status, 1) || raw_read_status[0] == 0x00) { + uint16_t raw_read_status; + if (!this->read_data(raw_read_status) || raw_read_status == 0x00) { this->status_set_warning(); ESP_LOGW(TAG, "Data not ready yet!"); return; } - if (!this->write_command_(SCD30_CMD_READ_MEASUREMENT)) { + if (!this->write_command(SCD30_CMD_READ_MEASUREMENT)) { ESP_LOGW(TAG, "Error reading measurement!"); this->status_set_warning(); return; @@ -166,7 +160,7 @@ void SCD30Component::update() { this->set_timeout(50, [this]() { uint16_t raw_data[6]; - if (!this->read_data_(raw_data, 6)) { + if (!this->read_data(raw_data, 6)) { this->status_set_warning(); return; } @@ -197,77 +191,16 @@ void SCD30Component::update() { } bool SCD30Component::is_data_ready_() { - if (!this->write_command_(SCD30_CMD_GET_DATA_READY_STATUS)) { + if (!this->write_command(SCD30_CMD_GET_DATA_READY_STATUS)) { return false; } delay(4); uint16_t is_data_ready; - if (!this->read_data_(&is_data_ready, 1)) { + if (!this->read_data(&is_data_ready, 1)) { return false; } return is_data_ready == 1; } -bool SCD30Component::write_command_(uint16_t command) { - // Warning ugly, trick the I2Ccomponent base by setting register to the first 8 bit. - return this->write_byte(command >> 8, command & 0xFF); -} - -bool SCD30Component::write_command_(uint16_t command, uint16_t data) { - uint8_t raw[5]; - raw[0] = command >> 8; - raw[1] = command & 0xFF; - raw[2] = data >> 8; - raw[3] = data & 0xFF; - raw[4] = sht_crc_(raw[2], raw[3]); - return this->write(raw, 5) == i2c::ERROR_OK; -} - -uint8_t SCD30Component::sht_crc_(uint8_t data1, uint8_t data2) { - uint8_t bit; - uint8_t crc = 0xFF; - - crc ^= data1; - for (bit = 8; bit > 0; --bit) { - if (crc & 0x80) { - crc = (crc << 1) ^ 0x131; - } else { - crc = (crc << 1); - } - } - - crc ^= data2; - for (bit = 8; bit > 0; --bit) { - if (crc & 0x80) { - crc = (crc << 1) ^ 0x131; - } else { - crc = (crc << 1); - } - } - - return crc; -} - -bool SCD30Component::read_data_(uint16_t *data, uint8_t len) { - const uint8_t num_bytes = len * 3; - std::vector buf(num_bytes); - - if (this->read(buf.data(), num_bytes) != i2c::ERROR_OK) { - return false; - } - - for (uint8_t i = 0; i < len; i++) { - const uint8_t j = 3 * i; - uint8_t crc = sht_crc_(buf[j], buf[j + 1]); - if (crc != buf[j + 2]) { - ESP_LOGE(TAG, "CRC8 Checksum invalid! 0x%02X != 0x%02X", buf[j + 2], crc); - return false; - } - data[i] = (buf[j] << 8) | buf[j + 1]; - } - - return true; -} - } // namespace scd30 } // namespace esphome diff --git a/esphome/components/scd30/scd30.h b/esphome/components/scd30/scd30.h index 64193d0cb6..c434bf0dea 100644 --- a/esphome/components/scd30/scd30.h +++ b/esphome/components/scd30/scd30.h @@ -2,13 +2,13 @@ #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" -#include "esphome/components/i2c/i2c.h" +#include "esphome/components/sensirion_common/i2c_sensirion.h" namespace esphome { namespace scd30 { /// This class implements support for the Sensirion scd30 i2c GAS (VOC and CO2eq) sensors. -class SCD30Component : public Component, public i2c::I2CDevice { +class SCD30Component : public Component, public sensirion_common::SensirionI2CDevice { public: void set_co2_sensor(sensor::Sensor *co2) { co2_sensor_ = co2; } void set_humidity_sensor(sensor::Sensor *humidity) { humidity_sensor_ = humidity; } @@ -27,10 +27,6 @@ class SCD30Component : public Component, public i2c::I2CDevice { float get_setup_priority() const override { return setup_priority::DATA; } protected: - bool write_command_(uint16_t command); - bool write_command_(uint16_t command, uint16_t data); - bool read_data_(uint16_t *data, uint8_t len); - uint8_t sht_crc_(uint8_t data1, uint8_t data2); bool is_data_ready_(); enum ErrorCode { diff --git a/esphome/components/scd30/sensor.py b/esphome/components/scd30/sensor.py index cd25649f2a..3cfd861a63 100644 --- a/esphome/components/scd30/sensor.py +++ b/esphome/components/scd30/sensor.py @@ -2,6 +2,7 @@ from esphome import core import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor +from esphome.components import sensirion_common from esphome.const import ( CONF_ID, CONF_HUMIDITY, @@ -18,9 +19,12 @@ from esphome.const import ( ) DEPENDENCIES = ["i2c"] +AUTO_LOAD = ["sensirion_common"] scd30_ns = cg.esphome_ns.namespace("scd30") -SCD30Component = scd30_ns.class_("SCD30Component", cg.Component, i2c.I2CDevice) +SCD30Component = scd30_ns.class_( + "SCD30Component", cg.Component, sensirion_common.SensirionI2CDevice +) CONF_AUTOMATIC_SELF_CALIBRATION = "automatic_self_calibration" CONF_ALTITUDE_COMPENSATION = "altitude_compensation" diff --git a/esphome/components/scd4x/scd4x.cpp b/esphome/components/scd4x/scd4x.cpp index 4bd512394f..559c95df32 100644 --- a/esphome/components/scd4x/scd4x.cpp +++ b/esphome/components/scd4x/scd4x.cpp @@ -25,15 +25,8 @@ void SCD4XComponent::setup() { // the sensor needs 1000 ms to enter the idle state this->set_timeout(1000, [this]() { - // Check if measurement is ready before reading the value - if (!this->write_command_(SCD4X_CMD_GET_DATA_READY_STATUS)) { - ESP_LOGE(TAG, "Failed to write data ready status command"); - this->mark_failed(); - return; - } - - uint16_t raw_read_status[1]; - if (!this->read_data_(raw_read_status, 1)) { + uint16_t raw_read_status; + if (!this->get_register(SCD4X_CMD_GET_DATA_READY_STATUS, raw_read_status)) { ESP_LOGE(TAG, "Failed to read data ready status"); this->mark_failed(); return; @@ -41,9 +34,9 @@ void SCD4XComponent::setup() { uint32_t stop_measurement_delay = 0; // In order to query the device periodic measurement must be ceased - if (raw_read_status[0]) { + if (raw_read_status) { ESP_LOGD(TAG, "Sensor has data available, stopping periodic measurement"); - if (!this->write_command_(SCD4X_CMD_STOP_MEASUREMENTS)) { + if (!this->write_command(SCD4X_CMD_STOP_MEASUREMENTS)) { ESP_LOGE(TAG, "Failed to stop measurements"); this->mark_failed(); return; @@ -53,15 +46,8 @@ void SCD4XComponent::setup() { stop_measurement_delay = 500; } this->set_timeout(stop_measurement_delay, [this]() { - if (!this->write_command_(SCD4X_CMD_GET_SERIAL_NUMBER)) { - ESP_LOGE(TAG, "Failed to write get serial command"); - this->error_code_ = COMMUNICATION_FAILED; - this->mark_failed(); - return; - } - uint16_t raw_serial_number[3]; - if (!this->read_data_(raw_serial_number, 3)) { + if (!this->get_register(SCD4X_CMD_GET_SERIAL_NUMBER, raw_serial_number, 3, 1)) { ESP_LOGE(TAG, "Failed to read serial number"); this->error_code_ = SERIAL_NUMBER_IDENTIFICATION_FAILED; this->mark_failed(); @@ -70,8 +56,8 @@ void SCD4XComponent::setup() { ESP_LOGD(TAG, "Serial number %02d.%02d.%02d", (uint16_t(raw_serial_number[0]) >> 8), uint16_t(raw_serial_number[0] & 0xFF), (uint16_t(raw_serial_number[1]) >> 8)); - if (!this->write_command_(SCD4X_CMD_TEMPERATURE_OFFSET, - (uint16_t)(temperature_offset_ * SCD4X_TEMPERATURE_OFFSET_MULTIPLIER))) { + if (!this->write_command(SCD4X_CMD_TEMPERATURE_OFFSET, + (uint16_t)(temperature_offset_ * SCD4X_TEMPERATURE_OFFSET_MULTIPLIER))) { ESP_LOGE(TAG, "Error setting temperature offset."); this->error_code_ = MEASUREMENT_INIT_FAILED; this->mark_failed(); @@ -88,7 +74,7 @@ void SCD4XComponent::setup() { return; } } else { - if (!this->write_command_(SCD4X_CMD_ALTITUDE_COMPENSATION, altitude_compensation_)) { + if (!this->write_command(SCD4X_CMD_ALTITUDE_COMPENSATION, altitude_compensation_)) { ESP_LOGE(TAG, "Error setting altitude compensation."); this->error_code_ = MEASUREMENT_INIT_FAILED; this->mark_failed(); @@ -96,7 +82,7 @@ void SCD4XComponent::setup() { } } - if (!this->write_command_(SCD4X_CMD_AUTOMATIC_SELF_CALIBRATION, enable_asc_ ? 1 : 0)) { + if (!this->write_command(SCD4X_CMD_AUTOMATIC_SELF_CALIBRATION, enable_asc_ ? 1 : 0)) { ESP_LOGE(TAG, "Error setting automatic self calibration."); this->error_code_ = MEASUREMENT_INIT_FAILED; this->mark_failed(); @@ -104,7 +90,7 @@ void SCD4XComponent::setup() { } // Finally start sensor measurements - if (!this->write_command_(SCD4X_CMD_START_CONTINUOUS_MEASUREMENTS)) { + if (!this->write_command(SCD4X_CMD_START_CONTINUOUS_MEASUREMENTS)) { ESP_LOGE(TAG, "Error starting continuous measurements."); this->error_code_ = MEASUREMENT_INIT_FAILED; this->mark_failed(); @@ -164,19 +150,19 @@ void SCD4XComponent::update() { } // Check if data is ready - if (!this->write_command_(SCD4X_CMD_GET_DATA_READY_STATUS)) { + if (!this->write_command(SCD4X_CMD_GET_DATA_READY_STATUS)) { this->status_set_warning(); return; } - uint16_t raw_read_status[1]; - if (!this->read_data_(raw_read_status, 1) || raw_read_status[0] == 0x00) { + uint16_t raw_read_status; + if (!this->read_data(raw_read_status) || raw_read_status == 0x00) { this->status_set_warning(); ESP_LOGW(TAG, "Data not ready yet!"); return; } - if (!this->write_command_(SCD4X_CMD_READ_MEASUREMENT)) { + if (!this->write_command(SCD4X_CMD_READ_MEASUREMENT)) { ESP_LOGW(TAG, "Error reading measurement!"); this->status_set_warning(); return; @@ -184,7 +170,7 @@ void SCD4XComponent::update() { // Read off sensor data uint16_t raw_data[3]; - if (!this->read_data_(raw_data, 3)) { + if (!this->read_data(raw_data, 3)) { this->status_set_warning(); return; } @@ -218,7 +204,7 @@ void SCD4XComponent::set_ambient_pressure_compensation(float pressure_in_bar) { } bool SCD4XComponent::update_ambient_pressure_compensation_(uint16_t pressure_in_hpa) { - if (this->write_command_(SCD4X_CMD_AMBIENT_PRESSURE_COMPENSATION, pressure_in_hpa)) { + if (this->write_command(SCD4X_CMD_AMBIENT_PRESSURE_COMPENSATION, pressure_in_hpa)) { ESP_LOGD(TAG, "setting ambient pressure compensation to %d hPa", pressure_in_hpa); return true; } else { @@ -227,70 +213,5 @@ bool SCD4XComponent::update_ambient_pressure_compensation_(uint16_t pressure_in_ } } -uint8_t SCD4XComponent::sht_crc_(uint8_t data1, uint8_t data2) { - uint8_t bit; - uint8_t crc = 0xFF; - - crc ^= data1; - for (bit = 8; bit > 0; --bit) { - if (crc & 0x80) { - crc = (crc << 1) ^ 0x131; - } else { - crc = (crc << 1); - } - } - - crc ^= data2; - for (bit = 8; bit > 0; --bit) { - if (crc & 0x80) { - crc = (crc << 1) ^ 0x131; - } else { - crc = (crc << 1); - } - } - - return crc; -} - -bool SCD4XComponent::read_data_(uint16_t *data, uint8_t len) { - const uint8_t num_bytes = len * 3; - std::vector buf(num_bytes); - - if (this->read(buf.data(), num_bytes) != i2c::ERROR_OK) { - return false; - } - - for (uint8_t i = 0; i < len; i++) { - const uint8_t j = 3 * i; - uint8_t crc = sht_crc_(buf[j], buf[j + 1]); - if (crc != buf[j + 2]) { - ESP_LOGE(TAG, "CRC8 Checksum invalid! 0x%02X != 0x%02X", buf[j + 2], crc); - return false; - } - data[i] = (buf[j] << 8) | buf[j + 1]; - } - return true; -} - -bool SCD4XComponent::write_command_(uint16_t command) { - const uint8_t num_bytes = 2; - uint8_t buffer[num_bytes]; - - buffer[0] = (command >> 8); - buffer[1] = command & 0xff; - - return this->write(buffer, num_bytes) == i2c::ERROR_OK; -} - -bool SCD4XComponent::write_command_(uint16_t command, uint16_t data) { - uint8_t raw[5]; - raw[0] = command >> 8; - raw[1] = command & 0xFF; - raw[2] = data >> 8; - raw[3] = data & 0xFF; - raw[4] = sht_crc_(raw[2], raw[3]); - return this->write(raw, 5) == i2c::ERROR_OK; -} - } // namespace scd4x } // namespace esphome diff --git a/esphome/components/scd4x/scd4x.h b/esphome/components/scd4x/scd4x.h index 4fe2bf14cc..3e534bcf98 100644 --- a/esphome/components/scd4x/scd4x.h +++ b/esphome/components/scd4x/scd4x.h @@ -2,14 +2,14 @@ #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" -#include "esphome/components/i2c/i2c.h" +#include "esphome/components/sensirion_common/i2c_sensirion.h" namespace esphome { namespace scd4x { enum ERRORCODE { COMMUNICATION_FAILED, SERIAL_NUMBER_IDENTIFICATION_FAILED, MEASUREMENT_INIT_FAILED, UNKNOWN }; -class SCD4XComponent : public PollingComponent, public i2c::I2CDevice { +class SCD4XComponent : public PollingComponent, public sensirion_common::SensirionI2CDevice { public: float get_setup_priority() const override { return setup_priority::DATA; } void setup() override; @@ -27,10 +27,6 @@ class SCD4XComponent : public PollingComponent, public i2c::I2CDevice { void set_humidity_sensor(sensor::Sensor *humidity) { humidity_sensor_ = humidity; } protected: - uint8_t sht_crc_(uint8_t data1, uint8_t data2); - bool read_data_(uint16_t *data, uint8_t len); - bool write_command_(uint16_t command); - bool write_command_(uint16_t command, uint16_t data); bool update_ambient_pressure_compensation_(uint16_t pressure_in_hpa); ERRORCODE error_code_; diff --git a/esphome/components/scd4x/sensor.py b/esphome/components/scd4x/sensor.py index 3e814ffe78..6ab0e1ba99 100644 --- a/esphome/components/scd4x/sensor.py +++ b/esphome/components/scd4x/sensor.py @@ -1,7 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor - +from esphome.components import sensirion_common from esphome.const import ( CONF_ID, CONF_CO2, @@ -21,9 +21,12 @@ from esphome.const import ( CODEOWNERS = ["@sjtrny"] DEPENDENCIES = ["i2c"] +AUTO_LOAD = ["sensirion_common"] scd4x_ns = cg.esphome_ns.namespace("scd4x") -SCD4XComponent = scd4x_ns.class_("SCD4XComponent", cg.PollingComponent, i2c.I2CDevice) +SCD4XComponent = scd4x_ns.class_( + "SCD4XComponent", cg.PollingComponent, sensirion_common.SensirionI2CDevice +) CONF_AUTOMATIC_SELF_CALIBRATION = "automatic_self_calibration" CONF_ALTITUDE_COMPENSATION = "altitude_compensation" diff --git a/esphome/components/sdp3x/sdp3x.cpp b/esphome/components/sdp3x/sdp3x.cpp index eb1543f2c2..251d59607a 100644 --- a/esphome/components/sdp3x/sdp3x.cpp +++ b/esphome/components/sdp3x/sdp3x.cpp @@ -7,55 +7,50 @@ namespace esphome { namespace sdp3x { static const char *const TAG = "sdp3x.sensor"; -static const uint8_t SDP3X_SOFT_RESET[2] = {0x00, 0x06}; -static const uint8_t SDP3X_READ_ID1[2] = {0x36, 0x7C}; -static const uint8_t SDP3X_READ_ID2[2] = {0xE1, 0x02}; -static const uint8_t SDP3X_START_DP_AVG[2] = {0x36, 0x15}; -static const uint8_t SDP3X_START_MASS_FLOW_AVG[2] = {0x36, 0x03}; -static const uint8_t SDP3X_STOP_MEAS[2] = {0x3F, 0xF9}; +static const uint16_t SDP3X_SOFT_RESET = 0x0006; +static const uint16_t SDP3X_READ_ID1 = 0x367C; +static const uint16_t SDP3X_READ_ID2 = 0xE102; +static const uint16_t SDP3X_START_DP_AVG = 0x3615; +static const uint16_t SDP3X_START_MASS_FLOW_AVG = 0x3603; +static const uint16_t SDP3X_STOP_MEAS = 0x3FF9; void SDP3XComponent::update() { this->read_pressure_(); } void SDP3XComponent::setup() { ESP_LOGD(TAG, "Setting up SDP3X..."); - if (this->write(SDP3X_STOP_MEAS, 2) != i2c::ERROR_OK) { + if (!this->write_command(SDP3X_STOP_MEAS)) { ESP_LOGW(TAG, "Stop SDP3X failed!"); // This sometimes fails for no good reason } - if (this->write(SDP3X_SOFT_RESET, 2) != i2c::ERROR_OK) { + if (!this->write_command(SDP3X_SOFT_RESET)) { ESP_LOGW(TAG, "Soft Reset SDP3X failed!"); // This sometimes fails for no good reason } this->set_timeout(20, [this] { - if (this->write(SDP3X_READ_ID1, 2) != i2c::ERROR_OK) { + if (!this->write_command(SDP3X_READ_ID1)) { ESP_LOGE(TAG, "Read ID1 SDP3X failed!"); this->mark_failed(); return; } - if (this->write(SDP3X_READ_ID2, 2) != i2c::ERROR_OK) { + if (!this->write_command(SDP3X_READ_ID2)) { ESP_LOGE(TAG, "Read ID2 SDP3X failed!"); this->mark_failed(); return; } - uint8_t data[18]; - if (this->read(data, 18) != i2c::ERROR_OK) { + uint16_t data[6]; + if (this->read_data(data, 6) != i2c::ERROR_OK) { ESP_LOGE(TAG, "Read ID SDP3X failed!"); this->mark_failed(); return; } - if (!(check_crc_(&data[0], 2, data[2]) && check_crc_(&data[3], 2, data[5]))) { - ESP_LOGE(TAG, "CRC ID SDP3X failed!"); - this->mark_failed(); - return; - } // SDP8xx // ref: // https://www.sensirion.com/fileadmin/user_upload/customers/sensirion/Dokumente/8_Differential_Pressure/Datasheets/Sensirion_Differential_Pressure_Datasheet_SDP8xx_Digital.pdf - if (data[2] == 0x02) { - switch (data[3]) { + if (data[1] >> 8 == 0x02) { + switch (data[1] & 0xFF) { case 0x01: // SDP800-500Pa ESP_LOGCONFIG(TAG, "Sensor is SDP800-500Pa"); break; @@ -75,15 +70,16 @@ void SDP3XComponent::setup() { ESP_LOGCONFIG(TAG, "Sensor is SDP810-125Pa"); break; } - } else if (data[2] == 0x01) { - if (data[3] == 0x01) { + } else if (data[1] >> 8 == 0x01) { + if ((data[1] & 0xFF) == 0x01) { ESP_LOGCONFIG(TAG, "Sensor is SDP31-500Pa"); - } else if (data[3] == 0x02) { + } else if ((data[1] & 0xFF) == 0x02) { ESP_LOGCONFIG(TAG, "Sensor is SDP32-125Pa"); } } - if (this->write(measurement_mode_ == DP_AVG ? SDP3X_START_DP_AVG : SDP3X_START_MASS_FLOW_AVG, 2) != i2c::ERROR_OK) { + if (this->write_command(measurement_mode_ == DP_AVG ? SDP3X_START_DP_AVG : SDP3X_START_MASS_FLOW_AVG) != + i2c::ERROR_OK) { ESP_LOGE(TAG, "Start Measurements SDP3X failed!"); this->mark_failed(); return; @@ -101,22 +97,16 @@ void SDP3XComponent::dump_config() { } void SDP3XComponent::read_pressure_() { - uint8_t data[9]; - if (this->read(data, 9) != i2c::ERROR_OK) { + uint16_t data[3]; + if (this->read_data(data, 3) != i2c::ERROR_OK) { ESP_LOGW(TAG, "Couldn't read SDP3X data!"); this->status_set_warning(); return; } - if (!(check_crc_(&data[0], 2, data[2]) && check_crc_(&data[3], 2, data[5]) && check_crc_(&data[6], 2, data[8]))) { - ESP_LOGW(TAG, "Invalid SDP3X data!"); - this->status_set_warning(); - return; - } - - int16_t pressure_raw = encode_uint16(data[0], data[1]); - int16_t temperature_raw = encode_uint16(data[3], data[4]); - int16_t scale_factor_raw = encode_uint16(data[6], data[7]); + int16_t pressure_raw = data[0]; + int16_t temperature_raw = data[1]; + int16_t scale_factor_raw = data[2]; // scale factor is in Pa - convert to hPa float pressure = pressure_raw / (scale_factor_raw * 100.0f); ESP_LOGV(TAG, "Got raw pressure=%d, raw scale factor =%d, raw temperature=%d ", pressure_raw, scale_factor_raw, @@ -129,26 +119,5 @@ void SDP3XComponent::read_pressure_() { float SDP3XComponent::get_setup_priority() const { return setup_priority::DATA; } -// Check CRC function from SDP3X sample code provided by sensirion -// Returns true if a checksum is OK -bool SDP3XComponent::check_crc_(const uint8_t data[], uint8_t size, uint8_t checksum) { - uint8_t crc = 0xFF; - - // calculates 8-Bit checksum with given polynomial 0x31 (x^8 + x^5 + x^4 + 1) - for (int i = 0; i < size; i++) { - crc ^= (data[i]); - for (uint8_t bit = 8; bit > 0; --bit) { - if (crc & 0x80) { - crc = (crc << 1) ^ 0x31; - } else { - crc = (crc << 1); - } - } - } - - // verify checksum - return (crc == checksum); -} - } // namespace sdp3x } // namespace esphome diff --git a/esphome/components/sdp3x/sdp3x.h b/esphome/components/sdp3x/sdp3x.h index 0e74d0883d..e3d3533c74 100644 --- a/esphome/components/sdp3x/sdp3x.h +++ b/esphome/components/sdp3x/sdp3x.h @@ -2,14 +2,14 @@ #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" -#include "esphome/components/i2c/i2c.h" +#include "esphome/components/sensirion_common/i2c_sensirion.h" namespace esphome { namespace sdp3x { enum MeasurementMode { MASS_FLOW_AVG, DP_AVG }; -class SDP3XComponent : public PollingComponent, public i2c::I2CDevice, public sensor::Sensor { +class SDP3XComponent : public PollingComponent, public sensirion_common::SensirionI2CDevice, public sensor::Sensor { public: /// Schedule temperature+pressure readings. void update() override; @@ -23,8 +23,6 @@ class SDP3XComponent : public PollingComponent, public i2c::I2CDevice, public se protected: /// Internal method to read the pressure from the component after it has been scheduled. void read_pressure_(); - - bool check_crc_(const uint8_t data[], uint8_t size, uint8_t checksum); MeasurementMode measurement_mode_; }; diff --git a/esphome/components/sdp3x/sensor.py b/esphome/components/sdp3x/sensor.py index 66ee475b11..60608818a2 100644 --- a/esphome/components/sdp3x/sensor.py +++ b/esphome/components/sdp3x/sensor.py @@ -1,6 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor +from esphome.components import sensirion_common from esphome.const import ( DEVICE_CLASS_PRESSURE, STATE_CLASS_MEASUREMENT, @@ -8,10 +9,13 @@ from esphome.const import ( ) DEPENDENCIES = ["i2c"] +AUTO_LOAD = ["sensirion_common"] CODEOWNERS = ["@Azimath"] sdp3x_ns = cg.esphome_ns.namespace("sdp3x") -SDP3XComponent = sdp3x_ns.class_("SDP3XComponent", cg.PollingComponent, i2c.I2CDevice) +SDP3XComponent = sdp3x_ns.class_( + "SDP3XComponent", cg.PollingComponent, sensirion_common.SensirionI2CDevice +) MeasurementMode = sdp3x_ns.enum("MeasurementMode") diff --git a/esphome/components/sensirion_common/__init__.py b/esphome/components/sensirion_common/__init__.py new file mode 100644 index 0000000000..b27f37099d --- /dev/null +++ b/esphome/components/sensirion_common/__init__.py @@ -0,0 +1,10 @@ +import esphome.codegen as cg + +from esphome.components import i2c + + +CODEOWNERS = ["@martgras"] + +sensirion_common_ns = cg.esphome_ns.namespace("sensirion_common") + +SensirionI2CDevice = sensirion_common_ns.class_("SensirionI2CDevice", i2c.I2CDevice) diff --git a/esphome/components/sensirion_common/i2c_sensirion.cpp b/esphome/components/sensirion_common/i2c_sensirion.cpp new file mode 100644 index 0000000000..a2232a7d1b --- /dev/null +++ b/esphome/components/sensirion_common/i2c_sensirion.cpp @@ -0,0 +1,128 @@ +#include "i2c_sensirion.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" +#include + +namespace esphome { +namespace sensirion_common { + +static const char *const TAG = "sensirion_i2c"; +// To avoid memory allocations for small writes a stack buffer is used +static const size_t BUFFER_STACK_SIZE = 16; + +bool SensirionI2CDevice::read_data(uint16_t *data, uint8_t len) { + const uint8_t num_bytes = len * 3; + std::vector buf(num_bytes); + + last_error_ = this->read(buf.data(), num_bytes); + if (last_error_ != i2c::ERROR_OK) { + return false; + } + + for (uint8_t i = 0; i < len; i++) { + const uint8_t j = 3 * i; + uint8_t crc = sht_crc_(buf[j], buf[j + 1]); + if (crc != buf[j + 2]) { + ESP_LOGE(TAG, "CRC8 Checksum invalid at pos %d! 0x%02X != 0x%02X", i, buf[j + 2], crc); + last_error_ = i2c::ERROR_CRC; + return false; + } + data[i] = encode_uint16(buf[j], buf[j + 1]); + } + return true; +} +/*** + * write command with parameters and insert crc + * use stack array for less than 4 paramaters. Most sensirion i2c commands have less parameters + */ +bool SensirionI2CDevice::write_command_(uint16_t command, CommandLen command_len, const uint16_t *data, + uint8_t data_len) { + uint8_t temp_stack[BUFFER_STACK_SIZE]; + std::unique_ptr temp_heap; + uint8_t *temp; + size_t required_buffer_len = data_len * 3 + 2; + + // Is a dynamic allocation required ? + if (required_buffer_len >= BUFFER_STACK_SIZE) { + temp_heap = std::unique_ptr(new uint8_t[required_buffer_len]); + temp = temp_heap.get(); + } else { + temp = temp_stack; + } + // First byte or word is the command + uint8_t raw_idx = 0; + if (command_len == 1) { + temp[raw_idx++] = command & 0xFF; + } else { + // command is 2 bytes +#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ + temp[raw_idx++] = command >> 8; + temp[raw_idx++] = command & 0xFF; +#else + temp[raw_idx++] = command & 0xFF; + temp[raw_idx++] = command >> 8; +#endif + } + // add parameters folllowed by crc + // skipped if len == 0 + for (size_t i = 0; i < data_len; i++) { +#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ + temp[raw_idx++] = data[i] >> 8; + temp[raw_idx++] = data[i] & 0xFF; +#else + temp[raw_idx++] = data[i] & 0xFF; + temp[raw_idx++] = data[i] >> 8; +#endif + temp[raw_idx++] = sht_crc_(data[i]); + } + last_error_ = this->write(temp, raw_idx); + return last_error_ == i2c::ERROR_OK; +} + +bool SensirionI2CDevice::get_register_(uint16_t reg, CommandLen command_len, uint16_t *data, uint8_t len, + uint8_t delay_ms) { + if (!this->write_command_(reg, command_len, nullptr, 0)) { + ESP_LOGE(TAG, "Failed to write i2c register=0x%X (%d) err=%d,", reg, command_len, this->last_error_); + return false; + } + delay(delay_ms); + bool result = this->read_data(data, len); + if (!result) { + ESP_LOGE(TAG, "Failed to read data from register=0x%X err=%d,", reg, this->last_error_); + } + return result; +} + +// The 8-bit CRC checksum is transmitted after each data word +uint8_t SensirionI2CDevice::sht_crc_(uint16_t data) { + uint8_t bit; + uint8_t crc = 0xFF; +#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ + crc ^= data >> 8; +#else + crc ^= data & 0xFF; +#endif + for (bit = 8; bit > 0; --bit) { + if (crc & 0x80) { + crc = (crc << 1) ^ crc_polynomial_; + } else { + crc = (crc << 1); + } + } +#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ + crc ^= data & 0xFF; +#else + crc ^= data >> 8; +#endif + for (bit = 8; bit > 0; --bit) { + if (crc & 0x80) { + crc = (crc << 1) ^ crc_polynomial_; + } else { + crc = (crc << 1); + } + } + return crc; +} + +} // namespace sensirion_common +} // namespace esphome diff --git a/esphome/components/sensirion_common/i2c_sensirion.h b/esphome/components/sensirion_common/i2c_sensirion.h new file mode 100644 index 0000000000..88e1d59984 --- /dev/null +++ b/esphome/components/sensirion_common/i2c_sensirion.h @@ -0,0 +1,155 @@ +#pragma once +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace sensirion_common { + +/** + * Implementation of a i2c functions for Sensirion sensors + * Sensirion data requires crc checking. + * Each 16 bit word is/must be followed 8 bit CRC code + * (Applies to read and write - note the i2c command code doesn't need a CRC) + * Format: + * | 16 Bit Command Code | 16 bit Data word 1 | CRC of DW 1 | 16 bit Data word 1 | CRC of DW 2 | .. + */ +class SensirionI2CDevice : public i2c::I2CDevice { + public: + enum CommandLen : uint8_t { ADDR_8_BIT = 1, ADDR_16_BIT = 2 }; + + /** Read data words from i2c device. + * handles crc check used by Sensirion sensors + * @param data pointer to raw result + * @param len number of words to read + * @return true if reading succeded + */ + bool read_data(uint16_t *data, uint8_t len); + + /** Read 1 data word from i2c device. + * @param data reference to raw result + * @return true if reading succeded + */ + bool read_data(uint16_t &data) { return this->read_data(&data, 1); } + + /** get data words from i2c register. + * handles crc check used by Sensirion sensors + * @param i2c register + * @param data pointer to raw result + * @param len number of words to read + * @param delay milliseconds to to wait between sending the i2c commmand and reading the result + * @return true if reading succeded + */ + bool get_register(uint16_t command, uint16_t *data, uint8_t len, uint8_t delay = 0) { + return get_register_(command, ADDR_16_BIT, data, len, delay); + } + /** Read 1 data word from 16 bit i2c register. + * @param i2c register + * @param data reference to raw result + * @param delay milliseconds to to wait between sending the i2c commmand and reading the result + * @return true if reading succeded + */ + bool get_register(uint16_t i2c_register, uint16_t &data, uint8_t delay = 0) { + return this->get_register_(i2c_register, ADDR_16_BIT, &data, 1, delay); + } + + /** get data words from i2c register. + * handles crc check used by Sensirion sensors + * @param i2c register + * @param data pointer to raw result + * @param len number of words to read + * @param delay milliseconds to to wait between sending the i2c commmand and reading the result + * @return true if reading succeded + */ + bool get_8bit_register(uint8_t i2c_register, uint16_t *data, uint8_t len, uint8_t delay = 0) { + return get_register_(i2c_register, ADDR_8_BIT, data, len, delay); + } + + /** Read 1 data word from 8 bit i2c register. + * @param i2c register + * @param data reference to raw result + * @param delay milliseconds to to wait between sending the i2c commmand and reading the result + * @return true if reading succeded + */ + bool get_8bit_register(uint8_t i2c_register, uint16_t &data, uint8_t delay = 0) { + return this->get_register_(i2c_register, ADDR_8_BIT, &data, 1, delay); + } + + /** Write a command to the i2c device. + * @param command i2c command to send + * @return true if reading succeded + */ + template bool write_command(T i2c_register) { return write_command(i2c_register, nullptr, 0); } + + /** Write a command and one data word to the i2c device . + * @param command i2c command to send + * @param data argument for the i2c command + * @return true if reading succeded + */ + template bool write_command(T i2c_register, uint16_t data) { return write_command(i2c_register, &data, 1); } + + /** Write a command with arguments as words + * @param i2c_register i2c command to send - an be uint8_t or uint16_t + * @param data vector arguments for the i2c command + * @return true if reading succeded + */ + template bool write_command(T i2c_register, const std::vector &data) { + return write_command_(i2c_register, sizeof(T), data.data(), data.size()); + } + + /** Write a command with arguments as words + * @param i2c_register i2c command to send - an be uint8_t or uint16_t + * @param data arguments for the i2c command + * @param len number of arguments (words) + * @return true if reading succeded + */ + template bool write_command(T i2c_register, const uint16_t *data, uint8_t len) { + // limit to 8 or 16 bit only + static_assert(sizeof(i2c_register) == 1 || sizeof(i2c_register) == 2, + "only 8 or 16 bit command types are supported."); + return write_command_(i2c_register, CommandLen(sizeof(T)), data, len); + } + + protected: + uint8_t crc_polynomial_{0x31u}; // default for sensirion + /** Write a command with arguments as words + * @param command i2c command to send can be uint8_t or uint16_t + * @param command_len either 1 for short 8 bit command or 2 for 16 bit command codes + * @param data arguments for the i2c command + * @param data_len number of arguments (words) + * @return true if reading succeded + */ + bool write_command_(uint16_t command, CommandLen command_len, const uint16_t *data, uint8_t data_len); + + /** get data words from i2c register. + * handles crc check used by Sensirion sensors + * @param i2c register + * @param command_len either 1 for short 8 bit command or 2 for 16 bit command codes + * @param data pointer to raw result + * @param len number of words to read + * @param delay milliseconds to to wait between sending the i2c commmand and reading the result + * @return true if reading succeded + */ + bool get_register_(uint16_t reg, CommandLen command_len, uint16_t *data, uint8_t len, uint8_t delay); + + /** 8-bit CRC checksum that is transmitted after each data word for read and write operation + * @param command i2c command to send + * @param data data word for which the crc8 checksum is calculated + * @param len number of arguments (words) + * @return 8 Bit CRC + */ + uint8_t sht_crc_(uint16_t data); + + /** 8-bit CRC checksum that is transmitted after each data word for read and write operation + * @param command i2c command to send + * @param data1 high byte of data word + * @param data2 low byte of data word + * @return 8 Bit CRC + */ + uint8_t sht_crc_(uint8_t data1, uint8_t data2) { return sht_crc_(encode_uint16(data1, data2)); } + + /** last error code from i2c operation + */ + i2c::ErrorCode last_error_; +}; + +} // namespace sensirion_common +} // namespace esphome diff --git a/esphome/components/sgp30/sensor.py b/esphome/components/sgp30/sensor.py index 14a078b501..0029e2c515 100644 --- a/esphome/components/sgp30/sensor.py +++ b/esphome/components/sgp30/sensor.py @@ -1,10 +1,13 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import i2c, sensor +from esphome.components import i2c, sensor, sensirion_common + from esphome.const import ( CONF_ID, CONF_BASELINE, CONF_ECO2, + CONF_STORE_BASELINE, + CONF_TEMPERATURE_SOURCE, CONF_TVOC, ICON_RADIATOR, DEVICE_CLASS_CARBON_DIOXIDE, @@ -17,17 +20,19 @@ from esphome.const import ( ) DEPENDENCIES = ["i2c"] +AUTO_LOAD = ["sensirion_common"] sgp30_ns = cg.esphome_ns.namespace("sgp30") -SGP30Component = sgp30_ns.class_("SGP30Component", cg.PollingComponent, i2c.I2CDevice) +SGP30Component = sgp30_ns.class_( + "SGP30Component", cg.PollingComponent, sensirion_common.SensirionI2CDevice +) CONF_ECO2_BASELINE = "eco2_baseline" CONF_TVOC_BASELINE = "tvoc_baseline" -CONF_STORE_BASELINE = "store_baseline" CONF_UPTIME = "uptime" CONF_COMPENSATION = "compensation" CONF_HUMIDITY_SOURCE = "humidity_source" -CONF_TEMPERATURE_SOURCE = "temperature_source" + CONFIG_SCHEMA = ( cv.Schema( diff --git a/esphome/components/sgp30/sgp30.cpp b/esphome/components/sgp30/sgp30.cpp index b55097fcd0..a6572620c4 100644 --- a/esphome/components/sgp30/sgp30.cpp +++ b/esphome/components/sgp30/sgp30.cpp @@ -36,14 +36,8 @@ void SGP30Component::setup() { ESP_LOGCONFIG(TAG, "Setting up SGP30..."); // Serial Number identification - if (!this->write_command_(SGP30_CMD_GET_SERIAL_ID)) { - this->error_code_ = COMMUNICATION_FAILED; - this->mark_failed(); - return; - } uint16_t raw_serial_number[3]; - - if (!this->read_data_(raw_serial_number, 3)) { + if (!this->get_register(SGP30_CMD_GET_SERIAL_ID, raw_serial_number, 3)) { this->mark_failed(); return; } @@ -52,16 +46,12 @@ void SGP30Component::setup() { ESP_LOGD(TAG, "Serial Number: %" PRIu64, this->serial_number_); // Featureset identification for future use - if (!this->write_command_(SGP30_CMD_GET_FEATURESET)) { + uint16_t raw_featureset; + if (!this->get_register(SGP30_CMD_GET_FEATURESET, raw_featureset)) { this->mark_failed(); return; } - uint16_t raw_featureset[1]; - if (!this->read_data_(raw_featureset, 1)) { - this->mark_failed(); - return; - } - this->featureset_ = raw_featureset[0]; + this->featureset_ = raw_featureset; if (uint16_t(this->featureset_ >> 12) != 0x0) { if (uint16_t(this->featureset_ >> 12) == 0x1) { // ID matching a different sensor: SGPC3 @@ -76,7 +66,7 @@ void SGP30Component::setup() { ESP_LOGD(TAG, "Product version: 0x%0X", uint16_t(this->featureset_ & 0x1FF)); // Sensor initialization - if (!this->write_command_(SGP30_CMD_IAQ_INIT)) { + if (!this->write_command(SGP30_CMD_IAQ_INIT)) { ESP_LOGE(TAG, "Sensor sgp30_iaq_init failed."); this->error_code_ = MEASUREMENT_INIT_FAILED; this->mark_failed(); @@ -119,14 +109,14 @@ bool SGP30Component::is_sensor_baseline_reliable_() { void SGP30Component::read_iaq_baseline_() { if (this->is_sensor_baseline_reliable_()) { - if (!this->write_command_(SGP30_CMD_GET_IAQ_BASELINE)) { + if (!this->write_command(SGP30_CMD_GET_IAQ_BASELINE)) { ESP_LOGD(TAG, "Error getting baseline"); this->status_set_warning(); return; } this->set_timeout(50, [this]() { uint16_t raw_data[2]; - if (!this->read_data_(raw_data, 2)) { + if (!this->read_data(raw_data, 2)) { this->status_set_warning(); return; } @@ -274,14 +264,14 @@ void SGP30Component::dump_config() { } void SGP30Component::update() { - if (!this->write_command_(SGP30_CMD_MEASURE_IAQ)) { + if (!this->write_command(SGP30_CMD_MEASURE_IAQ)) { this->status_set_warning(); return; } this->seconds_since_last_store_ += this->update_interval_ / 1000; this->set_timeout(50, [this]() { uint16_t raw_data[2]; - if (!this->read_data_(raw_data, 2)) { + if (!this->read_data(raw_data, 2)) { this->status_set_warning(); return; } @@ -305,56 +295,5 @@ void SGP30Component::update() { }); } -bool SGP30Component::write_command_(uint16_t command) { - // Warning ugly, trick the I2Ccomponent base by setting register to the first 8 bit. - return this->write_byte(command >> 8, command & 0xFF); -} - -uint8_t SGP30Component::sht_crc_(uint8_t data1, uint8_t data2) { - uint8_t bit; - uint8_t crc = 0xFF; - - crc ^= data1; - for (bit = 8; bit > 0; --bit) { - if (crc & 0x80) { - crc = (crc << 1) ^ 0x131; - } else { - crc = (crc << 1); - } - } - - crc ^= data2; - for (bit = 8; bit > 0; --bit) { - if (crc & 0x80) { - crc = (crc << 1) ^ 0x131; - } else { - crc = (crc << 1); - } - } - - return crc; -} - -bool SGP30Component::read_data_(uint16_t *data, uint8_t len) { - const uint8_t num_bytes = len * 3; - std::vector buf(num_bytes); - - if (this->read(buf.data(), num_bytes) != i2c::ERROR_OK) { - return false; - } - - for (uint8_t i = 0; i < len; i++) { - const uint8_t j = 3 * i; - uint8_t crc = sht_crc_(buf[j], buf[j + 1]); - if (crc != buf[j + 2]) { - ESP_LOGE(TAG, "CRC8 Checksum invalid! 0x%02X != 0x%02X", buf[j + 2], crc); - return false; - } - data[i] = (buf[j] << 8) | buf[j + 1]; - } - - return true; -} - } // namespace sgp30 } // namespace esphome diff --git a/esphome/components/sgp30/sgp30.h b/esphome/components/sgp30/sgp30.h index 91a1c1e9c7..d61eee00db 100644 --- a/esphome/components/sgp30/sgp30.h +++ b/esphome/components/sgp30/sgp30.h @@ -2,7 +2,7 @@ #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" -#include "esphome/components/i2c/i2c.h" +#include "esphome/components/sensirion_common/i2c_sensirion.h" #include "esphome/core/preferences.h" #include @@ -15,7 +15,7 @@ struct SGP30Baselines { } PACKED; /// This class implements support for the Sensirion SGP30 i2c GAS (VOC and CO2eq) sensors. -class SGP30Component : public PollingComponent, public i2c::I2CDevice { +class SGP30Component : public PollingComponent, public sensirion_common::SensirionI2CDevice { public: void set_eco2_sensor(sensor::Sensor *eco2) { eco2_sensor_ = eco2; } void set_tvoc_sensor(sensor::Sensor *tvoc) { tvoc_sensor_ = tvoc; } @@ -33,13 +33,10 @@ class SGP30Component : public PollingComponent, public i2c::I2CDevice { float get_setup_priority() const override { return setup_priority::DATA; } protected: - bool write_command_(uint16_t command); - bool read_data_(uint16_t *data, uint8_t len); void send_env_data_(); void read_iaq_baseline_(); bool is_sensor_baseline_reliable_(); void write_iaq_baseline_(uint16_t eco2_baseline, uint16_t tvoc_baseline); - uint8_t sht_crc_(uint8_t data1, uint8_t data2); uint64_t serial_number_; uint16_t featureset_; uint32_t required_warm_up_time_; diff --git a/esphome/components/sgp40/sensor.py b/esphome/components/sgp40/sensor.py index 6f27b54fb0..ee267d6062 100644 --- a/esphome/components/sgp40/sensor.py +++ b/esphome/components/sgp40/sensor.py @@ -1,25 +1,30 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import i2c, sensor +from esphome.components import i2c, sensor, sensirion_common + from esphome.const import ( + CONF_STORE_BASELINE, + CONF_TEMPERATURE_SOURCE, ICON_RADIATOR, DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, STATE_CLASS_MEASUREMENT, ) DEPENDENCIES = ["i2c"] +AUTO_LOAD = ["sensirion_common"] CODEOWNERS = ["@SenexCrenshaw"] sgp40_ns = cg.esphome_ns.namespace("sgp40") SGP40Component = sgp40_ns.class_( - "SGP40Component", sensor.Sensor, cg.PollingComponent, i2c.I2CDevice + "SGP40Component", + sensor.Sensor, + cg.PollingComponent, + sensirion_common.SensirionI2CDevice, ) CONF_COMPENSATION = "compensation" CONF_HUMIDITY_SOURCE = "humidity_source" -CONF_TEMPERATURE_SOURCE = "temperature_source" -CONF_STORE_BASELINE = "store_baseline" CONF_VOC_BASELINE = "voc_baseline" CONFIG_SCHEMA = ( diff --git a/esphome/components/sgp40/sgp40.cpp b/esphome/components/sgp40/sgp40.cpp index 829c00a218..9d78572b50 100644 --- a/esphome/components/sgp40/sgp40.cpp +++ b/esphome/components/sgp40/sgp40.cpp @@ -12,14 +12,14 @@ void SGP40Component::setup() { ESP_LOGCONFIG(TAG, "Setting up SGP40..."); // Serial Number identification - if (!this->write_command_(SGP40_CMD_GET_SERIAL_ID)) { + if (!this->write_command(SGP40_CMD_GET_SERIAL_ID)) { this->error_code_ = COMMUNICATION_FAILED; this->mark_failed(); return; } uint16_t raw_serial_number[3]; - if (!this->read_data_(raw_serial_number, 3)) { + if (!this->read_data(raw_serial_number, 3)) { this->mark_failed(); return; } @@ -28,19 +28,19 @@ void SGP40Component::setup() { ESP_LOGD(TAG, "Serial Number: %" PRIu64, this->serial_number_); // Featureset identification for future use - if (!this->write_command_(SGP40_CMD_GET_FEATURESET)) { + if (!this->write_command(SGP40_CMD_GET_FEATURESET)) { ESP_LOGD(TAG, "raw_featureset write_command_ failed"); this->mark_failed(); return; } - uint16_t raw_featureset[1]; - if (!this->read_data_(raw_featureset, 1)) { + uint16_t raw_featureset; + if (!this->read_data(raw_featureset)) { ESP_LOGD(TAG, "raw_featureset read_data_ failed"); this->mark_failed(); return; } - this->featureset_ = raw_featureset[0]; + this->featureset_ = raw_featureset; if ((this->featureset_ & 0x1FF) != SGP40_FEATURESET) { ESP_LOGD(TAG, "Product feature set failed 0x%0X , expecting 0x%0X", uint16_t(this->featureset_ & 0x1FF), SGP40_FEATURESET); @@ -95,21 +95,21 @@ void SGP40Component::setup() { void SGP40Component::self_test_() { ESP_LOGD(TAG, "Self-test started"); - if (!this->write_command_(SGP40_CMD_SELF_TEST)) { + if (!this->write_command(SGP40_CMD_SELF_TEST)) { this->error_code_ = COMMUNICATION_FAILED; ESP_LOGD(TAG, "Self-test communication failed"); this->mark_failed(); } this->set_timeout(250, [this]() { - uint16_t reply[1]; - if (!this->read_data_(reply, 1)) { + uint16_t reply; + if (!this->read_data(reply)) { ESP_LOGD(TAG, "Self-test read_data_ failed"); this->mark_failed(); return; } - if (reply[0] == 0xD400) { + if (reply == 0xD400) { this->self_test_complete_ = true; ESP_LOGD(TAG, "Self-test completed"); return; @@ -192,51 +192,28 @@ uint16_t SGP40Component::measure_raw_() { temperature = 25; } - uint8_t command[8]; - - command[0] = 0x26; - command[1] = 0x0F; - + uint16_t data[2]; uint16_t rhticks = llround((uint16_t)((humidity * 65535) / 100)); - command[2] = rhticks >> 8; - command[3] = rhticks & 0xFF; - command[4] = generate_crc_(command + 2, 2); uint16_t tempticks = (uint16_t)(((temperature + 45) * 65535) / 175); - command[5] = tempticks >> 8; - command[6] = tempticks & 0xFF; - command[7] = generate_crc_(command + 5, 2); + // first paramater is the relative humidity ticks + data[0] = rhticks; + // second paramater is the temperature ticks + data[1] = tempticks; - if (this->write(command, 8) != i2c::ERROR_OK) { + if (!this->write_command(SGP40_CMD_MEASURE_RAW, data, 2)) { this->status_set_warning(); - ESP_LOGD(TAG, "write error"); - return UINT16_MAX; + ESP_LOGD(TAG, "write error (%d)", this->last_error_); + return false; } delay(30); - uint16_t raw_data[1]; - if (!this->read_data_(raw_data, 1)) { + uint16_t raw_data; + if (!this->read_data(raw_data)) { this->status_set_warning(); ESP_LOGD(TAG, "read_data_ error"); return UINT16_MAX; } - return raw_data[0]; -} - -uint8_t SGP40Component::generate_crc_(const uint8_t *data, uint8_t datalen) { - // calculates 8-Bit checksum with given polynomial - uint8_t crc = SGP40_CRC8_INIT; - - for (uint8_t i = 0; i < datalen; i++) { - crc ^= data[i]; - for (uint8_t b = 0; b < 8; b++) { - if (crc & 0x80) { - crc = (crc << 1) ^ SGP40_CRC8_POLYNOMIAL; - } else { - crc <<= 1; - } - } - } - return crc; + return raw_data; } void SGP40Component::update_voc_index() { @@ -293,56 +270,5 @@ void SGP40Component::dump_config() { } } -bool SGP40Component::write_command_(uint16_t command) { - // Warning ugly, trick the I2Ccomponent base by setting register to the first 8 bit. - return this->write_byte(command >> 8, command & 0xFF); -} - -uint8_t SGP40Component::sht_crc_(uint8_t data1, uint8_t data2) { - uint8_t bit; - uint8_t crc = 0xFF; - - crc ^= data1; - for (bit = 8; bit > 0; --bit) { - if (crc & 0x80) { - crc = (crc << 1) ^ 0x131; - } else { - crc = (crc << 1); - } - } - - crc ^= data2; - for (bit = 8; bit > 0; --bit) { - if (crc & 0x80) { - crc = (crc << 1) ^ 0x131; - } else { - crc = (crc << 1); - } - } - - return crc; -} - -bool SGP40Component::read_data_(uint16_t *data, uint8_t len) { - const uint8_t num_bytes = len * 3; - std::vector buf(num_bytes); - - if (this->read(buf.data(), num_bytes) != i2c::ERROR_OK) { - return false; - } - - for (uint8_t i = 0; i < len; i++) { - const uint8_t j = 3 * i; - uint8_t crc = sht_crc_(buf[j], buf[j + 1]); - if (crc != buf[j + 2]) { - ESP_LOGE(TAG, "CRC8 Checksum invalid! 0x%02X != 0x%02X", buf[j + 2], crc); - return false; - } - data[i] = (buf[j] << 8) | buf[j + 1]; - } - - return true; -} - } // namespace sgp40 } // namespace esphome diff --git a/esphome/components/sgp40/sgp40.h b/esphome/components/sgp40/sgp40.h index c854b21060..c5b7d2dfa0 100644 --- a/esphome/components/sgp40/sgp40.h +++ b/esphome/components/sgp40/sgp40.h @@ -2,7 +2,7 @@ #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" -#include "esphome/components/i2c/i2c.h" +#include "esphome/components/sensirion_common/i2c_sensirion.h" #include "esphome/core/application.h" #include "esphome/core/preferences.h" #include "sensirion_voc_algorithm.h" @@ -28,6 +28,7 @@ static const uint8_t SGP40_WORD_LEN = 2; ///< 2 bytes per word static const uint16_t SGP40_CMD_GET_SERIAL_ID = 0x3682; static const uint16_t SGP40_CMD_GET_FEATURESET = 0x202f; static const uint16_t SGP40_CMD_SELF_TEST = 0x280e; +static const uint16_t SGP40_CMD_MEASURE_RAW = 0x260F; // Shortest time interval of 3H for storing baseline values. // Prevents wear of the flash because of too many write operations @@ -39,7 +40,7 @@ const uint32_t MAXIMUM_STORAGE_DIFF = 50; class SGP40Component; /// This class implements support for the Sensirion sgp40 i2c GAS (VOC) sensors. -class SGP40Component : public PollingComponent, public sensor::Sensor, public i2c::I2CDevice { +class SGP40Component : public PollingComponent, public sensor::Sensor, public sensirion_common::SensirionI2CDevice { public: void set_humidity_sensor(sensor::Sensor *humidity) { humidity_sensor_ = humidity; } void set_temperature_sensor(sensor::Sensor *temperature) { temperature_sensor_ = temperature; } @@ -55,11 +56,8 @@ class SGP40Component : public PollingComponent, public sensor::Sensor, public i2 /// Input sensor for humidity and temperature compensation. sensor::Sensor *humidity_sensor_{nullptr}; sensor::Sensor *temperature_sensor_{nullptr}; - bool write_command_(uint16_t command); - bool read_data_(uint16_t *data, uint8_t len); int16_t sensirion_init_sensors_(); int16_t sgp40_probe_(); - uint8_t sht_crc_(uint8_t data1, uint8_t data2); uint64_t serial_number_; uint16_t featureset_; int32_t measure_voc_index_(); diff --git a/esphome/components/sht3xd/sensor.py b/esphome/components/sht3xd/sensor.py index b9e7bce733..8e1ef426ad 100644 --- a/esphome/components/sht3xd/sensor.py +++ b/esphome/components/sht3xd/sensor.py @@ -1,6 +1,6 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import i2c, sensor +from esphome.components import i2c, sensor, sensirion_common from esphome.const import ( CONF_HUMIDITY, CONF_ID, @@ -13,10 +13,11 @@ from esphome.const import ( ) DEPENDENCIES = ["i2c"] +AUTO_LOAD = ["sensirion_common"] sht3xd_ns = cg.esphome_ns.namespace("sht3xd") SHT3XDComponent = sht3xd_ns.class_( - "SHT3XDComponent", cg.PollingComponent, i2c.I2CDevice + "SHT3XDComponent", cg.PollingComponent, sensirion_common.SensirionI2CDevice ) CONFIG_SCHEMA = ( diff --git a/esphome/components/sht3xd/sht3xd.cpp b/esphome/components/sht3xd/sht3xd.cpp index e7981b64cf..4e1c9742bc 100644 --- a/esphome/components/sht3xd/sht3xd.cpp +++ b/esphome/components/sht3xd/sht3xd.cpp @@ -17,13 +17,8 @@ static const uint16_t SHT3XD_COMMAND_FETCH_DATA = 0xE000; void SHT3XDComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up SHT3xD..."); - if (!this->write_command_(SHT3XD_COMMAND_READ_SERIAL_NUMBER)) { - this->mark_failed(); - return; - } - uint16_t raw_serial_number[2]; - if (!this->read_data_(raw_serial_number, 2)) { + if (!this->get_register(SHT3XD_COMMAND_READ_SERIAL_NUMBER, raw_serial_number, 2)) { this->mark_failed(); return; } @@ -45,16 +40,16 @@ float SHT3XDComponent::get_setup_priority() const { return setup_priority::DATA; void SHT3XDComponent::update() { if (this->status_has_warning()) { ESP_LOGD(TAG, "Retrying to reconnect the sensor."); - this->write_command_(SHT3XD_COMMAND_SOFT_RESET); + this->write_command(SHT3XD_COMMAND_SOFT_RESET); } - if (!this->write_command_(SHT3XD_COMMAND_POLLING_H)) { + if (!this->write_command(SHT3XD_COMMAND_POLLING_H)) { this->status_set_warning(); return; } this->set_timeout(50, [this]() { uint16_t raw_data[2]; - if (!this->read_data_(raw_data, 2)) { + if (!this->read_data(raw_data, 2)) { this->status_set_warning(); return; } @@ -71,56 +66,5 @@ void SHT3XDComponent::update() { }); } -bool SHT3XDComponent::write_command_(uint16_t command) { - // Warning ugly, trick the I2Ccomponent base by setting register to the first 8 bit. - return this->write_byte(command >> 8, command & 0xFF); -} - -uint8_t sht_crc(uint8_t data1, uint8_t data2) { - uint8_t bit; - uint8_t crc = 0xFF; - - crc ^= data1; - for (bit = 8; bit > 0; --bit) { - if (crc & 0x80) { - crc = (crc << 1) ^ 0x131; - } else { - crc = (crc << 1); - } - } - - crc ^= data2; - for (bit = 8; bit > 0; --bit) { - if (crc & 0x80) { - crc = (crc << 1) ^ 0x131; - } else { - crc = (crc << 1); - } - } - - return crc; -} - -bool SHT3XDComponent::read_data_(uint16_t *data, uint8_t len) { - const uint8_t num_bytes = len * 3; - std::vector buf(num_bytes); - - if (this->read(buf.data(), num_bytes) != i2c::ERROR_OK) { - return false; - } - - for (uint8_t i = 0; i < len; i++) { - const uint8_t j = 3 * i; - uint8_t crc = sht_crc(buf[j], buf[j + 1]); - if (crc != buf[j + 2]) { - ESP_LOGE(TAG, "CRC8 Checksum invalid! 0x%02X != 0x%02X", buf[j + 2], crc); - return false; - } - data[i] = (buf[j] << 8) | buf[j + 1]; - } - - return true; -} - } // namespace sht3xd } // namespace esphome diff --git a/esphome/components/sht3xd/sht3xd.h b/esphome/components/sht3xd/sht3xd.h index 709f8aebe7..3164aa0687 100644 --- a/esphome/components/sht3xd/sht3xd.h +++ b/esphome/components/sht3xd/sht3xd.h @@ -2,13 +2,13 @@ #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" -#include "esphome/components/i2c/i2c.h" +#include "esphome/components/sensirion_common/i2c_sensirion.h" namespace esphome { namespace sht3xd { /// This class implements support for the SHT3x-DIS family of temperature+humidity i2c sensors. -class SHT3XDComponent : public PollingComponent, public i2c::I2CDevice { +class SHT3XDComponent : public PollingComponent, public sensirion_common::SensirionI2CDevice { public: void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; } @@ -19,9 +19,6 @@ class SHT3XDComponent : public PollingComponent, public i2c::I2CDevice { void update() override; protected: - bool write_command_(uint16_t command); - bool read_data_(uint16_t *data, uint8_t len); - sensor::Sensor *temperature_sensor_; sensor::Sensor *humidity_sensor_; }; diff --git a/esphome/components/sht4x/sensor.py b/esphome/components/sht4x/sensor.py index a66ca1a526..9fb8fc969e 100644 --- a/esphome/components/sht4x/sensor.py +++ b/esphome/components/sht4x/sensor.py @@ -1,6 +1,6 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import i2c, sensor +from esphome.components import i2c, sensor, sensirion_common from esphome.const import ( CONF_ID, CONF_TEMPERATURE, @@ -16,10 +16,13 @@ from esphome.const import ( CODEOWNERS = ["@sjtrny"] DEPENDENCIES = ["i2c"] +AUTO_LOAD = ["sensirion_common"] sht4x_ns = cg.esphome_ns.namespace("sht4x") -SHT4XComponent = sht4x_ns.class_("SHT4XComponent", cg.PollingComponent, i2c.I2CDevice) +SHT4XComponent = sht4x_ns.class_( + "SHT4XComponent", cg.PollingComponent, sensirion_common.SensirionI2CDevice +) CONF_PRECISION = "precision" SHT4XPRECISION = sht4x_ns.enum("SHT4XPRECISION") diff --git a/esphome/components/sht4x/sht4x.cpp b/esphome/components/sht4x/sht4x.cpp index 248f32c4de..bdc3e62d2f 100644 --- a/esphome/components/sht4x/sht4x.cpp +++ b/esphome/components/sht4x/sht4x.cpp @@ -50,31 +50,28 @@ void SHT4XComponent::setup() { void SHT4XComponent::dump_config() { LOG_I2C_DEVICE(this); } void SHT4XComponent::update() { - uint8_t cmd[] = {MEASURECOMMANDS[this->precision_]}; - // Send command - this->write(cmd, 1); + this->write_command(MEASURECOMMANDS[this->precision_]); this->set_timeout(10, [this]() { - const uint8_t num_bytes = 6; - uint8_t buffer[num_bytes]; + uint16_t buffer[2]; // Read measurement - bool read_status = this->read_bytes_raw(buffer, num_bytes); + bool read_status = this->read_data(buffer, 2); if (read_status) { // Evaluate and publish measurements if (this->temp_sensor_ != nullptr) { - // Temp is contained in the first 16 bits - float sensor_value_temp = (buffer[0] << 8) + buffer[1]; + // Temp is contained in the first result word + float sensor_value_temp = buffer[0]; float temp = -45 + 175 * sensor_value_temp / 65535; this->temp_sensor_->publish_state(temp); } if (this->humidity_sensor_ != nullptr) { - // Relative humidity is in the last 16 bits - float sensor_value_rh = (buffer[3] << 8) + buffer[4]; + // Relative humidity is in the second result word + float sensor_value_rh = buffer[1]; float rh = -6 + 125 * sensor_value_rh / 65535; this->humidity_sensor_->publish_state(rh); diff --git a/esphome/components/sht4x/sht4x.h b/esphome/components/sht4x/sht4x.h index 8694bd9879..01553d5c15 100644 --- a/esphome/components/sht4x/sht4x.h +++ b/esphome/components/sht4x/sht4x.h @@ -2,7 +2,7 @@ #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" -#include "esphome/components/i2c/i2c.h" +#include "esphome/components/sensirion_common/i2c_sensirion.h" namespace esphome { namespace sht4x { @@ -13,7 +13,7 @@ enum SHT4XHEATERPOWER { SHT4X_HEATERPOWER_HIGH, SHT4X_HEATERPOWER_MED, SHT4X_HEA enum SHT4XHEATERTIME { SHT4X_HEATERTIME_LONG = 1100, SHT4X_HEATERTIME_SHORT = 110 }; -class SHT4XComponent : public PollingComponent, public i2c::I2CDevice { +class SHT4XComponent : public PollingComponent, public sensirion_common::SensirionI2CDevice { public: float get_setup_priority() const override { return setup_priority::DATA; } void setup() override; diff --git a/esphome/components/shtcx/sensor.py b/esphome/components/shtcx/sensor.py index ba2283a9b4..c8b56cfe30 100644 --- a/esphome/components/shtcx/sensor.py +++ b/esphome/components/shtcx/sensor.py @@ -1,6 +1,6 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import i2c, sensor +from esphome.components import i2c, sensor, sensirion_common from esphome.const import ( CONF_HUMIDITY, CONF_ID, @@ -13,9 +13,12 @@ from esphome.const import ( ) DEPENDENCIES = ["i2c"] +AUTO_LOAD = ["sensirion_common"] shtcx_ns = cg.esphome_ns.namespace("shtcx") -SHTCXComponent = shtcx_ns.class_("SHTCXComponent", cg.PollingComponent, i2c.I2CDevice) +SHTCXComponent = shtcx_ns.class_( + "SHTCXComponent", cg.PollingComponent, sensirion_common.SensirionI2CDevice +) SHTCXType = shtcx_ns.enum("SHTCXType") diff --git a/esphome/components/shtcx/shtcx.cpp b/esphome/components/shtcx/shtcx.cpp index 4112270c02..0de56a8044 100644 --- a/esphome/components/shtcx/shtcx.cpp +++ b/esphome/components/shtcx/shtcx.cpp @@ -29,14 +29,14 @@ void SHTCXComponent::setup() { this->wake_up(); this->soft_reset(); - if (!this->write_command_(SHTCX_COMMAND_READ_ID_REGISTER)) { + if (!this->write_command(SHTCX_COMMAND_READ_ID_REGISTER)) { ESP_LOGE(TAG, "Error requesting Device ID"); this->mark_failed(); return; } uint16_t device_id_register; - if (!this->read_data_(&device_id_register, 1)) { + if (!this->read_data(&device_id_register, 1)) { ESP_LOGE(TAG, "Error reading Device ID"); this->mark_failed(); return; @@ -76,7 +76,7 @@ void SHTCXComponent::update() { if (this->type_ != SHTCX_TYPE_SHTC1) { this->wake_up(); } - if (!this->write_command_(SHTCX_COMMAND_POLLING_H)) { + if (!this->write_command(SHTCX_COMMAND_POLLING_H)) { ESP_LOGE(TAG, "sensor polling failed"); if (this->temperature_sensor_ != nullptr) this->temperature_sensor_->publish_state(NAN); @@ -90,7 +90,7 @@ void SHTCXComponent::update() { float temperature = NAN; float humidity = NAN; uint16_t raw_data[2]; - if (!this->read_data_(raw_data, 2)) { + if (!this->read_data(raw_data, 2)) { ESP_LOGE(TAG, "sensor read failed"); this->status_set_warning(); } else { @@ -110,65 +110,14 @@ void SHTCXComponent::update() { }); } -bool SHTCXComponent::write_command_(uint16_t command) { - // Warning ugly, trick the I2Ccomponent base by setting register to the first 8 bit. - return this->write_byte(command >> 8, command & 0xFF); -} - -uint8_t sht_crc(uint8_t data1, uint8_t data2) { - uint8_t bit; - uint8_t crc = 0xFF; - - crc ^= data1; - for (bit = 8; bit > 0; --bit) { - if (crc & 0x80) { - crc = (crc << 1) ^ 0x131; - } else { - crc = (crc << 1); - } - } - - crc ^= data2; - for (bit = 8; bit > 0; --bit) { - if (crc & 0x80) { - crc = (crc << 1) ^ 0x131; - } else { - crc = (crc << 1); - } - } - - return crc; -} - -bool SHTCXComponent::read_data_(uint16_t *data, uint8_t len) { - const uint8_t num_bytes = len * 3; - std::vector buf(num_bytes); - - if (this->read(buf.data(), num_bytes) != i2c::ERROR_OK) { - return false; - } - - for (uint8_t i = 0; i < len; i++) { - const uint8_t j = 3 * i; - uint8_t crc = sht_crc(buf[j], buf[j + 1]); - if (crc != buf[j + 2]) { - ESP_LOGE(TAG, "CRC8 Checksum invalid! 0x%02X != 0x%02X", buf[j + 2], crc); - return false; - } - data[i] = (buf[j] << 8) | buf[j + 1]; - } - - return true; -} - void SHTCXComponent::soft_reset() { - this->write_command_(SHTCX_COMMAND_SOFT_RESET); + this->write_command(SHTCX_COMMAND_SOFT_RESET); delayMicroseconds(200); } -void SHTCXComponent::sleep() { this->write_command_(SHTCX_COMMAND_SLEEP); } +void SHTCXComponent::sleep() { this->write_command(SHTCX_COMMAND_SLEEP); } void SHTCXComponent::wake_up() { - this->write_command_(SHTCX_COMMAND_WAKEUP); + this->write_command(SHTCX_COMMAND_WAKEUP); delayMicroseconds(200); } diff --git a/esphome/components/shtcx/shtcx.h b/esphome/components/shtcx/shtcx.h index cb2b46d348..c44fb9d9c1 100644 --- a/esphome/components/shtcx/shtcx.h +++ b/esphome/components/shtcx/shtcx.h @@ -2,7 +2,7 @@ #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" -#include "esphome/components/i2c/i2c.h" +#include "esphome/components/sensirion_common/i2c_sensirion.h" namespace esphome { namespace shtcx { @@ -10,7 +10,7 @@ namespace shtcx { enum SHTCXType { SHTCX_TYPE_SHTC3 = 0, SHTCX_TYPE_SHTC1, SHTCX_TYPE_UNKNOWN }; /// This class implements support for the SHT3x-DIS family of temperature+humidity i2c sensors. -class SHTCXComponent : public PollingComponent, public i2c::I2CDevice { +class SHTCXComponent : public PollingComponent, public sensirion_common::SensirionI2CDevice { public: void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; } @@ -24,8 +24,6 @@ class SHTCXComponent : public PollingComponent, public i2c::I2CDevice { void wake_up(); protected: - bool write_command_(uint16_t command); - bool read_data_(uint16_t *data, uint8_t len); SHTCXType type_; uint16_t sensor_id_; sensor::Sensor *temperature_sensor_; diff --git a/esphome/components/sps30/sensor.py b/esphome/components/sps30/sensor.py index 27264cf942..89cb25c24f 100644 --- a/esphome/components/sps30/sensor.py +++ b/esphome/components/sps30/sensor.py @@ -1,6 +1,6 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import i2c, sensor +from esphome.components import i2c, sensor, sensirion_common from esphome.const import ( CONF_ID, CONF_PM_1_0, @@ -26,9 +26,12 @@ from esphome.const import ( ) DEPENDENCIES = ["i2c"] +AUTO_LOAD = ["sensirion_common"] sps30_ns = cg.esphome_ns.namespace("sps30") -SPS30Component = sps30_ns.class_("SPS30Component", cg.PollingComponent, i2c.I2CDevice) +SPS30Component = sps30_ns.class_( + "SPS30Component", cg.PollingComponent, sensirion_common.SensirionI2CDevice +) CONFIG_SCHEMA = ( cv.Schema( diff --git a/esphome/components/sps30/sps30.cpp b/esphome/components/sps30/sps30.cpp index 2bd7bcb458..2885125a8a 100644 --- a/esphome/components/sps30/sps30.cpp +++ b/esphome/components/sps30/sps30.cpp @@ -22,30 +22,18 @@ static const uint8_t MAX_SKIPPED_DATA_CYCLES_BEFORE_ERROR = 5; void SPS30Component::setup() { ESP_LOGCONFIG(TAG, "Setting up sps30..."); - this->write_command_(SPS30_CMD_SOFT_RESET); + this->write_command(SPS30_CMD_SOFT_RESET); /// Deferred Sensor initialization this->set_timeout(500, [this]() { /// Firmware version identification - if (!this->write_command_(SPS30_CMD_GET_FIRMWARE_VERSION)) { - this->error_code_ = FIRMWARE_VERSION_REQUEST_FAILED; - this->mark_failed(); - return; - } - - if (!this->read_data_(&raw_firmware_version_, 1)) { + if (!this->get_register(SPS30_CMD_GET_FIRMWARE_VERSION, raw_firmware_version_, 1)) { this->error_code_ = FIRMWARE_VERSION_READ_FAILED; this->mark_failed(); return; } /// Serial number identification - if (!this->write_command_(SPS30_CMD_GET_SERIAL_NUMBER)) { - this->error_code_ = SERIAL_NUMBER_REQUEST_FAILED; - this->mark_failed(); - return; - } - uint16_t raw_serial_number[8]; - if (!this->read_data_(raw_serial_number, 8)) { + if (!this->get_register(SPS30_CMD_GET_SERIAL_NUMBER, raw_serial_number, 8, 1)) { this->error_code_ = SERIAL_NUMBER_READ_FAILED; this->mark_failed(); return; @@ -109,7 +97,7 @@ void SPS30Component::update() { /// Check if warning flag active (sensor reconnected?) if (this->status_has_warning()) { ESP_LOGD(TAG, "Trying to reconnect the sensor..."); - if (this->write_command_(SPS30_CMD_SOFT_RESET)) { + if (this->write_command(SPS30_CMD_SOFT_RESET)) { ESP_LOGD(TAG, "Sensor has soft-reset successfully. Waiting for reconnection in 500ms..."); this->set_timeout(500, [this]() { this->start_continuous_measurement_(); @@ -124,13 +112,13 @@ void SPS30Component::update() { return; } /// Check if measurement is ready before reading the value - if (!this->write_command_(SPS30_CMD_GET_DATA_READY_STATUS)) { + if (!this->write_command(SPS30_CMD_GET_DATA_READY_STATUS)) { this->status_set_warning(); return; } uint16_t raw_read_status; - if (!this->read_data_(&raw_read_status, 1) || raw_read_status == 0x00) { + if (!this->read_data(&raw_read_status, 1) || raw_read_status == 0x00) { ESP_LOGD(TAG, "Sensor measurement not ready yet."); this->skipped_data_read_cycles_++; /// The following logic is required to address the cases when a sensor is quickly replaced before it's marked @@ -142,7 +130,7 @@ void SPS30Component::update() { return; } - if (!this->write_command_(SPS30_CMD_READ_MEASUREMENT)) { + if (!this->write_command(SPS30_CMD_READ_MEASUREMENT)) { ESP_LOGW(TAG, "Error reading measurement status!"); this->status_set_warning(); return; @@ -150,7 +138,7 @@ void SPS30Component::update() { this->set_timeout(50, [this]() { uint16_t raw_data[20]; - if (!this->read_data_(raw_data, 20)) { + if (!this->read_data(raw_data, 20)) { ESP_LOGW(TAG, "Error reading measurement data!"); this->status_set_warning(); return; @@ -205,69 +193,18 @@ void SPS30Component::update() { }); } -bool SPS30Component::write_command_(uint16_t command) { - // Warning ugly, trick the I2Ccomponent base by setting register to the first 8 bit. - return this->write_byte(command >> 8, command & 0xFF); -} - -uint8_t SPS30Component::sht_crc_(uint8_t data1, uint8_t data2) { - uint8_t bit; - uint8_t crc = 0xFF; - - crc ^= data1; - for (bit = 8; bit > 0; --bit) { - if (crc & 0x80) { - crc = (crc << 1) ^ 0x131; - } else { - crc = (crc << 1); - } - } - - crc ^= data2; - for (bit = 8; bit > 0; --bit) { - if (crc & 0x80) { - crc = (crc << 1) ^ 0x131; - } else { - crc = (crc << 1); - } - } - - return crc; -} - bool SPS30Component::start_continuous_measurement_() { uint8_t data[4]; data[0] = SPS30_CMD_START_CONTINUOUS_MEASUREMENTS & 0xFF; data[1] = 0x03; data[2] = 0x00; data[3] = sht_crc_(0x03, 0x00); - if (!this->write_bytes(SPS30_CMD_START_CONTINUOUS_MEASUREMENTS >> 8, data, 4)) { + if (!this->write_command(SPS30_CMD_START_CONTINUOUS_MEASUREMENTS, SPS30_CMD_START_CONTINUOUS_MEASUREMENTS_ARG)) { ESP_LOGE(TAG, "Error initiating measurements"); return false; } return true; } -bool SPS30Component::read_data_(uint16_t *data, uint8_t len) { - const uint8_t num_bytes = len * 3; - std::vector buf(num_bytes); - - if (this->read(buf.data(), num_bytes) != i2c::ERROR_OK) { - return false; - } - - for (uint8_t i = 0; i < len; i++) { - const uint8_t j = 3 * i; - uint8_t crc = sht_crc_(buf[j], buf[j + 1]); - if (crc != buf[j + 2]) { - ESP_LOGE(TAG, "CRC8 Checksum invalid! 0x%02X != 0x%02X", buf[j + 2], crc); - return false; - } - data[i] = (buf[j] << 8) | buf[j + 1]; - } - - return true; -} - } // namespace sps30 } // namespace esphome diff --git a/esphome/components/sps30/sps30.h b/esphome/components/sps30/sps30.h index bae33a46e1..9a93df8597 100644 --- a/esphome/components/sps30/sps30.h +++ b/esphome/components/sps30/sps30.h @@ -2,14 +2,14 @@ #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" -#include "esphome/components/i2c/i2c.h" +#include "esphome/components/sensirion_common/i2c_sensirion.h" namespace esphome { namespace sps30 { /// This class implements support for the Sensirion SPS30 i2c/UART Particulate Matter /// PM1.0, PM2.5, PM4, PM10 Air Quality sensors. -class SPS30Component : public PollingComponent, public i2c::I2CDevice { +class SPS30Component : public PollingComponent, public sensirion_common::SensirionI2CDevice { public: void set_pm_1_0_sensor(sensor::Sensor *pm_1_0) { pm_1_0_sensor_ = pm_1_0; } void set_pm_2_5_sensor(sensor::Sensor *pm_2_5) { pm_2_5_sensor_ = pm_2_5; } @@ -29,9 +29,6 @@ class SPS30Component : public PollingComponent, public i2c::I2CDevice { float get_setup_priority() const override { return setup_priority::DATA; } protected: - bool write_command_(uint16_t command); - bool read_data_(uint16_t *data, uint8_t len); - uint8_t sht_crc_(uint8_t data1, uint8_t data2); char serial_number_[17] = {0}; /// Terminating NULL character uint16_t raw_firmware_version_; bool start_continuous_measurement_(); diff --git a/esphome/components/sts3x/sensor.py b/esphome/components/sts3x/sensor.py index aa7573aaf2..a99503a2b8 100644 --- a/esphome/components/sts3x/sensor.py +++ b/esphome/components/sts3x/sensor.py @@ -8,6 +8,7 @@ from esphome.const import ( ) DEPENDENCIES = ["i2c"] +AUTO_LOAD = ["sensirion_common"] sts3x_ns = cg.esphome_ns.namespace("sts3x") diff --git a/esphome/components/sts3x/sts3x.cpp b/esphome/components/sts3x/sts3x.cpp index ce166f2055..5af808b6e7 100644 --- a/esphome/components/sts3x/sts3x.cpp +++ b/esphome/components/sts3x/sts3x.cpp @@ -19,13 +19,13 @@ static const uint16_t STS3X_COMMAND_FETCH_DATA = 0xE000; void STS3XComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up STS3x..."); - if (!this->write_command_(STS3X_COMMAND_READ_SERIAL_NUMBER)) { + if (!this->write_command(STS3X_COMMAND_READ_SERIAL_NUMBER)) { this->mark_failed(); return; } uint16_t raw_serial_number[2]; - if (!this->read_data_(raw_serial_number, 1)) { + if (!this->read_data(raw_serial_number, 1)) { this->mark_failed(); return; } @@ -46,16 +46,16 @@ float STS3XComponent::get_setup_priority() const { return setup_priority::DATA; void STS3XComponent::update() { if (this->status_has_warning()) { ESP_LOGD(TAG, "Retrying to reconnect the sensor."); - this->write_command_(STS3X_COMMAND_SOFT_RESET); + this->write_command(STS3X_COMMAND_SOFT_RESET); } - if (!this->write_command_(STS3X_COMMAND_POLLING_H)) { + if (!this->write_command(STS3X_COMMAND_POLLING_H)) { this->status_set_warning(); return; } this->set_timeout(50, [this]() { uint16_t raw_data[1]; - if (!this->read_data_(raw_data, 1)) { + if (!this->read_data(raw_data, 1)) { this->status_set_warning(); return; } @@ -67,56 +67,5 @@ void STS3XComponent::update() { }); } -bool STS3XComponent::write_command_(uint16_t command) { - // Warning ugly, trick the I2Ccomponent base by setting register to the first 8 bit. - return this->write_byte(command >> 8, command & 0xFF); -} - -uint8_t sts3x_crc(uint8_t data1, uint8_t data2) { - uint8_t bit; - uint8_t crc = 0xFF; - - crc ^= data1; - for (bit = 8; bit > 0; --bit) { - if (crc & 0x80) { - crc = (crc << 1) ^ 0x131; - } else { - crc = (crc << 1); - } - } - - crc ^= data2; - for (bit = 8; bit > 0; --bit) { - if (crc & 0x80) { - crc = (crc << 1) ^ 0x131; - } else { - crc = (crc << 1); - } - } - - return crc; -} - -bool STS3XComponent::read_data_(uint16_t *data, uint8_t len) { - const uint8_t num_bytes = len * 3; - std::vector buf(num_bytes); - - if (this->read(buf.data(), num_bytes) != i2c::ERROR_OK) { - return false; - } - - for (uint8_t i = 0; i < len; i++) { - const uint8_t j = 3 * i; - uint8_t crc = sts3x_crc(buf[j], buf[j + 1]); - if (crc != buf[j + 2]) { - ESP_LOGE(TAG, "CRC8 Checksum invalid! 0x%02X != 0x%02X", buf[j + 2], crc); - return false; - } - data[i] = (buf[j] << 8) | buf[j + 1]; - } - - return true; -} - } // namespace sts3x } // namespace esphome diff --git a/esphome/components/sts3x/sts3x.h b/esphome/components/sts3x/sts3x.h index 436cf938d8..261033efad 100644 --- a/esphome/components/sts3x/sts3x.h +++ b/esphome/components/sts3x/sts3x.h @@ -2,22 +2,18 @@ #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" -#include "esphome/components/i2c/i2c.h" +#include "esphome/components/sensirion_common/i2c_sensirion.h" namespace esphome { namespace sts3x { /// This class implements support for the ST3x-DIS family of temperature i2c sensors. -class STS3XComponent : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice { +class STS3XComponent : public sensor::Sensor, public PollingComponent, public sensirion_common::SensirionI2CDevice { public: void setup() override; void dump_config() override; float get_setup_priority() const override; void update() override; - - protected: - bool write_command_(uint16_t command); - bool read_data_(uint16_t *data, uint8_t len); }; } // namespace sts3x diff --git a/esphome/const.py b/esphome/const.py index 46c9a659be..9096e66f4e 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -644,6 +644,7 @@ CONF_STEP_MODE = "step_mode" CONF_STEP_PIN = "step_pin" CONF_STOP = "stop" CONF_STOP_ACTION = "stop_action" +CONF_STORE_BASELINE = "store_baseline" CONF_SUBNET = "subnet" CONF_SUBSTITUTIONS = "substitutions" CONF_SUPPLEMENTAL_COOLING_ACTION = "supplemental_cooling_action" @@ -680,6 +681,7 @@ CONF_TARGET_TEMPERATURE_LOW_COMMAND_TOPIC = "target_temperature_low_command_topi CONF_TARGET_TEMPERATURE_LOW_STATE_TOPIC = "target_temperature_low_state_topic" CONF_TARGET_TEMPERATURE_STATE_TOPIC = "target_temperature_state_topic" CONF_TEMPERATURE = "temperature" +CONF_TEMPERATURE_SOURCE = "temperature_source" CONF_TEMPERATURE_STEP = "temperature_step" CONF_TEXT_SENSORS = "text_sensors" CONF_THEN = "then" From a519e5c4754f655bd130e726173e45010ff1e90b Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 13 Apr 2022 12:26:25 +1200 Subject: [PATCH 0382/1729] Fix silent config errors (#3380) --- esphome/config.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/esphome/config.py b/esphome/config.py index af6c5b0b64..a878f3ef79 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -161,6 +161,7 @@ class Config(OrderedDict, fv.FinalValidateConfig): # type: (ConfigPath) -> Optional[vol.Invalid] for err in self.errors: if self.get_deepest_path(err.path) == path: + self.errors.remove(err) return err return None @@ -647,7 +648,7 @@ class FinalValidateValidationStep(ConfigValidationStep): fv.full_config.reset(token) -def validate_config(config, command_line_substitutions): +def validate_config(config, command_line_substitutions) -> Config: result = Config() loader.clear_component_meta_finders() @@ -734,9 +735,6 @@ def validate_config(config, command_line_substitutions): result.add_validation_step(LoadValidationStep(key, config[key])) result.run_validation_steps() - if result.errors: - return result - for domain, conf in config.items(): result.add_validation_step(LoadValidationStep(domain, conf)) result.add_validation_step(IDPassValidationStep()) @@ -991,5 +989,10 @@ def read_config(command_line_substitutions): errstr += f" {errline}" safe_print(errstr) safe_print(indent(dump_dict(res, path)[0])) + + for err in res.errors: + safe_print(color(Fore.BOLD_RED, err.msg)) + safe_print("") + return None return OrderedDict(res) From b622a8fa5896a67d44ddae3f77c9d9381ca6b81e Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 13 Apr 2022 12:26:55 +1200 Subject: [PATCH 0383/1729] Move PN532OnTagTrigger to nfc::NfcOnTagTrigger (#3379) --- esphome/components/nfc/__init__.py | 5 +++++ esphome/components/nfc/automation.cpp | 9 +++++++++ esphome/components/nfc/automation.h | 17 +++++++++++++++++ esphome/components/pn532/__init__.py | 7 ++----- esphome/components/pn532/pn532.cpp | 7 ++----- esphome/components/pn532/pn532.h | 15 +++++---------- 6 files changed, 40 insertions(+), 20 deletions(-) create mode 100644 esphome/components/nfc/automation.cpp create mode 100644 esphome/components/nfc/automation.h diff --git a/esphome/components/nfc/__init__.py b/esphome/components/nfc/__init__.py index b795a5d5ca..c3bbc50bf9 100644 --- a/esphome/components/nfc/__init__.py +++ b/esphome/components/nfc/__init__.py @@ -1,3 +1,4 @@ +from esphome import automation import esphome.codegen as cg CODEOWNERS = ["@jesserockz"] @@ -5,3 +6,7 @@ CODEOWNERS = ["@jesserockz"] nfc_ns = cg.esphome_ns.namespace("nfc") NfcTag = nfc_ns.class_("NfcTag") + +NfcOnTagTrigger = nfc_ns.class_( + "NfcOnTagTrigger", automation.Trigger.template(cg.std_string, NfcTag) +) diff --git a/esphome/components/nfc/automation.cpp b/esphome/components/nfc/automation.cpp new file mode 100644 index 0000000000..ff00340df0 --- /dev/null +++ b/esphome/components/nfc/automation.cpp @@ -0,0 +1,9 @@ +#include "automation.h" + +namespace esphome { +namespace nfc { + +void NfcOnTagTrigger::process(const std::unique_ptr &tag) { this->trigger(format_uid(tag->get_uid()), *tag); } + +} // namespace nfc +} // namespace esphome diff --git a/esphome/components/nfc/automation.h b/esphome/components/nfc/automation.h new file mode 100644 index 0000000000..565b71bdd9 --- /dev/null +++ b/esphome/components/nfc/automation.h @@ -0,0 +1,17 @@ +#pragma once + +#include +#include "esphome/core/automation.h" + +#include "nfc.h" + +namespace esphome { +namespace nfc { + +class NfcOnTagTrigger : public Trigger { + public: + void process(const std::unique_ptr &tag); +}; + +} // namespace nfc +} // namespace esphome diff --git a/esphome/components/pn532/__init__.py b/esphome/components/pn532/__init__.py index b902e8e3d0..2f120bc983 100644 --- a/esphome/components/pn532/__init__.py +++ b/esphome/components/pn532/__init__.py @@ -14,9 +14,6 @@ CONF_ON_FINISHED_WRITE = "on_finished_write" pn532_ns = cg.esphome_ns.namespace("pn532") PN532 = pn532_ns.class_("PN532", cg.PollingComponent) -PN532OnTagTrigger = pn532_ns.class_( - "PN532OnTagTrigger", automation.Trigger.template(cg.std_string, nfc.NfcTag) -) PN532OnFinishedWriteTrigger = pn532_ns.class_( "PN532OnFinishedWriteTrigger", automation.Trigger.template() ) @@ -30,7 +27,7 @@ PN532_SCHEMA = cv.Schema( cv.GenerateID(): cv.declare_id(PN532), cv.Optional(CONF_ON_TAG): automation.validate_automation( { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PN532OnTagTrigger), + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(nfc.NfcOnTagTrigger), } ), cv.Optional(CONF_ON_FINISHED_WRITE): automation.validate_automation( @@ -42,7 +39,7 @@ PN532_SCHEMA = cv.Schema( ), cv.Optional(CONF_ON_TAG_REMOVED): automation.validate_automation( { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PN532OnTagTrigger), + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(nfc.NfcOnTagTrigger), } ), } diff --git a/esphome/components/pn532/pn532.cpp b/esphome/components/pn532/pn532.cpp index 0c46ff8a57..7ebf328cff 100644 --- a/esphome/components/pn532/pn532.cpp +++ b/esphome/components/pn532/pn532.cpp @@ -144,9 +144,9 @@ void PN532::loop() { } if (nfcid.size() == this->current_uid_.size()) { - bool same_uid = false; + bool same_uid = true; for (size_t i = 0; i < nfcid.size(); i++) - same_uid |= nfcid[i] == this->current_uid_[i]; + same_uid &= nfcid[i] == this->current_uid_[i]; if (same_uid) return; } @@ -376,9 +376,6 @@ bool PN532BinarySensor::process(std::vector &data) { this->found_ = true; return true; } -void PN532OnTagTrigger::process(const std::unique_ptr &tag) { - this->trigger(nfc::format_uid(tag->get_uid()), *tag); -} } // namespace pn532 } // namespace esphome diff --git a/esphome/components/pn532/pn532.h b/esphome/components/pn532/pn532.h index 692a5011e6..4f688dacc2 100644 --- a/esphome/components/pn532/pn532.h +++ b/esphome/components/pn532/pn532.h @@ -5,6 +5,7 @@ #include "esphome/components/binary_sensor/binary_sensor.h" #include "esphome/components/nfc/nfc_tag.h" #include "esphome/components/nfc/nfc.h" +#include "esphome/components/nfc/automation.h" namespace esphome { namespace pn532 { @@ -16,7 +17,6 @@ static const uint8_t PN532_COMMAND_INDATAEXCHANGE = 0x40; static const uint8_t PN532_COMMAND_INLISTPASSIVETARGET = 0x4A; class PN532BinarySensor; -class PN532OnTagTrigger; class PN532 : public PollingComponent { public: @@ -30,8 +30,8 @@ class PN532 : public PollingComponent { void loop() override; void register_tag(PN532BinarySensor *tag) { this->binary_sensors_.push_back(tag); } - void register_ontag_trigger(PN532OnTagTrigger *trig) { this->triggers_ontag_.push_back(trig); } - void register_ontagremoved_trigger(PN532OnTagTrigger *trig) { this->triggers_ontagremoved_.push_back(trig); } + void register_ontag_trigger(nfc::NfcOnTagTrigger *trig) { this->triggers_ontag_.push_back(trig); } + void register_ontagremoved_trigger(nfc::NfcOnTagTrigger *trig) { this->triggers_ontagremoved_.push_back(trig); } void add_on_finished_write_callback(std::function callback) { this->on_finished_write_callback_.add(std::move(callback)); @@ -79,8 +79,8 @@ class PN532 : public PollingComponent { bool requested_read_{false}; std::vector binary_sensors_; - std::vector triggers_ontag_; - std::vector triggers_ontagremoved_; + std::vector triggers_ontag_; + std::vector triggers_ontagremoved_; std::vector current_uid_; nfc::NdefMessage *next_task_message_to_write_; enum NfcTask { @@ -115,11 +115,6 @@ class PN532BinarySensor : public binary_sensor::BinarySensor { bool found_{false}; }; -class PN532OnTagTrigger : public Trigger { - public: - void process(const std::unique_ptr &tag); -}; - class PN532OnFinishedWriteTrigger : public Trigger<> { public: explicit PN532OnFinishedWriteTrigger(PN532 *parent) { From 8be704e591ce236e027448d1b40f72ab74378295 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 13 Apr 2022 12:55:26 +1200 Subject: [PATCH 0384/1729] Allow specifying deep sleep wakup clock time (#3312) --- esphome/components/deep_sleep/__init__.py | 34 ++++++++--- .../deep_sleep/deep_sleep_component.cpp | 5 +- .../deep_sleep/deep_sleep_component.h | 60 +++++++++++++++++++ esphome/components/time/real_time_clock.cpp | 25 ++++++++ esphome/components/time/real_time_clock.h | 2 + 5 files changed, 118 insertions(+), 8 deletions(-) diff --git a/esphome/components/deep_sleep/__init__.py b/esphome/components/deep_sleep/__init__.py index 24fc0cabb0..058358fa67 100644 --- a/esphome/components/deep_sleep/__init__.py +++ b/esphome/components/deep_sleep/__init__.py @@ -1,13 +1,18 @@ import esphome.codegen as cg +from esphome.components import time import esphome.config_validation as cv from esphome import pins, automation from esphome.const import ( + CONF_HOUR, CONF_ID, + CONF_MINUTE, CONF_MODE, CONF_NUMBER, CONF_PINS, CONF_RUN_DURATION, + CONF_SECOND, CONF_SLEEP_DURATION, + CONF_TIME_ID, CONF_WAKEUP_PIN, ) @@ -112,6 +117,7 @@ CONF_TOUCH_WAKEUP = "touch_wakeup" CONF_DEFAULT = "default" CONF_GPIO_WAKEUP_REASON = "gpio_wakeup_reason" CONF_TOUCH_WAKEUP_REASON = "touch_wakeup_reason" +CONF_UNTIL = "until" WAKEUP_CAUSES_SCHEMA = cv.Schema( { @@ -202,13 +208,19 @@ async def to_code(config): cg.add_define("USE_DEEP_SLEEP") -DEEP_SLEEP_ENTER_SCHEMA = automation.maybe_simple_id( - { - cv.GenerateID(): cv.use_id(DeepSleepComponent), - cv.Optional(CONF_SLEEP_DURATION): cv.templatable( - cv.positive_time_period_milliseconds - ), - } +DEEP_SLEEP_ENTER_SCHEMA = cv.All( + automation.maybe_simple_id( + { + cv.GenerateID(): cv.use_id(DeepSleepComponent), + cv.Exclusive(CONF_SLEEP_DURATION, "time"): cv.templatable( + cv.positive_time_period_milliseconds + ), + # Only on ESP32 due to how long the RTC on ESP8266 can stay asleep + cv.Exclusive(CONF_UNTIL, "time"): cv.All(cv.only_on_esp32, cv.time_of_day), + cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock), + } + ), + cv.has_none_or_all_keys(CONF_UNTIL, CONF_TIME_ID), ) @@ -228,6 +240,14 @@ async def deep_sleep_enter_to_code(config, action_id, template_arg, args): if CONF_SLEEP_DURATION in config: template_ = await cg.templatable(config[CONF_SLEEP_DURATION], args, cg.int32) cg.add(var.set_sleep_duration(template_)) + + if CONF_UNTIL in config: + until = config[CONF_UNTIL] + cg.add(var.set_until(until[CONF_HOUR], until[CONF_MINUTE], until[CONF_SECOND])) + + time_ = await cg.get_variable(config[CONF_TIME_ID]) + cg.add(var.set_time(time_)) + return var diff --git a/esphome/components/deep_sleep/deep_sleep_component.cpp b/esphome/components/deep_sleep/deep_sleep_component.cpp index 82751b538b..1bb70e0d7e 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.cpp +++ b/esphome/components/deep_sleep/deep_sleep_component.cpp @@ -1,6 +1,7 @@ #include "deep_sleep_component.h" -#include "esphome/core/log.h" +#include #include "esphome/core/application.h" +#include "esphome/core/log.h" #ifdef USE_ESP8266 #include @@ -101,6 +102,8 @@ void DeepSleepComponent::begin_sleep(bool manual) { #endif ESP_LOGI(TAG, "Beginning Deep Sleep"); + if (this->sleep_duration_.has_value()) + ESP_LOGI(TAG, "Sleeping for %" PRId64 "us", *this->sleep_duration_); App.run_safe_shutdown_hooks(); diff --git a/esphome/components/deep_sleep/deep_sleep_component.h b/esphome/components/deep_sleep/deep_sleep_component.h index 057d992427..5e90d4b89d 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.h +++ b/esphome/components/deep_sleep/deep_sleep_component.h @@ -9,6 +9,10 @@ #include #endif +#ifdef USE_TIME +#include "esphome/components/time/real_time_clock.h" +#endif + namespace esphome { namespace deep_sleep { @@ -116,15 +120,71 @@ template class EnterDeepSleepAction : public Action { EnterDeepSleepAction(DeepSleepComponent *deep_sleep) : deep_sleep_(deep_sleep) {} TEMPLATABLE_VALUE(uint32_t, sleep_duration); +#ifdef USE_TIME + void set_until(uint8_t hour, uint8_t minute, uint8_t second) { + this->hour_ = hour; + this->minute_ = minute; + this->second_ = second; + } + + void set_time(time::RealTimeClock *time) { this->time_ = time; } +#endif + void play(Ts... x) override { if (this->sleep_duration_.has_value()) { this->deep_sleep_->set_sleep_duration(this->sleep_duration_.value(x...)); } +#ifdef USE_TIME + + if (this->hour_.has_value()) { + auto time = this->time_->now(); + const uint32_t timestamp_now = time.timestamp; + + bool after_time = false; + if (time.hour > this->hour_) { + after_time = true; + } else { + if (time.hour == this->hour_) { + if (time.minute > this->minute_) { + after_time = true; + } else { + if (time.minute == this->minute_) { + if (time.second > this->second_) { + after_time = true; + } + } + } + } + } + + time.hour = *this->hour_; + time.minute = *this->minute_; + time.second = *this->second_; + time.recalc_timestamp_utc(); + + time_t timestamp = time.timestamp; // timestamp in local time zone + + if (after_time) + timestamp += 60 * 60 * 24; + + int32_t offset = time::ESPTime::timezone_offset(); + timestamp -= offset; // Change timestamp to utc + const uint32_t ms_left = (timestamp - timestamp_now) * 1000; + this->deep_sleep_->set_sleep_duration(ms_left); + } +#endif this->deep_sleep_->begin_sleep(true); } protected: DeepSleepComponent *deep_sleep_; +#ifdef USE_TIME + optional hour_; + optional minute_; + optional second_; + + time::RealTimeClock *time_; +#endif }; template class PreventDeepSleepAction : public Action { diff --git a/esphome/components/time/real_time_clock.cpp b/esphome/components/time/real_time_clock.cpp index 0469ba2c37..36c5f4161d 100644 --- a/esphome/components/time/real_time_clock.cpp +++ b/esphome/components/time/real_time_clock.cpp @@ -176,6 +176,31 @@ void ESPTime::recalc_timestamp_utc(bool use_day_of_year) { res += this->second; this->timestamp = res; } + +int32_t ESPTime::timezone_offset() { + int32_t offset = 0; + time_t now = ::time(nullptr); + auto local = ESPTime::from_epoch_local(now); + auto utc = ESPTime::from_epoch_utc(now); + bool negative = utc.hour > local.hour && local.day_of_year <= utc.day_of_year; + + if (utc.minute > local.minute) { + local.minute += 60; + local.hour -= 1; + } + offset += (local.minute - utc.minute) * 60; + + if (negative) { + offset -= (utc.hour - local.hour) * 3600; + } else { + if (utc.hour > local.hour) { + local.hour += 24; + } + offset += (local.hour - utc.hour) * 3600; + } + return offset; +} + bool ESPTime::operator<(ESPTime other) { return this->timestamp < other.timestamp; } bool ESPTime::operator<=(ESPTime other) { return this->timestamp <= other.timestamp; } bool ESPTime::operator==(ESPTime other) { return this->timestamp == other.timestamp; } diff --git a/esphome/components/time/real_time_clock.h b/esphome/components/time/real_time_clock.h index c45deb0be5..b22c6f04d7 100644 --- a/esphome/components/time/real_time_clock.h +++ b/esphome/components/time/real_time_clock.h @@ -88,6 +88,8 @@ struct ESPTime { /// Convert this ESPTime instance back to a tm struct. struct tm to_c_tm(); + static int32_t timezone_offset(); + /// Increment this clock instance by one second. void increment_second(); /// Increment this clock instance by one day. From ad57faa9a9808545da35a61a6c3b9d2a016d1c49 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 13 Apr 2022 13:42:28 +1200 Subject: [PATCH 0385/1729] Bump version to 2022.4.0b1 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 9096e66f4e..01d2d59c3d 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2022.4.0-dev" +__version__ = "2022.4.0b1" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From b778eed4193d14c261e4faee497077454fca987e Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 13 Apr 2022 13:42:28 +1200 Subject: [PATCH 0386/1729] Bump version to 2022.5.0-dev --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 9096e66f4e..fa5baf4fe2 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2022.4.0-dev" +__version__ = "2022.5.0-dev" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From b4a86ce6cfc110d71dfdcb25ea02a69d96c8c02e Mon Sep 17 00:00:00 2001 From: matthias882 <30553262+matthias882@users.noreply.github.com> Date: Wed, 13 Apr 2022 23:36:16 +0200 Subject: [PATCH 0387/1729] Changes accuracy of single cell voltage (#3387) --- esphome/components/daly_bms/sensor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/daly_bms/sensor.py b/esphome/components/daly_bms/sensor.py index e2e8528317..2274a2153a 100644 --- a/esphome/components/daly_bms/sensor.py +++ b/esphome/components/daly_bms/sensor.py @@ -98,6 +98,8 @@ CELL_VOLTAGE_SCHEMA = sensor.sensor_schema( unit_of_measurement=UNIT_VOLT, device_class=DEVICE_CLASS_VOLTAGE, state_class=STATE_CLASS_MEASUREMENT, + icon=ICON_FLASH, + accuracy_decimals=3, ) CONFIG_SCHEMA = cv.All( From 047c18eac0ca65a6609c58d4fd419e389968031f Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 14 Apr 2022 11:25:31 +1200 Subject: [PATCH 0388/1729] Add default object_id_generator for mqtt (#3389) --- esphome/components/mqtt/mqtt_client.cpp | 7 ++++++- esphome/components/mqtt/mqtt_client.h | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index 3c6ce7cdfc..12a43dc232 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -556,7 +556,12 @@ void MQTTClientComponent::disable_last_will() { this->last_will_.topic = ""; } void MQTTClientComponent::disable_discovery() { this->discovery_info_ = MQTTDiscoveryInfo{ - .prefix = "", .retain = false, .clean = false, .unique_id_generator = MQTT_LEGACY_UNIQUE_ID_GENERATOR}; + .prefix = "", + .retain = false, + .clean = false, + .unique_id_generator = MQTT_LEGACY_UNIQUE_ID_GENERATOR, + .object_id_generator = MQTT_NONE_OBJECT_ID_GENERATOR, + }; } void MQTTClientComponent::on_shutdown() { if (!this->shutdown_message_.topic.empty()) { diff --git a/esphome/components/mqtt/mqtt_client.h b/esphome/components/mqtt/mqtt_client.h index 4880bbaa5b..20b174a66f 100644 --- a/esphome/components/mqtt/mqtt_client.h +++ b/esphome/components/mqtt/mqtt_client.h @@ -277,6 +277,7 @@ class MQTTClientComponent : public Component { .retain = true, .clean = false, .unique_id_generator = MQTT_LEGACY_UNIQUE_ID_GENERATOR, + .object_id_generator = MQTT_NONE_OBJECT_ID_GENERATOR, }; std::string topic_prefix_{}; MQTTMessage log_message_; From 70a35656e463252ac6bfad44da9e41d5894de3d8 Mon Sep 17 00:00:00 2001 From: rnauber <7414650+rnauber@users.noreply.github.com> Date: Thu, 14 Apr 2022 03:13:51 +0200 Subject: [PATCH 0389/1729] Add support for Shelly Dimmer 2 (#2954) Co-authored-by: Niclas Larsson Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Co-authored-by: Jernej Kos Co-authored-by: Richard Nauber --- CODEOWNERS | 1 + esphome/components/shelly_dimmer/LICENSE.txt | 2 + esphome/components/shelly_dimmer/__init__.py | 1 + esphome/components/shelly_dimmer/dev_table.h | 158 +++ esphome/components/shelly_dimmer/light.py | 219 ++++ .../shelly_dimmer/shelly_dimmer.cpp | 526 ++++++++ .../components/shelly_dimmer/shelly_dimmer.h | 117 ++ .../components/shelly_dimmer/stm32flash.cpp | 1061 +++++++++++++++++ esphome/components/shelly_dimmer/stm32flash.h | 129 ++ esphome/core/defines.h | 6 + tests/test1.yaml | 12 + 11 files changed, 2232 insertions(+) create mode 100644 esphome/components/shelly_dimmer/LICENSE.txt create mode 100644 esphome/components/shelly_dimmer/__init__.py create mode 100644 esphome/components/shelly_dimmer/dev_table.h create mode 100644 esphome/components/shelly_dimmer/light.py create mode 100644 esphome/components/shelly_dimmer/shelly_dimmer.cpp create mode 100644 esphome/components/shelly_dimmer/shelly_dimmer.h create mode 100644 esphome/components/shelly_dimmer/stm32flash.cpp create mode 100644 esphome/components/shelly_dimmer/stm32flash.h diff --git a/CODEOWNERS b/CODEOWNERS index 7595fc52e2..02945ec0a4 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -173,6 +173,7 @@ esphome/components/select/* @esphome/core esphome/components/sensirion_common/* @martgras esphome/components/sensor/* @esphome/core esphome/components/sgp40/* @SenexCrenshaw +esphome/components/shelly_dimmer/* @edge90 @rnauber esphome/components/sht4x/* @sjtrny esphome/components/shutdown/* @esphome/core @jsuanet esphome/components/sim800l/* @glmnet diff --git a/esphome/components/shelly_dimmer/LICENSE.txt b/esphome/components/shelly_dimmer/LICENSE.txt new file mode 100644 index 0000000000..524fe0d514 --- /dev/null +++ b/esphome/components/shelly_dimmer/LICENSE.txt @@ -0,0 +1,2 @@ +The firmware files for the STM microcontroller (shelly-dimmer-stm32_*.bin) are taken from +https://github.com/jamesturton/shelly-dimmer-stm32 and GPLv3 licensed. diff --git a/esphome/components/shelly_dimmer/__init__.py b/esphome/components/shelly_dimmer/__init__.py new file mode 100644 index 0000000000..accefbbc34 --- /dev/null +++ b/esphome/components/shelly_dimmer/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@rnauber", "@edge90"] diff --git a/esphome/components/shelly_dimmer/dev_table.h b/esphome/components/shelly_dimmer/dev_table.h new file mode 100644 index 0000000000..f4bf7778f2 --- /dev/null +++ b/esphome/components/shelly_dimmer/dev_table.h @@ -0,0 +1,158 @@ +/* + stm32flash - Open Source ST STM32 flash program for Arduino + Copyright (C) 2010 Geoffrey McRae + Copyright (C) 2014-2015 Antonio Borneo + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +*/ +#pragma once + +#include "esphome/core/defines.h" +#ifdef USE_SHD_FIRMWARE_DATA +#include "stm32flash.h" + +namespace esphome { +namespace shelly_dimmer { + +constexpr uint32_t SZ_128 = 0x00000080; +constexpr uint32_t SZ_256 = 0x00000100; +constexpr uint32_t SZ_1K = 0x00000400; +constexpr uint32_t SZ_2K = 0x00000800; +constexpr uint32_t SZ_16K = 0x00004000; +constexpr uint32_t SZ_32K = 0x00008000; +constexpr uint32_t SZ_64K = 0x00010000; +constexpr uint32_t SZ_128K = 0x00020000; +constexpr uint32_t SZ_256K = 0x00040000; + +/* + * Page-size for page-by-page flash erase. + * Arrays are zero terminated; last non-zero value is automatically repeated + */ + +/* fixed size pages */ +constexpr uint32_t p_128[] = {SZ_128, 0}; // NOLINT +constexpr uint32_t p_256[] = {SZ_256, 0}; // NOLINT +constexpr uint32_t p_1k[] = {SZ_1K, 0}; // NOLINT +constexpr uint32_t p_2k[] = {SZ_2K, 0}; // NOLINT +/* F2 and F4 page size */ +constexpr uint32_t f2f4[] = {SZ_16K, SZ_16K, SZ_16K, SZ_16K, SZ_64K, SZ_128K, 0}; // NOLINT +/* F4 dual bank page size */ +constexpr uint32_t f4db[] = {SZ_16K, SZ_16K, SZ_16K, SZ_16K, SZ_64K, SZ_128K, SZ_128K, // NOLINT + SZ_128K, SZ_16K, SZ_16K, SZ_16K, SZ_16K, SZ_64K, SZ_128K, 0}; +/* F7 page size */ +constexpr uint32_t f7[] = {SZ_32K, SZ_32K, SZ_32K, SZ_32K, SZ_128K, SZ_256K, 0}; // NOLINT + +/* + * Device table, corresponds to the "Bootloader device-dependant parameters" + * table in ST document AN2606. + * Note that the option bytes upper range is inclusive! + */ +constexpr stm32_dev_t DEVICES[] = { + /* ID "name" SRAM-address-range FLASH-address-range PPS PSize + Option-byte-addr-range System-mem-addr-range Flags */ + /* F0 */ + {0x440, "STM32F030x8/F05xxx", 0x20000800, 0x20002000, 0x08000000, 0x08010000, 4, p_1k, 0x1FFFF800, 0x1FFFF80F, + 0x1FFFEC00, 0x1FFFF800, 0}, + {0x442, "STM32F030xC/F09xxx", 0x20001800, 0x20008000, 0x08000000, 0x08040000, 2, p_2k, 0x1FFFF800, 0x1FFFF80F, + 0x1FFFC800, 0x1FFFF800, F_OBLL}, + {0x444, "STM32F03xx4/6", 0x20000800, 0x20001000, 0x08000000, 0x08008000, 4, p_1k, 0x1FFFF800, 0x1FFFF80F, + 0x1FFFEC00, 0x1FFFF800, 0}, + {0x445, "STM32F04xxx/F070x6", 0x20001800, 0x20001800, 0x08000000, 0x08008000, 4, p_1k, 0x1FFFF800, 0x1FFFF80F, + 0x1FFFC400, 0x1FFFF800, 0}, + {0x448, "STM32F070xB/F071xx/F72xx", 0x20001800, 0x20004000, 0x08000000, 0x08020000, 2, p_2k, 0x1FFFF800, 0x1FFFF80F, + 0x1FFFC800, 0x1FFFF800, 0}, + /* F1 */ + {0x412, "STM32F10xxx Low-density", 0x20000200, 0x20002800, 0x08000000, 0x08008000, 4, p_1k, 0x1FFFF800, 0x1FFFF80F, + 0x1FFFF000, 0x1FFFF800, 0}, + {0x410, "STM32F10xxx Medium-density", 0x20000200, 0x20005000, 0x08000000, 0x08020000, 4, p_1k, 0x1FFFF800, + 0x1FFFF80F, 0x1FFFF000, 0x1FFFF800, 0}, + {0x414, "STM32F10xxx High-density", 0x20000200, 0x20010000, 0x08000000, 0x08080000, 2, p_2k, 0x1FFFF800, 0x1FFFF80F, + 0x1FFFF000, 0x1FFFF800, 0}, + {0x420, "STM32F10xxx Medium-density VL", 0x20000200, 0x20002000, 0x08000000, 0x08020000, 4, p_1k, 0x1FFFF800, + 0x1FFFF80F, 0x1FFFF000, 0x1FFFF800, 0}, + {0x428, "STM32F10xxx High-density VL", 0x20000200, 0x20008000, 0x08000000, 0x08080000, 2, p_2k, 0x1FFFF800, + 0x1FFFF80F, 0x1FFFF000, 0x1FFFF800, 0}, + {0x418, "STM32F105xx/F107xx", 0x20001000, 0x20010000, 0x08000000, 0x08040000, 2, p_2k, 0x1FFFF800, 0x1FFFF80F, + 0x1FFFB000, 0x1FFFF800, 0}, + {0x430, "STM32F10xxx XL-density", 0x20000800, 0x20018000, 0x08000000, 0x08100000, 2, p_2k, 0x1FFFF800, 0x1FFFF80F, + 0x1FFFE000, 0x1FFFF800, 0}, + /* F2 */ + {0x411, "STM32F2xxxx", 0x20002000, 0x20020000, 0x08000000, 0x08100000, 1, f2f4, 0x1FFFC000, 0x1FFFC00F, 0x1FFF0000, + 0x1FFF7800, 0}, + /* F3 */ + {0x432, "STM32F373xx/F378xx", 0x20001400, 0x20008000, 0x08000000, 0x08040000, 2, p_2k, 0x1FFFF800, 0x1FFFF80F, + 0x1FFFD800, 0x1FFFF800, 0}, + {0x422, "STM32F302xB(C)/F303xB(C)/F358xx", 0x20001400, 0x2000A000, 0x08000000, 0x08040000, 2, p_2k, 0x1FFFF800, + 0x1FFFF80F, 0x1FFFD800, 0x1FFFF800, 0}, + {0x439, "STM32F301xx/F302x4(6/8)/F318xx", 0x20001800, 0x20004000, 0x08000000, 0x08010000, 2, p_2k, 0x1FFFF800, + 0x1FFFF80F, 0x1FFFD800, 0x1FFFF800, 0}, + {0x438, "STM32F303x4(6/8)/F334xx/F328xx", 0x20001800, 0x20003000, 0x08000000, 0x08010000, 2, p_2k, 0x1FFFF800, + 0x1FFFF80F, 0x1FFFD800, 0x1FFFF800, 0}, + {0x446, "STM32F302xD(E)/F303xD(E)/F398xx", 0x20001800, 0x20010000, 0x08000000, 0x08080000, 2, p_2k, 0x1FFFF800, + 0x1FFFF80F, 0x1FFFD800, 0x1FFFF800, 0}, + /* F4 */ + {0x413, "STM32F40xxx/41xxx", 0x20003000, 0x20020000, 0x08000000, 0x08100000, 1, f2f4, 0x1FFFC000, 0x1FFFC00F, + 0x1FFF0000, 0x1FFF7800, 0}, + {0x419, "STM32F42xxx/43xxx", 0x20003000, 0x20030000, 0x08000000, 0x08200000, 1, f4db, 0x1FFEC000, 0x1FFFC00F, + 0x1FFF0000, 0x1FFF7800, 0}, + {0x423, "STM32F401xB(C)", 0x20003000, 0x20010000, 0x08000000, 0x08040000, 1, f2f4, 0x1FFFC000, 0x1FFFC00F, + 0x1FFF0000, 0x1FFF7800, 0}, + {0x433, "STM32F401xD(E)", 0x20003000, 0x20018000, 0x08000000, 0x08080000, 1, f2f4, 0x1FFFC000, 0x1FFFC00F, + 0x1FFF0000, 0x1FFF7800, 0}, + {0x458, "STM32F410xx", 0x20003000, 0x20008000, 0x08000000, 0x08020000, 1, f2f4, 0x1FFFC000, 0x1FFFC00F, 0x1FFF0000, + 0x1FFF7800, 0}, + {0x431, "STM32F411xx", 0x20003000, 0x20020000, 0x08000000, 0x08080000, 1, f2f4, 0x1FFFC000, 0x1FFFC00F, 0x1FFF0000, + 0x1FFF7800, 0}, + {0x421, "STM32F446xx", 0x20003000, 0x20020000, 0x08000000, 0x08080000, 1, f2f4, 0x1FFFC000, 0x1FFFC00F, 0x1FFF0000, + 0x1FFF7800, 0}, + {0x434, "STM32F469xx", 0x20003000, 0x20060000, 0x08000000, 0x08200000, 1, f4db, 0x1FFEC000, 0x1FFFC00F, 0x1FFF0000, + 0x1FFF7800, 0}, + /* F7 */ + {0x449, "STM32F74xxx/75xxx", 0x20004000, 0x20050000, 0x08000000, 0x08100000, 1, f7, 0x1FFF0000, 0x1FFF001F, + 0x1FF00000, 0x1FF0EDC0, 0}, + /* L0 */ + {0x425, "STM32L031xx/041xx", 0x20001000, 0x20002000, 0x08000000, 0x08008000, 32, p_128, 0x1FF80000, 0x1FF8001F, + 0x1FF00000, 0x1FF01000, 0}, + {0x417, "STM32L05xxx/06xxx", 0x20001000, 0x20002000, 0x08000000, 0x08010000, 32, p_128, 0x1FF80000, 0x1FF8001F, + 0x1FF00000, 0x1FF01000, 0}, + {0x447, "STM32L07xxx/08xxx", 0x20002000, 0x20005000, 0x08000000, 0x08030000, 32, p_128, 0x1FF80000, 0x1FF8001F, + 0x1FF00000, 0x1FF02000, 0}, + /* L1 */ + {0x416, "STM32L1xxx6(8/B)", 0x20000800, 0x20004000, 0x08000000, 0x08020000, 16, p_256, 0x1FF80000, 0x1FF8001F, + 0x1FF00000, 0x1FF01000, F_NO_ME}, + {0x429, "STM32L1xxx6(8/B)A", 0x20001000, 0x20008000, 0x08000000, 0x08020000, 16, p_256, 0x1FF80000, 0x1FF8001F, + 0x1FF00000, 0x1FF01000, 0}, + {0x427, "STM32L1xxxC", 0x20001000, 0x20008000, 0x08000000, 0x08040000, 16, p_256, 0x1FF80000, 0x1FF8001F, + 0x1FF00000, 0x1FF02000, 0}, + {0x436, "STM32L1xxxD", 0x20001000, 0x2000C000, 0x08000000, 0x08060000, 16, p_256, 0x1FF80000, 0x1FF8009F, + 0x1FF00000, 0x1FF02000, 0}, + {0x437, "STM32L1xxxE", 0x20001000, 0x20014000, 0x08000000, 0x08080000, 16, p_256, 0x1FF80000, 0x1FF8009F, + 0x1FF00000, 0x1FF02000, F_NO_ME}, + /* L4 */ + {0x415, "STM32L476xx/486xx", 0x20003100, 0x20018000, 0x08000000, 0x08100000, 1, p_2k, 0x1FFF7800, 0x1FFFF80F, + 0x1FFF0000, 0x1FFF7000, 0}, + /* These are not (yet) in AN2606: */ + {0x641, "Medium_Density PL", 0x20000200, 0x20005000, 0x08000000, 0x08020000, 4, p_1k, 0x1FFFF800, 0x1FFFF80F, + 0x1FFFF000, 0x1FFFF800, 0}, + {0x9a8, "STM32W-128K", 0x20000200, 0x20002000, 0x08000000, 0x08020000, 4, p_1k, 0x08040800, 0x0804080F, 0x08040000, + 0x08040800, 0}, + {0x9b0, "STM32W-256K", 0x20000200, 0x20004000, 0x08000000, 0x08040000, 4, p_2k, 0x08040800, 0x0804080F, 0x08040000, + 0x08040800, 0}, + {0x0, "", 0x0, 0x0, 0x0, 0x0, 0x0, nullptr, 0x0, 0x0, 0x0, 0x0, 0x0}, +}; + +} // namespace shelly_dimmer +} // namespace esphome +#endif diff --git a/esphome/components/shelly_dimmer/light.py b/esphome/components/shelly_dimmer/light.py new file mode 100644 index 0000000000..003498c090 --- /dev/null +++ b/esphome/components/shelly_dimmer/light.py @@ -0,0 +1,219 @@ +from pathlib import Path +import hashlib +import re +import requests + + +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import light, sensor, uart +from esphome.const import ( + CONF_OUTPUT_ID, + CONF_GAMMA_CORRECT, + CONF_POWER, + CONF_VOLTAGE, + CONF_CURRENT, + CONF_VERSION, + CONF_URL, + CONF_UPDATE_INTERVAL, + UNIT_VOLT, + UNIT_AMPERE, + UNIT_WATT, + DEVICE_CLASS_POWER, + DEVICE_CLASS_VOLTAGE, +) +from esphome.core import HexInt, CORE + +DOMAIN = "shelly_dimmer" +DEPENDENCIES = ["sensor", "uart"] + +shelly_dimmer_ns = cg.esphome_ns.namespace("shelly_dimmer") +ShellyDimmer = shelly_dimmer_ns.class_( + "ShellyDimmer", light.LightOutput, cg.PollingComponent, uart.UARTDevice +) + +CONF_FIRMWARE = "firmware" +CONF_SHA256 = "sha256" +CONF_UPDATE = "update" + +CONF_LEADING_EDGE = "leading_edge" +CONF_WARMUP_BRIGHTNESS = "warmup_brightness" +# CONF_WARMUP_TIME = "warmup_time" +CONF_MIN_BRIGHTNESS = "min_brightness" +CONF_MAX_BRIGHTNESS = "max_brightness" + +CONF_NRST_PIN = "nrst_pin" +CONF_BOOT0_PIN = "boot0_pin" + +KNOWN_FIRMWARE = { + "51.5": ( + "https://github.com/jamesturton/shelly-dimmer-stm32/releases/download/v51.5/shelly-dimmer-stm32_v51.5.bin", + "553fc1d78ed113227af7683eaa9c26189a961c4ea9a48000fb5aa8f8ac5d7b60", + ), + "51.6": ( + "https://github.com/jamesturton/shelly-dimmer-stm32/releases/download/v51.6/shelly-dimmer-stm32_v51.6.bin", + "eda483e111c914723a33f5088f1397d5c0b19333db4a88dc965636b976c16c36", + ), +} + + +def parse_firmware_version(value): + match = re.match(r"(\d+).(\d+)", value) + if match is None: + raise ValueError(f"Not a valid version number {value}") + major = int(match[1]) + minor = int(match[2]) + return major, minor + + +def get_firmware(value): + if not value[CONF_UPDATE]: + return None + + def dl(url): + try: + req = requests.get(url) + req.raise_for_status() + except requests.exceptions.RequestException as e: + raise cv.Invalid(f"Could not download firmware file ({url}): {e}") + + h = hashlib.new("sha256") + h.update(req.content) + return req.content, h.hexdigest() + + url = value[CONF_URL] + + if CONF_SHA256 in value: # we have a hash, enable caching + path = ( + Path(CORE.config_dir) + / ".esphome" + / DOMAIN + / (value[CONF_SHA256] + "_fw_stm.bin") + ) + + if not path.is_file(): + firmware_data, dl_hash = dl(url) + + if dl_hash != value[CONF_SHA256]: + raise cv.Invalid( + f"Hash mismatch for {url}: {dl_hash} != {value[CONF_SHA256]}" + ) + + path.parent.mkdir(exist_ok=True, parents=True) + path.write_bytes(firmware_data) + + else: + firmware_data = path.read_bytes() + else: # no caching, download every time + firmware_data, dl_hash = dl(url) + + return [HexInt(x) for x in firmware_data] + + +def validate_firmware(value): + config = value.copy() + if CONF_URL not in config: + try: + config[CONF_URL], config[CONF_SHA256] = KNOWN_FIRMWARE[config[CONF_VERSION]] + except KeyError as e: + raise cv.Invalid( + f"Firmware {config[CONF_VERSION]} is unknown, please specify an '{CONF_URL}' ..." + ) from e + get_firmware(config) + return config + + +def validate_sha256(value): + value = cv.string(value) + if not value.isalnum() or not len(value) == 64: + raise ValueError(f"Not a valid SHA256 hex string: {value}") + return value + + +def validate_version(value): + parse_firmware_version(value) + return value + + +CONFIG_SCHEMA = ( + light.BRIGHTNESS_ONLY_LIGHT_SCHEMA.extend( + { + cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(ShellyDimmer), + cv.Optional(CONF_FIRMWARE, default="51.6"): cv.maybe_simple_value( + { + cv.Optional(CONF_URL): cv.url, + cv.Optional(CONF_SHA256): validate_sha256, + cv.Required(CONF_VERSION): validate_version, + cv.Optional(CONF_UPDATE, default=False): cv.boolean, + }, + validate_firmware, # converts a simple version key to generate the full url + key=CONF_VERSION, + ), + cv.Optional(CONF_NRST_PIN, default="GPIO5"): pins.gpio_output_pin_schema, + cv.Optional(CONF_BOOT0_PIN, default="GPIO4"): pins.gpio_output_pin_schema, + cv.Optional(CONF_LEADING_EDGE, default=False): cv.boolean, + cv.Optional(CONF_WARMUP_BRIGHTNESS, default=100): cv.uint16_t, + # cv.Optional(CONF_WARMUP_TIME, default=20): cv.uint16_t, + cv.Optional(CONF_MIN_BRIGHTNESS, default=0): cv.uint16_t, + cv.Optional(CONF_MAX_BRIGHTNESS, default=1000): cv.uint16_t, + cv.Optional(CONF_POWER): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_POWER, + ), + cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + ), + cv.Optional(CONF_CURRENT): sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + device_class=DEVICE_CLASS_POWER, + accuracy_decimals=2, + ), + # Change the default gamma_correct setting. + cv.Optional(CONF_GAMMA_CORRECT, default=1.0): cv.positive_float, + } + ) + .extend(cv.polling_component_schema("10s")) + .extend(uart.UART_DEVICE_SCHEMA) +) + + +def to_code(config): + fw_hex = get_firmware(config[CONF_FIRMWARE]) + fw_major, fw_minor = parse_firmware_version(config[CONF_FIRMWARE][CONF_VERSION]) + + if fw_hex is not None: + cg.add_define("USE_SHD_FIRMWARE_DATA", fw_hex) + cg.add_define("USE_SHD_FIRMWARE_MAJOR_VERSION", fw_major) + cg.add_define("USE_SHD_FIRMWARE_MINOR_VERSION", fw_minor) + + var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) + yield cg.register_component(var, config) + config.pop( + CONF_UPDATE_INTERVAL + ) # drop UPDATE_INTERVAL as it does not apply to the light component + + yield light.register_light(var, config) + yield uart.register_uart_device(var, config) + + nrst_pin = yield cg.gpio_pin_expression(config[CONF_NRST_PIN]) + cg.add(var.set_nrst_pin(nrst_pin)) + boot0_pin = yield cg.gpio_pin_expression(config[CONF_BOOT0_PIN]) + cg.add(var.set_boot0_pin(boot0_pin)) + + cg.add(var.set_leading_edge(config[CONF_LEADING_EDGE])) + cg.add(var.set_warmup_brightness(config[CONF_WARMUP_BRIGHTNESS])) + # cg.add(var.set_warmup_time(config[CONF_WARMUP_TIME])) + cg.add(var.set_min_brightness(config[CONF_MIN_BRIGHTNESS])) + cg.add(var.set_max_brightness(config[CONF_MAX_BRIGHTNESS])) + + for key in [CONF_POWER, CONF_VOLTAGE, CONF_CURRENT]: + if key not in config: + continue + + conf = config[key] + sens = yield sensor.new_sensor(conf) + cg.add(getattr(var, f"set_{key}_sensor")(sens)) diff --git a/esphome/components/shelly_dimmer/shelly_dimmer.cpp b/esphome/components/shelly_dimmer/shelly_dimmer.cpp new file mode 100644 index 0000000000..3b79d0bf57 --- /dev/null +++ b/esphome/components/shelly_dimmer/shelly_dimmer.cpp @@ -0,0 +1,526 @@ +#include "esphome/core/defines.h" +#include "esphome/core/helpers.h" + +#include "shelly_dimmer.h" +#ifdef USE_SHD_FIRMWARE_DATA +#include "stm32flash.h" +#endif + +#ifndef USE_ESP_IDF +#include +#endif + +#include +#include +#include +#include + +namespace { + +constexpr char TAG[] = "shelly_dimmer"; + +constexpr uint8_t SHELLY_DIMMER_ACK_TIMEOUT = 200; // ms +constexpr uint8_t SHELLY_DIMMER_MAX_RETRIES = 3; +constexpr uint16_t SHELLY_DIMMER_MAX_BRIGHTNESS = 1000; // 100% + +// Protocol framing. +constexpr uint8_t SHELLY_DIMMER_PROTO_START_BYTE = 0x01; +constexpr uint8_t SHELLY_DIMMER_PROTO_END_BYTE = 0x04; + +// Supported commands. +constexpr uint8_t SHELLY_DIMMER_PROTO_CMD_SWITCH = 0x01; +constexpr uint8_t SHELLY_DIMMER_PROTO_CMD_POLL = 0x10; +constexpr uint8_t SHELLY_DIMMER_PROTO_CMD_VERSION = 0x11; +constexpr uint8_t SHELLY_DIMMER_PROTO_CMD_SETTINGS = 0x20; + +// Command payload sizes. +constexpr uint8_t SHELLY_DIMMER_PROTO_CMD_SWITCH_SIZE = 2; +constexpr uint8_t SHELLY_DIMMER_PROTO_CMD_SETTINGS_SIZE = 10; +constexpr uint8_t SHELLY_DIMMER_PROTO_MAX_FRAME_SIZE = 4 + 72 + 3; + +// STM Firmware +#ifdef USE_SHD_FIRMWARE_DATA +constexpr uint8_t STM_FIRMWARE[] PROGMEM = USE_SHD_FIRMWARE_DATA; +constexpr uint32_t STM_FIRMWARE_SIZE_IN_BYTES = sizeof(STM_FIRMWARE); +#endif + +// Scaling Constants +constexpr float POWER_SCALING_FACTOR = 880373; +constexpr float VOLTAGE_SCALING_FACTOR = 347800; +constexpr float CURRENT_SCALING_FACTOR = 1448; + +// Esentially std::size() for pre c++17 +template constexpr size_t size(const T (&/*unused*/)[N]) noexcept { return N; } + +} // Anonymous namespace + +namespace esphome { +namespace shelly_dimmer { + +/// Computes a crappy checksum as defined by the Shelly Dimmer protocol. +uint16_t shelly_dimmer_checksum(const uint8_t *buf, int len) { + return std::accumulate(buf, buf + len, 0); +} + +void ShellyDimmer::setup() { + this->pin_nrst_->setup(); + this->pin_boot0_->setup(); + + ESP_LOGI(TAG, "Initializing Shelly Dimmer..."); + + // Reset the STM32 and check the firmware version. + for (int i = 0; i < 2; i++) { + this->reset_normal_boot_(); + this->send_command_(SHELLY_DIMMER_PROTO_CMD_VERSION, nullptr, 0); + ESP_LOGI(TAG, "STM32 current firmware version: %d.%d, desired version: %d.%d", this->version_major_, + this->version_minor_, USE_SHD_FIRMWARE_MAJOR_VERSION, USE_SHD_FIRMWARE_MINOR_VERSION); + if (this->version_major_ != USE_SHD_FIRMWARE_MAJOR_VERSION || + this->version_minor_ != USE_SHD_FIRMWARE_MINOR_VERSION) { +#ifdef USE_SHD_FIRMWARE_DATA + // Update firmware if needed. + ESP_LOGW(TAG, "Unsupported STM32 firmware version, flashing"); + if (i > 0) { + // Upgrade was already performed but the reported version is still not right. + ESP_LOGE(TAG, "STM32 firmware upgrade already performed, but version is still incorrect"); + this->mark_failed(); + return; + } + + if (!this->upgrade_firmware_()) { + ESP_LOGW(TAG, "Failed to upgrade firmware"); + this->mark_failed(); + return; + } + + // Firmware upgrade completed, do the checks again. + continue; +#else + ESP_LOGW(TAG, "Firmware version mismatch, put 'update: true' in the yaml to flash an update."); + this->mark_failed(); + return; +#endif + } + break; + } + + this->send_settings_(); + // Do an immediate poll to refresh current state. + this->send_command_(SHELLY_DIMMER_PROTO_CMD_POLL, nullptr, 0); + + this->ready_ = true; +} + +void ShellyDimmer::update() { this->send_command_(SHELLY_DIMMER_PROTO_CMD_POLL, nullptr, 0); } + +void ShellyDimmer::dump_config() { + ESP_LOGCONFIG(TAG, "ShellyDimmer:"); + LOG_PIN(" NRST Pin: ", this->pin_nrst_); + LOG_PIN(" BOOT0 Pin: ", this->pin_boot0_); + + ESP_LOGCONFIG(TAG, " Leading Edge: %s", YESNO(this->leading_edge_)); + ESP_LOGCONFIG(TAG, " Warmup Brightness: %d", this->warmup_brightness_); + // ESP_LOGCONFIG(TAG, " Warmup Time: %d", this->warmup_time_); + // ESP_LOGCONFIG(TAG, " Fade Rate: %d", this->fade_rate_); + ESP_LOGCONFIG(TAG, " Minimum Brightness: %d", this->min_brightness_); + ESP_LOGCONFIG(TAG, " Maximum Brightness: %d", this->max_brightness_); + + LOG_UPDATE_INTERVAL(this); + + ESP_LOGCONFIG(TAG, " STM32 current firmware version: %d.%d ", this->version_major_, this->version_minor_); + ESP_LOGCONFIG(TAG, " STM32 required firmware version: %d.%d", USE_SHD_FIRMWARE_MAJOR_VERSION, + USE_SHD_FIRMWARE_MINOR_VERSION); + + if (this->version_major_ != USE_SHD_FIRMWARE_MAJOR_VERSION || + this->version_minor_ != USE_SHD_FIRMWARE_MINOR_VERSION) { + ESP_LOGE(TAG, " Firmware version mismatch, put 'update: true' in the yaml to flash an update."); + } +} + +void ShellyDimmer::write_state(light::LightState *state) { + if (!this->ready_) { + return; + } + + float brightness; + state->current_values_as_brightness(&brightness); + + const uint16_t brightness_int = this->convert_brightness_(brightness); + if (brightness_int == this->brightness_) { + ESP_LOGV(TAG, "Not sending unchanged value"); + return; + } + ESP_LOGD(TAG, "Brightness update: %d (raw: %f)", brightness_int, brightness); + + this->send_brightness_(brightness_int); +} +#ifdef USE_SHD_FIRMWARE_DATA +bool ShellyDimmer::upgrade_firmware_() { + ESP_LOGW(TAG, "Starting STM32 firmware upgrade"); + this->reset_dfu_boot_(); + + // Could be constexpr in c++17 + static const auto CLOSE = [](stm32_t *stm32) { stm32_close(stm32); }; + + // Cleanup with RAII + std::unique_ptr stm32{stm32_init(this, STREAM_SERIAL, 1), CLOSE}; + + if (!stm32) { + ESP_LOGW(TAG, "Failed to initialize STM32"); + return false; + } + + // Erase STM32 flash. + if (stm32_erase_memory(stm32.get(), 0, STM32_MASS_ERASE) != STM32_ERR_OK) { + ESP_LOGW(TAG, "Failed to erase STM32 flash memory"); + return false; + } + + static constexpr uint32_t BUFFER_SIZE = 256; + + // Copy the STM32 firmware over in 256-byte chunks. Note that the firmware is stored + // in flash memory so all accesses need to be 4-byte aligned. + uint8_t buffer[BUFFER_SIZE]; + const uint8_t *p = STM_FIRMWARE; + uint32_t offset = 0; + uint32_t addr = stm32->dev->fl_start; + const uint32_t end = addr + STM_FIRMWARE_SIZE_IN_BYTES; + + while (addr < end && offset < STM_FIRMWARE_SIZE_IN_BYTES) { + const uint32_t left_of_buffer = std::min(end - addr, BUFFER_SIZE); + const uint32_t len = std::min(left_of_buffer, STM_FIRMWARE_SIZE_IN_BYTES - offset); + + if (len == 0) { + break; + } + + std::memcpy(buffer, p, BUFFER_SIZE); + p += BUFFER_SIZE; + + if (stm32_write_memory(stm32.get(), addr, buffer, len) != STM32_ERR_OK) { + ESP_LOGW(TAG, "Failed to write to STM32 flash memory"); + return false; + } + + addr += len; + offset += len; + } + + ESP_LOGI(TAG, "STM32 firmware upgrade successful"); + + return true; +} +#endif + +uint16_t ShellyDimmer::convert_brightness_(float brightness) { + // Special case for zero as only zero means turn off completely. + if (brightness == 0.0) { + return 0; + } + + return remap(brightness, 0.0f, 1.0f, this->min_brightness_, this->max_brightness_); +} + +void ShellyDimmer::send_brightness_(uint16_t brightness) { + const uint8_t payload[] = { + // Brightness (%) * 10. + static_cast(brightness & 0xff), + static_cast(brightness >> 8), + }; + static_assert(size(payload) == SHELLY_DIMMER_PROTO_CMD_SWITCH_SIZE, "Invalid payload size"); + + this->send_command_(SHELLY_DIMMER_PROTO_CMD_SWITCH, payload, SHELLY_DIMMER_PROTO_CMD_SWITCH_SIZE); + + this->brightness_ = brightness; +} + +void ShellyDimmer::send_settings_() { + const uint16_t fade_rate = std::min(uint16_t{100}, this->fade_rate_); + + float brightness = 0.0; + if (this->state_ != nullptr) { + this->state_->current_values_as_brightness(&brightness); + } + const uint16_t brightness_int = this->convert_brightness_(brightness); + ESP_LOGD(TAG, "Brightness update: %d (raw: %f)", brightness_int, brightness); + + const uint8_t payload[] = { + // Brightness (%) * 10. + static_cast(brightness_int & 0xff), + static_cast(brightness_int >> 8), + // Leading / trailing edge [0x01 = leading, 0x02 = trailing]. + this->leading_edge_ ? uint8_t{0x01} : uint8_t{0x02}, + 0x00, + // Fade rate. + static_cast(fade_rate & 0xff), + static_cast(fade_rate >> 8), + // Warmup brightness. + static_cast(this->warmup_brightness_ & 0xff), + static_cast(this->warmup_brightness_ >> 8), + // Warmup time. + static_cast(this->warmup_time_ & 0xff), + static_cast(this->warmup_time_ >> 8), + }; + static_assert(size(payload) == SHELLY_DIMMER_PROTO_CMD_SETTINGS_SIZE, "Invalid payload size"); + + this->send_command_(SHELLY_DIMMER_PROTO_CMD_SETTINGS, payload, SHELLY_DIMMER_PROTO_CMD_SETTINGS_SIZE); + + // Also send brightness separately as it is ignored above. + this->send_brightness_(brightness_int); +} + +bool ShellyDimmer::send_command_(uint8_t cmd, const uint8_t *const payload, uint8_t len) { + ESP_LOGD(TAG, "Sending command: 0x%02x (%d bytes) payload 0x%s", cmd, len, format_hex(payload, len).c_str()); + + // Prepare a command frame. + uint8_t frame[SHELLY_DIMMER_PROTO_MAX_FRAME_SIZE]; + const size_t frame_len = this->frame_command_(frame, cmd, payload, len); + + // Write the frame and wait for acknowledgement. + int retries = SHELLY_DIMMER_MAX_RETRIES; + while (retries--) { + this->write_array(frame, frame_len); + this->flush(); + + ESP_LOGD(TAG, "Command sent, waiting for reply"); + const uint32_t tx_time = millis(); + while (millis() - tx_time < SHELLY_DIMMER_ACK_TIMEOUT) { + if (this->read_frame_()) { + return true; + } + delay(1); + } + ESP_LOGW(TAG, "Timeout while waiting for reply"); + } + ESP_LOGW(TAG, "Failed to send command"); + return false; +} + +size_t ShellyDimmer::frame_command_(uint8_t *data, uint8_t cmd, const uint8_t *const payload, size_t len) { + size_t pos = 0; + + // Generate a frame. + data[0] = SHELLY_DIMMER_PROTO_START_BYTE; + data[1] = ++this->seq_; + data[2] = cmd; + data[3] = len; + pos += 4; + + if (payload != nullptr) { + std::memcpy(data + 4, payload, len); + pos += len; + } + + // Calculate checksum for the payload. + const uint16_t csum = shelly_dimmer_checksum(data + 1, 3 + len); + data[pos++] = static_cast(csum >> 8); + data[pos++] = static_cast(csum & 0xff); + data[pos++] = SHELLY_DIMMER_PROTO_END_BYTE; + return pos; +} + +int ShellyDimmer::handle_byte_(uint8_t c) { + const uint8_t pos = this->buffer_pos_; + + if (pos == 0) { + // Must be start byte. + return c == SHELLY_DIMMER_PROTO_START_BYTE ? 1 : -1; + } else if (pos < 4) { + // Header. + return 1; + } + + // Decode payload length from header. + const uint8_t payload_len = this->buffer_[3]; + if ((4 + payload_len + 3) > SHELLY_DIMMER_BUFFER_SIZE) { + return -1; + } + + if (pos < 4 + payload_len + 1) { + // Payload. + return 1; + } + + if (pos == 4 + payload_len + 1) { + // Verify checksum. + const uint16_t csum = (this->buffer_[pos - 1] << 8 | c); + const uint16_t csum_verify = shelly_dimmer_checksum(&this->buffer_[1], 3 + payload_len); + if (csum != csum_verify) { + return -1; + } + return 1; + } + + if (pos == 4 + payload_len + 2) { + // Must be end byte. + return c == SHELLY_DIMMER_PROTO_END_BYTE ? 0 : -1; + } + return -1; +} + +bool ShellyDimmer::read_frame_() { + while (this->available()) { + const uint8_t c = this->read(); + this->buffer_[this->buffer_pos_] = c; + + ESP_LOGV(TAG, "Read byte: 0x%02x (pos %d)", c, this->buffer_pos_); + + switch (this->handle_byte_(c)) { + case 0: { + // Frame successfully received. + this->handle_frame_(); + this->buffer_pos_ = 0; + return true; + } + case -1: { + // Failure. + this->buffer_pos_ = 0; + break; + } + case 1: { + // Need more data. + this->buffer_pos_++; + break; + } + } + } + return false; +} + +bool ShellyDimmer::handle_frame_() { + const uint8_t seq = this->buffer_[1]; + const uint8_t cmd = this->buffer_[2]; + const uint8_t payload_len = this->buffer_[3]; + + ESP_LOGD(TAG, "Got frame: 0x%02x", cmd); + + // Compare with expected identifier as the frame is always a response to + // our previously sent command. + if (seq != this->seq_) { + return false; + } + + const uint8_t *payload = &this->buffer_[4]; + + // Handle response. + switch (cmd) { + case SHELLY_DIMMER_PROTO_CMD_POLL: { + if (payload_len < 16) { + return false; + } + + const uint8_t hw_version = payload[0]; + // payload[1] is unused. + const uint16_t brightness = encode_uint16(payload[3], payload[2]); + + const uint32_t power_raw = encode_uint32(payload[7], payload[6], payload[5], payload[4]); + + const uint32_t voltage_raw = encode_uint32(payload[11], payload[10], payload[9], payload[8]); + + const uint32_t current_raw = encode_uint32(payload[15], payload[14], payload[13], payload[12]); + + const uint16_t fade_rate = payload[16]; + + float power = 0; + if (power_raw > 0) { + power = POWER_SCALING_FACTOR / static_cast(power_raw); + } + + float voltage = 0; + if (voltage_raw > 0) { + voltage = VOLTAGE_SCALING_FACTOR / static_cast(voltage_raw); + } + + float current = 0; + if (current_raw > 0) { + current = CURRENT_SCALING_FACTOR / static_cast(current_raw); + } + + ESP_LOGI(TAG, "Got dimmer data:"); + ESP_LOGI(TAG, " HW version: %d", hw_version); + ESP_LOGI(TAG, " Brightness: %d", brightness); + ESP_LOGI(TAG, " Fade rate: %d", fade_rate); + ESP_LOGI(TAG, " Power: %f W", power); + ESP_LOGI(TAG, " Voltage: %f V", voltage); + ESP_LOGI(TAG, " Current: %f A", current); + + // Update sensors. + if (this->power_sensor_ != nullptr) { + this->power_sensor_->publish_state(power); + } + if (this->voltage_sensor_ != nullptr) { + this->voltage_sensor_->publish_state(voltage); + } + if (this->current_sensor_ != nullptr) { + this->current_sensor_->publish_state(current); + } + + return true; + } + case SHELLY_DIMMER_PROTO_CMD_VERSION: { + if (payload_len < 2) { + return false; + } + + this->version_minor_ = payload[0]; + this->version_major_ = payload[1]; + return true; + } + case SHELLY_DIMMER_PROTO_CMD_SWITCH: + case SHELLY_DIMMER_PROTO_CMD_SETTINGS: { + return !(payload_len < 1 || payload[0] != 0x01); + } + default: { + return false; + } + } +} + +void ShellyDimmer::reset_(bool boot0) { + ESP_LOGD(TAG, "Reset STM32, boot0=%d", boot0); + + this->pin_boot0_->digital_write(boot0); + this->pin_nrst_->digital_write(false); + + // Wait 50ms for the STM32 to reset. + delay(50); // NOLINT + + // Clear receive buffer. + while (this->available()) { + this->read(); + } + + this->pin_nrst_->digital_write(true); + // Wait 50ms for the STM32 to boot. + delay(50); // NOLINT + + ESP_LOGD(TAG, "Reset STM32 done"); +} + +void ShellyDimmer::reset_normal_boot_() { + // set NONE parity in normal mode + +#ifndef USE_ESP_IDF // workaround for reconfiguring the uart + Serial.end(); + Serial.begin(115200, SERIAL_8N1); + Serial.flush(); +#endif + + this->flush(); + this->reset_(false); +} + +void ShellyDimmer::reset_dfu_boot_() { + // set EVEN parity in bootloader mode + +#ifndef USE_ESP_IDF // workaround for reconfiguring the uart + Serial.end(); + Serial.begin(115200, SERIAL_8E1); + Serial.flush(); +#endif + + this->flush(); + this->reset_(true); +} + +} // namespace shelly_dimmer +} // namespace esphome diff --git a/esphome/components/shelly_dimmer/shelly_dimmer.h b/esphome/components/shelly_dimmer/shelly_dimmer.h new file mode 100644 index 0000000000..b7d476279e --- /dev/null +++ b/esphome/components/shelly_dimmer/shelly_dimmer.h @@ -0,0 +1,117 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/log.h" +#include "esphome/components/light/light_output.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/uart/uart.h" + +#include + +namespace esphome { +namespace shelly_dimmer { + +class ShellyDimmer : public PollingComponent, public light::LightOutput, public uart::UARTDevice { + private: + static constexpr uint16_t SHELLY_DIMMER_BUFFER_SIZE = 256; + + public: + float get_setup_priority() const override { return setup_priority::LATE; } + + void setup() override; + void update() override; + void dump_config() override; + + light::LightTraits get_traits() override { + auto traits = light::LightTraits(); + traits.set_supported_color_modes({light::ColorMode::BRIGHTNESS}); + return traits; + } + + void setup_state(light::LightState *state) override { this->state_ = state; } + void write_state(light::LightState *state) override; + + void set_nrst_pin(GPIOPin *nrst_pin) { this->pin_nrst_ = nrst_pin; } + void set_boot0_pin(GPIOPin *boot0_pin) { this->pin_boot0_ = boot0_pin; } + + void set_leading_edge(bool leading_edge) { this->leading_edge_ = leading_edge; } + void set_warmup_brightness(uint16_t warmup_brightness) { this->warmup_brightness_ = warmup_brightness; } + void set_warmup_time(uint16_t warmup_time) { this->warmup_time_ = warmup_time; } + void set_fade_rate(uint16_t fade_rate) { this->fade_rate_ = fade_rate; } + void set_min_brightness(uint16_t min_brightness) { this->min_brightness_ = min_brightness; } + void set_max_brightness(uint16_t max_brightness) { this->max_brightness_ = max_brightness; } + + void set_power_sensor(sensor::Sensor *power_sensor) { this->power_sensor_ = power_sensor; } + void set_voltage_sensor(sensor::Sensor *voltage_sensor) { this->voltage_sensor_ = voltage_sensor; } + void set_current_sensor(sensor::Sensor *current_sensor) { this->current_sensor_ = current_sensor; } + + protected: + GPIOPin *pin_nrst_; + GPIOPin *pin_boot0_; + + // Frame parser state. + uint8_t seq_{0}; + std::array buffer_; + uint8_t buffer_pos_{0}; + + // Firmware version. + uint8_t version_major_; + uint8_t version_minor_; + + // Configuration. + bool leading_edge_{false}; + uint16_t warmup_brightness_{100}; + uint16_t warmup_time_{20}; + uint16_t fade_rate_{0}; + uint16_t min_brightness_{0}; + uint16_t max_brightness_{1000}; + + light::LightState *state_{nullptr}; + sensor::Sensor *power_sensor_{nullptr}; + sensor::Sensor *voltage_sensor_{nullptr}; + sensor::Sensor *current_sensor_{nullptr}; + + bool ready_{false}; + uint16_t brightness_; + + /// Convert relative brightness into a dimmer brightness value. + uint16_t convert_brightness_(float brightness); + + /// Sends the given brightness value. + void send_brightness_(uint16_t brightness); + + /// Sends dimmer configuration. + void send_settings_(); + + /// Performs a firmware upgrade. + bool upgrade_firmware_(); + + /// Sends a command and waits for an acknowledgement. + bool send_command_(uint8_t cmd, const uint8_t *payload, uint8_t len); + + /// Frames a given command payload. + size_t frame_command_(uint8_t *data, uint8_t cmd, const uint8_t *payload, size_t len); + + /// Handles a single byte as part of a protocol frame. + /// + /// Returns -1 on failure, 0 when finished and 1 when more bytes needed. + int handle_byte_(uint8_t c); + + /// Reads a response frame. + bool read_frame_(); + + /// Handles a complete frame. + bool handle_frame_(); + + /// Reset STM32 with the BOOT0 pin set to the given value. + void reset_(bool boot0); + + /// Reset STM32 to boot the regular firmware. + void reset_normal_boot_(); + + /// Reset STM32 to boot into DFU mode to enable firmware upgrades. + void reset_dfu_boot_(); +}; + +} // namespace shelly_dimmer +} // namespace esphome diff --git a/esphome/components/shelly_dimmer/stm32flash.cpp b/esphome/components/shelly_dimmer/stm32flash.cpp new file mode 100644 index 0000000000..4c777776fb --- /dev/null +++ b/esphome/components/shelly_dimmer/stm32flash.cpp @@ -0,0 +1,1061 @@ +/* + stm32flash - Open Source ST STM32 flash program for Arduino + Copyright 2010 Geoffrey McRae + Copyright 2012-2014 Tormod Volden + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +*/ + +#include "esphome/core/defines.h" +#ifdef USE_SHD_FIRMWARE_DATA + +#include + +#include "stm32flash.h" +#include "debug.h" + +#include "dev_table.h" +#include "esphome/core/log.h" + +#include +#include + +namespace { + +constexpr uint8_t STM32_ACK = 0x79; +constexpr uint8_t STM32_NACK = 0x1F; +constexpr uint8_t STM32_BUSY = 0x76; + +constexpr uint8_t STM32_CMD_INIT = 0x7F; +constexpr uint8_t STM32_CMD_GET = 0x00; /* get the version and command supported */ +constexpr uint8_t STM32_CMD_GVR = 0x01; /* get version and read protection status */ +constexpr uint8_t STM32_CMD_GID = 0x02; /* get ID */ +constexpr uint8_t STM32_CMD_RM = 0x11; /* read memory */ +constexpr uint8_t STM32_CMD_GO = 0x21; /* go */ +constexpr uint8_t STM32_CMD_WM = 0x31; /* write memory */ +constexpr uint8_t STM32_CMD_WM_NS = 0x32; /* no-stretch write memory */ +constexpr uint8_t STM32_CMD_ER = 0x43; /* erase */ +constexpr uint8_t STM32_CMD_EE = 0x44; /* extended erase */ +constexpr uint8_t STM32_CMD_EE_NS = 0x45; /* extended erase no-stretch */ +constexpr uint8_t STM32_CMD_WP = 0x63; /* write protect */ +constexpr uint8_t STM32_CMD_WP_NS = 0x64; /* write protect no-stretch */ +constexpr uint8_t STM32_CMD_UW = 0x73; /* write unprotect */ +constexpr uint8_t STM32_CMD_UW_NS = 0x74; /* write unprotect no-stretch */ +constexpr uint8_t STM32_CMD_RP = 0x82; /* readout protect */ +constexpr uint8_t STM32_CMD_RP_NS = 0x83; /* readout protect no-stretch */ +constexpr uint8_t STM32_CMD_UR = 0x92; /* readout unprotect */ +constexpr uint8_t STM32_CMD_UR_NS = 0x93; /* readout unprotect no-stretch */ +constexpr uint8_t STM32_CMD_CRC = 0xA1; /* compute CRC */ +constexpr uint8_t STM32_CMD_ERR = 0xFF; /* not a valid command */ + +constexpr uint32_t STM32_RESYNC_TIMEOUT = 35 * 1000; /* milliseconds */ +constexpr uint32_t STM32_MASSERASE_TIMEOUT = 35 * 1000; /* milliseconds */ +constexpr uint32_t STM32_PAGEERASE_TIMEOUT = 5 * 1000; /* milliseconds */ +constexpr uint32_t STM32_BLKWRITE_TIMEOUT = 1 * 1000; /* milliseconds */ +constexpr uint32_t STM32_WUNPROT_TIMEOUT = 1 * 1000; /* milliseconds */ +constexpr uint32_t STM32_WPROT_TIMEOUT = 1 * 1000; /* milliseconds */ +constexpr uint32_t STM32_RPROT_TIMEOUT = 1 * 1000; /* milliseconds */ +constexpr uint32_t DEFAULT_TIMEOUT = 5 * 1000; /* milliseconds */ + +constexpr uint8_t STM32_CMD_GET_LENGTH = 17; /* bytes in the reply */ + +/* Reset code for ARMv7-M (Cortex-M3) and ARMv6-M (Cortex-M0) + * see ARMv7-M or ARMv6-M Architecture Reference Manual (table B3-8) + * or "The definitive guide to the ARM Cortex-M3", section 14.4. + */ +constexpr uint8_t STM_RESET_CODE[] = { + 0x01, 0x49, // ldr r1, [pc, #4] ; () + 0x02, 0x4A, // ldr r2, [pc, #8] ; () + 0x0A, 0x60, // str r2, [r1, #0] + 0xfe, 0xe7, // endless: b endless + 0x0c, 0xed, 0x00, 0xe0, // .word 0xe000ed0c = NVIC AIRCR register address + 0x04, 0x00, 0xfa, 0x05 // .word 0x05fa0004 = VECTKEY | SYSRESETREQ +}; + +constexpr uint32_t STM_RESET_CODE_SIZE = sizeof(STM_RESET_CODE); + +/* RM0360, Empty check + * On STM32F070x6 and STM32F030xC devices only, internal empty check flag is + * implemented to allow easy programming of the virgin devices by the boot loader. This flag is + * used when BOOT0 pin is defining Main Flash memory as the target boot space. When the + * flag is set, the device is considered as empty and System memory (boot loader) is selected + * instead of the Main Flash as a boot space to allow user to program the Flash memory. + * This flag is updated only during Option bytes loading: it is set when the content of the + * address 0x08000 0000 is read as 0xFFFF FFFF, otherwise it is cleared. It means a power + * on or setting of OBL_LAUNCH bit in FLASH_CR register is needed to clear this flag after + * programming of a virgin device to execute user code after System reset. + */ +constexpr uint8_t STM_OBL_LAUNCH_CODE[] = { + 0x01, 0x49, // ldr r1, [pc, #4] ; () + 0x02, 0x4A, // ldr r2, [pc, #8] ; () + 0x0A, 0x60, // str r2, [r1, #0] + 0xfe, 0xe7, // endless: b endless + 0x10, 0x20, 0x02, 0x40, // address: FLASH_CR = 40022010 + 0x00, 0x20, 0x00, 0x00 // value: OBL_LAUNCH = 00002000 +}; + +constexpr uint32_t STM_OBL_LAUNCH_CODE_SIZE = sizeof(STM_OBL_LAUNCH_CODE); + +constexpr char TAG[] = "stm32flash"; + +} // Anonymous namespace + +namespace esphome { +namespace shelly_dimmer { + +namespace { + +int flash_addr_to_page_ceil(const stm32_t *stm, uint32_t addr) { + if (!(addr >= stm->dev->fl_start && addr <= stm->dev->fl_end)) + return 0; + + int page = 0; + addr -= stm->dev->fl_start; + const auto *psize = stm->dev->fl_ps; + + while (addr >= psize[0]) { + addr -= psize[0]; + page++; + if (psize[1]) + psize++; + } + + return addr ? page + 1 : page; +} + +stm32_err_t stm32_get_ack_timeout(const stm32_t *stm, uint32_t timeout) { + auto *stream = stm->stream; + uint8_t rxbyte; + + if (!(stm->flags & STREAM_OPT_RETRY)) + timeout = 0; + + if (timeout == 0) + timeout = DEFAULT_TIMEOUT; + + const uint32_t start_time = millis(); + do { + yield(); + if (!stream->available()) { + if (millis() < start_time + timeout) + continue; + ESP_LOGD(TAG, "Failed to read ACK timeout=%i", timeout); + return STM32_ERR_UNKNOWN; + } + + stream->read_byte(&rxbyte); + + if (rxbyte == STM32_ACK) + return STM32_ERR_OK; + if (rxbyte == STM32_NACK) + return STM32_ERR_NACK; + if (rxbyte != STM32_BUSY) { + ESP_LOGD(TAG, "Got byte 0x%02x instead of ACK", rxbyte); + return STM32_ERR_UNKNOWN; + } + } while (true); +} + +stm32_err_t stm32_get_ack(const stm32_t *stm) { return stm32_get_ack_timeout(stm, 0); } + +stm32_err_t stm32_send_command_timeout(const stm32_t *stm, const uint8_t cmd, const uint32_t timeout) { + auto *const stream = stm->stream; + + static constexpr auto BUFFER_SIZE = 2; + const uint8_t buf[] = { + cmd, + static_cast(cmd ^ 0xFF), + }; + static_assert(sizeof(buf) == BUFFER_SIZE, "Buf expected to be 2 bytes"); + + stream->write_array(buf, BUFFER_SIZE); + stream->flush(); + + stm32_err_t s_err = stm32_get_ack_timeout(stm, timeout); + if (s_err == STM32_ERR_OK) + return STM32_ERR_OK; + if (s_err == STM32_ERR_NACK) { + ESP_LOGD(TAG, "Got NACK from device on command 0x%02x", cmd); + } else { + ESP_LOGD(TAG, "Unexpected reply from device on command 0x%02x", cmd); + } + return STM32_ERR_UNKNOWN; +} + +stm32_err_t stm32_send_command(const stm32_t *stm, const uint8_t cmd) { + return stm32_send_command_timeout(stm, cmd, 0); +} + +/* if we have lost sync, send a wrong command and expect a NACK */ +stm32_err_t stm32_resync(const stm32_t *stm) { + auto *const stream = stm->stream; + uint32_t t0 = millis(); + auto t1 = t0; + + static constexpr auto BUFFER_SIZE = 2; + const uint8_t buf[] = { + STM32_CMD_ERR, + static_cast(STM32_CMD_ERR ^ 0xFF), + }; + static_assert(sizeof(buf) == BUFFER_SIZE, "Buf expected to be 2 bytes"); + + uint8_t ack; + while (t1 < t0 + STM32_RESYNC_TIMEOUT) { + stream->write_array(buf, BUFFER_SIZE); + stream->flush(); + if (!stream->read_array(&ack, 1)) { + t1 = millis(); + continue; + } + if (ack == STM32_NACK) + return STM32_ERR_OK; + t1 = millis(); + } + return STM32_ERR_UNKNOWN; +} + +/* + * some command receive reply frame with variable length, and length is + * embedded in reply frame itself. + * We can guess the length, but if we guess wrong the protocol gets out + * of sync. + * Use resync for frame oriented interfaces (e.g. I2C) and byte-by-byte + * read for byte oriented interfaces (e.g. UART). + * + * to run safely, data buffer should be allocated for 256+1 bytes + * + * len is value of the first byte in the frame. + */ +stm32_err_t stm32_guess_len_cmd(const stm32_t *stm, const uint8_t cmd, uint8_t *const data, unsigned int len) { + auto *const stream = stm->stream; + + if (stm32_send_command(stm, cmd) != STM32_ERR_OK) + return STM32_ERR_UNKNOWN; + if (stm->flags & STREAM_OPT_BYTE) { + /* interface is UART-like */ + if (!stream->read_array(data, 1)) + return STM32_ERR_UNKNOWN; + len = data[0]; + if (!stream->read_array(data + 1, len + 1)) + return STM32_ERR_UNKNOWN; + return STM32_ERR_OK; + } + + const auto ret = stream->read_array(data, len + 2); + if (ret && len == data[0]) + return STM32_ERR_OK; + if (!ret) { + /* restart with only one byte */ + if (stm32_resync(stm) != STM32_ERR_OK) + return STM32_ERR_UNKNOWN; + if (stm32_send_command(stm, cmd) != STM32_ERR_OK) + return STM32_ERR_UNKNOWN; + if (!stream->read_array(data, 1)) + return STM32_ERR_UNKNOWN; + } + + ESP_LOGD(TAG, "Re sync (len = %d)", data[0]); + if (stm32_resync(stm) != STM32_ERR_OK) + return STM32_ERR_UNKNOWN; + + len = data[0]; + if (stm32_send_command(stm, cmd) != STM32_ERR_OK) + return STM32_ERR_UNKNOWN; + + if (!stream->read_array(data, len + 2)) + return STM32_ERR_UNKNOWN; + return STM32_ERR_OK; +} + +/* + * Some interface, e.g. UART, requires a specific init sequence to let STM32 + * autodetect the interface speed. + * The sequence is only required one time after reset. + * This function sends the init sequence and, in case of timeout, recovers + * the interface. + */ +stm32_err_t stm32_send_init_seq(const stm32_t *stm) { + auto *const stream = stm->stream; + + stream->write_array(&STM32_CMD_INIT, 1); + stream->flush(); + + uint8_t byte; + bool ret = stream->read_array(&byte, 1); + if (ret && byte == STM32_ACK) + return STM32_ERR_OK; + if (ret && byte == STM32_NACK) { + /* We could get error later, but let's continue, for now. */ + ESP_LOGD(TAG, "Warning: the interface was not closed properly."); + return STM32_ERR_OK; + } + if (!ret) { + ESP_LOGD(TAG, "Failed to init device."); + return STM32_ERR_UNKNOWN; + } + + /* + * Check if previous STM32_CMD_INIT was taken as first byte + * of a command. Send a new byte, we should get back a NACK. + */ + stream->write_array(&STM32_CMD_INIT, 1); + stream->flush(); + + ret = stream->read_array(&byte, 1); + if (ret && byte == STM32_NACK) + return STM32_ERR_OK; + ESP_LOGD(TAG, "Failed to init device."); + return STM32_ERR_UNKNOWN; +} + +stm32_err_t stm32_mass_erase(const stm32_t *stm) { + auto *const stream = stm->stream; + + if (stm32_send_command(stm, stm->cmd->er) != STM32_ERR_OK) { + ESP_LOGD(TAG, "Can't initiate chip mass erase!"); + return STM32_ERR_UNKNOWN; + } + + /* regular erase (0x43) */ + if (stm->cmd->er == STM32_CMD_ER) { + const auto s_err = stm32_send_command_timeout(stm, 0xFF, STM32_MASSERASE_TIMEOUT); + if (s_err != STM32_ERR_OK) { + return STM32_ERR_UNKNOWN; + } + return STM32_ERR_OK; + } + + /* extended erase */ + static constexpr auto BUFFER_SIZE = 3; + const uint8_t buf[] = { + 0xFF, /* 0xFFFF the magic number for mass erase */ + 0xFF, 0x00, /* checksum */ + }; + static_assert(sizeof(buf) == BUFFER_SIZE, "Expected the buffer to be 3 bytes"); + stream->write_array(buf, 3); + stream->flush(); + + const auto s_err = stm32_get_ack_timeout(stm, STM32_MASSERASE_TIMEOUT); + if (s_err != STM32_ERR_OK) { + ESP_LOGD(TAG, "Mass erase failed. Try specifying the number of pages to be erased."); + return STM32_ERR_UNKNOWN; + } + return STM32_ERR_OK; +} + +template std::unique_ptr malloc_array_raii(size_t size) { + // Could be constexpr in c++17 + static const auto DELETOR = [](T *memory) { + free(memory); // NOLINT + }; + return std::unique_ptr{static_cast(malloc(size)), // NOLINT + DELETOR}; +} + +stm32_err_t stm32_pages_erase(const stm32_t *stm, const uint32_t spage, const uint32_t pages) { + auto *const stream = stm->stream; + uint8_t cs = 0; + int i = 0; + + /* The erase command reported by the bootloader is either 0x43, 0x44 or 0x45 */ + /* 0x44 is Extended Erase, a 2 byte based protocol and needs to be handled differently. */ + /* 0x45 is clock no-stretching version of Extended Erase for I2C port. */ + if (stm32_send_command(stm, stm->cmd->er) != STM32_ERR_OK) { + ESP_LOGD(TAG, "Can't initiate chip mass erase!"); + return STM32_ERR_UNKNOWN; + } + + /* regular erase (0x43) */ + if (stm->cmd->er == STM32_CMD_ER) { + // Free memory with RAII + auto buf = malloc_array_raii(1 + pages + 1); + + if (!buf) + return STM32_ERR_UNKNOWN; + + buf[i++] = pages - 1; + cs ^= (pages - 1); + for (auto pg_num = spage; pg_num < (pages + spage); pg_num++) { + buf[i++] = pg_num; + cs ^= pg_num; + } + buf[i++] = cs; + stream->write_array(&buf[0], i); + stream->flush(); + + const auto s_err = stm32_get_ack_timeout(stm, pages * STM32_PAGEERASE_TIMEOUT); + if (s_err != STM32_ERR_OK) { + return STM32_ERR_UNKNOWN; + } + return STM32_ERR_OK; + } + + /* extended erase */ + + // Free memory with RAII + auto buf = malloc_array_raii(2 + 2 * pages + 1); + + if (!buf) + return STM32_ERR_UNKNOWN; + + /* Number of pages to be erased - 1, two bytes, MSB first */ + uint8_t pg_byte = (pages - 1) >> 8; + buf[i++] = pg_byte; + cs ^= pg_byte; + pg_byte = (pages - 1) & 0xFF; + buf[i++] = pg_byte; + cs ^= pg_byte; + + for (auto pg_num = spage; pg_num < spage + pages; pg_num++) { + pg_byte = pg_num >> 8; + cs ^= pg_byte; + buf[i++] = pg_byte; + pg_byte = pg_num & 0xFF; + cs ^= pg_byte; + buf[i++] = pg_byte; + } + buf[i++] = cs; + stream->write_array(&buf[0], i); + stream->flush(); + + const auto s_err = stm32_get_ack_timeout(stm, pages * STM32_PAGEERASE_TIMEOUT); + if (s_err != STM32_ERR_OK) { + ESP_LOGD(TAG, "Page-by-page erase failed. Check the maximum pages your device supports."); + return STM32_ERR_UNKNOWN; + } + + return STM32_ERR_OK; +} + +template stm32_err_t stm32_check_ack_timeout(const stm32_err_t s_err, const T &&log) { + switch (s_err) { + case STM32_ERR_OK: + return STM32_ERR_OK; + case STM32_ERR_NACK: + log(); + // TODO: c++17 [[fallthrough]] + /* fallthrough */ + default: + return STM32_ERR_UNKNOWN; + } +} + +/* detect CPU endian */ +bool cpu_le() { + static constexpr int N = 1; + + // returns true if little endian + return *reinterpret_cast(&N) == 1; +} + +uint32_t le_u32(const uint32_t v) { + if (!cpu_le()) + return ((v & 0xFF000000) >> 24) | ((v & 0x00FF0000) >> 8) | ((v & 0x0000FF00) << 8) | ((v & 0x000000FF) << 24); + return v; +} + +template void populate_buffer_with_address(uint8_t (&buffer)[N], uint32_t address) { + buffer[0] = static_cast(address >> 24); + buffer[1] = static_cast((address >> 16) & 0xFF); + buffer[2] = static_cast((address >> 8) & 0xFF); + buffer[3] = static_cast(address & 0xFF); + buffer[4] = static_cast(buffer[0] ^ buffer[1] ^ buffer[2] ^ buffer[3]); +} + +} // Anonymous namespace + +} // namespace shelly_dimmer +} // namespace esphome + +namespace esphome { +namespace shelly_dimmer { + +/* find newer command by higher code */ +#define newer(prev, a) (((prev) == STM32_CMD_ERR) ? (a) : (((prev) > (a)) ? (prev) : (a))) + +stm32_t *stm32_init(uart::UARTDevice *stream, const uint8_t flags, const char init) { + uint8_t buf[257]; + + // Could be constexpr in c++17 + static const auto CLOSE = [](stm32_t *stm32) { stm32_close(stm32); }; + + // Cleanup with RAII + std::unique_ptr stm{static_cast(calloc(sizeof(stm32_t), 1)), // NOLINT + CLOSE}; + + if (!stm) { + return nullptr; + } + stm->stream = stream; + stm->flags = flags; + + stm->cmd = static_cast(malloc(sizeof(stm32_cmd_t))); // NOLINT + if (!stm->cmd) { + return nullptr; + } + memset(stm->cmd, STM32_CMD_ERR, sizeof(stm32_cmd_t)); + + if ((stm->flags & STREAM_OPT_CMD_INIT) && init) { + if (stm32_send_init_seq(stm.get()) != STM32_ERR_OK) + return nullptr; // NOLINT + } + + /* get the version and read protection status */ + if (stm32_send_command(stm.get(), STM32_CMD_GVR) != STM32_ERR_OK) { + return nullptr; // NOLINT + } + + /* From AN, only UART bootloader returns 3 bytes */ + { + const auto len = (stm->flags & STREAM_OPT_GVR_ETX) ? 3 : 1; + if (!stream->read_array(buf, len)) + return nullptr; // NOLINT + stm->version = buf[0]; + stm->option1 = (stm->flags & STREAM_OPT_GVR_ETX) ? buf[1] : 0; + stm->option2 = (stm->flags & STREAM_OPT_GVR_ETX) ? buf[2] : 0; + if (stm32_get_ack(stm.get()) != STM32_ERR_OK) { + return nullptr; + } + } + + { + const auto len = ([&]() { + /* get the bootloader information */ + if (stm->cmd_get_reply) { + for (auto i = 0; stm->cmd_get_reply[i].length; ++i) { + if (stm->version == stm->cmd_get_reply[i].version) { + return stm->cmd_get_reply[i].length; + } + } + } + + return STM32_CMD_GET_LENGTH; + })(); + + if (stm32_guess_len_cmd(stm.get(), STM32_CMD_GET, buf, len) != STM32_ERR_OK) + return nullptr; + } + + const auto stop = buf[0] + 1; + stm->bl_version = buf[1]; + int new_cmds = 0; + for (auto i = 1; i < stop; ++i) { + const auto val = buf[i + 1]; + switch (val) { + case STM32_CMD_GET: + stm->cmd->get = val; + break; + case STM32_CMD_GVR: + stm->cmd->gvr = val; + break; + case STM32_CMD_GID: + stm->cmd->gid = val; + break; + case STM32_CMD_RM: + stm->cmd->rm = val; + break; + case STM32_CMD_GO: + stm->cmd->go = val; + break; + case STM32_CMD_WM: + case STM32_CMD_WM_NS: + stm->cmd->wm = newer(stm->cmd->wm, val); + break; + case STM32_CMD_ER: + case STM32_CMD_EE: + case STM32_CMD_EE_NS: + stm->cmd->er = newer(stm->cmd->er, val); + break; + case STM32_CMD_WP: + case STM32_CMD_WP_NS: + stm->cmd->wp = newer(stm->cmd->wp, val); + break; + case STM32_CMD_UW: + case STM32_CMD_UW_NS: + stm->cmd->uw = newer(stm->cmd->uw, val); + break; + case STM32_CMD_RP: + case STM32_CMD_RP_NS: + stm->cmd->rp = newer(stm->cmd->rp, val); + break; + case STM32_CMD_UR: + case STM32_CMD_UR_NS: + stm->cmd->ur = newer(stm->cmd->ur, val); + break; + case STM32_CMD_CRC: + stm->cmd->crc = newer(stm->cmd->crc, val); + break; + default: + if (new_cmds++ == 0) { + ESP_LOGD(TAG, "GET returns unknown commands (0x%2x", val); + } else { + ESP_LOGD(TAG, ", 0x%2x", val); + } + } + } + if (new_cmds) + ESP_LOGD(TAG, ")"); + if (stm32_get_ack(stm.get()) != STM32_ERR_OK) { + return nullptr; + } + + if (stm->cmd->get == STM32_CMD_ERR || stm->cmd->gvr == STM32_CMD_ERR || stm->cmd->gid == STM32_CMD_ERR) { + ESP_LOGD(TAG, "Error: bootloader did not returned correct information from GET command"); + return nullptr; + } + + /* get the device ID */ + if (stm32_guess_len_cmd(stm.get(), stm->cmd->gid, buf, 1) != STM32_ERR_OK) { + return nullptr; + } + const auto returned = buf[0] + 1; + if (returned < 2) { + ESP_LOGD(TAG, "Only %d bytes sent in the PID, unknown/unsupported device", returned); + return nullptr; + } + stm->pid = (buf[1] << 8) | buf[2]; + if (returned > 2) { + ESP_LOGD(TAG, "This bootloader returns %d extra bytes in PID:", returned); + for (auto i = 2; i <= returned; i++) + ESP_LOGD(TAG, " %02x", buf[i]); + } + if (stm32_get_ack(stm.get()) != STM32_ERR_OK) { + return nullptr; + } + + stm->dev = DEVICES; + while (stm->dev->id != 0x00 && stm->dev->id != stm->pid) + ++stm->dev; + + if (!stm->dev->id) { + ESP_LOGD(TAG, "Unknown/unsupported device (Device ID: 0x%03x)", stm->pid); + return nullptr; + } + + // TODO: Would be much better if the unique_ptr was returned from this function + // Release ownership of unique_ptr + return stm.release(); // NOLINT +} + +void stm32_close(stm32_t *stm) { + if (stm) + free(stm->cmd); // NOLINT + free(stm); // NOLINT +} + +stm32_err_t stm32_read_memory(const stm32_t *stm, const uint32_t address, uint8_t *data, const unsigned int len) { + auto *const stream = stm->stream; + + if (!len) + return STM32_ERR_OK; + + if (len > 256) { + ESP_LOGD(TAG, "Error: READ length limit at 256 bytes"); + return STM32_ERR_UNKNOWN; + } + + if (stm->cmd->rm == STM32_CMD_ERR) { + ESP_LOGD(TAG, "Error: READ command not implemented in bootloader."); + return STM32_ERR_NO_CMD; + } + + if (stm32_send_command(stm, stm->cmd->rm) != STM32_ERR_OK) + return STM32_ERR_UNKNOWN; + + static constexpr auto BUFFER_SIZE = 5; + uint8_t buf[BUFFER_SIZE]; + populate_buffer_with_address(buf, address); + + stream->write_array(buf, BUFFER_SIZE); + stream->flush(); + + if (stm32_get_ack(stm) != STM32_ERR_OK) + return STM32_ERR_UNKNOWN; + + if (stm32_send_command(stm, len - 1) != STM32_ERR_OK) + return STM32_ERR_UNKNOWN; + + if (!stream->read_array(data, len)) + return STM32_ERR_UNKNOWN; + + return STM32_ERR_OK; +} + +stm32_err_t stm32_write_memory(const stm32_t *stm, uint32_t address, const uint8_t *data, const unsigned int len) { + auto *const stream = stm->stream; + + if (!len) + return STM32_ERR_OK; + + if (len > 256) { + ESP_LOGD(TAG, "Error: READ length limit at 256 bytes"); + return STM32_ERR_UNKNOWN; + } + + /* must be 32bit aligned */ + if (address & 0x3) { + ESP_LOGD(TAG, "Error: WRITE address must be 4 byte aligned"); + return STM32_ERR_UNKNOWN; + } + + if (stm->cmd->wm == STM32_CMD_ERR) { + ESP_LOGD(TAG, "Error: WRITE command not implemented in bootloader."); + return STM32_ERR_NO_CMD; + } + + /* send the address and checksum */ + if (stm32_send_command(stm, stm->cmd->wm) != STM32_ERR_OK) + return STM32_ERR_UNKNOWN; + + static constexpr auto BUFFER_SIZE = 5; + uint8_t buf1[BUFFER_SIZE]; + populate_buffer_with_address(buf1, address); + + stream->write_array(buf1, BUFFER_SIZE); + stream->flush(); + if (stm32_get_ack(stm) != STM32_ERR_OK) + return STM32_ERR_UNKNOWN; + + const unsigned int aligned_len = (len + 3) & ~3; + uint8_t cs = aligned_len - 1; + uint8_t buf[256 + 2]; + + buf[0] = aligned_len - 1; + for (auto i = 0; i < len; i++) { + cs ^= data[i]; + buf[i + 1] = data[i]; + } + /* padding data */ + for (auto i = len; i < aligned_len; i++) { + cs ^= 0xFF; + buf[i + 1] = 0xFF; + } + buf[aligned_len + 1] = cs; + stream->write_array(buf, aligned_len + 2); + stream->flush(); + + const auto s_err = stm32_get_ack_timeout(stm, STM32_BLKWRITE_TIMEOUT); + if (s_err != STM32_ERR_OK) { + return STM32_ERR_UNKNOWN; + } + return STM32_ERR_OK; +} + +stm32_err_t stm32_wunprot_memory(const stm32_t *stm) { + if (stm->cmd->uw == STM32_CMD_ERR) { + ESP_LOGD(TAG, "Error: WRITE UNPROTECT command not implemented in bootloader."); + return STM32_ERR_NO_CMD; + } + + if (stm32_send_command(stm, stm->cmd->uw) != STM32_ERR_OK) + return STM32_ERR_UNKNOWN; + + return stm32_check_ack_timeout(stm32_get_ack_timeout(stm, STM32_WUNPROT_TIMEOUT), + []() { ESP_LOGD(TAG, "Error: Failed to WRITE UNPROTECT"); }); +} + +stm32_err_t stm32_wprot_memory(const stm32_t *stm) { + if (stm->cmd->wp == STM32_CMD_ERR) { + ESP_LOGD(TAG, "Error: WRITE PROTECT command not implemented in bootloader."); + return STM32_ERR_NO_CMD; + } + + if (stm32_send_command(stm, stm->cmd->wp) != STM32_ERR_OK) + return STM32_ERR_UNKNOWN; + + return stm32_check_ack_timeout(stm32_get_ack_timeout(stm, STM32_WPROT_TIMEOUT), + []() { ESP_LOGD(TAG, "Error: Failed to WRITE PROTECT"); }); +} + +stm32_err_t stm32_runprot_memory(const stm32_t *stm) { + if (stm->cmd->ur == STM32_CMD_ERR) { + ESP_LOGD(TAG, "Error: READOUT UNPROTECT command not implemented in bootloader."); + return STM32_ERR_NO_CMD; + } + + if (stm32_send_command(stm, stm->cmd->ur) != STM32_ERR_OK) + return STM32_ERR_UNKNOWN; + + return stm32_check_ack_timeout(stm32_get_ack_timeout(stm, STM32_MASSERASE_TIMEOUT), + []() { ESP_LOGD(TAG, "Error: Failed to READOUT UNPROTECT"); }); +} + +stm32_err_t stm32_readprot_memory(const stm32_t *stm) { + if (stm->cmd->rp == STM32_CMD_ERR) { + ESP_LOGD(TAG, "Error: READOUT PROTECT command not implemented in bootloader."); + return STM32_ERR_NO_CMD; + } + + if (stm32_send_command(stm, stm->cmd->rp) != STM32_ERR_OK) + return STM32_ERR_UNKNOWN; + + return stm32_check_ack_timeout(stm32_get_ack_timeout(stm, STM32_RPROT_TIMEOUT), + []() { ESP_LOGD(TAG, "Error: Failed to READOUT PROTECT"); }); +} + +stm32_err_t stm32_erase_memory(const stm32_t *stm, uint32_t spage, uint32_t pages) { + if (!pages || spage > STM32_MAX_PAGES || ((pages != STM32_MASS_ERASE) && ((spage + pages) > STM32_MAX_PAGES))) + return STM32_ERR_OK; + + if (stm->cmd->er == STM32_CMD_ERR) { + ESP_LOGD(TAG, "Error: ERASE command not implemented in bootloader."); + return STM32_ERR_NO_CMD; + } + + if (pages == STM32_MASS_ERASE) { + /* + * Not all chips support mass erase. + * Mass erase can be obtained executing a "readout protect" + * followed by "readout un-protect". This method is not + * suggested because can hang the target if a debug SWD/JTAG + * is connected. When the target enters in "readout + * protection" mode it will consider the debug connection as + * a tentative of intrusion and will hang. + * Erasing the flash page-by-page is the safer way to go. + */ + if (!(stm->dev->flags & F_NO_ME)) + return stm32_mass_erase(stm); + + pages = flash_addr_to_page_ceil(stm, stm->dev->fl_end); + } + + /* + * Some device, like STM32L152, cannot erase more than 512 pages in + * one command. Split the call. + */ + static constexpr uint32_t MAX_PAGE_SIZE = 512; + while (pages) { + const auto n = std::min(pages, MAX_PAGE_SIZE); + const auto s_err = stm32_pages_erase(stm, spage, n); + if (s_err != STM32_ERR_OK) + return s_err; + spage += n; + pages -= n; + } + return STM32_ERR_OK; +} + +static stm32_err_t stm32_run_raw_code(const stm32_t *stm, uint32_t target_address, const uint8_t *code, + uint32_t code_size) { + static constexpr uint32_t BUFFER_SIZE = 256; + + const auto stack_le = le_u32(0x20002000); + const auto code_address_le = le_u32(target_address + 8 + 1); // thumb mode address (!) + uint32_t length = code_size + 8; + + /* Must be 32-bit aligned */ + if (target_address & 0x3) { + ESP_LOGD(TAG, "Error: code address must be 4 byte aligned"); + return STM32_ERR_UNKNOWN; + } + + // Could be constexpr in c++17 + static const auto DELETOR = [](uint8_t *memory) { + free(memory); // NOLINT + }; + + // Free memory with RAII + std::unique_ptr mem{static_cast(malloc(length)), // NOLINT + DELETOR}; + + if (!mem) + return STM32_ERR_UNKNOWN; + + memcpy(mem.get(), &stack_le, sizeof(stack_le)); + memcpy(mem.get() + 4, &code_address_le, sizeof(code_address_le)); + memcpy(mem.get() + 8, code, code_size); + + auto *pos = mem.get(); + auto address = target_address; + while (length > 0) { + const auto w = std::min(length, BUFFER_SIZE); + if (stm32_write_memory(stm, address, pos, w) != STM32_ERR_OK) { + return STM32_ERR_UNKNOWN; + } + + address += w; + pos += w; + length -= w; + } + + return stm32_go(stm, target_address); +} + +stm32_err_t stm32_go(const stm32_t *stm, const uint32_t address) { + auto *const stream = stm->stream; + + if (stm->cmd->go == STM32_CMD_ERR) { + ESP_LOGD(TAG, "Error: GO command not implemented in bootloader."); + return STM32_ERR_NO_CMD; + } + + if (stm32_send_command(stm, stm->cmd->go) != STM32_ERR_OK) + return STM32_ERR_UNKNOWN; + + static constexpr auto BUFFER_SIZE = 5; + uint8_t buf[BUFFER_SIZE]; + populate_buffer_with_address(buf, address); + + stream->write_array(buf, BUFFER_SIZE); + stream->flush(); + + if (stm32_get_ack(stm) != STM32_ERR_OK) + return STM32_ERR_UNKNOWN; + return STM32_ERR_OK; +} + +stm32_err_t stm32_reset_device(const stm32_t *stm) { + const auto target_address = stm->dev->ram_start; + + if (stm->dev->flags & F_OBLL) { + /* set the OBL_LAUNCH bit to reset device (see RM0360, 2.5) */ + return stm32_run_raw_code(stm, target_address, STM_OBL_LAUNCH_CODE, STM_OBL_LAUNCH_CODE_SIZE); + } else { + return stm32_run_raw_code(stm, target_address, STM_RESET_CODE, STM_RESET_CODE_SIZE); + } +} + +stm32_err_t stm32_crc_memory(const stm32_t *stm, const uint32_t address, const uint32_t length, uint32_t *const crc) { + static constexpr auto BUFFER_SIZE = 5; + auto *const stream = stm->stream; + + if (address & 0x3 || length & 0x3) { + ESP_LOGD(TAG, "Start and end addresses must be 4 byte aligned"); + return STM32_ERR_UNKNOWN; + } + + if (stm->cmd->crc == STM32_CMD_ERR) { + ESP_LOGD(TAG, "Error: CRC command not implemented in bootloader."); + return STM32_ERR_NO_CMD; + } + + if (stm32_send_command(stm, stm->cmd->crc) != STM32_ERR_OK) + return STM32_ERR_UNKNOWN; + + { + static constexpr auto BUFFER_SIZE = 5; + uint8_t buf[BUFFER_SIZE]; + populate_buffer_with_address(buf, address); + + stream->write_array(buf, BUFFER_SIZE); + stream->flush(); + } + + if (stm32_get_ack(stm) != STM32_ERR_OK) + return STM32_ERR_UNKNOWN; + + { + static constexpr auto BUFFER_SIZE = 5; + uint8_t buf[BUFFER_SIZE]; + populate_buffer_with_address(buf, address); + + stream->write_array(buf, BUFFER_SIZE); + stream->flush(); + } + + if (stm32_get_ack(stm) != STM32_ERR_OK) + return STM32_ERR_UNKNOWN; + + if (stm32_get_ack(stm) != STM32_ERR_OK) + return STM32_ERR_UNKNOWN; + + { + uint8_t buf[BUFFER_SIZE]; + if (!stream->read_array(buf, BUFFER_SIZE)) + return STM32_ERR_UNKNOWN; + + if (buf[4] != (buf[0] ^ buf[1] ^ buf[2] ^ buf[3])) + return STM32_ERR_UNKNOWN; + + *crc = (buf[0] << 24) | (buf[1] << 16) | (buf[2] << 8) | buf[3]; + } + + return STM32_ERR_OK; +} + +/* + * CRC computed by STM32 is similar to the standard crc32_be() + * implemented, for example, in Linux kernel in ./lib/crc32.c + * But STM32 computes it on units of 32 bits word and swaps the + * bytes of the word before the computation. + * Due to byte swap, I cannot use any CRC available in existing + * libraries, so here is a simple not optimized implementation. + */ +uint32_t stm32_sw_crc(uint32_t crc, uint8_t *buf, unsigned int len) { + static constexpr uint32_t CRCPOLY_BE = 0x04c11db7; + static constexpr uint32_t CRC_MSBMASK = 0x80000000; + + if (len & 0x3) { + ESP_LOGD(TAG, "Buffer length must be multiple of 4 bytes"); + return 0; + } + + while (len) { + uint32_t data = *buf++; + data |= *buf++ << 8; + data |= *buf++ << 16; + data |= *buf++ << 24; + len -= 4; + + crc ^= data; + + for (size_t i = 0; i < 32; ++i) { + if (crc & CRC_MSBMASK) { + crc = (crc << 1) ^ CRCPOLY_BE; + } else { + crc = (crc << 1); + } + } + } + return crc; +} + +stm32_err_t stm32_crc_wrapper(const stm32_t *stm, uint32_t address, uint32_t length, uint32_t *crc) { + static constexpr uint32_t CRC_INIT_VALUE = 0xFFFFFFFF; + static constexpr uint32_t BUFFER_SIZE = 256; + + uint8_t buf[BUFFER_SIZE]; + + if (address & 0x3 || length & 0x3) { + ESP_LOGD(TAG, "Start and end addresses must be 4 byte aligned"); + return STM32_ERR_UNKNOWN; + } + + if (stm->cmd->crc != STM32_CMD_ERR) + return stm32_crc_memory(stm, address, length, crc); + + const auto start = address; + const auto total_len = length; + uint32_t current_crc = CRC_INIT_VALUE; + while (length) { + const auto len = std::min(BUFFER_SIZE, length); + if (stm32_read_memory(stm, address, buf, len) != STM32_ERR_OK) { + ESP_LOGD(TAG, "Failed to read memory at address 0x%08x, target write-protected?", address); + return STM32_ERR_UNKNOWN; + } + current_crc = stm32_sw_crc(current_crc, buf, len); + length -= len; + address += len; + + ESP_LOGD(TAG, "\rCRC address 0x%08x (%.2f%%) ", address, (100.0f / (float) total_len) * (float) (address - start)); + } + ESP_LOGD(TAG, "Done."); + *crc = current_crc; + return STM32_ERR_OK; +} + +} // namespace shelly_dimmer +} // namespace esphome +#endif diff --git a/esphome/components/shelly_dimmer/stm32flash.h b/esphome/components/shelly_dimmer/stm32flash.h new file mode 100644 index 0000000000..c561375c38 --- /dev/null +++ b/esphome/components/shelly_dimmer/stm32flash.h @@ -0,0 +1,129 @@ +/* + stm32flash - Open Source ST STM32 flash program for Arduino + Copyright (C) 2010 Geoffrey McRae + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +*/ + +#pragma once + +#include "esphome/core/defines.h" +#ifdef USE_SHD_FIRMWARE_DATA + +#include +#include "esphome/components/uart/uart.h" + +namespace esphome { +namespace shelly_dimmer { + +/* flags */ +constexpr auto STREAM_OPT_BYTE = (1 << 0); /* byte (not frame) oriented */ +constexpr auto STREAM_OPT_GVR_ETX = (1 << 1); /* cmd GVR returns protection status */ +constexpr auto STREAM_OPT_CMD_INIT = (1 << 2); /* use INIT cmd to autodetect speed */ +constexpr auto STREAM_OPT_RETRY = (1 << 3); /* allowed read() retry after timeout */ +constexpr auto STREAM_OPT_I2C = (1 << 4); /* i2c */ +constexpr auto STREAM_OPT_STRETCH_W = (1 << 5); /* warning for no-stretching commands */ + +constexpr auto STREAM_SERIAL = (STREAM_OPT_BYTE | STREAM_OPT_GVR_ETX | STREAM_OPT_CMD_INIT | STREAM_OPT_RETRY); +constexpr auto STREAM_I2C = (STREAM_OPT_I2C | STREAM_OPT_STRETCH_W); + +constexpr auto STM32_MAX_RX_FRAME = 256; /* cmd read memory */ +constexpr auto STM32_MAX_TX_FRAME = (1 + 256 + 1); /* cmd write memory */ + +constexpr auto STM32_MAX_PAGES = 0x0000ffff; +constexpr auto STM32_MASS_ERASE = 0x00100000; /* > 2 x max_pages */ + +using stm32_err_t = enum Stm32Err { + STM32_ERR_OK = 0, + STM32_ERR_UNKNOWN, /* Generic error */ + STM32_ERR_NACK, + STM32_ERR_NO_CMD, /* Command not available in bootloader */ +}; + +using flags_t = enum Flags { + F_NO_ME = 1 << 0, /* Mass-Erase not supported */ + F_OBLL = 1 << 1, /* OBL_LAUNCH required */ +}; + +using stm32_cmd_t = struct Stm32Cmd { + uint8_t get; + uint8_t gvr; + uint8_t gid; + uint8_t rm; + uint8_t go; + uint8_t wm; + uint8_t er; /* this may be extended erase */ + uint8_t wp; + uint8_t uw; + uint8_t rp; + uint8_t ur; + uint8_t crc; +}; + +using stm32_dev_t = struct Stm32Dev { // NOLINT + const uint16_t id; + const char *name; + const uint32_t ram_start, ram_end; + const uint32_t fl_start, fl_end; + const uint16_t fl_pps; // pages per sector + const uint32_t *fl_ps; // page size + const uint32_t opt_start, opt_end; + const uint32_t mem_start, mem_end; + const uint32_t flags; +}; + +using stm32_t = struct Stm32 { + uart::UARTDevice *stream; + uint8_t flags; + struct VarlenCmd *cmd_get_reply; + uint8_t bl_version; + uint8_t version; + uint8_t option1, option2; + uint16_t pid; + stm32_cmd_t *cmd; + const stm32_dev_t *dev; +}; + +/* + * Specify the length of reply for command GET + * This is helpful for frame-oriented protocols, e.g. i2c, to avoid time + * consuming try-fail-timeout-retry operation. + * On byte-oriented protocols, i.e. UART, this information would be skipped + * after read the first byte, so not needed. + */ +struct VarlenCmd { + uint8_t version; + uint8_t length; +}; + +stm32_t *stm32_init(uart::UARTDevice *stream, uint8_t flags, char init); +void stm32_close(stm32_t *stm); +stm32_err_t stm32_read_memory(const stm32_t *stm, uint32_t address, uint8_t *data, unsigned int len); +stm32_err_t stm32_write_memory(const stm32_t *stm, uint32_t address, const uint8_t *data, unsigned int len); +stm32_err_t stm32_wunprot_memory(const stm32_t *stm); +stm32_err_t stm32_wprot_memory(const stm32_t *stm); +stm32_err_t stm32_erase_memory(const stm32_t *stm, uint32_t spage, uint32_t pages); +stm32_err_t stm32_go(const stm32_t *stm, uint32_t address); +stm32_err_t stm32_reset_device(const stm32_t *stm); +stm32_err_t stm32_readprot_memory(const stm32_t *stm); +stm32_err_t stm32_runprot_memory(const stm32_t *stm); +stm32_err_t stm32_crc_memory(const stm32_t *stm, uint32_t address, uint32_t length, uint32_t *crc); +stm32_err_t stm32_crc_wrapper(const stm32_t *stm, uint32_t address, uint32_t length, uint32_t *crc); +uint32_t stm32_sw_crc(uint32_t crc, uint8_t *buf, unsigned int len); + +} // namespace shelly_dimmer +} // namespace esphome + +#endif // USE_SHD_FIRMWARE_DATA diff --git a/esphome/core/defines.h b/esphome/core/defines.h index f304f847a5..c854e2b987 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -93,3 +93,9 @@ //#define USE_BSEC // Requires a library with proprietary license. #define USE_DASHBOARD_IMPORT + +// Dummy firmware payload for shelly_dimmer +#define USE_SHD_FIRMWARE_MAJOR_VERSION 56 +#define USE_SHD_FIRMWARE_MINOR_VERSION 5 +#define USE_SHD_FIRMWARE_DATA \ + {} diff --git a/tests/test1.yaml b/tests/test1.yaml index 77c4a76bda..98a3ffcf4b 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1751,6 +1751,18 @@ light: to: 25 - single_light_id: ${roomname}_lights + - platform: shelly_dimmer + name: "Shelly Dimmer Light" + power: + name: "Shelly Dimmer Power" + voltage: + name: "Shelly Dimmer Voltage" + current: + name: "Shelly Dimmer Current" + max_brightness: 500 + firmware: "51.6" + uart_id: uart0 + remote_transmitter: - pin: 32 carrier_duty_percent: 100% From 6bac551d9fb10731841e7270840fe45015a66bb3 Mon Sep 17 00:00:00 2001 From: Joe Date: Wed, 13 Apr 2022 21:16:13 -0400 Subject: [PATCH 0390/1729] Add BedJet BLE climate component (#2452) --- CODEOWNERS | 1 + esphome/components/bedjet/__init__.py | 1 + esphome/components/bedjet/bedjet.cpp | 642 ++++++++++++++++++++++ esphome/components/bedjet/bedjet.h | 121 ++++ esphome/components/bedjet/bedjet_base.cpp | 123 +++++ esphome/components/bedjet/bedjet_base.h | 159 ++++++ esphome/components/bedjet/bedjet_const.h | 78 +++ esphome/components/bedjet/climate.py | 42 ++ tests/test1.yaml | 5 + 9 files changed, 1172 insertions(+) create mode 100644 esphome/components/bedjet/__init__.py create mode 100644 esphome/components/bedjet/bedjet.cpp create mode 100644 esphome/components/bedjet/bedjet.h create mode 100644 esphome/components/bedjet/bedjet_base.cpp create mode 100644 esphome/components/bedjet/bedjet_base.h create mode 100644 esphome/components/bedjet/bedjet_const.h create mode 100644 esphome/components/bedjet/climate.py diff --git a/CODEOWNERS b/CODEOWNERS index 02945ec0a4..c9df669f03 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -28,6 +28,7 @@ esphome/components/atc_mithermometer/* @ahpohl esphome/components/b_parasite/* @rbaron esphome/components/ballu/* @bazuchan esphome/components/bang_bang/* @OttoWinter +esphome/components/bedjet/* @jhansche esphome/components/bh1750/* @OttoWinter esphome/components/binary_sensor/* @esphome/core esphome/components/bl0940/* @tobias- diff --git a/esphome/components/bedjet/__init__.py b/esphome/components/bedjet/__init__.py new file mode 100644 index 0000000000..16821fc016 --- /dev/null +++ b/esphome/components/bedjet/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@jhansche"] diff --git a/esphome/components/bedjet/bedjet.cpp b/esphome/components/bedjet/bedjet.cpp new file mode 100644 index 0000000000..1a932da0c5 --- /dev/null +++ b/esphome/components/bedjet/bedjet.cpp @@ -0,0 +1,642 @@ +#include "bedjet.h" +#include "esphome/core/log.h" + +#ifdef USE_ESP32 + +namespace esphome { +namespace bedjet { + +using namespace esphome::climate; + +/// Converts a BedJet temp step into degrees Celsius. +float bedjet_temp_to_c(const uint8_t temp) { + // BedJet temp is "C*2"; to get C, divide by 2. + return temp / 2.0f; +} + +/// Converts a BedJet fan step to a speed percentage, in the range of 5% to 100%. +uint8_t bedjet_fan_step_to_speed(const uint8_t fan) { + // 0 = 5% + // 19 = 100% + return 5 * fan + 5; +} + +static const std::string *bedjet_fan_step_to_fan_mode(const uint8_t fan_step) { + if (fan_step >= 0 && fan_step <= 19) + return &BEDJET_FAN_STEP_NAME_STRINGS[fan_step]; + return nullptr; +} + +static uint8_t bedjet_fan_speed_to_step(const std::string &fan_step_percent) { + for (int i = 0; i < sizeof(BEDJET_FAN_STEP_NAME_STRINGS); i++) { + if (fan_step_percent == BEDJET_FAN_STEP_NAME_STRINGS[i]) { + return i; + } + } + return -1; +} + +void Bedjet::upgrade_firmware() { + auto *pkt = this->codec_->get_button_request(MAGIC_UPDATE); + auto status = this->write_bedjet_packet_(pkt); + + if (status) { + ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); + } +} + +void Bedjet::dump_config() { + LOG_CLIMATE("", "BedJet Climate", this); + auto traits = this->get_traits(); + + ESP_LOGCONFIG(TAG, " Supported modes:"); + for (auto mode : traits.get_supported_modes()) { + ESP_LOGCONFIG(TAG, " - %s", LOG_STR_ARG(climate_mode_to_string(mode))); + } + + ESP_LOGCONFIG(TAG, " Supported fan modes:"); + for (const auto &mode : traits.get_supported_fan_modes()) { + ESP_LOGCONFIG(TAG, " - %s", LOG_STR_ARG(climate_fan_mode_to_string(mode))); + } + for (const auto &mode : traits.get_supported_custom_fan_modes()) { + ESP_LOGCONFIG(TAG, " - %s (c)", mode.c_str()); + } + + ESP_LOGCONFIG(TAG, " Supported presets:"); + for (auto preset : traits.get_supported_presets()) { + ESP_LOGCONFIG(TAG, " - %s", LOG_STR_ARG(climate_preset_to_string(preset))); + } + for (const auto &preset : traits.get_supported_custom_presets()) { + ESP_LOGCONFIG(TAG, " - %s (c)", preset.c_str()); + } +} + +void Bedjet::setup() { + this->codec_ = make_unique(); + + // restore set points + auto restore = this->restore_state_(); + if (restore.has_value()) { + ESP_LOGI(TAG, "Restored previous saved state."); + restore->apply(this); + } else { + // Initial status is unknown until we connect + this->reset_state_(); + } + +#ifdef USE_TIME + this->setup_time_(); +#endif +} + +/** Resets states to defaults. */ +void Bedjet::reset_state_() { + this->mode = climate::CLIMATE_MODE_OFF; + this->action = climate::CLIMATE_ACTION_IDLE; + this->target_temperature = NAN; + this->current_temperature = NAN; + this->preset.reset(); + this->custom_preset.reset(); + this->publish_state(); +} + +void Bedjet::loop() {} + +void Bedjet::control(const ClimateCall &call) { + ESP_LOGD(TAG, "Received Bedjet::control"); + if (this->node_state != espbt::ClientState::ESTABLISHED) { + ESP_LOGW(TAG, "Not connected, cannot handle control call yet."); + return; + } + + if (call.get_mode().has_value()) { + ClimateMode mode = *call.get_mode(); + BedjetPacket *pkt; + switch (mode) { + case climate::CLIMATE_MODE_OFF: + pkt = this->codec_->get_button_request(BTN_OFF); + break; + case climate::CLIMATE_MODE_HEAT: + pkt = this->codec_->get_button_request(BTN_EXTHT); + break; + case climate::CLIMATE_MODE_FAN_ONLY: + pkt = this->codec_->get_button_request(BTN_COOL); + break; + case climate::CLIMATE_MODE_DRY: + pkt = this->codec_->get_button_request(BTN_DRY); + break; + default: + ESP_LOGW(TAG, "Unsupported mode: %d", mode); + return; + } + + auto status = this->write_bedjet_packet_(pkt); + + if (status) { + ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); + } else { + this->force_refresh_ = true; + this->mode = mode; + // We're using (custom) preset for Turbo & M1-3 presets, so changing climate mode will clear those + this->custom_preset.reset(); + this->preset.reset(); + } + } + + if (call.get_target_temperature().has_value()) { + auto target_temp = *call.get_target_temperature(); + auto *pkt = this->codec_->get_set_target_temp_request(target_temp); + auto status = this->write_bedjet_packet_(pkt); + + if (status) { + ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); + } else { + this->target_temperature = target_temp; + } + } + + if (call.get_preset().has_value()) { + ClimatePreset preset = *call.get_preset(); + BedjetPacket *pkt; + + if (preset == climate::CLIMATE_PRESET_BOOST) { + pkt = this->codec_->get_button_request(BTN_TURBO); + } else { + ESP_LOGW(TAG, "Unsupported preset: %d", preset); + return; + } + + auto status = this->write_bedjet_packet_(pkt); + if (status) { + ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); + } else { + // We use BOOST preset for TURBO mode, which is a short-lived/high-heat mode. + this->mode = climate::CLIMATE_MODE_HEAT; + this->preset = preset; + this->custom_preset.reset(); + this->force_refresh_ = true; + } + } else if (call.get_custom_preset().has_value()) { + std::string preset = *call.get_custom_preset(); + BedjetPacket *pkt; + + if (preset == "M1") { + pkt = this->codec_->get_button_request(BTN_M1); + } else if (preset == "M2") { + pkt = this->codec_->get_button_request(BTN_M2); + } else if (preset == "M3") { + pkt = this->codec_->get_button_request(BTN_M3); + } else { + ESP_LOGW(TAG, "Unsupported preset: %s", preset.c_str()); + return; + } + + auto status = this->write_bedjet_packet_(pkt); + if (status) { + ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); + } else { + this->force_refresh_ = true; + this->custom_preset = preset; + this->preset.reset(); + } + } + + if (call.get_fan_mode().has_value()) { + // Climate fan mode only supports low/med/high, but the BedJet supports 5-100% increments. + // We can still support a ClimateCall that requests low/med/high, and just translate it to a step increment here. + auto fan_mode = *call.get_fan_mode(); + BedjetPacket *pkt; + if (fan_mode == climate::CLIMATE_FAN_LOW) { + pkt = this->codec_->get_set_fan_speed_request(3 /* = 20% */); + } else if (fan_mode == climate::CLIMATE_FAN_MEDIUM) { + pkt = this->codec_->get_set_fan_speed_request(9 /* = 50% */); + } else if (fan_mode == climate::CLIMATE_FAN_HIGH) { + pkt = this->codec_->get_set_fan_speed_request(14 /* = 75% */); + } else { + ESP_LOGW(TAG, "[%s] Unsupported fan mode: %s", this->get_name().c_str(), + LOG_STR_ARG(climate_fan_mode_to_string(fan_mode))); + return; + } + + auto status = this->write_bedjet_packet_(pkt); + if (status) { + ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); + } else { + this->force_refresh_ = true; + } + } else if (call.get_custom_fan_mode().has_value()) { + auto fan_mode = *call.get_custom_fan_mode(); + auto fan_step = bedjet_fan_speed_to_step(fan_mode); + if (fan_step >= 0 && fan_step <= 19) { + ESP_LOGV(TAG, "[%s] Converted fan mode %s to bedjet fan step %d", this->get_name().c_str(), fan_mode.c_str(), + fan_step); + // The index should represent the fan_step index. + BedjetPacket *pkt = this->codec_->get_set_fan_speed_request(fan_step); + auto status = this->write_bedjet_packet_(pkt); + if (status) { + ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); + } else { + this->force_refresh_ = true; + } + } + } +} + +void Bedjet::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) { + switch (event) { + case ESP_GATTC_DISCONNECT_EVT: { + ESP_LOGV(TAG, "Disconnected: reason=%d", param->disconnect.reason); + this->status_set_warning(); + break; + } + case ESP_GATTC_SEARCH_CMPL_EVT: { + auto *chr = this->parent_->get_characteristic(BEDJET_SERVICE_UUID, BEDJET_COMMAND_UUID); + if (chr == nullptr) { + ESP_LOGW(TAG, "[%s] No control service found at device, not a BedJet..?", this->get_name().c_str()); + break; + } + this->char_handle_cmd_ = chr->handle; + + chr = this->parent_->get_characteristic(BEDJET_SERVICE_UUID, BEDJET_STATUS_UUID); + if (chr == nullptr) { + ESP_LOGW(TAG, "[%s] No status service found at device, not a BedJet..?", this->get_name().c_str()); + break; + } + + this->char_handle_status_ = chr->handle; + // We also need to obtain the config descriptor for this handle. + // Otherwise once we set node_state=Established, the parent will flush all handles/descriptors, and we won't be + // able to look it up. + auto *descr = this->parent_->get_config_descriptor(this->char_handle_status_); + if (descr == nullptr) { + ESP_LOGW(TAG, "No config descriptor for status handle 0x%x. Will not be able to receive status notifications", + this->char_handle_status_); + } else if (descr->uuid.get_uuid().len != ESP_UUID_LEN_16 || + descr->uuid.get_uuid().uuid.uuid16 != ESP_GATT_UUID_CHAR_CLIENT_CONFIG) { + ESP_LOGW(TAG, "Config descriptor 0x%x (uuid %s) is not a client config char uuid", this->char_handle_status_, + descr->uuid.to_string().c_str()); + } else { + this->config_descr_status_ = descr->handle; + } + + chr = this->parent_->get_characteristic(BEDJET_SERVICE_UUID, BEDJET_NAME_UUID); + if (chr != nullptr) { + this->char_handle_name_ = chr->handle; + auto status = esp_ble_gattc_read_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_name_, + ESP_GATT_AUTH_REQ_NONE); + if (status) { + ESP_LOGI(TAG, "[%s] Unable to read name characteristic: %d", this->get_name().c_str(), status); + } + } + + ESP_LOGD(TAG, "Services complete: obtained char handles."); + this->node_state = espbt::ClientState::ESTABLISHED; + + this->set_notify_(true); + +#ifdef USE_TIME + if (this->time_id_.has_value()) { + this->send_local_time_(); + } +#endif + break; + } + case ESP_GATTC_WRITE_DESCR_EVT: { + if (param->write.status != ESP_GATT_OK) { + // ESP_GATT_INVALID_ATTR_LEN + ESP_LOGW(TAG, "Error writing descr at handle 0x%04d, status=%d", param->write.handle, param->write.status); + break; + } + // [16:44:44][V][bedjet:279]: [JOENJET] Register for notify event success: h=0x002a s=0 + // This might be the enable-notify descriptor? (or disable-notify) + ESP_LOGV(TAG, "[%s] Write to handle 0x%04x status=%d", this->get_name().c_str(), param->write.handle, + param->write.status); + break; + } + case ESP_GATTC_WRITE_CHAR_EVT: { + if (param->write.status != ESP_GATT_OK) { + ESP_LOGW(TAG, "Error writing char at handle 0x%04d, status=%d", param->write.handle, param->write.status); + break; + } + if (param->write.handle == this->char_handle_cmd_) { + if (this->force_refresh_) { + // Command write was successful. Publish the pending state, hoping that notify will kick in. + this->publish_state(); + } + } + break; + } + case ESP_GATTC_READ_CHAR_EVT: { + if (param->read.conn_id != this->parent_->conn_id) + break; + if (param->read.status != ESP_GATT_OK) { + ESP_LOGW(TAG, "Error reading char at handle %d, status=%d", param->read.handle, param->read.status); + break; + } + if (param->read.handle == this->char_handle_status_) { + // This is the additional packet that doesn't fit in the notify packet. + this->codec_->decode_extra(param->read.value, param->read.value_len); + } else if (param->read.handle == this->char_handle_name_) { + // The data should represent the name. + if (param->read.status == ESP_GATT_OK && param->read.value_len > 0) { + std::string bedjet_name(reinterpret_cast(param->read.value), param->read.value_len); + // this->set_name(bedjet_name); + ESP_LOGV(TAG, "[%s] Got BedJet name: '%s'", this->get_name().c_str(), bedjet_name.c_str()); + } + } + break; + } + case ESP_GATTC_REG_FOR_NOTIFY_EVT: { + // This event means that ESP received the request to enable notifications on the client side. But we also have to + // tell the server that we want it to send notifications. Normally BLEClient parent would handle this + // automatically, but as soon as we set our status to Established, the parent is going to purge all the + // service/char/descriptor handles, and then get_config_descriptor() won't work anymore. There's no way to disable + // the BLEClient parent behavior, so our only option is to write the handle anyway, and hope a double-write + // doesn't break anything. + + if (param->reg_for_notify.handle != this->char_handle_status_) { + ESP_LOGW(TAG, "[%s] Register for notify on unexpected handle 0x%04x, expecting 0x%04x", + this->get_name().c_str(), param->reg_for_notify.handle, this->char_handle_status_); + break; + } + + this->write_notify_config_descriptor_(true); + this->last_notify_ = 0; + this->force_refresh_ = true; + break; + } + case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: { + // This event is not handled by the parent BLEClient, so we need to do this either way. + if (param->unreg_for_notify.handle != this->char_handle_status_) { + ESP_LOGW(TAG, "[%s] Unregister for notify on unexpected handle 0x%04x, expecting 0x%04x", + this->get_name().c_str(), param->unreg_for_notify.handle, this->char_handle_status_); + break; + } + + this->write_notify_config_descriptor_(false); + this->last_notify_ = 0; + // Now we wait until the next update() poll to re-register notify... + break; + } + case ESP_GATTC_NOTIFY_EVT: { + if (param->notify.handle != this->char_handle_status_) { + ESP_LOGW(TAG, "[%s] Unexpected notify handle, wanted %04X, got %04X", this->get_name().c_str(), + this->char_handle_status_, param->notify.handle); + break; + } + + // FIXME: notify events come in every ~200-300 ms, which is too fast to be helpful. So we + // throttle the updates to once every MIN_NOTIFY_THROTTLE (5 seconds). + // Another idea would be to keep notify off by default, and use update() as an opportunity to turn on + // notify to get enough data to update status, then turn off notify again. + + uint32_t now = millis(); + auto delta = now - this->last_notify_; + + if (this->last_notify_ == 0 || delta > MIN_NOTIFY_THROTTLE || this->force_refresh_) { + bool needs_extra = this->codec_->decode_notify(param->notify.value, param->notify.value_len); + this->last_notify_ = now; + + if (needs_extra) { + // this means the packet was partial, so read the status characteristic to get the second part. + auto status = esp_ble_gattc_read_char(this->parent_->gattc_if, this->parent_->conn_id, + this->char_handle_status_, ESP_GATT_AUTH_REQ_NONE); + if (status) { + ESP_LOGI(TAG, "[%s] Unable to read extended status packet", this->get_name().c_str()); + } + } + + if (this->force_refresh_) { + // If we requested an immediate update, do that now. + this->update(); + this->force_refresh_ = false; + } + } + break; + } + default: + ESP_LOGVV(TAG, "[%s] gattc unhandled event: enum=%d", this->get_name().c_str(), event); + break; + } +} + +/** Reimplementation of BLEClient.gattc_event_handler() for ESP_GATTC_REG_FOR_NOTIFY_EVT. + * + * This is a copy of ble_client's automatic handling of `ESP_GATTC_REG_FOR_NOTIFY_EVT`, in order + * to undo the same on unregister. It also allows us to maintain the config descriptor separately, + * since the parent BLEClient is going to purge all descriptors once we set our connection status + * to `Established`. + */ +uint8_t Bedjet::write_notify_config_descriptor_(bool enable) { + auto handle = this->config_descr_status_; + if (handle == 0) { + ESP_LOGW(TAG, "No descriptor found for notify of handle 0x%x", this->char_handle_status_); + return -1; + } + + // NOTE: BLEClient uses `uint8_t*` of length 1, but BLE spec requires 16 bits. + uint8_t notify_en[] = {0, 0}; + notify_en[0] = enable; + auto status = + esp_ble_gattc_write_char_descr(this->parent_->gattc_if, this->parent_->conn_id, handle, sizeof(notify_en), + ¬ify_en[0], ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE); + if (status) { + ESP_LOGW(TAG, "esp_ble_gattc_write_char_descr error, status=%d", status); + return status; + } + ESP_LOGD(TAG, "[%s] wrote notify=%s to status config 0x%04x", this->get_name().c_str(), enable ? "true" : "false", + handle); + return ESP_GATT_OK; +} + +#ifdef USE_TIME +/** Attempts to sync the local time (via `time_id`) to the BedJet device. */ +void Bedjet::send_local_time_() { + if (this->node_state != espbt::ClientState::ESTABLISHED) { + ESP_LOGV(TAG, "[%s] Not connected, cannot send time.", this->get_name().c_str()); + return; + } + auto *time_id = *this->time_id_; + time::ESPTime now = time_id->now(); + if (now.is_valid()) { + uint8_t hour = now.hour; + uint8_t minute = now.minute; + BedjetPacket *pkt = this->codec_->get_set_time_request(hour, minute); + auto status = this->write_bedjet_packet_(pkt); + if (status) { + ESP_LOGW(TAG, "Failed setting BedJet clock: %d", status); + } else { + ESP_LOGD(TAG, "[%s] BedJet clock set to: %d:%02d", this->get_name().c_str(), hour, minute); + } + } +} + +/** Initializes time sync callbacks to support syncing current time to the BedJet. */ +void Bedjet::setup_time_() { + if (this->time_id_.has_value()) { + this->send_local_time_(); + auto *time_id = *this->time_id_; + time_id->add_on_time_sync_callback([this] { this->send_local_time_(); }); + time::ESPTime now = time_id->now(); + ESP_LOGD(TAG, "Using time component to set BedJet clock: %d:%02d", now.hour, now.minute); + } else { + ESP_LOGI(TAG, "`time_id` is not configured: will not sync BedJet clock."); + } +} +#endif + +/** Writes one BedjetPacket to the BLE client on the BEDJET_COMMAND_UUID. */ +uint8_t Bedjet::write_bedjet_packet_(BedjetPacket *pkt) { + if (this->node_state != espbt::ClientState::ESTABLISHED) { + if (!this->parent_->enabled) { + ESP_LOGI(TAG, "[%s] Cannot write packet: Not connected, enabled=false", this->get_name().c_str()); + } else { + ESP_LOGW(TAG, "[%s] Cannot write packet: Not connected", this->get_name().c_str()); + } + return -1; + } + auto status = esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_cmd_, + pkt->data_length + 1, (uint8_t *) &pkt->command, ESP_GATT_WRITE_TYPE_NO_RSP, + ESP_GATT_AUTH_REQ_NONE); + return status; +} + +/** Configures the local ESP BLE client to register (`true`) or unregister (`false`) for status notifications. */ +uint8_t Bedjet::set_notify_(const bool enable) { + uint8_t status; + if (enable) { + status = esp_ble_gattc_register_for_notify(this->parent_->gattc_if, this->parent_->remote_bda, + this->char_handle_status_); + if (status) { + ESP_LOGW(TAG, "[%s] esp_ble_gattc_register_for_notify failed, status=%d", this->get_name().c_str(), status); + } + } else { + status = esp_ble_gattc_unregister_for_notify(this->parent_->gattc_if, this->parent_->remote_bda, + this->char_handle_status_); + if (status) { + ESP_LOGW(TAG, "[%s] esp_ble_gattc_unregister_for_notify failed, status=%d", this->get_name().c_str(), status); + } + } + ESP_LOGV(TAG, "[%s] set_notify: enable=%d; result=%d", this->get_name().c_str(), enable, status); + return status; +} + +/** Attempts to update the climate device from the last received BedjetStatusPacket. + * + * @return `true` if the status has been applied; `false` if there is nothing to apply. + */ +bool Bedjet::update_status_() { + if (!this->codec_->has_status()) + return false; + + BedjetStatusPacket status = *this->codec_->get_status_packet(); + + auto converted_temp = bedjet_temp_to_c(status.target_temp_step); + if (converted_temp > 0) + this->target_temperature = converted_temp; + converted_temp = bedjet_temp_to_c(status.ambient_temp_step); + if (converted_temp > 0) + this->current_temperature = converted_temp; + + const auto *fan_mode_name = bedjet_fan_step_to_fan_mode(status.fan_step); + if (fan_mode_name != nullptr) { + this->custom_fan_mode = *fan_mode_name; + } + + // TODO: Get biorhythm data to determine which preset (M1-3) is running, if any. + switch (status.mode) { + case MODE_WAIT: // Biorhythm "wait" step: device is idle + case MODE_STANDBY: + this->mode = climate::CLIMATE_MODE_OFF; + this->action = climate::CLIMATE_ACTION_IDLE; + this->fan_mode = climate::CLIMATE_FAN_OFF; + this->custom_preset.reset(); + this->preset.reset(); + break; + + case MODE_HEAT: + case MODE_EXTHT: + this->mode = climate::CLIMATE_MODE_HEAT; + this->action = climate::CLIMATE_ACTION_HEATING; + this->custom_preset.reset(); + this->preset.reset(); + break; + + case MODE_COOL: + this->mode = climate::CLIMATE_MODE_FAN_ONLY; + this->action = climate::CLIMATE_ACTION_COOLING; + this->custom_preset.reset(); + this->preset.reset(); + break; + + case MODE_DRY: + this->mode = climate::CLIMATE_MODE_DRY; + this->action = climate::CLIMATE_ACTION_DRYING; + this->custom_preset.reset(); + this->preset.reset(); + break; + + case MODE_TURBO: + this->preset = climate::CLIMATE_PRESET_BOOST; + this->custom_preset.reset(); + this->mode = climate::CLIMATE_MODE_HEAT; + this->action = climate::CLIMATE_ACTION_HEATING; + break; + + default: + ESP_LOGW(TAG, "[%s] Unexpected mode: 0x%02X", this->get_name().c_str(), status.mode); + break; + } + + if (this->is_valid_()) { + this->publish_state(); + this->codec_->clear_status(); + this->status_clear_warning(); + } + + return true; +} + +void Bedjet::update() { + ESP_LOGV(TAG, "[%s] update()", this->get_name().c_str()); + + if (this->node_state != espbt::ClientState::ESTABLISHED) { + if (!this->parent()->enabled) { + ESP_LOGD(TAG, "[%s] Not connected, because enabled=false", this->get_name().c_str()); + } else { + // Possibly still trying to connect. + ESP_LOGD(TAG, "[%s] Not connected, enabled=true", this->get_name().c_str()); + } + + return; + } + + auto result = this->update_status_(); + if (!result) { + uint32_t now = millis(); + uint32_t diff = now - this->last_notify_; + + if (this->last_notify_ == 0) { + // This means we're connected and haven't received a notification, so it likely means that the BedJet is off. + // However, it could also mean that it's running, but failing to send notifications. + // We can try to unregister for notifications now, and then re-register, hoping to clear it up... + // But how do we know for sure which state we're in, and how do we actually clear out the buggy state? + + ESP_LOGI(TAG, "[%s] Still waiting for first GATT notify event.", this->get_name().c_str()); + this->set_notify_(false); + } else if (diff > NOTIFY_WARN_THRESHOLD) { + ESP_LOGW(TAG, "[%s] Last GATT notify was %d seconds ago.", this->get_name().c_str(), diff / 1000); + } + + if (this->timeout_ > 0 && diff > this->timeout_ && this->parent()->enabled) { + ESP_LOGW(TAG, "[%s] Timed out after %d sec. Retrying...", this->get_name().c_str(), this->timeout_); + this->parent()->set_enabled(false); + this->parent()->set_enabled(true); + } + } +} + +} // namespace bedjet +} // namespace esphome + +#endif diff --git a/esphome/components/bedjet/bedjet.h b/esphome/components/bedjet/bedjet.h new file mode 100644 index 0000000000..b061d2b5ec --- /dev/null +++ b/esphome/components/bedjet/bedjet.h @@ -0,0 +1,121 @@ +#pragma once + +#include "esphome/components/ble_client/ble_client.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/climate/climate.h" +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "bedjet_base.h" + +#ifdef USE_TIME +#include "esphome/components/time/real_time_clock.h" +#endif + +#ifdef USE_ESP32 + +#include + +namespace esphome { +namespace bedjet { + +namespace espbt = esphome::esp32_ble_tracker; + +static const espbt::ESPBTUUID BEDJET_SERVICE_UUID = espbt::ESPBTUUID::from_raw("00001000-bed0-0080-aa55-4265644a6574"); +static const espbt::ESPBTUUID BEDJET_STATUS_UUID = espbt::ESPBTUUID::from_raw("00002000-bed0-0080-aa55-4265644a6574"); +static const espbt::ESPBTUUID BEDJET_COMMAND_UUID = espbt::ESPBTUUID::from_raw("00002004-bed0-0080-aa55-4265644a6574"); +static const espbt::ESPBTUUID BEDJET_NAME_UUID = espbt::ESPBTUUID::from_raw("00002001-bed0-0080-aa55-4265644a6574"); + +class Bedjet : public climate::Climate, public esphome::ble_client::BLEClientNode, public PollingComponent { + public: + void setup() override; + void loop() override; + void update() override; + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } + +#ifdef USE_TIME + void set_time_id(time::RealTimeClock *time_id) { this->time_id_ = time_id; } +#endif + void set_status_timeout(uint32_t timeout) { this->timeout_ = timeout; } + + /** Attempts to check for and apply firmware updates. */ + void upgrade_firmware(); + + climate::ClimateTraits traits() override { + auto traits = climate::ClimateTraits(); + traits.set_supports_action(true); + traits.set_supports_current_temperature(true); + traits.set_supported_modes({ + climate::CLIMATE_MODE_OFF, + climate::CLIMATE_MODE_HEAT, + // climate::CLIMATE_MODE_TURBO // Not supported by Climate: see presets instead + climate::CLIMATE_MODE_FAN_ONLY, + climate::CLIMATE_MODE_DRY, + }); + + // It would be better if we had a slider for the fan modes. + traits.set_supported_custom_fan_modes(BEDJET_FAN_STEP_NAMES_SET); + traits.set_supported_presets({ + // If we support NONE, then have to decide what happens if the user switches to it (turn off?) + // climate::CLIMATE_PRESET_NONE, + // Climate doesn't have a "TURBO" mode, but we can use the BOOST preset instead. + climate::CLIMATE_PRESET_BOOST, + }); + traits.set_supported_custom_presets({ + // We could fetch biodata from bedjet and set these names that way. + // But then we have to invert the lookup in order to send the right preset. + // For now, we can leave them as M1-3 to match the remote buttons. + "M1", + "M2", + "M3", + }); + traits.set_visual_min_temperature(19.0); + traits.set_visual_max_temperature(43.0); + traits.set_visual_temperature_step(1.0); + return traits; + } + + protected: + void control(const climate::ClimateCall &call) override; + +#ifdef USE_TIME + void setup_time_(); + void send_local_time_(); + optional time_id_{}; +#endif + + uint32_t timeout_{DEFAULT_STATUS_TIMEOUT}; + + static const uint32_t MIN_NOTIFY_THROTTLE = 5000; + static const uint32_t NOTIFY_WARN_THRESHOLD = 300000; + static const uint32_t DEFAULT_STATUS_TIMEOUT = 900000; + + uint8_t set_notify_(bool enable); + uint8_t write_bedjet_packet_(BedjetPacket *pkt); + void reset_state_(); + bool update_status_(); + + bool is_valid_() { + // FIXME: find a better way to check this? + return !std::isnan(this->current_temperature) && !std::isnan(this->target_temperature) && + this->current_temperature > 1 && this->target_temperature > 1; + } + + uint32_t last_notify_ = 0; + bool force_refresh_ = false; + + std::unique_ptr codec_; + uint16_t char_handle_cmd_; + uint16_t char_handle_name_; + uint16_t char_handle_status_; + uint16_t config_descr_status_; + + uint8_t write_notify_config_descriptor_(bool enable); +}; + +} // namespace bedjet +} // namespace esphome + +#endif diff --git a/esphome/components/bedjet/bedjet_base.cpp b/esphome/components/bedjet/bedjet_base.cpp new file mode 100644 index 0000000000..99f1df96d3 --- /dev/null +++ b/esphome/components/bedjet/bedjet_base.cpp @@ -0,0 +1,123 @@ +#include "bedjet_base.h" +#include +#include + +namespace esphome { +namespace bedjet { + +/// Converts a BedJet temp step into degrees Fahrenheit. +float bedjet_temp_to_f(const uint8_t temp) { + // BedJet temp is "C*2"; to get F, multiply by 0.9 (half 1.8) and add 32. + return 0.9f * temp + 32.0f; +} + +/** Cleans up the packet before sending. */ +BedjetPacket *BedjetCodec::clean_packet_() { + // So far no commands require more than 2 bytes of data. + assert(this->packet_.data_length <= 2); + for (int i = this->packet_.data_length; i < 2; i++) { + this->packet_.data[i] = '\0'; + } + ESP_LOGV(TAG, "Created packet: %02X, %02X %02X", this->packet_.command, this->packet_.data[0], this->packet_.data[1]); + return &this->packet_; +} + +/** Returns a BedjetPacket that will initiate a BedjetButton press. */ +BedjetPacket *BedjetCodec::get_button_request(BedjetButton button) { + this->packet_.command = CMD_BUTTON; + this->packet_.data_length = 1; + this->packet_.data[0] = button; + return this->clean_packet_(); +} + +/** Returns a BedjetPacket that will set the device's target `temperature`. */ +BedjetPacket *BedjetCodec::get_set_target_temp_request(float temperature) { + this->packet_.command = CMD_SET_TEMP; + this->packet_.data_length = 1; + this->packet_.data[0] = temperature * 2; + return this->clean_packet_(); +} + +/** Returns a BedjetPacket that will set the device's target fan speed. */ +BedjetPacket *BedjetCodec::get_set_fan_speed_request(const uint8_t fan_step) { + this->packet_.command = CMD_SET_FAN; + this->packet_.data_length = 1; + this->packet_.data[0] = fan_step; + return this->clean_packet_(); +} + +/** Returns a BedjetPacket that will set the device's current time. */ +BedjetPacket *BedjetCodec::get_set_time_request(const uint8_t hour, const uint8_t minute) { + this->packet_.command = CMD_SET_TIME; + this->packet_.data_length = 2; + this->packet_.data[0] = hour; + this->packet_.data[1] = minute; + return this->clean_packet_(); +} + +/** Decodes the extra bytes that were received after being notified with a partial packet. */ +void BedjetCodec::decode_extra(const uint8_t *data, uint16_t length) { + ESP_LOGV(TAG, "Received extra: %d bytes: %d %d %d %d", length, data[1], data[2], data[3], data[4]); + uint8_t offset = this->last_buffer_size_; + if (offset > 0 && length + offset <= sizeof(BedjetStatusPacket)) { + memcpy(((uint8_t *) (&this->buf_)) + offset, data, length); + ESP_LOGV(TAG, + "Extra bytes: skip1=0x%08x, skip2=0x%04x, skip3=0x%02x; update phase=0x%02x, " + "flags=BedjetFlags ", + this->buf_._skip_1_, this->buf_._skip_2_, this->buf_._skip_3_, this->buf_.update_phase, + this->buf_.flags & 0x20 ? '1' : '0', this->buf_.flags & 0x10 ? '1' : '0', + this->buf_.flags & 0x04 ? '1' : '0', this->buf_.flags & 0x01 ? '1' : '0', + this->buf_.flags & ~(0x20 | 0x10 | 0x04 | 0x01)); + } else { + ESP_LOGI(TAG, "Could not determine where to append to, last offset=%d, max size=%u, new size would be %d", offset, + sizeof(BedjetStatusPacket), length + offset); + } +} + +/** Decodes the incoming status packet received on the BEDJET_STATUS_UUID. + * + * @return `true` if the packet was decoded and represents a "partial" packet; `false` otherwise. + */ +bool BedjetCodec::decode_notify(const uint8_t *data, uint16_t length) { + ESP_LOGV(TAG, "Received: %d bytes: %d %d %d %d", length, data[1], data[2], data[3], data[4]); + + if (data[1] == PACKET_FORMAT_V3_HOME && data[3] == PACKET_TYPE_STATUS) { + this->status_packet_.reset(); + + // Clear old buffer + memset(&this->buf_, 0, sizeof(BedjetStatusPacket)); + // Copy new data into buffer + memcpy(&this->buf_, data, length); + this->last_buffer_size_ = length; + + // TODO: validate the packet checksum? + if (this->buf_.mode >= 0 && this->buf_.mode < 7 && this->buf_.target_temp_step >= 38 && + this->buf_.target_temp_step <= 86 && this->buf_.actual_temp_step > 1 && this->buf_.actual_temp_step <= 100 && + this->buf_.ambient_temp_step > 1 && this->buf_.ambient_temp_step <= 100) { + // and save it for the update() loop + this->status_packet_ = this->buf_; + return this->buf_.is_partial == 1; + } else { + // TODO: log a warning if we detect that we connected to a non-V3 device. + ESP_LOGW(TAG, "Received potentially invalid packet (len %d):", length); + } + } else if (data[1] == PACKET_FORMAT_DEBUG || data[3] == PACKET_TYPE_DEBUG) { + // We don't actually know the packet format for this. Dump packets to log, in case a pattern presents itself. + ESP_LOGV(TAG, + "received DEBUG packet: set1=%01fF, set2=%01fF, air=%01fF; [7]=%d, [8]=%d, [9]=%d, [10]=%d, [11]=%d, " + "[12]=%d, [-1]=%d", + bedjet_temp_to_f(data[4]), bedjet_temp_to_f(data[5]), bedjet_temp_to_f(data[6]), data[7], data[8], data[9], + data[10], data[11], data[12], data[length - 1]); + + if (this->has_status()) { + this->status_packet_->ambient_temp_step = data[6]; + } + } else { + // TODO: log a warning if we detect that we connected to a non-V3 device. + } + + return false; +} + +} // namespace bedjet +} // namespace esphome diff --git a/esphome/components/bedjet/bedjet_base.h b/esphome/components/bedjet/bedjet_base.h new file mode 100644 index 0000000000..c63b70cb9a --- /dev/null +++ b/esphome/components/bedjet/bedjet_base.h @@ -0,0 +1,159 @@ +#pragma once + +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +#include "bedjet_const.h" + +namespace esphome { +namespace bedjet { + +struct BedjetPacket { + uint8_t data_length; + BedjetCommand command; + uint8_t data[2]; +}; + +struct BedjetFlags { + /* uint8_t */ + int a_ : 1; // 0x80 + int b_ : 1; // 0x40 + int conn_test_passed : 1; ///< (0x20) Bit is set `1` if the last connection test passed. + int leds_enabled : 1; ///< (0x10) Bit is set `1` if the LEDs on the device are enabled. + int c_ : 1; // 0x08 + int units_setup : 1; ///< (0x04) Bit is set `1` if the device's units have been configured. + int d_ : 1; // 0x02 + int beeps_muted : 1; ///< (0x01) Bit is set `1` if the device's sound output is muted. +} __attribute__((packed)); + +enum BedjetPacketFormat : uint8_t { + PACKET_FORMAT_DEBUG = 0x05, // 5 + PACKET_FORMAT_V3_HOME = 0x56, // 86 +}; + +enum BedjetPacketType : uint8_t { + PACKET_TYPE_STATUS = 0x1, + PACKET_TYPE_DEBUG = 0x2, +}; + +/** The format of a BedJet V3 status packet. */ +struct BedjetStatusPacket { + // [0] + uint8_t is_partial : 8; ///< `1` indicates that this is a partial packet, and more data can be read directly from the + ///< characteristic. + BedjetPacketFormat packet_format : 8; ///< BedjetPacketFormat::PACKET_FORMAT_V3_HOME for BedJet V3 status packet + ///< format. BedjetPacketFormat::PACKET_FORMAT_DEBUG for debugging packets. + uint8_t + expecting_length : 8; ///< The expected total length of the status packet after merging the additional packet. + BedjetPacketType packet_type : 8; ///< Typically BedjetPacketType::PACKET_TYPE_STATUS for BedJet V3 status packet. + + // [4] + uint8_t time_remaining_hrs : 8; ///< Hours remaining in program runtime + uint8_t time_remaining_mins : 8; ///< Minutes remaining in program runtime + uint8_t time_remaining_secs : 8; ///< Seconds remaining in program runtime + + // [7] + uint8_t actual_temp_step : 8; ///< Actual temp of the air blown by the BedJet fan; value represents `2 * + ///< degrees_celsius`. See #bedjet_temp_to_c and #bedjet_temp_to_f + uint8_t target_temp_step : 8; ///< Target temp that the BedJet will try to heat to. See #actual_temp_step. + + // [9] + BedjetMode mode : 8; ///< BedJet operating mode. + + // [10] + uint8_t fan_step : 8; ///< BedJet fan speed; value is in the 0-19 range, representing 5% increments (5%-100%): `5 + 5 + ///< * fan_step` + uint8_t max_hrs : 8; ///< Max hours of mode runtime + uint8_t max_mins : 8; ///< Max minutes of mode runtime + uint8_t min_temp_step : 8; ///< Min temp allowed in mode. See #actual_temp_step. + uint8_t max_temp_step : 8; ///< Max temp allowed in mode. See #actual_temp_step. + + // [15-16] + uint16_t turbo_time : 16; ///< Time remaining in BedjetMode::MODE_TURBO. + + // [17] + uint8_t ambient_temp_step : 8; ///< Current ambient air temp. This is the coldest air the BedJet can blow. See + ///< #actual_temp_step. + uint8_t shutdown_reason : 8; ///< The reason for the last device shutdown. + + // [19-25]; the initial partial packet cuts off here after [19] + // Skip 7 bytes? + uint32_t _skip_1_ : 32; // Unknown 19-22 = 0x01810112 + + uint16_t _skip_2_ : 16; // Unknown 23-24 = 0x1310 + uint8_t _skip_3_ : 8; // Unknown 25 = 0x00 + + // [26] + // 0x18(24) = "Connection test has completed OK" + // 0x1a(26) = "Firmware update is not needed" + uint8_t update_phase : 8; ///< The current status/phase of a firmware update. + + // [27] + // FIXME: cannot nest packed struct of matching length here? + /* BedjetFlags */ uint8_t flags : 8; /// See BedjetFlags for the packed byte flags. + // [28-31]; 20+11 bytes + uint32_t _skip_4_ : 32; // Unknown + +} __attribute__((packed)); + +/** This class is responsible for encoding command packets and decoding status packets. + * + * Status Packets + * ============== + * The BedJet protocol depends on registering for notifications on the esphome::BedJet::BEDJET_SERVICE_UUID + * characteristic. If the BedJet is on, it will send rapid updates as notifications. If it is off, + * it generally will not notify of any status. + * + * As the BedJet V3's BedjetStatusPacket exceeds the buffer size allowed for BLE notification packets, + * the notification packet will contain `BedjetStatusPacket::is_partial == 1`. When that happens, an additional + * read of the esphome::BedJet::BEDJET_SERVICE_UUID characteristic will contain the second portion of the + * full status packet. + * + * Command Packets + * =============== + * This class supports encoding a number of BedjetPacket commands: + * - Button press + * This simulates a press of one of the BedjetButton values. + * - BedjetPacket#command = BedjetCommand::CMD_BUTTON + * - BedjetPacket#data [0] contains the BedjetButton value + * - Set target temp + * This sets the BedJet's target temp to a concrete temperature value. + * - BedjetPacket#command = BedjetCommand::CMD_SET_TEMP + * - BedjetPacket#data [0] contains the BedJet temp value; see BedjetStatusPacket#actual_temp_step + * - Set fan speed + * This sets the BedJet fan speed. + * - BedjetPacket#command = BedjetCommand::CMD_SET_FAN + * - BedjetPacket#data [0] contains the BedJet fan step in the range 0-19. + * - Set current time + * The BedJet needs to have its clock set properly in order to run the biorhythm programs, which might + * contain time-of-day based step rules. + * - BedjetPacket#command = BedjetCommand::CMD_SET_TIME + * - BedjetPacket#data [0] is hours, [1] is minutes + */ +class BedjetCodec { + public: + BedjetPacket *get_button_request(BedjetButton button); + BedjetPacket *get_set_target_temp_request(float temperature); + BedjetPacket *get_set_fan_speed_request(uint8_t fan_step); + BedjetPacket *get_set_time_request(uint8_t hour, uint8_t minute); + + bool decode_notify(const uint8_t *data, uint16_t length); + void decode_extra(const uint8_t *data, uint16_t length); + + inline bool has_status() { return this->status_packet_.has_value(); } + const optional &get_status_packet() const { return this->status_packet_; } + void clear_status() { this->status_packet_.reset(); } + + protected: + BedjetPacket *clean_packet_(); + + uint8_t last_buffer_size_ = 0; + + BedjetPacket packet_; + + optional status_packet_; + BedjetStatusPacket buf_; +}; + +} // namespace bedjet +} // namespace esphome diff --git a/esphome/components/bedjet/bedjet_const.h b/esphome/components/bedjet/bedjet_const.h new file mode 100644 index 0000000000..e6bfa45d3a --- /dev/null +++ b/esphome/components/bedjet/bedjet_const.h @@ -0,0 +1,78 @@ +#pragma once + +#include + +namespace esphome { +namespace bedjet { + +static const char *const TAG = "bedjet"; + +enum BedjetMode : uint8_t { + /// BedJet is Off + MODE_STANDBY = 0, + /// BedJet is in Heat mode (limited to 4 hours) + MODE_HEAT = 1, + /// BedJet is in Turbo mode (high heat, limited time) + MODE_TURBO = 2, + /// BedJet is in Extended Heat mode (limited to 10 hours) + MODE_EXTHT = 3, + /// BedJet is in Cool mode (actually "Fan only" mode) + MODE_COOL = 4, + /// BedJet is in Dry mode (high speed, no heat) + MODE_DRY = 5, + /// BedJet is in "wait" mode, a step during a biorhythm program + MODE_WAIT = 6, +}; + +enum BedjetButton : uint8_t { + /// Turn BedJet off + BTN_OFF = 0x1, + /// Enter Cool mode (fan only) + BTN_COOL = 0x2, + /// Enter Heat mode (limited to 4 hours) + BTN_HEAT = 0x3, + /// Enter Turbo mode (high heat, limited to 10 minutes) + BTN_TURBO = 0x4, + /// Enter Dry mode (high speed, no heat) + BTN_DRY = 0x5, + /// Enter Extended Heat mode (limited to 10 hours) + BTN_EXTHT = 0x6, + + /// Start the M1 biorhythm/preset program + BTN_M1 = 0x20, + /// Start the M2 biorhythm/preset program + BTN_M2 = 0x21, + /// Start the M3 biorhythm/preset program + BTN_M3 = 0x22, + + /* These are "MAGIC" buttons */ + + /// Turn debug mode on/off + MAGIC_DEBUG_ON = 0x40, + MAGIC_DEBUG_OFF = 0x41, + /// Perform a connection test. + MAGIC_CONNTEST = 0x42, + /// Request a firmware update. This will also restart the Bedjet. + MAGIC_UPDATE = 0x43, +}; + +enum BedjetCommand : uint8_t { + CMD_BUTTON = 0x1, + CMD_SET_TEMP = 0x3, + CMD_STATUS = 0x6, + CMD_SET_FAN = 0x7, + CMD_SET_TIME = 0x8, +}; + +#define BEDJET_FAN_STEP_NAMES_ \ + { \ + " 5%", " 10%", " 15%", " 20%", " 25%", " 30%", " 35%", " 40%", " 45%", " 50%", " 55%", " 60%", " 65%", " 70%", \ + " 75%", " 80%", " 85%", " 90%", " 95%", "100%" \ + } + +static const char *const BEDJET_FAN_STEP_NAMES[20] = BEDJET_FAN_STEP_NAMES_; +static const std::string BEDJET_FAN_STEP_NAME_STRINGS[20] = BEDJET_FAN_STEP_NAMES_; +static const std::set BEDJET_FAN_STEP_NAMES_SET BEDJET_FAN_STEP_NAMES_; + +} // namespace bedjet +} // namespace esphome diff --git a/esphome/components/bedjet/climate.py b/esphome/components/bedjet/climate.py new file mode 100644 index 0000000000..49353934f6 --- /dev/null +++ b/esphome/components/bedjet/climate.py @@ -0,0 +1,42 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import climate, ble_client, time +from esphome.const import ( + CONF_ID, + CONF_RECEIVE_TIMEOUT, + CONF_TIME_ID, +) + +CODEOWNERS = ["@jhansche"] +DEPENDENCIES = ["ble_client"] + +bedjet_ns = cg.esphome_ns.namespace("bedjet") +Bedjet = bedjet_ns.class_( + "Bedjet", climate.Climate, ble_client.BLEClientNode, cg.PollingComponent +) + +CONFIG_SCHEMA = ( + climate.CLIMATE_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(Bedjet), + cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock), + cv.Optional( + CONF_RECEIVE_TIMEOUT, default="0s" + ): cv.positive_time_period_milliseconds, + } + ) + .extend(ble_client.BLE_CLIENT_SCHEMA) + .extend(cv.polling_component_schema("30s")) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await climate.register_climate(var, config) + await ble_client.register_ble_node(var, config) + if CONF_TIME_ID in config: + time_ = await cg.get_variable(config[CONF_TIME_ID]) + cg.add(var.set_time_id(time_)) + if CONF_RECEIVE_TIMEOUT in config: + cg.add(var.set_status_timeout(config[CONF_RECEIVE_TIMEOUT])) diff --git a/tests/test1.yaml b/tests/test1.yaml index 98a3ffcf4b..375499942b 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -291,6 +291,8 @@ ble_client: on_disconnect: then: - switch.turn_on: ble1_status + - mac_address: C4:4F:33:11:22:33 + id: my_bedjet_ble_client mcp23s08: - id: "mcp23s08_hub" cs_pin: GPIO12 @@ -1870,6 +1872,9 @@ climate: ble_client_id: ble_blah unit_of_measurement: c icon: mdi:stove + - platform: bedjet + name: My Bedjet + ble_client_id: my_bedjet_ble_client script: - id: climate_custom From 93b628d9a856461e570b9e195f8dbd4c04346cde Mon Sep 17 00:00:00 2001 From: Janez Troha <239513+dz0ny@users.noreply.github.com> Date: Thu, 14 Apr 2022 03:42:43 +0200 Subject: [PATCH 0391/1729] Allocate smaller amount of buffer for JSON (#3384) --- esphome/components/json/json_util.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/esphome/components/json/json_util.cpp b/esphome/components/json/json_util.cpp index 10179c9954..2bd8112255 100644 --- a/esphome/components/json/json_util.cpp +++ b/esphome/components/json/json_util.cpp @@ -23,13 +23,13 @@ std::string build_json(const json_build_t &f) { #ifdef USE_ESP8266 const size_t free_heap = ESP.getMaxFreeBlockSize(); // NOLINT(readability-static-accessed-through-instance) #elif defined(USE_ESP32) - const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL); + const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT); #endif - const size_t request_size = std::min(free_heap - 2048, (size_t) 5120); + const size_t request_size = std::min(free_heap, (size_t) 512); DynamicJsonDocument json_document(request_size); - if (json_document.memoryPool().buffer() == nullptr) { + if (json_document.capacity() == 0) { ESP_LOGE(TAG, "Could not allocate memory for JSON document! Requested %u bytes, largest free heap block: %u bytes", request_size, free_heap); return "{}"; @@ -37,7 +37,7 @@ std::string build_json(const json_build_t &f) { JsonObject root = json_document.to(); f(root); json_document.shrinkToFit(); - + ESP_LOGV(TAG, "Size after shrink %u bytes", json_document.capacity()); std::string output; serializeJson(json_document, output); return output; @@ -51,13 +51,13 @@ void parse_json(const std::string &data, const json_parse_t &f) { #ifdef USE_ESP8266 const size_t free_heap = ESP.getMaxFreeBlockSize(); // NOLINT(readability-static-accessed-through-instance) #elif defined(USE_ESP32) - const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL); + const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT); #endif bool pass = false; - size_t request_size = std::min(free_heap - 2048, (size_t)(data.size() * 1.5)); + size_t request_size = std::min(free_heap, (size_t)(data.size() * 1.5)); do { DynamicJsonDocument json_document(request_size); - if (json_document.memoryPool().buffer() == nullptr) { + if (json_document.capacity() == 0) { ESP_LOGE(TAG, "Could not allocate memory for JSON document! Requested %u bytes, free heap: %u", request_size, free_heap); return; From c59adf612f8ccdc3f415e99ce538ba1c8fc6c928 Mon Sep 17 00:00:00 2001 From: matthias882 <30553262+matthias882@users.noreply.github.com> Date: Wed, 13 Apr 2022 23:36:16 +0200 Subject: [PATCH 0392/1729] Changes accuracy of single cell voltage (#3387) --- esphome/components/daly_bms/sensor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/daly_bms/sensor.py b/esphome/components/daly_bms/sensor.py index e2e8528317..2274a2153a 100644 --- a/esphome/components/daly_bms/sensor.py +++ b/esphome/components/daly_bms/sensor.py @@ -98,6 +98,8 @@ CELL_VOLTAGE_SCHEMA = sensor.sensor_schema( unit_of_measurement=UNIT_VOLT, device_class=DEVICE_CLASS_VOLTAGE, state_class=STATE_CLASS_MEASUREMENT, + icon=ICON_FLASH, + accuracy_decimals=3, ) CONFIG_SCHEMA = cv.All( From d5134e88b16edaeb9e39339cdd1f3b7ddd645d52 Mon Sep 17 00:00:00 2001 From: rnauber <7414650+rnauber@users.noreply.github.com> Date: Thu, 14 Apr 2022 03:13:51 +0200 Subject: [PATCH 0393/1729] Add support for Shelly Dimmer 2 (#2954) Co-authored-by: Niclas Larsson Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Co-authored-by: Jernej Kos Co-authored-by: Richard Nauber --- CODEOWNERS | 1 + esphome/components/shelly_dimmer/LICENSE.txt | 2 + esphome/components/shelly_dimmer/__init__.py | 1 + esphome/components/shelly_dimmer/dev_table.h | 158 +++ esphome/components/shelly_dimmer/light.py | 219 ++++ .../shelly_dimmer/shelly_dimmer.cpp | 526 ++++++++ .../components/shelly_dimmer/shelly_dimmer.h | 117 ++ .../components/shelly_dimmer/stm32flash.cpp | 1061 +++++++++++++++++ esphome/components/shelly_dimmer/stm32flash.h | 129 ++ esphome/core/defines.h | 6 + tests/test1.yaml | 12 + 11 files changed, 2232 insertions(+) create mode 100644 esphome/components/shelly_dimmer/LICENSE.txt create mode 100644 esphome/components/shelly_dimmer/__init__.py create mode 100644 esphome/components/shelly_dimmer/dev_table.h create mode 100644 esphome/components/shelly_dimmer/light.py create mode 100644 esphome/components/shelly_dimmer/shelly_dimmer.cpp create mode 100644 esphome/components/shelly_dimmer/shelly_dimmer.h create mode 100644 esphome/components/shelly_dimmer/stm32flash.cpp create mode 100644 esphome/components/shelly_dimmer/stm32flash.h diff --git a/CODEOWNERS b/CODEOWNERS index 7595fc52e2..02945ec0a4 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -173,6 +173,7 @@ esphome/components/select/* @esphome/core esphome/components/sensirion_common/* @martgras esphome/components/sensor/* @esphome/core esphome/components/sgp40/* @SenexCrenshaw +esphome/components/shelly_dimmer/* @edge90 @rnauber esphome/components/sht4x/* @sjtrny esphome/components/shutdown/* @esphome/core @jsuanet esphome/components/sim800l/* @glmnet diff --git a/esphome/components/shelly_dimmer/LICENSE.txt b/esphome/components/shelly_dimmer/LICENSE.txt new file mode 100644 index 0000000000..524fe0d514 --- /dev/null +++ b/esphome/components/shelly_dimmer/LICENSE.txt @@ -0,0 +1,2 @@ +The firmware files for the STM microcontroller (shelly-dimmer-stm32_*.bin) are taken from +https://github.com/jamesturton/shelly-dimmer-stm32 and GPLv3 licensed. diff --git a/esphome/components/shelly_dimmer/__init__.py b/esphome/components/shelly_dimmer/__init__.py new file mode 100644 index 0000000000..accefbbc34 --- /dev/null +++ b/esphome/components/shelly_dimmer/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@rnauber", "@edge90"] diff --git a/esphome/components/shelly_dimmer/dev_table.h b/esphome/components/shelly_dimmer/dev_table.h new file mode 100644 index 0000000000..f4bf7778f2 --- /dev/null +++ b/esphome/components/shelly_dimmer/dev_table.h @@ -0,0 +1,158 @@ +/* + stm32flash - Open Source ST STM32 flash program for Arduino + Copyright (C) 2010 Geoffrey McRae + Copyright (C) 2014-2015 Antonio Borneo + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +*/ +#pragma once + +#include "esphome/core/defines.h" +#ifdef USE_SHD_FIRMWARE_DATA +#include "stm32flash.h" + +namespace esphome { +namespace shelly_dimmer { + +constexpr uint32_t SZ_128 = 0x00000080; +constexpr uint32_t SZ_256 = 0x00000100; +constexpr uint32_t SZ_1K = 0x00000400; +constexpr uint32_t SZ_2K = 0x00000800; +constexpr uint32_t SZ_16K = 0x00004000; +constexpr uint32_t SZ_32K = 0x00008000; +constexpr uint32_t SZ_64K = 0x00010000; +constexpr uint32_t SZ_128K = 0x00020000; +constexpr uint32_t SZ_256K = 0x00040000; + +/* + * Page-size for page-by-page flash erase. + * Arrays are zero terminated; last non-zero value is automatically repeated + */ + +/* fixed size pages */ +constexpr uint32_t p_128[] = {SZ_128, 0}; // NOLINT +constexpr uint32_t p_256[] = {SZ_256, 0}; // NOLINT +constexpr uint32_t p_1k[] = {SZ_1K, 0}; // NOLINT +constexpr uint32_t p_2k[] = {SZ_2K, 0}; // NOLINT +/* F2 and F4 page size */ +constexpr uint32_t f2f4[] = {SZ_16K, SZ_16K, SZ_16K, SZ_16K, SZ_64K, SZ_128K, 0}; // NOLINT +/* F4 dual bank page size */ +constexpr uint32_t f4db[] = {SZ_16K, SZ_16K, SZ_16K, SZ_16K, SZ_64K, SZ_128K, SZ_128K, // NOLINT + SZ_128K, SZ_16K, SZ_16K, SZ_16K, SZ_16K, SZ_64K, SZ_128K, 0}; +/* F7 page size */ +constexpr uint32_t f7[] = {SZ_32K, SZ_32K, SZ_32K, SZ_32K, SZ_128K, SZ_256K, 0}; // NOLINT + +/* + * Device table, corresponds to the "Bootloader device-dependant parameters" + * table in ST document AN2606. + * Note that the option bytes upper range is inclusive! + */ +constexpr stm32_dev_t DEVICES[] = { + /* ID "name" SRAM-address-range FLASH-address-range PPS PSize + Option-byte-addr-range System-mem-addr-range Flags */ + /* F0 */ + {0x440, "STM32F030x8/F05xxx", 0x20000800, 0x20002000, 0x08000000, 0x08010000, 4, p_1k, 0x1FFFF800, 0x1FFFF80F, + 0x1FFFEC00, 0x1FFFF800, 0}, + {0x442, "STM32F030xC/F09xxx", 0x20001800, 0x20008000, 0x08000000, 0x08040000, 2, p_2k, 0x1FFFF800, 0x1FFFF80F, + 0x1FFFC800, 0x1FFFF800, F_OBLL}, + {0x444, "STM32F03xx4/6", 0x20000800, 0x20001000, 0x08000000, 0x08008000, 4, p_1k, 0x1FFFF800, 0x1FFFF80F, + 0x1FFFEC00, 0x1FFFF800, 0}, + {0x445, "STM32F04xxx/F070x6", 0x20001800, 0x20001800, 0x08000000, 0x08008000, 4, p_1k, 0x1FFFF800, 0x1FFFF80F, + 0x1FFFC400, 0x1FFFF800, 0}, + {0x448, "STM32F070xB/F071xx/F72xx", 0x20001800, 0x20004000, 0x08000000, 0x08020000, 2, p_2k, 0x1FFFF800, 0x1FFFF80F, + 0x1FFFC800, 0x1FFFF800, 0}, + /* F1 */ + {0x412, "STM32F10xxx Low-density", 0x20000200, 0x20002800, 0x08000000, 0x08008000, 4, p_1k, 0x1FFFF800, 0x1FFFF80F, + 0x1FFFF000, 0x1FFFF800, 0}, + {0x410, "STM32F10xxx Medium-density", 0x20000200, 0x20005000, 0x08000000, 0x08020000, 4, p_1k, 0x1FFFF800, + 0x1FFFF80F, 0x1FFFF000, 0x1FFFF800, 0}, + {0x414, "STM32F10xxx High-density", 0x20000200, 0x20010000, 0x08000000, 0x08080000, 2, p_2k, 0x1FFFF800, 0x1FFFF80F, + 0x1FFFF000, 0x1FFFF800, 0}, + {0x420, "STM32F10xxx Medium-density VL", 0x20000200, 0x20002000, 0x08000000, 0x08020000, 4, p_1k, 0x1FFFF800, + 0x1FFFF80F, 0x1FFFF000, 0x1FFFF800, 0}, + {0x428, "STM32F10xxx High-density VL", 0x20000200, 0x20008000, 0x08000000, 0x08080000, 2, p_2k, 0x1FFFF800, + 0x1FFFF80F, 0x1FFFF000, 0x1FFFF800, 0}, + {0x418, "STM32F105xx/F107xx", 0x20001000, 0x20010000, 0x08000000, 0x08040000, 2, p_2k, 0x1FFFF800, 0x1FFFF80F, + 0x1FFFB000, 0x1FFFF800, 0}, + {0x430, "STM32F10xxx XL-density", 0x20000800, 0x20018000, 0x08000000, 0x08100000, 2, p_2k, 0x1FFFF800, 0x1FFFF80F, + 0x1FFFE000, 0x1FFFF800, 0}, + /* F2 */ + {0x411, "STM32F2xxxx", 0x20002000, 0x20020000, 0x08000000, 0x08100000, 1, f2f4, 0x1FFFC000, 0x1FFFC00F, 0x1FFF0000, + 0x1FFF7800, 0}, + /* F3 */ + {0x432, "STM32F373xx/F378xx", 0x20001400, 0x20008000, 0x08000000, 0x08040000, 2, p_2k, 0x1FFFF800, 0x1FFFF80F, + 0x1FFFD800, 0x1FFFF800, 0}, + {0x422, "STM32F302xB(C)/F303xB(C)/F358xx", 0x20001400, 0x2000A000, 0x08000000, 0x08040000, 2, p_2k, 0x1FFFF800, + 0x1FFFF80F, 0x1FFFD800, 0x1FFFF800, 0}, + {0x439, "STM32F301xx/F302x4(6/8)/F318xx", 0x20001800, 0x20004000, 0x08000000, 0x08010000, 2, p_2k, 0x1FFFF800, + 0x1FFFF80F, 0x1FFFD800, 0x1FFFF800, 0}, + {0x438, "STM32F303x4(6/8)/F334xx/F328xx", 0x20001800, 0x20003000, 0x08000000, 0x08010000, 2, p_2k, 0x1FFFF800, + 0x1FFFF80F, 0x1FFFD800, 0x1FFFF800, 0}, + {0x446, "STM32F302xD(E)/F303xD(E)/F398xx", 0x20001800, 0x20010000, 0x08000000, 0x08080000, 2, p_2k, 0x1FFFF800, + 0x1FFFF80F, 0x1FFFD800, 0x1FFFF800, 0}, + /* F4 */ + {0x413, "STM32F40xxx/41xxx", 0x20003000, 0x20020000, 0x08000000, 0x08100000, 1, f2f4, 0x1FFFC000, 0x1FFFC00F, + 0x1FFF0000, 0x1FFF7800, 0}, + {0x419, "STM32F42xxx/43xxx", 0x20003000, 0x20030000, 0x08000000, 0x08200000, 1, f4db, 0x1FFEC000, 0x1FFFC00F, + 0x1FFF0000, 0x1FFF7800, 0}, + {0x423, "STM32F401xB(C)", 0x20003000, 0x20010000, 0x08000000, 0x08040000, 1, f2f4, 0x1FFFC000, 0x1FFFC00F, + 0x1FFF0000, 0x1FFF7800, 0}, + {0x433, "STM32F401xD(E)", 0x20003000, 0x20018000, 0x08000000, 0x08080000, 1, f2f4, 0x1FFFC000, 0x1FFFC00F, + 0x1FFF0000, 0x1FFF7800, 0}, + {0x458, "STM32F410xx", 0x20003000, 0x20008000, 0x08000000, 0x08020000, 1, f2f4, 0x1FFFC000, 0x1FFFC00F, 0x1FFF0000, + 0x1FFF7800, 0}, + {0x431, "STM32F411xx", 0x20003000, 0x20020000, 0x08000000, 0x08080000, 1, f2f4, 0x1FFFC000, 0x1FFFC00F, 0x1FFF0000, + 0x1FFF7800, 0}, + {0x421, "STM32F446xx", 0x20003000, 0x20020000, 0x08000000, 0x08080000, 1, f2f4, 0x1FFFC000, 0x1FFFC00F, 0x1FFF0000, + 0x1FFF7800, 0}, + {0x434, "STM32F469xx", 0x20003000, 0x20060000, 0x08000000, 0x08200000, 1, f4db, 0x1FFEC000, 0x1FFFC00F, 0x1FFF0000, + 0x1FFF7800, 0}, + /* F7 */ + {0x449, "STM32F74xxx/75xxx", 0x20004000, 0x20050000, 0x08000000, 0x08100000, 1, f7, 0x1FFF0000, 0x1FFF001F, + 0x1FF00000, 0x1FF0EDC0, 0}, + /* L0 */ + {0x425, "STM32L031xx/041xx", 0x20001000, 0x20002000, 0x08000000, 0x08008000, 32, p_128, 0x1FF80000, 0x1FF8001F, + 0x1FF00000, 0x1FF01000, 0}, + {0x417, "STM32L05xxx/06xxx", 0x20001000, 0x20002000, 0x08000000, 0x08010000, 32, p_128, 0x1FF80000, 0x1FF8001F, + 0x1FF00000, 0x1FF01000, 0}, + {0x447, "STM32L07xxx/08xxx", 0x20002000, 0x20005000, 0x08000000, 0x08030000, 32, p_128, 0x1FF80000, 0x1FF8001F, + 0x1FF00000, 0x1FF02000, 0}, + /* L1 */ + {0x416, "STM32L1xxx6(8/B)", 0x20000800, 0x20004000, 0x08000000, 0x08020000, 16, p_256, 0x1FF80000, 0x1FF8001F, + 0x1FF00000, 0x1FF01000, F_NO_ME}, + {0x429, "STM32L1xxx6(8/B)A", 0x20001000, 0x20008000, 0x08000000, 0x08020000, 16, p_256, 0x1FF80000, 0x1FF8001F, + 0x1FF00000, 0x1FF01000, 0}, + {0x427, "STM32L1xxxC", 0x20001000, 0x20008000, 0x08000000, 0x08040000, 16, p_256, 0x1FF80000, 0x1FF8001F, + 0x1FF00000, 0x1FF02000, 0}, + {0x436, "STM32L1xxxD", 0x20001000, 0x2000C000, 0x08000000, 0x08060000, 16, p_256, 0x1FF80000, 0x1FF8009F, + 0x1FF00000, 0x1FF02000, 0}, + {0x437, "STM32L1xxxE", 0x20001000, 0x20014000, 0x08000000, 0x08080000, 16, p_256, 0x1FF80000, 0x1FF8009F, + 0x1FF00000, 0x1FF02000, F_NO_ME}, + /* L4 */ + {0x415, "STM32L476xx/486xx", 0x20003100, 0x20018000, 0x08000000, 0x08100000, 1, p_2k, 0x1FFF7800, 0x1FFFF80F, + 0x1FFF0000, 0x1FFF7000, 0}, + /* These are not (yet) in AN2606: */ + {0x641, "Medium_Density PL", 0x20000200, 0x20005000, 0x08000000, 0x08020000, 4, p_1k, 0x1FFFF800, 0x1FFFF80F, + 0x1FFFF000, 0x1FFFF800, 0}, + {0x9a8, "STM32W-128K", 0x20000200, 0x20002000, 0x08000000, 0x08020000, 4, p_1k, 0x08040800, 0x0804080F, 0x08040000, + 0x08040800, 0}, + {0x9b0, "STM32W-256K", 0x20000200, 0x20004000, 0x08000000, 0x08040000, 4, p_2k, 0x08040800, 0x0804080F, 0x08040000, + 0x08040800, 0}, + {0x0, "", 0x0, 0x0, 0x0, 0x0, 0x0, nullptr, 0x0, 0x0, 0x0, 0x0, 0x0}, +}; + +} // namespace shelly_dimmer +} // namespace esphome +#endif diff --git a/esphome/components/shelly_dimmer/light.py b/esphome/components/shelly_dimmer/light.py new file mode 100644 index 0000000000..003498c090 --- /dev/null +++ b/esphome/components/shelly_dimmer/light.py @@ -0,0 +1,219 @@ +from pathlib import Path +import hashlib +import re +import requests + + +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import light, sensor, uart +from esphome.const import ( + CONF_OUTPUT_ID, + CONF_GAMMA_CORRECT, + CONF_POWER, + CONF_VOLTAGE, + CONF_CURRENT, + CONF_VERSION, + CONF_URL, + CONF_UPDATE_INTERVAL, + UNIT_VOLT, + UNIT_AMPERE, + UNIT_WATT, + DEVICE_CLASS_POWER, + DEVICE_CLASS_VOLTAGE, +) +from esphome.core import HexInt, CORE + +DOMAIN = "shelly_dimmer" +DEPENDENCIES = ["sensor", "uart"] + +shelly_dimmer_ns = cg.esphome_ns.namespace("shelly_dimmer") +ShellyDimmer = shelly_dimmer_ns.class_( + "ShellyDimmer", light.LightOutput, cg.PollingComponent, uart.UARTDevice +) + +CONF_FIRMWARE = "firmware" +CONF_SHA256 = "sha256" +CONF_UPDATE = "update" + +CONF_LEADING_EDGE = "leading_edge" +CONF_WARMUP_BRIGHTNESS = "warmup_brightness" +# CONF_WARMUP_TIME = "warmup_time" +CONF_MIN_BRIGHTNESS = "min_brightness" +CONF_MAX_BRIGHTNESS = "max_brightness" + +CONF_NRST_PIN = "nrst_pin" +CONF_BOOT0_PIN = "boot0_pin" + +KNOWN_FIRMWARE = { + "51.5": ( + "https://github.com/jamesturton/shelly-dimmer-stm32/releases/download/v51.5/shelly-dimmer-stm32_v51.5.bin", + "553fc1d78ed113227af7683eaa9c26189a961c4ea9a48000fb5aa8f8ac5d7b60", + ), + "51.6": ( + "https://github.com/jamesturton/shelly-dimmer-stm32/releases/download/v51.6/shelly-dimmer-stm32_v51.6.bin", + "eda483e111c914723a33f5088f1397d5c0b19333db4a88dc965636b976c16c36", + ), +} + + +def parse_firmware_version(value): + match = re.match(r"(\d+).(\d+)", value) + if match is None: + raise ValueError(f"Not a valid version number {value}") + major = int(match[1]) + minor = int(match[2]) + return major, minor + + +def get_firmware(value): + if not value[CONF_UPDATE]: + return None + + def dl(url): + try: + req = requests.get(url) + req.raise_for_status() + except requests.exceptions.RequestException as e: + raise cv.Invalid(f"Could not download firmware file ({url}): {e}") + + h = hashlib.new("sha256") + h.update(req.content) + return req.content, h.hexdigest() + + url = value[CONF_URL] + + if CONF_SHA256 in value: # we have a hash, enable caching + path = ( + Path(CORE.config_dir) + / ".esphome" + / DOMAIN + / (value[CONF_SHA256] + "_fw_stm.bin") + ) + + if not path.is_file(): + firmware_data, dl_hash = dl(url) + + if dl_hash != value[CONF_SHA256]: + raise cv.Invalid( + f"Hash mismatch for {url}: {dl_hash} != {value[CONF_SHA256]}" + ) + + path.parent.mkdir(exist_ok=True, parents=True) + path.write_bytes(firmware_data) + + else: + firmware_data = path.read_bytes() + else: # no caching, download every time + firmware_data, dl_hash = dl(url) + + return [HexInt(x) for x in firmware_data] + + +def validate_firmware(value): + config = value.copy() + if CONF_URL not in config: + try: + config[CONF_URL], config[CONF_SHA256] = KNOWN_FIRMWARE[config[CONF_VERSION]] + except KeyError as e: + raise cv.Invalid( + f"Firmware {config[CONF_VERSION]} is unknown, please specify an '{CONF_URL}' ..." + ) from e + get_firmware(config) + return config + + +def validate_sha256(value): + value = cv.string(value) + if not value.isalnum() or not len(value) == 64: + raise ValueError(f"Not a valid SHA256 hex string: {value}") + return value + + +def validate_version(value): + parse_firmware_version(value) + return value + + +CONFIG_SCHEMA = ( + light.BRIGHTNESS_ONLY_LIGHT_SCHEMA.extend( + { + cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(ShellyDimmer), + cv.Optional(CONF_FIRMWARE, default="51.6"): cv.maybe_simple_value( + { + cv.Optional(CONF_URL): cv.url, + cv.Optional(CONF_SHA256): validate_sha256, + cv.Required(CONF_VERSION): validate_version, + cv.Optional(CONF_UPDATE, default=False): cv.boolean, + }, + validate_firmware, # converts a simple version key to generate the full url + key=CONF_VERSION, + ), + cv.Optional(CONF_NRST_PIN, default="GPIO5"): pins.gpio_output_pin_schema, + cv.Optional(CONF_BOOT0_PIN, default="GPIO4"): pins.gpio_output_pin_schema, + cv.Optional(CONF_LEADING_EDGE, default=False): cv.boolean, + cv.Optional(CONF_WARMUP_BRIGHTNESS, default=100): cv.uint16_t, + # cv.Optional(CONF_WARMUP_TIME, default=20): cv.uint16_t, + cv.Optional(CONF_MIN_BRIGHTNESS, default=0): cv.uint16_t, + cv.Optional(CONF_MAX_BRIGHTNESS, default=1000): cv.uint16_t, + cv.Optional(CONF_POWER): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_POWER, + ), + cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + ), + cv.Optional(CONF_CURRENT): sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + device_class=DEVICE_CLASS_POWER, + accuracy_decimals=2, + ), + # Change the default gamma_correct setting. + cv.Optional(CONF_GAMMA_CORRECT, default=1.0): cv.positive_float, + } + ) + .extend(cv.polling_component_schema("10s")) + .extend(uart.UART_DEVICE_SCHEMA) +) + + +def to_code(config): + fw_hex = get_firmware(config[CONF_FIRMWARE]) + fw_major, fw_minor = parse_firmware_version(config[CONF_FIRMWARE][CONF_VERSION]) + + if fw_hex is not None: + cg.add_define("USE_SHD_FIRMWARE_DATA", fw_hex) + cg.add_define("USE_SHD_FIRMWARE_MAJOR_VERSION", fw_major) + cg.add_define("USE_SHD_FIRMWARE_MINOR_VERSION", fw_minor) + + var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) + yield cg.register_component(var, config) + config.pop( + CONF_UPDATE_INTERVAL + ) # drop UPDATE_INTERVAL as it does not apply to the light component + + yield light.register_light(var, config) + yield uart.register_uart_device(var, config) + + nrst_pin = yield cg.gpio_pin_expression(config[CONF_NRST_PIN]) + cg.add(var.set_nrst_pin(nrst_pin)) + boot0_pin = yield cg.gpio_pin_expression(config[CONF_BOOT0_PIN]) + cg.add(var.set_boot0_pin(boot0_pin)) + + cg.add(var.set_leading_edge(config[CONF_LEADING_EDGE])) + cg.add(var.set_warmup_brightness(config[CONF_WARMUP_BRIGHTNESS])) + # cg.add(var.set_warmup_time(config[CONF_WARMUP_TIME])) + cg.add(var.set_min_brightness(config[CONF_MIN_BRIGHTNESS])) + cg.add(var.set_max_brightness(config[CONF_MAX_BRIGHTNESS])) + + for key in [CONF_POWER, CONF_VOLTAGE, CONF_CURRENT]: + if key not in config: + continue + + conf = config[key] + sens = yield sensor.new_sensor(conf) + cg.add(getattr(var, f"set_{key}_sensor")(sens)) diff --git a/esphome/components/shelly_dimmer/shelly_dimmer.cpp b/esphome/components/shelly_dimmer/shelly_dimmer.cpp new file mode 100644 index 0000000000..3b79d0bf57 --- /dev/null +++ b/esphome/components/shelly_dimmer/shelly_dimmer.cpp @@ -0,0 +1,526 @@ +#include "esphome/core/defines.h" +#include "esphome/core/helpers.h" + +#include "shelly_dimmer.h" +#ifdef USE_SHD_FIRMWARE_DATA +#include "stm32flash.h" +#endif + +#ifndef USE_ESP_IDF +#include +#endif + +#include +#include +#include +#include + +namespace { + +constexpr char TAG[] = "shelly_dimmer"; + +constexpr uint8_t SHELLY_DIMMER_ACK_TIMEOUT = 200; // ms +constexpr uint8_t SHELLY_DIMMER_MAX_RETRIES = 3; +constexpr uint16_t SHELLY_DIMMER_MAX_BRIGHTNESS = 1000; // 100% + +// Protocol framing. +constexpr uint8_t SHELLY_DIMMER_PROTO_START_BYTE = 0x01; +constexpr uint8_t SHELLY_DIMMER_PROTO_END_BYTE = 0x04; + +// Supported commands. +constexpr uint8_t SHELLY_DIMMER_PROTO_CMD_SWITCH = 0x01; +constexpr uint8_t SHELLY_DIMMER_PROTO_CMD_POLL = 0x10; +constexpr uint8_t SHELLY_DIMMER_PROTO_CMD_VERSION = 0x11; +constexpr uint8_t SHELLY_DIMMER_PROTO_CMD_SETTINGS = 0x20; + +// Command payload sizes. +constexpr uint8_t SHELLY_DIMMER_PROTO_CMD_SWITCH_SIZE = 2; +constexpr uint8_t SHELLY_DIMMER_PROTO_CMD_SETTINGS_SIZE = 10; +constexpr uint8_t SHELLY_DIMMER_PROTO_MAX_FRAME_SIZE = 4 + 72 + 3; + +// STM Firmware +#ifdef USE_SHD_FIRMWARE_DATA +constexpr uint8_t STM_FIRMWARE[] PROGMEM = USE_SHD_FIRMWARE_DATA; +constexpr uint32_t STM_FIRMWARE_SIZE_IN_BYTES = sizeof(STM_FIRMWARE); +#endif + +// Scaling Constants +constexpr float POWER_SCALING_FACTOR = 880373; +constexpr float VOLTAGE_SCALING_FACTOR = 347800; +constexpr float CURRENT_SCALING_FACTOR = 1448; + +// Esentially std::size() for pre c++17 +template constexpr size_t size(const T (&/*unused*/)[N]) noexcept { return N; } + +} // Anonymous namespace + +namespace esphome { +namespace shelly_dimmer { + +/// Computes a crappy checksum as defined by the Shelly Dimmer protocol. +uint16_t shelly_dimmer_checksum(const uint8_t *buf, int len) { + return std::accumulate(buf, buf + len, 0); +} + +void ShellyDimmer::setup() { + this->pin_nrst_->setup(); + this->pin_boot0_->setup(); + + ESP_LOGI(TAG, "Initializing Shelly Dimmer..."); + + // Reset the STM32 and check the firmware version. + for (int i = 0; i < 2; i++) { + this->reset_normal_boot_(); + this->send_command_(SHELLY_DIMMER_PROTO_CMD_VERSION, nullptr, 0); + ESP_LOGI(TAG, "STM32 current firmware version: %d.%d, desired version: %d.%d", this->version_major_, + this->version_minor_, USE_SHD_FIRMWARE_MAJOR_VERSION, USE_SHD_FIRMWARE_MINOR_VERSION); + if (this->version_major_ != USE_SHD_FIRMWARE_MAJOR_VERSION || + this->version_minor_ != USE_SHD_FIRMWARE_MINOR_VERSION) { +#ifdef USE_SHD_FIRMWARE_DATA + // Update firmware if needed. + ESP_LOGW(TAG, "Unsupported STM32 firmware version, flashing"); + if (i > 0) { + // Upgrade was already performed but the reported version is still not right. + ESP_LOGE(TAG, "STM32 firmware upgrade already performed, but version is still incorrect"); + this->mark_failed(); + return; + } + + if (!this->upgrade_firmware_()) { + ESP_LOGW(TAG, "Failed to upgrade firmware"); + this->mark_failed(); + return; + } + + // Firmware upgrade completed, do the checks again. + continue; +#else + ESP_LOGW(TAG, "Firmware version mismatch, put 'update: true' in the yaml to flash an update."); + this->mark_failed(); + return; +#endif + } + break; + } + + this->send_settings_(); + // Do an immediate poll to refresh current state. + this->send_command_(SHELLY_DIMMER_PROTO_CMD_POLL, nullptr, 0); + + this->ready_ = true; +} + +void ShellyDimmer::update() { this->send_command_(SHELLY_DIMMER_PROTO_CMD_POLL, nullptr, 0); } + +void ShellyDimmer::dump_config() { + ESP_LOGCONFIG(TAG, "ShellyDimmer:"); + LOG_PIN(" NRST Pin: ", this->pin_nrst_); + LOG_PIN(" BOOT0 Pin: ", this->pin_boot0_); + + ESP_LOGCONFIG(TAG, " Leading Edge: %s", YESNO(this->leading_edge_)); + ESP_LOGCONFIG(TAG, " Warmup Brightness: %d", this->warmup_brightness_); + // ESP_LOGCONFIG(TAG, " Warmup Time: %d", this->warmup_time_); + // ESP_LOGCONFIG(TAG, " Fade Rate: %d", this->fade_rate_); + ESP_LOGCONFIG(TAG, " Minimum Brightness: %d", this->min_brightness_); + ESP_LOGCONFIG(TAG, " Maximum Brightness: %d", this->max_brightness_); + + LOG_UPDATE_INTERVAL(this); + + ESP_LOGCONFIG(TAG, " STM32 current firmware version: %d.%d ", this->version_major_, this->version_minor_); + ESP_LOGCONFIG(TAG, " STM32 required firmware version: %d.%d", USE_SHD_FIRMWARE_MAJOR_VERSION, + USE_SHD_FIRMWARE_MINOR_VERSION); + + if (this->version_major_ != USE_SHD_FIRMWARE_MAJOR_VERSION || + this->version_minor_ != USE_SHD_FIRMWARE_MINOR_VERSION) { + ESP_LOGE(TAG, " Firmware version mismatch, put 'update: true' in the yaml to flash an update."); + } +} + +void ShellyDimmer::write_state(light::LightState *state) { + if (!this->ready_) { + return; + } + + float brightness; + state->current_values_as_brightness(&brightness); + + const uint16_t brightness_int = this->convert_brightness_(brightness); + if (brightness_int == this->brightness_) { + ESP_LOGV(TAG, "Not sending unchanged value"); + return; + } + ESP_LOGD(TAG, "Brightness update: %d (raw: %f)", brightness_int, brightness); + + this->send_brightness_(brightness_int); +} +#ifdef USE_SHD_FIRMWARE_DATA +bool ShellyDimmer::upgrade_firmware_() { + ESP_LOGW(TAG, "Starting STM32 firmware upgrade"); + this->reset_dfu_boot_(); + + // Could be constexpr in c++17 + static const auto CLOSE = [](stm32_t *stm32) { stm32_close(stm32); }; + + // Cleanup with RAII + std::unique_ptr stm32{stm32_init(this, STREAM_SERIAL, 1), CLOSE}; + + if (!stm32) { + ESP_LOGW(TAG, "Failed to initialize STM32"); + return false; + } + + // Erase STM32 flash. + if (stm32_erase_memory(stm32.get(), 0, STM32_MASS_ERASE) != STM32_ERR_OK) { + ESP_LOGW(TAG, "Failed to erase STM32 flash memory"); + return false; + } + + static constexpr uint32_t BUFFER_SIZE = 256; + + // Copy the STM32 firmware over in 256-byte chunks. Note that the firmware is stored + // in flash memory so all accesses need to be 4-byte aligned. + uint8_t buffer[BUFFER_SIZE]; + const uint8_t *p = STM_FIRMWARE; + uint32_t offset = 0; + uint32_t addr = stm32->dev->fl_start; + const uint32_t end = addr + STM_FIRMWARE_SIZE_IN_BYTES; + + while (addr < end && offset < STM_FIRMWARE_SIZE_IN_BYTES) { + const uint32_t left_of_buffer = std::min(end - addr, BUFFER_SIZE); + const uint32_t len = std::min(left_of_buffer, STM_FIRMWARE_SIZE_IN_BYTES - offset); + + if (len == 0) { + break; + } + + std::memcpy(buffer, p, BUFFER_SIZE); + p += BUFFER_SIZE; + + if (stm32_write_memory(stm32.get(), addr, buffer, len) != STM32_ERR_OK) { + ESP_LOGW(TAG, "Failed to write to STM32 flash memory"); + return false; + } + + addr += len; + offset += len; + } + + ESP_LOGI(TAG, "STM32 firmware upgrade successful"); + + return true; +} +#endif + +uint16_t ShellyDimmer::convert_brightness_(float brightness) { + // Special case for zero as only zero means turn off completely. + if (brightness == 0.0) { + return 0; + } + + return remap(brightness, 0.0f, 1.0f, this->min_brightness_, this->max_brightness_); +} + +void ShellyDimmer::send_brightness_(uint16_t brightness) { + const uint8_t payload[] = { + // Brightness (%) * 10. + static_cast(brightness & 0xff), + static_cast(brightness >> 8), + }; + static_assert(size(payload) == SHELLY_DIMMER_PROTO_CMD_SWITCH_SIZE, "Invalid payload size"); + + this->send_command_(SHELLY_DIMMER_PROTO_CMD_SWITCH, payload, SHELLY_DIMMER_PROTO_CMD_SWITCH_SIZE); + + this->brightness_ = brightness; +} + +void ShellyDimmer::send_settings_() { + const uint16_t fade_rate = std::min(uint16_t{100}, this->fade_rate_); + + float brightness = 0.0; + if (this->state_ != nullptr) { + this->state_->current_values_as_brightness(&brightness); + } + const uint16_t brightness_int = this->convert_brightness_(brightness); + ESP_LOGD(TAG, "Brightness update: %d (raw: %f)", brightness_int, brightness); + + const uint8_t payload[] = { + // Brightness (%) * 10. + static_cast(brightness_int & 0xff), + static_cast(brightness_int >> 8), + // Leading / trailing edge [0x01 = leading, 0x02 = trailing]. + this->leading_edge_ ? uint8_t{0x01} : uint8_t{0x02}, + 0x00, + // Fade rate. + static_cast(fade_rate & 0xff), + static_cast(fade_rate >> 8), + // Warmup brightness. + static_cast(this->warmup_brightness_ & 0xff), + static_cast(this->warmup_brightness_ >> 8), + // Warmup time. + static_cast(this->warmup_time_ & 0xff), + static_cast(this->warmup_time_ >> 8), + }; + static_assert(size(payload) == SHELLY_DIMMER_PROTO_CMD_SETTINGS_SIZE, "Invalid payload size"); + + this->send_command_(SHELLY_DIMMER_PROTO_CMD_SETTINGS, payload, SHELLY_DIMMER_PROTO_CMD_SETTINGS_SIZE); + + // Also send brightness separately as it is ignored above. + this->send_brightness_(brightness_int); +} + +bool ShellyDimmer::send_command_(uint8_t cmd, const uint8_t *const payload, uint8_t len) { + ESP_LOGD(TAG, "Sending command: 0x%02x (%d bytes) payload 0x%s", cmd, len, format_hex(payload, len).c_str()); + + // Prepare a command frame. + uint8_t frame[SHELLY_DIMMER_PROTO_MAX_FRAME_SIZE]; + const size_t frame_len = this->frame_command_(frame, cmd, payload, len); + + // Write the frame and wait for acknowledgement. + int retries = SHELLY_DIMMER_MAX_RETRIES; + while (retries--) { + this->write_array(frame, frame_len); + this->flush(); + + ESP_LOGD(TAG, "Command sent, waiting for reply"); + const uint32_t tx_time = millis(); + while (millis() - tx_time < SHELLY_DIMMER_ACK_TIMEOUT) { + if (this->read_frame_()) { + return true; + } + delay(1); + } + ESP_LOGW(TAG, "Timeout while waiting for reply"); + } + ESP_LOGW(TAG, "Failed to send command"); + return false; +} + +size_t ShellyDimmer::frame_command_(uint8_t *data, uint8_t cmd, const uint8_t *const payload, size_t len) { + size_t pos = 0; + + // Generate a frame. + data[0] = SHELLY_DIMMER_PROTO_START_BYTE; + data[1] = ++this->seq_; + data[2] = cmd; + data[3] = len; + pos += 4; + + if (payload != nullptr) { + std::memcpy(data + 4, payload, len); + pos += len; + } + + // Calculate checksum for the payload. + const uint16_t csum = shelly_dimmer_checksum(data + 1, 3 + len); + data[pos++] = static_cast(csum >> 8); + data[pos++] = static_cast(csum & 0xff); + data[pos++] = SHELLY_DIMMER_PROTO_END_BYTE; + return pos; +} + +int ShellyDimmer::handle_byte_(uint8_t c) { + const uint8_t pos = this->buffer_pos_; + + if (pos == 0) { + // Must be start byte. + return c == SHELLY_DIMMER_PROTO_START_BYTE ? 1 : -1; + } else if (pos < 4) { + // Header. + return 1; + } + + // Decode payload length from header. + const uint8_t payload_len = this->buffer_[3]; + if ((4 + payload_len + 3) > SHELLY_DIMMER_BUFFER_SIZE) { + return -1; + } + + if (pos < 4 + payload_len + 1) { + // Payload. + return 1; + } + + if (pos == 4 + payload_len + 1) { + // Verify checksum. + const uint16_t csum = (this->buffer_[pos - 1] << 8 | c); + const uint16_t csum_verify = shelly_dimmer_checksum(&this->buffer_[1], 3 + payload_len); + if (csum != csum_verify) { + return -1; + } + return 1; + } + + if (pos == 4 + payload_len + 2) { + // Must be end byte. + return c == SHELLY_DIMMER_PROTO_END_BYTE ? 0 : -1; + } + return -1; +} + +bool ShellyDimmer::read_frame_() { + while (this->available()) { + const uint8_t c = this->read(); + this->buffer_[this->buffer_pos_] = c; + + ESP_LOGV(TAG, "Read byte: 0x%02x (pos %d)", c, this->buffer_pos_); + + switch (this->handle_byte_(c)) { + case 0: { + // Frame successfully received. + this->handle_frame_(); + this->buffer_pos_ = 0; + return true; + } + case -1: { + // Failure. + this->buffer_pos_ = 0; + break; + } + case 1: { + // Need more data. + this->buffer_pos_++; + break; + } + } + } + return false; +} + +bool ShellyDimmer::handle_frame_() { + const uint8_t seq = this->buffer_[1]; + const uint8_t cmd = this->buffer_[2]; + const uint8_t payload_len = this->buffer_[3]; + + ESP_LOGD(TAG, "Got frame: 0x%02x", cmd); + + // Compare with expected identifier as the frame is always a response to + // our previously sent command. + if (seq != this->seq_) { + return false; + } + + const uint8_t *payload = &this->buffer_[4]; + + // Handle response. + switch (cmd) { + case SHELLY_DIMMER_PROTO_CMD_POLL: { + if (payload_len < 16) { + return false; + } + + const uint8_t hw_version = payload[0]; + // payload[1] is unused. + const uint16_t brightness = encode_uint16(payload[3], payload[2]); + + const uint32_t power_raw = encode_uint32(payload[7], payload[6], payload[5], payload[4]); + + const uint32_t voltage_raw = encode_uint32(payload[11], payload[10], payload[9], payload[8]); + + const uint32_t current_raw = encode_uint32(payload[15], payload[14], payload[13], payload[12]); + + const uint16_t fade_rate = payload[16]; + + float power = 0; + if (power_raw > 0) { + power = POWER_SCALING_FACTOR / static_cast(power_raw); + } + + float voltage = 0; + if (voltage_raw > 0) { + voltage = VOLTAGE_SCALING_FACTOR / static_cast(voltage_raw); + } + + float current = 0; + if (current_raw > 0) { + current = CURRENT_SCALING_FACTOR / static_cast(current_raw); + } + + ESP_LOGI(TAG, "Got dimmer data:"); + ESP_LOGI(TAG, " HW version: %d", hw_version); + ESP_LOGI(TAG, " Brightness: %d", brightness); + ESP_LOGI(TAG, " Fade rate: %d", fade_rate); + ESP_LOGI(TAG, " Power: %f W", power); + ESP_LOGI(TAG, " Voltage: %f V", voltage); + ESP_LOGI(TAG, " Current: %f A", current); + + // Update sensors. + if (this->power_sensor_ != nullptr) { + this->power_sensor_->publish_state(power); + } + if (this->voltage_sensor_ != nullptr) { + this->voltage_sensor_->publish_state(voltage); + } + if (this->current_sensor_ != nullptr) { + this->current_sensor_->publish_state(current); + } + + return true; + } + case SHELLY_DIMMER_PROTO_CMD_VERSION: { + if (payload_len < 2) { + return false; + } + + this->version_minor_ = payload[0]; + this->version_major_ = payload[1]; + return true; + } + case SHELLY_DIMMER_PROTO_CMD_SWITCH: + case SHELLY_DIMMER_PROTO_CMD_SETTINGS: { + return !(payload_len < 1 || payload[0] != 0x01); + } + default: { + return false; + } + } +} + +void ShellyDimmer::reset_(bool boot0) { + ESP_LOGD(TAG, "Reset STM32, boot0=%d", boot0); + + this->pin_boot0_->digital_write(boot0); + this->pin_nrst_->digital_write(false); + + // Wait 50ms for the STM32 to reset. + delay(50); // NOLINT + + // Clear receive buffer. + while (this->available()) { + this->read(); + } + + this->pin_nrst_->digital_write(true); + // Wait 50ms for the STM32 to boot. + delay(50); // NOLINT + + ESP_LOGD(TAG, "Reset STM32 done"); +} + +void ShellyDimmer::reset_normal_boot_() { + // set NONE parity in normal mode + +#ifndef USE_ESP_IDF // workaround for reconfiguring the uart + Serial.end(); + Serial.begin(115200, SERIAL_8N1); + Serial.flush(); +#endif + + this->flush(); + this->reset_(false); +} + +void ShellyDimmer::reset_dfu_boot_() { + // set EVEN parity in bootloader mode + +#ifndef USE_ESP_IDF // workaround for reconfiguring the uart + Serial.end(); + Serial.begin(115200, SERIAL_8E1); + Serial.flush(); +#endif + + this->flush(); + this->reset_(true); +} + +} // namespace shelly_dimmer +} // namespace esphome diff --git a/esphome/components/shelly_dimmer/shelly_dimmer.h b/esphome/components/shelly_dimmer/shelly_dimmer.h new file mode 100644 index 0000000000..b7d476279e --- /dev/null +++ b/esphome/components/shelly_dimmer/shelly_dimmer.h @@ -0,0 +1,117 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/log.h" +#include "esphome/components/light/light_output.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/uart/uart.h" + +#include + +namespace esphome { +namespace shelly_dimmer { + +class ShellyDimmer : public PollingComponent, public light::LightOutput, public uart::UARTDevice { + private: + static constexpr uint16_t SHELLY_DIMMER_BUFFER_SIZE = 256; + + public: + float get_setup_priority() const override { return setup_priority::LATE; } + + void setup() override; + void update() override; + void dump_config() override; + + light::LightTraits get_traits() override { + auto traits = light::LightTraits(); + traits.set_supported_color_modes({light::ColorMode::BRIGHTNESS}); + return traits; + } + + void setup_state(light::LightState *state) override { this->state_ = state; } + void write_state(light::LightState *state) override; + + void set_nrst_pin(GPIOPin *nrst_pin) { this->pin_nrst_ = nrst_pin; } + void set_boot0_pin(GPIOPin *boot0_pin) { this->pin_boot0_ = boot0_pin; } + + void set_leading_edge(bool leading_edge) { this->leading_edge_ = leading_edge; } + void set_warmup_brightness(uint16_t warmup_brightness) { this->warmup_brightness_ = warmup_brightness; } + void set_warmup_time(uint16_t warmup_time) { this->warmup_time_ = warmup_time; } + void set_fade_rate(uint16_t fade_rate) { this->fade_rate_ = fade_rate; } + void set_min_brightness(uint16_t min_brightness) { this->min_brightness_ = min_brightness; } + void set_max_brightness(uint16_t max_brightness) { this->max_brightness_ = max_brightness; } + + void set_power_sensor(sensor::Sensor *power_sensor) { this->power_sensor_ = power_sensor; } + void set_voltage_sensor(sensor::Sensor *voltage_sensor) { this->voltage_sensor_ = voltage_sensor; } + void set_current_sensor(sensor::Sensor *current_sensor) { this->current_sensor_ = current_sensor; } + + protected: + GPIOPin *pin_nrst_; + GPIOPin *pin_boot0_; + + // Frame parser state. + uint8_t seq_{0}; + std::array buffer_; + uint8_t buffer_pos_{0}; + + // Firmware version. + uint8_t version_major_; + uint8_t version_minor_; + + // Configuration. + bool leading_edge_{false}; + uint16_t warmup_brightness_{100}; + uint16_t warmup_time_{20}; + uint16_t fade_rate_{0}; + uint16_t min_brightness_{0}; + uint16_t max_brightness_{1000}; + + light::LightState *state_{nullptr}; + sensor::Sensor *power_sensor_{nullptr}; + sensor::Sensor *voltage_sensor_{nullptr}; + sensor::Sensor *current_sensor_{nullptr}; + + bool ready_{false}; + uint16_t brightness_; + + /// Convert relative brightness into a dimmer brightness value. + uint16_t convert_brightness_(float brightness); + + /// Sends the given brightness value. + void send_brightness_(uint16_t brightness); + + /// Sends dimmer configuration. + void send_settings_(); + + /// Performs a firmware upgrade. + bool upgrade_firmware_(); + + /// Sends a command and waits for an acknowledgement. + bool send_command_(uint8_t cmd, const uint8_t *payload, uint8_t len); + + /// Frames a given command payload. + size_t frame_command_(uint8_t *data, uint8_t cmd, const uint8_t *payload, size_t len); + + /// Handles a single byte as part of a protocol frame. + /// + /// Returns -1 on failure, 0 when finished and 1 when more bytes needed. + int handle_byte_(uint8_t c); + + /// Reads a response frame. + bool read_frame_(); + + /// Handles a complete frame. + bool handle_frame_(); + + /// Reset STM32 with the BOOT0 pin set to the given value. + void reset_(bool boot0); + + /// Reset STM32 to boot the regular firmware. + void reset_normal_boot_(); + + /// Reset STM32 to boot into DFU mode to enable firmware upgrades. + void reset_dfu_boot_(); +}; + +} // namespace shelly_dimmer +} // namespace esphome diff --git a/esphome/components/shelly_dimmer/stm32flash.cpp b/esphome/components/shelly_dimmer/stm32flash.cpp new file mode 100644 index 0000000000..4c777776fb --- /dev/null +++ b/esphome/components/shelly_dimmer/stm32flash.cpp @@ -0,0 +1,1061 @@ +/* + stm32flash - Open Source ST STM32 flash program for Arduino + Copyright 2010 Geoffrey McRae + Copyright 2012-2014 Tormod Volden + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +*/ + +#include "esphome/core/defines.h" +#ifdef USE_SHD_FIRMWARE_DATA + +#include + +#include "stm32flash.h" +#include "debug.h" + +#include "dev_table.h" +#include "esphome/core/log.h" + +#include +#include + +namespace { + +constexpr uint8_t STM32_ACK = 0x79; +constexpr uint8_t STM32_NACK = 0x1F; +constexpr uint8_t STM32_BUSY = 0x76; + +constexpr uint8_t STM32_CMD_INIT = 0x7F; +constexpr uint8_t STM32_CMD_GET = 0x00; /* get the version and command supported */ +constexpr uint8_t STM32_CMD_GVR = 0x01; /* get version and read protection status */ +constexpr uint8_t STM32_CMD_GID = 0x02; /* get ID */ +constexpr uint8_t STM32_CMD_RM = 0x11; /* read memory */ +constexpr uint8_t STM32_CMD_GO = 0x21; /* go */ +constexpr uint8_t STM32_CMD_WM = 0x31; /* write memory */ +constexpr uint8_t STM32_CMD_WM_NS = 0x32; /* no-stretch write memory */ +constexpr uint8_t STM32_CMD_ER = 0x43; /* erase */ +constexpr uint8_t STM32_CMD_EE = 0x44; /* extended erase */ +constexpr uint8_t STM32_CMD_EE_NS = 0x45; /* extended erase no-stretch */ +constexpr uint8_t STM32_CMD_WP = 0x63; /* write protect */ +constexpr uint8_t STM32_CMD_WP_NS = 0x64; /* write protect no-stretch */ +constexpr uint8_t STM32_CMD_UW = 0x73; /* write unprotect */ +constexpr uint8_t STM32_CMD_UW_NS = 0x74; /* write unprotect no-stretch */ +constexpr uint8_t STM32_CMD_RP = 0x82; /* readout protect */ +constexpr uint8_t STM32_CMD_RP_NS = 0x83; /* readout protect no-stretch */ +constexpr uint8_t STM32_CMD_UR = 0x92; /* readout unprotect */ +constexpr uint8_t STM32_CMD_UR_NS = 0x93; /* readout unprotect no-stretch */ +constexpr uint8_t STM32_CMD_CRC = 0xA1; /* compute CRC */ +constexpr uint8_t STM32_CMD_ERR = 0xFF; /* not a valid command */ + +constexpr uint32_t STM32_RESYNC_TIMEOUT = 35 * 1000; /* milliseconds */ +constexpr uint32_t STM32_MASSERASE_TIMEOUT = 35 * 1000; /* milliseconds */ +constexpr uint32_t STM32_PAGEERASE_TIMEOUT = 5 * 1000; /* milliseconds */ +constexpr uint32_t STM32_BLKWRITE_TIMEOUT = 1 * 1000; /* milliseconds */ +constexpr uint32_t STM32_WUNPROT_TIMEOUT = 1 * 1000; /* milliseconds */ +constexpr uint32_t STM32_WPROT_TIMEOUT = 1 * 1000; /* milliseconds */ +constexpr uint32_t STM32_RPROT_TIMEOUT = 1 * 1000; /* milliseconds */ +constexpr uint32_t DEFAULT_TIMEOUT = 5 * 1000; /* milliseconds */ + +constexpr uint8_t STM32_CMD_GET_LENGTH = 17; /* bytes in the reply */ + +/* Reset code for ARMv7-M (Cortex-M3) and ARMv6-M (Cortex-M0) + * see ARMv7-M or ARMv6-M Architecture Reference Manual (table B3-8) + * or "The definitive guide to the ARM Cortex-M3", section 14.4. + */ +constexpr uint8_t STM_RESET_CODE[] = { + 0x01, 0x49, // ldr r1, [pc, #4] ; () + 0x02, 0x4A, // ldr r2, [pc, #8] ; () + 0x0A, 0x60, // str r2, [r1, #0] + 0xfe, 0xe7, // endless: b endless + 0x0c, 0xed, 0x00, 0xe0, // .word 0xe000ed0c = NVIC AIRCR register address + 0x04, 0x00, 0xfa, 0x05 // .word 0x05fa0004 = VECTKEY | SYSRESETREQ +}; + +constexpr uint32_t STM_RESET_CODE_SIZE = sizeof(STM_RESET_CODE); + +/* RM0360, Empty check + * On STM32F070x6 and STM32F030xC devices only, internal empty check flag is + * implemented to allow easy programming of the virgin devices by the boot loader. This flag is + * used when BOOT0 pin is defining Main Flash memory as the target boot space. When the + * flag is set, the device is considered as empty and System memory (boot loader) is selected + * instead of the Main Flash as a boot space to allow user to program the Flash memory. + * This flag is updated only during Option bytes loading: it is set when the content of the + * address 0x08000 0000 is read as 0xFFFF FFFF, otherwise it is cleared. It means a power + * on or setting of OBL_LAUNCH bit in FLASH_CR register is needed to clear this flag after + * programming of a virgin device to execute user code after System reset. + */ +constexpr uint8_t STM_OBL_LAUNCH_CODE[] = { + 0x01, 0x49, // ldr r1, [pc, #4] ; () + 0x02, 0x4A, // ldr r2, [pc, #8] ; () + 0x0A, 0x60, // str r2, [r1, #0] + 0xfe, 0xe7, // endless: b endless + 0x10, 0x20, 0x02, 0x40, // address: FLASH_CR = 40022010 + 0x00, 0x20, 0x00, 0x00 // value: OBL_LAUNCH = 00002000 +}; + +constexpr uint32_t STM_OBL_LAUNCH_CODE_SIZE = sizeof(STM_OBL_LAUNCH_CODE); + +constexpr char TAG[] = "stm32flash"; + +} // Anonymous namespace + +namespace esphome { +namespace shelly_dimmer { + +namespace { + +int flash_addr_to_page_ceil(const stm32_t *stm, uint32_t addr) { + if (!(addr >= stm->dev->fl_start && addr <= stm->dev->fl_end)) + return 0; + + int page = 0; + addr -= stm->dev->fl_start; + const auto *psize = stm->dev->fl_ps; + + while (addr >= psize[0]) { + addr -= psize[0]; + page++; + if (psize[1]) + psize++; + } + + return addr ? page + 1 : page; +} + +stm32_err_t stm32_get_ack_timeout(const stm32_t *stm, uint32_t timeout) { + auto *stream = stm->stream; + uint8_t rxbyte; + + if (!(stm->flags & STREAM_OPT_RETRY)) + timeout = 0; + + if (timeout == 0) + timeout = DEFAULT_TIMEOUT; + + const uint32_t start_time = millis(); + do { + yield(); + if (!stream->available()) { + if (millis() < start_time + timeout) + continue; + ESP_LOGD(TAG, "Failed to read ACK timeout=%i", timeout); + return STM32_ERR_UNKNOWN; + } + + stream->read_byte(&rxbyte); + + if (rxbyte == STM32_ACK) + return STM32_ERR_OK; + if (rxbyte == STM32_NACK) + return STM32_ERR_NACK; + if (rxbyte != STM32_BUSY) { + ESP_LOGD(TAG, "Got byte 0x%02x instead of ACK", rxbyte); + return STM32_ERR_UNKNOWN; + } + } while (true); +} + +stm32_err_t stm32_get_ack(const stm32_t *stm) { return stm32_get_ack_timeout(stm, 0); } + +stm32_err_t stm32_send_command_timeout(const stm32_t *stm, const uint8_t cmd, const uint32_t timeout) { + auto *const stream = stm->stream; + + static constexpr auto BUFFER_SIZE = 2; + const uint8_t buf[] = { + cmd, + static_cast(cmd ^ 0xFF), + }; + static_assert(sizeof(buf) == BUFFER_SIZE, "Buf expected to be 2 bytes"); + + stream->write_array(buf, BUFFER_SIZE); + stream->flush(); + + stm32_err_t s_err = stm32_get_ack_timeout(stm, timeout); + if (s_err == STM32_ERR_OK) + return STM32_ERR_OK; + if (s_err == STM32_ERR_NACK) { + ESP_LOGD(TAG, "Got NACK from device on command 0x%02x", cmd); + } else { + ESP_LOGD(TAG, "Unexpected reply from device on command 0x%02x", cmd); + } + return STM32_ERR_UNKNOWN; +} + +stm32_err_t stm32_send_command(const stm32_t *stm, const uint8_t cmd) { + return stm32_send_command_timeout(stm, cmd, 0); +} + +/* if we have lost sync, send a wrong command and expect a NACK */ +stm32_err_t stm32_resync(const stm32_t *stm) { + auto *const stream = stm->stream; + uint32_t t0 = millis(); + auto t1 = t0; + + static constexpr auto BUFFER_SIZE = 2; + const uint8_t buf[] = { + STM32_CMD_ERR, + static_cast(STM32_CMD_ERR ^ 0xFF), + }; + static_assert(sizeof(buf) == BUFFER_SIZE, "Buf expected to be 2 bytes"); + + uint8_t ack; + while (t1 < t0 + STM32_RESYNC_TIMEOUT) { + stream->write_array(buf, BUFFER_SIZE); + stream->flush(); + if (!stream->read_array(&ack, 1)) { + t1 = millis(); + continue; + } + if (ack == STM32_NACK) + return STM32_ERR_OK; + t1 = millis(); + } + return STM32_ERR_UNKNOWN; +} + +/* + * some command receive reply frame with variable length, and length is + * embedded in reply frame itself. + * We can guess the length, but if we guess wrong the protocol gets out + * of sync. + * Use resync for frame oriented interfaces (e.g. I2C) and byte-by-byte + * read for byte oriented interfaces (e.g. UART). + * + * to run safely, data buffer should be allocated for 256+1 bytes + * + * len is value of the first byte in the frame. + */ +stm32_err_t stm32_guess_len_cmd(const stm32_t *stm, const uint8_t cmd, uint8_t *const data, unsigned int len) { + auto *const stream = stm->stream; + + if (stm32_send_command(stm, cmd) != STM32_ERR_OK) + return STM32_ERR_UNKNOWN; + if (stm->flags & STREAM_OPT_BYTE) { + /* interface is UART-like */ + if (!stream->read_array(data, 1)) + return STM32_ERR_UNKNOWN; + len = data[0]; + if (!stream->read_array(data + 1, len + 1)) + return STM32_ERR_UNKNOWN; + return STM32_ERR_OK; + } + + const auto ret = stream->read_array(data, len + 2); + if (ret && len == data[0]) + return STM32_ERR_OK; + if (!ret) { + /* restart with only one byte */ + if (stm32_resync(stm) != STM32_ERR_OK) + return STM32_ERR_UNKNOWN; + if (stm32_send_command(stm, cmd) != STM32_ERR_OK) + return STM32_ERR_UNKNOWN; + if (!stream->read_array(data, 1)) + return STM32_ERR_UNKNOWN; + } + + ESP_LOGD(TAG, "Re sync (len = %d)", data[0]); + if (stm32_resync(stm) != STM32_ERR_OK) + return STM32_ERR_UNKNOWN; + + len = data[0]; + if (stm32_send_command(stm, cmd) != STM32_ERR_OK) + return STM32_ERR_UNKNOWN; + + if (!stream->read_array(data, len + 2)) + return STM32_ERR_UNKNOWN; + return STM32_ERR_OK; +} + +/* + * Some interface, e.g. UART, requires a specific init sequence to let STM32 + * autodetect the interface speed. + * The sequence is only required one time after reset. + * This function sends the init sequence and, in case of timeout, recovers + * the interface. + */ +stm32_err_t stm32_send_init_seq(const stm32_t *stm) { + auto *const stream = stm->stream; + + stream->write_array(&STM32_CMD_INIT, 1); + stream->flush(); + + uint8_t byte; + bool ret = stream->read_array(&byte, 1); + if (ret && byte == STM32_ACK) + return STM32_ERR_OK; + if (ret && byte == STM32_NACK) { + /* We could get error later, but let's continue, for now. */ + ESP_LOGD(TAG, "Warning: the interface was not closed properly."); + return STM32_ERR_OK; + } + if (!ret) { + ESP_LOGD(TAG, "Failed to init device."); + return STM32_ERR_UNKNOWN; + } + + /* + * Check if previous STM32_CMD_INIT was taken as first byte + * of a command. Send a new byte, we should get back a NACK. + */ + stream->write_array(&STM32_CMD_INIT, 1); + stream->flush(); + + ret = stream->read_array(&byte, 1); + if (ret && byte == STM32_NACK) + return STM32_ERR_OK; + ESP_LOGD(TAG, "Failed to init device."); + return STM32_ERR_UNKNOWN; +} + +stm32_err_t stm32_mass_erase(const stm32_t *stm) { + auto *const stream = stm->stream; + + if (stm32_send_command(stm, stm->cmd->er) != STM32_ERR_OK) { + ESP_LOGD(TAG, "Can't initiate chip mass erase!"); + return STM32_ERR_UNKNOWN; + } + + /* regular erase (0x43) */ + if (stm->cmd->er == STM32_CMD_ER) { + const auto s_err = stm32_send_command_timeout(stm, 0xFF, STM32_MASSERASE_TIMEOUT); + if (s_err != STM32_ERR_OK) { + return STM32_ERR_UNKNOWN; + } + return STM32_ERR_OK; + } + + /* extended erase */ + static constexpr auto BUFFER_SIZE = 3; + const uint8_t buf[] = { + 0xFF, /* 0xFFFF the magic number for mass erase */ + 0xFF, 0x00, /* checksum */ + }; + static_assert(sizeof(buf) == BUFFER_SIZE, "Expected the buffer to be 3 bytes"); + stream->write_array(buf, 3); + stream->flush(); + + const auto s_err = stm32_get_ack_timeout(stm, STM32_MASSERASE_TIMEOUT); + if (s_err != STM32_ERR_OK) { + ESP_LOGD(TAG, "Mass erase failed. Try specifying the number of pages to be erased."); + return STM32_ERR_UNKNOWN; + } + return STM32_ERR_OK; +} + +template std::unique_ptr malloc_array_raii(size_t size) { + // Could be constexpr in c++17 + static const auto DELETOR = [](T *memory) { + free(memory); // NOLINT + }; + return std::unique_ptr{static_cast(malloc(size)), // NOLINT + DELETOR}; +} + +stm32_err_t stm32_pages_erase(const stm32_t *stm, const uint32_t spage, const uint32_t pages) { + auto *const stream = stm->stream; + uint8_t cs = 0; + int i = 0; + + /* The erase command reported by the bootloader is either 0x43, 0x44 or 0x45 */ + /* 0x44 is Extended Erase, a 2 byte based protocol and needs to be handled differently. */ + /* 0x45 is clock no-stretching version of Extended Erase for I2C port. */ + if (stm32_send_command(stm, stm->cmd->er) != STM32_ERR_OK) { + ESP_LOGD(TAG, "Can't initiate chip mass erase!"); + return STM32_ERR_UNKNOWN; + } + + /* regular erase (0x43) */ + if (stm->cmd->er == STM32_CMD_ER) { + // Free memory with RAII + auto buf = malloc_array_raii(1 + pages + 1); + + if (!buf) + return STM32_ERR_UNKNOWN; + + buf[i++] = pages - 1; + cs ^= (pages - 1); + for (auto pg_num = spage; pg_num < (pages + spage); pg_num++) { + buf[i++] = pg_num; + cs ^= pg_num; + } + buf[i++] = cs; + stream->write_array(&buf[0], i); + stream->flush(); + + const auto s_err = stm32_get_ack_timeout(stm, pages * STM32_PAGEERASE_TIMEOUT); + if (s_err != STM32_ERR_OK) { + return STM32_ERR_UNKNOWN; + } + return STM32_ERR_OK; + } + + /* extended erase */ + + // Free memory with RAII + auto buf = malloc_array_raii(2 + 2 * pages + 1); + + if (!buf) + return STM32_ERR_UNKNOWN; + + /* Number of pages to be erased - 1, two bytes, MSB first */ + uint8_t pg_byte = (pages - 1) >> 8; + buf[i++] = pg_byte; + cs ^= pg_byte; + pg_byte = (pages - 1) & 0xFF; + buf[i++] = pg_byte; + cs ^= pg_byte; + + for (auto pg_num = spage; pg_num < spage + pages; pg_num++) { + pg_byte = pg_num >> 8; + cs ^= pg_byte; + buf[i++] = pg_byte; + pg_byte = pg_num & 0xFF; + cs ^= pg_byte; + buf[i++] = pg_byte; + } + buf[i++] = cs; + stream->write_array(&buf[0], i); + stream->flush(); + + const auto s_err = stm32_get_ack_timeout(stm, pages * STM32_PAGEERASE_TIMEOUT); + if (s_err != STM32_ERR_OK) { + ESP_LOGD(TAG, "Page-by-page erase failed. Check the maximum pages your device supports."); + return STM32_ERR_UNKNOWN; + } + + return STM32_ERR_OK; +} + +template stm32_err_t stm32_check_ack_timeout(const stm32_err_t s_err, const T &&log) { + switch (s_err) { + case STM32_ERR_OK: + return STM32_ERR_OK; + case STM32_ERR_NACK: + log(); + // TODO: c++17 [[fallthrough]] + /* fallthrough */ + default: + return STM32_ERR_UNKNOWN; + } +} + +/* detect CPU endian */ +bool cpu_le() { + static constexpr int N = 1; + + // returns true if little endian + return *reinterpret_cast(&N) == 1; +} + +uint32_t le_u32(const uint32_t v) { + if (!cpu_le()) + return ((v & 0xFF000000) >> 24) | ((v & 0x00FF0000) >> 8) | ((v & 0x0000FF00) << 8) | ((v & 0x000000FF) << 24); + return v; +} + +template void populate_buffer_with_address(uint8_t (&buffer)[N], uint32_t address) { + buffer[0] = static_cast(address >> 24); + buffer[1] = static_cast((address >> 16) & 0xFF); + buffer[2] = static_cast((address >> 8) & 0xFF); + buffer[3] = static_cast(address & 0xFF); + buffer[4] = static_cast(buffer[0] ^ buffer[1] ^ buffer[2] ^ buffer[3]); +} + +} // Anonymous namespace + +} // namespace shelly_dimmer +} // namespace esphome + +namespace esphome { +namespace shelly_dimmer { + +/* find newer command by higher code */ +#define newer(prev, a) (((prev) == STM32_CMD_ERR) ? (a) : (((prev) > (a)) ? (prev) : (a))) + +stm32_t *stm32_init(uart::UARTDevice *stream, const uint8_t flags, const char init) { + uint8_t buf[257]; + + // Could be constexpr in c++17 + static const auto CLOSE = [](stm32_t *stm32) { stm32_close(stm32); }; + + // Cleanup with RAII + std::unique_ptr stm{static_cast(calloc(sizeof(stm32_t), 1)), // NOLINT + CLOSE}; + + if (!stm) { + return nullptr; + } + stm->stream = stream; + stm->flags = flags; + + stm->cmd = static_cast(malloc(sizeof(stm32_cmd_t))); // NOLINT + if (!stm->cmd) { + return nullptr; + } + memset(stm->cmd, STM32_CMD_ERR, sizeof(stm32_cmd_t)); + + if ((stm->flags & STREAM_OPT_CMD_INIT) && init) { + if (stm32_send_init_seq(stm.get()) != STM32_ERR_OK) + return nullptr; // NOLINT + } + + /* get the version and read protection status */ + if (stm32_send_command(stm.get(), STM32_CMD_GVR) != STM32_ERR_OK) { + return nullptr; // NOLINT + } + + /* From AN, only UART bootloader returns 3 bytes */ + { + const auto len = (stm->flags & STREAM_OPT_GVR_ETX) ? 3 : 1; + if (!stream->read_array(buf, len)) + return nullptr; // NOLINT + stm->version = buf[0]; + stm->option1 = (stm->flags & STREAM_OPT_GVR_ETX) ? buf[1] : 0; + stm->option2 = (stm->flags & STREAM_OPT_GVR_ETX) ? buf[2] : 0; + if (stm32_get_ack(stm.get()) != STM32_ERR_OK) { + return nullptr; + } + } + + { + const auto len = ([&]() { + /* get the bootloader information */ + if (stm->cmd_get_reply) { + for (auto i = 0; stm->cmd_get_reply[i].length; ++i) { + if (stm->version == stm->cmd_get_reply[i].version) { + return stm->cmd_get_reply[i].length; + } + } + } + + return STM32_CMD_GET_LENGTH; + })(); + + if (stm32_guess_len_cmd(stm.get(), STM32_CMD_GET, buf, len) != STM32_ERR_OK) + return nullptr; + } + + const auto stop = buf[0] + 1; + stm->bl_version = buf[1]; + int new_cmds = 0; + for (auto i = 1; i < stop; ++i) { + const auto val = buf[i + 1]; + switch (val) { + case STM32_CMD_GET: + stm->cmd->get = val; + break; + case STM32_CMD_GVR: + stm->cmd->gvr = val; + break; + case STM32_CMD_GID: + stm->cmd->gid = val; + break; + case STM32_CMD_RM: + stm->cmd->rm = val; + break; + case STM32_CMD_GO: + stm->cmd->go = val; + break; + case STM32_CMD_WM: + case STM32_CMD_WM_NS: + stm->cmd->wm = newer(stm->cmd->wm, val); + break; + case STM32_CMD_ER: + case STM32_CMD_EE: + case STM32_CMD_EE_NS: + stm->cmd->er = newer(stm->cmd->er, val); + break; + case STM32_CMD_WP: + case STM32_CMD_WP_NS: + stm->cmd->wp = newer(stm->cmd->wp, val); + break; + case STM32_CMD_UW: + case STM32_CMD_UW_NS: + stm->cmd->uw = newer(stm->cmd->uw, val); + break; + case STM32_CMD_RP: + case STM32_CMD_RP_NS: + stm->cmd->rp = newer(stm->cmd->rp, val); + break; + case STM32_CMD_UR: + case STM32_CMD_UR_NS: + stm->cmd->ur = newer(stm->cmd->ur, val); + break; + case STM32_CMD_CRC: + stm->cmd->crc = newer(stm->cmd->crc, val); + break; + default: + if (new_cmds++ == 0) { + ESP_LOGD(TAG, "GET returns unknown commands (0x%2x", val); + } else { + ESP_LOGD(TAG, ", 0x%2x", val); + } + } + } + if (new_cmds) + ESP_LOGD(TAG, ")"); + if (stm32_get_ack(stm.get()) != STM32_ERR_OK) { + return nullptr; + } + + if (stm->cmd->get == STM32_CMD_ERR || stm->cmd->gvr == STM32_CMD_ERR || stm->cmd->gid == STM32_CMD_ERR) { + ESP_LOGD(TAG, "Error: bootloader did not returned correct information from GET command"); + return nullptr; + } + + /* get the device ID */ + if (stm32_guess_len_cmd(stm.get(), stm->cmd->gid, buf, 1) != STM32_ERR_OK) { + return nullptr; + } + const auto returned = buf[0] + 1; + if (returned < 2) { + ESP_LOGD(TAG, "Only %d bytes sent in the PID, unknown/unsupported device", returned); + return nullptr; + } + stm->pid = (buf[1] << 8) | buf[2]; + if (returned > 2) { + ESP_LOGD(TAG, "This bootloader returns %d extra bytes in PID:", returned); + for (auto i = 2; i <= returned; i++) + ESP_LOGD(TAG, " %02x", buf[i]); + } + if (stm32_get_ack(stm.get()) != STM32_ERR_OK) { + return nullptr; + } + + stm->dev = DEVICES; + while (stm->dev->id != 0x00 && stm->dev->id != stm->pid) + ++stm->dev; + + if (!stm->dev->id) { + ESP_LOGD(TAG, "Unknown/unsupported device (Device ID: 0x%03x)", stm->pid); + return nullptr; + } + + // TODO: Would be much better if the unique_ptr was returned from this function + // Release ownership of unique_ptr + return stm.release(); // NOLINT +} + +void stm32_close(stm32_t *stm) { + if (stm) + free(stm->cmd); // NOLINT + free(stm); // NOLINT +} + +stm32_err_t stm32_read_memory(const stm32_t *stm, const uint32_t address, uint8_t *data, const unsigned int len) { + auto *const stream = stm->stream; + + if (!len) + return STM32_ERR_OK; + + if (len > 256) { + ESP_LOGD(TAG, "Error: READ length limit at 256 bytes"); + return STM32_ERR_UNKNOWN; + } + + if (stm->cmd->rm == STM32_CMD_ERR) { + ESP_LOGD(TAG, "Error: READ command not implemented in bootloader."); + return STM32_ERR_NO_CMD; + } + + if (stm32_send_command(stm, stm->cmd->rm) != STM32_ERR_OK) + return STM32_ERR_UNKNOWN; + + static constexpr auto BUFFER_SIZE = 5; + uint8_t buf[BUFFER_SIZE]; + populate_buffer_with_address(buf, address); + + stream->write_array(buf, BUFFER_SIZE); + stream->flush(); + + if (stm32_get_ack(stm) != STM32_ERR_OK) + return STM32_ERR_UNKNOWN; + + if (stm32_send_command(stm, len - 1) != STM32_ERR_OK) + return STM32_ERR_UNKNOWN; + + if (!stream->read_array(data, len)) + return STM32_ERR_UNKNOWN; + + return STM32_ERR_OK; +} + +stm32_err_t stm32_write_memory(const stm32_t *stm, uint32_t address, const uint8_t *data, const unsigned int len) { + auto *const stream = stm->stream; + + if (!len) + return STM32_ERR_OK; + + if (len > 256) { + ESP_LOGD(TAG, "Error: READ length limit at 256 bytes"); + return STM32_ERR_UNKNOWN; + } + + /* must be 32bit aligned */ + if (address & 0x3) { + ESP_LOGD(TAG, "Error: WRITE address must be 4 byte aligned"); + return STM32_ERR_UNKNOWN; + } + + if (stm->cmd->wm == STM32_CMD_ERR) { + ESP_LOGD(TAG, "Error: WRITE command not implemented in bootloader."); + return STM32_ERR_NO_CMD; + } + + /* send the address and checksum */ + if (stm32_send_command(stm, stm->cmd->wm) != STM32_ERR_OK) + return STM32_ERR_UNKNOWN; + + static constexpr auto BUFFER_SIZE = 5; + uint8_t buf1[BUFFER_SIZE]; + populate_buffer_with_address(buf1, address); + + stream->write_array(buf1, BUFFER_SIZE); + stream->flush(); + if (stm32_get_ack(stm) != STM32_ERR_OK) + return STM32_ERR_UNKNOWN; + + const unsigned int aligned_len = (len + 3) & ~3; + uint8_t cs = aligned_len - 1; + uint8_t buf[256 + 2]; + + buf[0] = aligned_len - 1; + for (auto i = 0; i < len; i++) { + cs ^= data[i]; + buf[i + 1] = data[i]; + } + /* padding data */ + for (auto i = len; i < aligned_len; i++) { + cs ^= 0xFF; + buf[i + 1] = 0xFF; + } + buf[aligned_len + 1] = cs; + stream->write_array(buf, aligned_len + 2); + stream->flush(); + + const auto s_err = stm32_get_ack_timeout(stm, STM32_BLKWRITE_TIMEOUT); + if (s_err != STM32_ERR_OK) { + return STM32_ERR_UNKNOWN; + } + return STM32_ERR_OK; +} + +stm32_err_t stm32_wunprot_memory(const stm32_t *stm) { + if (stm->cmd->uw == STM32_CMD_ERR) { + ESP_LOGD(TAG, "Error: WRITE UNPROTECT command not implemented in bootloader."); + return STM32_ERR_NO_CMD; + } + + if (stm32_send_command(stm, stm->cmd->uw) != STM32_ERR_OK) + return STM32_ERR_UNKNOWN; + + return stm32_check_ack_timeout(stm32_get_ack_timeout(stm, STM32_WUNPROT_TIMEOUT), + []() { ESP_LOGD(TAG, "Error: Failed to WRITE UNPROTECT"); }); +} + +stm32_err_t stm32_wprot_memory(const stm32_t *stm) { + if (stm->cmd->wp == STM32_CMD_ERR) { + ESP_LOGD(TAG, "Error: WRITE PROTECT command not implemented in bootloader."); + return STM32_ERR_NO_CMD; + } + + if (stm32_send_command(stm, stm->cmd->wp) != STM32_ERR_OK) + return STM32_ERR_UNKNOWN; + + return stm32_check_ack_timeout(stm32_get_ack_timeout(stm, STM32_WPROT_TIMEOUT), + []() { ESP_LOGD(TAG, "Error: Failed to WRITE PROTECT"); }); +} + +stm32_err_t stm32_runprot_memory(const stm32_t *stm) { + if (stm->cmd->ur == STM32_CMD_ERR) { + ESP_LOGD(TAG, "Error: READOUT UNPROTECT command not implemented in bootloader."); + return STM32_ERR_NO_CMD; + } + + if (stm32_send_command(stm, stm->cmd->ur) != STM32_ERR_OK) + return STM32_ERR_UNKNOWN; + + return stm32_check_ack_timeout(stm32_get_ack_timeout(stm, STM32_MASSERASE_TIMEOUT), + []() { ESP_LOGD(TAG, "Error: Failed to READOUT UNPROTECT"); }); +} + +stm32_err_t stm32_readprot_memory(const stm32_t *stm) { + if (stm->cmd->rp == STM32_CMD_ERR) { + ESP_LOGD(TAG, "Error: READOUT PROTECT command not implemented in bootloader."); + return STM32_ERR_NO_CMD; + } + + if (stm32_send_command(stm, stm->cmd->rp) != STM32_ERR_OK) + return STM32_ERR_UNKNOWN; + + return stm32_check_ack_timeout(stm32_get_ack_timeout(stm, STM32_RPROT_TIMEOUT), + []() { ESP_LOGD(TAG, "Error: Failed to READOUT PROTECT"); }); +} + +stm32_err_t stm32_erase_memory(const stm32_t *stm, uint32_t spage, uint32_t pages) { + if (!pages || spage > STM32_MAX_PAGES || ((pages != STM32_MASS_ERASE) && ((spage + pages) > STM32_MAX_PAGES))) + return STM32_ERR_OK; + + if (stm->cmd->er == STM32_CMD_ERR) { + ESP_LOGD(TAG, "Error: ERASE command not implemented in bootloader."); + return STM32_ERR_NO_CMD; + } + + if (pages == STM32_MASS_ERASE) { + /* + * Not all chips support mass erase. + * Mass erase can be obtained executing a "readout protect" + * followed by "readout un-protect". This method is not + * suggested because can hang the target if a debug SWD/JTAG + * is connected. When the target enters in "readout + * protection" mode it will consider the debug connection as + * a tentative of intrusion and will hang. + * Erasing the flash page-by-page is the safer way to go. + */ + if (!(stm->dev->flags & F_NO_ME)) + return stm32_mass_erase(stm); + + pages = flash_addr_to_page_ceil(stm, stm->dev->fl_end); + } + + /* + * Some device, like STM32L152, cannot erase more than 512 pages in + * one command. Split the call. + */ + static constexpr uint32_t MAX_PAGE_SIZE = 512; + while (pages) { + const auto n = std::min(pages, MAX_PAGE_SIZE); + const auto s_err = stm32_pages_erase(stm, spage, n); + if (s_err != STM32_ERR_OK) + return s_err; + spage += n; + pages -= n; + } + return STM32_ERR_OK; +} + +static stm32_err_t stm32_run_raw_code(const stm32_t *stm, uint32_t target_address, const uint8_t *code, + uint32_t code_size) { + static constexpr uint32_t BUFFER_SIZE = 256; + + const auto stack_le = le_u32(0x20002000); + const auto code_address_le = le_u32(target_address + 8 + 1); // thumb mode address (!) + uint32_t length = code_size + 8; + + /* Must be 32-bit aligned */ + if (target_address & 0x3) { + ESP_LOGD(TAG, "Error: code address must be 4 byte aligned"); + return STM32_ERR_UNKNOWN; + } + + // Could be constexpr in c++17 + static const auto DELETOR = [](uint8_t *memory) { + free(memory); // NOLINT + }; + + // Free memory with RAII + std::unique_ptr mem{static_cast(malloc(length)), // NOLINT + DELETOR}; + + if (!mem) + return STM32_ERR_UNKNOWN; + + memcpy(mem.get(), &stack_le, sizeof(stack_le)); + memcpy(mem.get() + 4, &code_address_le, sizeof(code_address_le)); + memcpy(mem.get() + 8, code, code_size); + + auto *pos = mem.get(); + auto address = target_address; + while (length > 0) { + const auto w = std::min(length, BUFFER_SIZE); + if (stm32_write_memory(stm, address, pos, w) != STM32_ERR_OK) { + return STM32_ERR_UNKNOWN; + } + + address += w; + pos += w; + length -= w; + } + + return stm32_go(stm, target_address); +} + +stm32_err_t stm32_go(const stm32_t *stm, const uint32_t address) { + auto *const stream = stm->stream; + + if (stm->cmd->go == STM32_CMD_ERR) { + ESP_LOGD(TAG, "Error: GO command not implemented in bootloader."); + return STM32_ERR_NO_CMD; + } + + if (stm32_send_command(stm, stm->cmd->go) != STM32_ERR_OK) + return STM32_ERR_UNKNOWN; + + static constexpr auto BUFFER_SIZE = 5; + uint8_t buf[BUFFER_SIZE]; + populate_buffer_with_address(buf, address); + + stream->write_array(buf, BUFFER_SIZE); + stream->flush(); + + if (stm32_get_ack(stm) != STM32_ERR_OK) + return STM32_ERR_UNKNOWN; + return STM32_ERR_OK; +} + +stm32_err_t stm32_reset_device(const stm32_t *stm) { + const auto target_address = stm->dev->ram_start; + + if (stm->dev->flags & F_OBLL) { + /* set the OBL_LAUNCH bit to reset device (see RM0360, 2.5) */ + return stm32_run_raw_code(stm, target_address, STM_OBL_LAUNCH_CODE, STM_OBL_LAUNCH_CODE_SIZE); + } else { + return stm32_run_raw_code(stm, target_address, STM_RESET_CODE, STM_RESET_CODE_SIZE); + } +} + +stm32_err_t stm32_crc_memory(const stm32_t *stm, const uint32_t address, const uint32_t length, uint32_t *const crc) { + static constexpr auto BUFFER_SIZE = 5; + auto *const stream = stm->stream; + + if (address & 0x3 || length & 0x3) { + ESP_LOGD(TAG, "Start and end addresses must be 4 byte aligned"); + return STM32_ERR_UNKNOWN; + } + + if (stm->cmd->crc == STM32_CMD_ERR) { + ESP_LOGD(TAG, "Error: CRC command not implemented in bootloader."); + return STM32_ERR_NO_CMD; + } + + if (stm32_send_command(stm, stm->cmd->crc) != STM32_ERR_OK) + return STM32_ERR_UNKNOWN; + + { + static constexpr auto BUFFER_SIZE = 5; + uint8_t buf[BUFFER_SIZE]; + populate_buffer_with_address(buf, address); + + stream->write_array(buf, BUFFER_SIZE); + stream->flush(); + } + + if (stm32_get_ack(stm) != STM32_ERR_OK) + return STM32_ERR_UNKNOWN; + + { + static constexpr auto BUFFER_SIZE = 5; + uint8_t buf[BUFFER_SIZE]; + populate_buffer_with_address(buf, address); + + stream->write_array(buf, BUFFER_SIZE); + stream->flush(); + } + + if (stm32_get_ack(stm) != STM32_ERR_OK) + return STM32_ERR_UNKNOWN; + + if (stm32_get_ack(stm) != STM32_ERR_OK) + return STM32_ERR_UNKNOWN; + + { + uint8_t buf[BUFFER_SIZE]; + if (!stream->read_array(buf, BUFFER_SIZE)) + return STM32_ERR_UNKNOWN; + + if (buf[4] != (buf[0] ^ buf[1] ^ buf[2] ^ buf[3])) + return STM32_ERR_UNKNOWN; + + *crc = (buf[0] << 24) | (buf[1] << 16) | (buf[2] << 8) | buf[3]; + } + + return STM32_ERR_OK; +} + +/* + * CRC computed by STM32 is similar to the standard crc32_be() + * implemented, for example, in Linux kernel in ./lib/crc32.c + * But STM32 computes it on units of 32 bits word and swaps the + * bytes of the word before the computation. + * Due to byte swap, I cannot use any CRC available in existing + * libraries, so here is a simple not optimized implementation. + */ +uint32_t stm32_sw_crc(uint32_t crc, uint8_t *buf, unsigned int len) { + static constexpr uint32_t CRCPOLY_BE = 0x04c11db7; + static constexpr uint32_t CRC_MSBMASK = 0x80000000; + + if (len & 0x3) { + ESP_LOGD(TAG, "Buffer length must be multiple of 4 bytes"); + return 0; + } + + while (len) { + uint32_t data = *buf++; + data |= *buf++ << 8; + data |= *buf++ << 16; + data |= *buf++ << 24; + len -= 4; + + crc ^= data; + + for (size_t i = 0; i < 32; ++i) { + if (crc & CRC_MSBMASK) { + crc = (crc << 1) ^ CRCPOLY_BE; + } else { + crc = (crc << 1); + } + } + } + return crc; +} + +stm32_err_t stm32_crc_wrapper(const stm32_t *stm, uint32_t address, uint32_t length, uint32_t *crc) { + static constexpr uint32_t CRC_INIT_VALUE = 0xFFFFFFFF; + static constexpr uint32_t BUFFER_SIZE = 256; + + uint8_t buf[BUFFER_SIZE]; + + if (address & 0x3 || length & 0x3) { + ESP_LOGD(TAG, "Start and end addresses must be 4 byte aligned"); + return STM32_ERR_UNKNOWN; + } + + if (stm->cmd->crc != STM32_CMD_ERR) + return stm32_crc_memory(stm, address, length, crc); + + const auto start = address; + const auto total_len = length; + uint32_t current_crc = CRC_INIT_VALUE; + while (length) { + const auto len = std::min(BUFFER_SIZE, length); + if (stm32_read_memory(stm, address, buf, len) != STM32_ERR_OK) { + ESP_LOGD(TAG, "Failed to read memory at address 0x%08x, target write-protected?", address); + return STM32_ERR_UNKNOWN; + } + current_crc = stm32_sw_crc(current_crc, buf, len); + length -= len; + address += len; + + ESP_LOGD(TAG, "\rCRC address 0x%08x (%.2f%%) ", address, (100.0f / (float) total_len) * (float) (address - start)); + } + ESP_LOGD(TAG, "Done."); + *crc = current_crc; + return STM32_ERR_OK; +} + +} // namespace shelly_dimmer +} // namespace esphome +#endif diff --git a/esphome/components/shelly_dimmer/stm32flash.h b/esphome/components/shelly_dimmer/stm32flash.h new file mode 100644 index 0000000000..c561375c38 --- /dev/null +++ b/esphome/components/shelly_dimmer/stm32flash.h @@ -0,0 +1,129 @@ +/* + stm32flash - Open Source ST STM32 flash program for Arduino + Copyright (C) 2010 Geoffrey McRae + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +*/ + +#pragma once + +#include "esphome/core/defines.h" +#ifdef USE_SHD_FIRMWARE_DATA + +#include +#include "esphome/components/uart/uart.h" + +namespace esphome { +namespace shelly_dimmer { + +/* flags */ +constexpr auto STREAM_OPT_BYTE = (1 << 0); /* byte (not frame) oriented */ +constexpr auto STREAM_OPT_GVR_ETX = (1 << 1); /* cmd GVR returns protection status */ +constexpr auto STREAM_OPT_CMD_INIT = (1 << 2); /* use INIT cmd to autodetect speed */ +constexpr auto STREAM_OPT_RETRY = (1 << 3); /* allowed read() retry after timeout */ +constexpr auto STREAM_OPT_I2C = (1 << 4); /* i2c */ +constexpr auto STREAM_OPT_STRETCH_W = (1 << 5); /* warning for no-stretching commands */ + +constexpr auto STREAM_SERIAL = (STREAM_OPT_BYTE | STREAM_OPT_GVR_ETX | STREAM_OPT_CMD_INIT | STREAM_OPT_RETRY); +constexpr auto STREAM_I2C = (STREAM_OPT_I2C | STREAM_OPT_STRETCH_W); + +constexpr auto STM32_MAX_RX_FRAME = 256; /* cmd read memory */ +constexpr auto STM32_MAX_TX_FRAME = (1 + 256 + 1); /* cmd write memory */ + +constexpr auto STM32_MAX_PAGES = 0x0000ffff; +constexpr auto STM32_MASS_ERASE = 0x00100000; /* > 2 x max_pages */ + +using stm32_err_t = enum Stm32Err { + STM32_ERR_OK = 0, + STM32_ERR_UNKNOWN, /* Generic error */ + STM32_ERR_NACK, + STM32_ERR_NO_CMD, /* Command not available in bootloader */ +}; + +using flags_t = enum Flags { + F_NO_ME = 1 << 0, /* Mass-Erase not supported */ + F_OBLL = 1 << 1, /* OBL_LAUNCH required */ +}; + +using stm32_cmd_t = struct Stm32Cmd { + uint8_t get; + uint8_t gvr; + uint8_t gid; + uint8_t rm; + uint8_t go; + uint8_t wm; + uint8_t er; /* this may be extended erase */ + uint8_t wp; + uint8_t uw; + uint8_t rp; + uint8_t ur; + uint8_t crc; +}; + +using stm32_dev_t = struct Stm32Dev { // NOLINT + const uint16_t id; + const char *name; + const uint32_t ram_start, ram_end; + const uint32_t fl_start, fl_end; + const uint16_t fl_pps; // pages per sector + const uint32_t *fl_ps; // page size + const uint32_t opt_start, opt_end; + const uint32_t mem_start, mem_end; + const uint32_t flags; +}; + +using stm32_t = struct Stm32 { + uart::UARTDevice *stream; + uint8_t flags; + struct VarlenCmd *cmd_get_reply; + uint8_t bl_version; + uint8_t version; + uint8_t option1, option2; + uint16_t pid; + stm32_cmd_t *cmd; + const stm32_dev_t *dev; +}; + +/* + * Specify the length of reply for command GET + * This is helpful for frame-oriented protocols, e.g. i2c, to avoid time + * consuming try-fail-timeout-retry operation. + * On byte-oriented protocols, i.e. UART, this information would be skipped + * after read the first byte, so not needed. + */ +struct VarlenCmd { + uint8_t version; + uint8_t length; +}; + +stm32_t *stm32_init(uart::UARTDevice *stream, uint8_t flags, char init); +void stm32_close(stm32_t *stm); +stm32_err_t stm32_read_memory(const stm32_t *stm, uint32_t address, uint8_t *data, unsigned int len); +stm32_err_t stm32_write_memory(const stm32_t *stm, uint32_t address, const uint8_t *data, unsigned int len); +stm32_err_t stm32_wunprot_memory(const stm32_t *stm); +stm32_err_t stm32_wprot_memory(const stm32_t *stm); +stm32_err_t stm32_erase_memory(const stm32_t *stm, uint32_t spage, uint32_t pages); +stm32_err_t stm32_go(const stm32_t *stm, uint32_t address); +stm32_err_t stm32_reset_device(const stm32_t *stm); +stm32_err_t stm32_readprot_memory(const stm32_t *stm); +stm32_err_t stm32_runprot_memory(const stm32_t *stm); +stm32_err_t stm32_crc_memory(const stm32_t *stm, uint32_t address, uint32_t length, uint32_t *crc); +stm32_err_t stm32_crc_wrapper(const stm32_t *stm, uint32_t address, uint32_t length, uint32_t *crc); +uint32_t stm32_sw_crc(uint32_t crc, uint8_t *buf, unsigned int len); + +} // namespace shelly_dimmer +} // namespace esphome + +#endif // USE_SHD_FIRMWARE_DATA diff --git a/esphome/core/defines.h b/esphome/core/defines.h index f304f847a5..c854e2b987 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -93,3 +93,9 @@ //#define USE_BSEC // Requires a library with proprietary license. #define USE_DASHBOARD_IMPORT + +// Dummy firmware payload for shelly_dimmer +#define USE_SHD_FIRMWARE_MAJOR_VERSION 56 +#define USE_SHD_FIRMWARE_MINOR_VERSION 5 +#define USE_SHD_FIRMWARE_DATA \ + {} diff --git a/tests/test1.yaml b/tests/test1.yaml index 77c4a76bda..98a3ffcf4b 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1751,6 +1751,18 @@ light: to: 25 - single_light_id: ${roomname}_lights + - platform: shelly_dimmer + name: "Shelly Dimmer Light" + power: + name: "Shelly Dimmer Power" + voltage: + name: "Shelly Dimmer Voltage" + current: + name: "Shelly Dimmer Current" + max_brightness: 500 + firmware: "51.6" + uart_id: uart0 + remote_transmitter: - pin: 32 carrier_duty_percent: 100% From 2243021b581ad1f855de5976e2152fcdc4e97f91 Mon Sep 17 00:00:00 2001 From: Janez Troha <239513+dz0ny@users.noreply.github.com> Date: Thu, 14 Apr 2022 03:42:43 +0200 Subject: [PATCH 0394/1729] Allocate smaller amount of buffer for JSON (#3384) --- esphome/components/json/json_util.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/esphome/components/json/json_util.cpp b/esphome/components/json/json_util.cpp index 10179c9954..2bd8112255 100644 --- a/esphome/components/json/json_util.cpp +++ b/esphome/components/json/json_util.cpp @@ -23,13 +23,13 @@ std::string build_json(const json_build_t &f) { #ifdef USE_ESP8266 const size_t free_heap = ESP.getMaxFreeBlockSize(); // NOLINT(readability-static-accessed-through-instance) #elif defined(USE_ESP32) - const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL); + const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT); #endif - const size_t request_size = std::min(free_heap - 2048, (size_t) 5120); + const size_t request_size = std::min(free_heap, (size_t) 512); DynamicJsonDocument json_document(request_size); - if (json_document.memoryPool().buffer() == nullptr) { + if (json_document.capacity() == 0) { ESP_LOGE(TAG, "Could not allocate memory for JSON document! Requested %u bytes, largest free heap block: %u bytes", request_size, free_heap); return "{}"; @@ -37,7 +37,7 @@ std::string build_json(const json_build_t &f) { JsonObject root = json_document.to(); f(root); json_document.shrinkToFit(); - + ESP_LOGV(TAG, "Size after shrink %u bytes", json_document.capacity()); std::string output; serializeJson(json_document, output); return output; @@ -51,13 +51,13 @@ void parse_json(const std::string &data, const json_parse_t &f) { #ifdef USE_ESP8266 const size_t free_heap = ESP.getMaxFreeBlockSize(); // NOLINT(readability-static-accessed-through-instance) #elif defined(USE_ESP32) - const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL); + const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT); #endif bool pass = false; - size_t request_size = std::min(free_heap - 2048, (size_t)(data.size() * 1.5)); + size_t request_size = std::min(free_heap, (size_t)(data.size() * 1.5)); do { DynamicJsonDocument json_document(request_size); - if (json_document.memoryPool().buffer() == nullptr) { + if (json_document.capacity() == 0) { ESP_LOGE(TAG, "Could not allocate memory for JSON document! Requested %u bytes, free heap: %u", request_size, free_heap); return; From dcb226b20221fd481d15e8f4925120b64b138fdf Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 14 Apr 2022 13:48:35 +1200 Subject: [PATCH 0395/1729] Bump version to 2022.4.0b2 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 01d2d59c3d..04fa5c2bf7 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2022.4.0b1" +__version__ = "2022.4.0b2" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From b605982f940f4ac831f179c7645a7dd3b034622a Mon Sep 17 00:00:00 2001 From: Michel van de Wetering Date: Mon, 18 Apr 2022 22:42:02 +0200 Subject: [PATCH 0396/1729] Fix power_delivered/produced_phase sensor deviceclass in DSMR (#3395) --- esphome/components/dsmr/sensor.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/esphome/components/dsmr/sensor.py b/esphome/components/dsmr/sensor.py index bb4722655c..0b0439baa4 100644 --- a/esphome/components/dsmr/sensor.py +++ b/esphome/components/dsmr/sensor.py @@ -143,37 +143,37 @@ CONFIG_SCHEMA = cv.Schema( cv.Optional("power_delivered_l1"): sensor.sensor_schema( unit_of_measurement=UNIT_KILOWATT, accuracy_decimals=3, - device_class=DEVICE_CLASS_CURRENT, + device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("power_delivered_l2"): sensor.sensor_schema( unit_of_measurement=UNIT_KILOWATT, accuracy_decimals=3, - device_class=DEVICE_CLASS_CURRENT, + device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("power_delivered_l3"): sensor.sensor_schema( unit_of_measurement=UNIT_KILOWATT, accuracy_decimals=3, - device_class=DEVICE_CLASS_CURRENT, + device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("power_returned_l1"): sensor.sensor_schema( unit_of_measurement=UNIT_KILOWATT, accuracy_decimals=3, - device_class=DEVICE_CLASS_CURRENT, + device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("power_returned_l2"): sensor.sensor_schema( unit_of_measurement=UNIT_KILOWATT, accuracy_decimals=3, - device_class=DEVICE_CLASS_CURRENT, + device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("power_returned_l3"): sensor.sensor_schema( unit_of_measurement=UNIT_KILOWATT, accuracy_decimals=3, - device_class=DEVICE_CLASS_CURRENT, + device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("reactive_power_delivered_l1"): sensor.sensor_schema( From 2064abe16de0dc90b108839cce551af3af21e17a Mon Sep 17 00:00:00 2001 From: rnauber <7414650+rnauber@users.noreply.github.com> Date: Mon, 18 Apr 2022 22:43:34 +0200 Subject: [PATCH 0397/1729] Shelly Dimmer: Delete obsolete LICENSE.txt (#3394) --- esphome/components/shelly_dimmer/LICENSE.txt | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 esphome/components/shelly_dimmer/LICENSE.txt diff --git a/esphome/components/shelly_dimmer/LICENSE.txt b/esphome/components/shelly_dimmer/LICENSE.txt deleted file mode 100644 index 524fe0d514..0000000000 --- a/esphome/components/shelly_dimmer/LICENSE.txt +++ /dev/null @@ -1,2 +0,0 @@ -The firmware files for the STM microcontroller (shelly-dimmer-stm32_*.bin) are taken from -https://github.com/jamesturton/shelly-dimmer-stm32 and GPLv3 licensed. From 6b393438e9177bd3e547e962cff9d6559a161407 Mon Sep 17 00:00:00 2001 From: Michel van de Wetering Date: Mon, 18 Apr 2022 22:42:02 +0200 Subject: [PATCH 0398/1729] Fix power_delivered/produced_phase sensor deviceclass in DSMR (#3395) --- esphome/components/dsmr/sensor.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/esphome/components/dsmr/sensor.py b/esphome/components/dsmr/sensor.py index bb4722655c..0b0439baa4 100644 --- a/esphome/components/dsmr/sensor.py +++ b/esphome/components/dsmr/sensor.py @@ -143,37 +143,37 @@ CONFIG_SCHEMA = cv.Schema( cv.Optional("power_delivered_l1"): sensor.sensor_schema( unit_of_measurement=UNIT_KILOWATT, accuracy_decimals=3, - device_class=DEVICE_CLASS_CURRENT, + device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("power_delivered_l2"): sensor.sensor_schema( unit_of_measurement=UNIT_KILOWATT, accuracy_decimals=3, - device_class=DEVICE_CLASS_CURRENT, + device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("power_delivered_l3"): sensor.sensor_schema( unit_of_measurement=UNIT_KILOWATT, accuracy_decimals=3, - device_class=DEVICE_CLASS_CURRENT, + device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("power_returned_l1"): sensor.sensor_schema( unit_of_measurement=UNIT_KILOWATT, accuracy_decimals=3, - device_class=DEVICE_CLASS_CURRENT, + device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("power_returned_l2"): sensor.sensor_schema( unit_of_measurement=UNIT_KILOWATT, accuracy_decimals=3, - device_class=DEVICE_CLASS_CURRENT, + device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("power_returned_l3"): sensor.sensor_schema( unit_of_measurement=UNIT_KILOWATT, accuracy_decimals=3, - device_class=DEVICE_CLASS_CURRENT, + device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional("reactive_power_delivered_l1"): sensor.sensor_schema( From 9283559c6b0649a4dc2f5b4795d9da6b49e3f771 Mon Sep 17 00:00:00 2001 From: rnauber <7414650+rnauber@users.noreply.github.com> Date: Mon, 18 Apr 2022 22:43:34 +0200 Subject: [PATCH 0399/1729] Shelly Dimmer: Delete obsolete LICENSE.txt (#3394) --- esphome/components/shelly_dimmer/LICENSE.txt | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 esphome/components/shelly_dimmer/LICENSE.txt diff --git a/esphome/components/shelly_dimmer/LICENSE.txt b/esphome/components/shelly_dimmer/LICENSE.txt deleted file mode 100644 index 524fe0d514..0000000000 --- a/esphome/components/shelly_dimmer/LICENSE.txt +++ /dev/null @@ -1,2 +0,0 @@ -The firmware files for the STM microcontroller (shelly-dimmer-stm32_*.bin) are taken from -https://github.com/jamesturton/shelly-dimmer-stm32 and GPLv3 licensed. From 712115b6ce682a03fce071f59a5d7e54b9c8905e Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 19 Apr 2022 12:33:38 +1200 Subject: [PATCH 0400/1729] Bump version to 2022.4.0b3 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 04fa5c2bf7..bca64e8175 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2022.4.0b2" +__version__ = "2022.4.0b3" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From 0767b92b62d8b54d2233f86bac2e23f8ab52cf94 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 20 Apr 2022 06:56:09 +1200 Subject: [PATCH 0401/1729] Dont require {} for wifi ap with defaults (#3404) --- esphome/components/wifi/__init__.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 20f43cb450..b56902df2f 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -126,6 +126,13 @@ WIFI_NETWORK_AP = WIFI_NETWORK_BASE.extend( } ) + +def wifi_network_ap(value): + if value is None: + value = {} + return WIFI_NETWORK_AP(value) + + WIFI_NETWORK_STA = WIFI_NETWORK_BASE.extend( { cv.Optional(CONF_BSSID): cv.mac_address, @@ -252,7 +259,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_PASSWORD): validate_password, cv.Optional(CONF_MANUAL_IP): STA_MANUAL_IP_SCHEMA, cv.Optional(CONF_EAP): EAP_AUTH_SCHEMA, - cv.Optional(CONF_AP): WIFI_NETWORK_AP, + cv.Optional(CONF_AP): wifi_network_ap, cv.Optional(CONF_DOMAIN, default=".local"): cv.domain_name, cv.Optional( CONF_REBOOT_TIMEOUT, default="15min" From 988d3ea8baf755e69525bc2d96f94424ea5b4b08 Mon Sep 17 00:00:00 2001 From: parats15 <72889410+parats15@users.noreply.github.com> Date: Wed, 20 Apr 2022 02:46:55 +0200 Subject: [PATCH 0402/1729] Multi conf for Teleinfo component (#3401) --- esphome/components/teleinfo/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/teleinfo/__init__.py b/esphome/components/teleinfo/__init__.py index 33b748a031..e289e42c81 100644 --- a/esphome/components/teleinfo/__init__.py +++ b/esphome/components/teleinfo/__init__.py @@ -4,6 +4,7 @@ from esphome.components import uart from esphome.const import CONF_ID CODEOWNERS = ["@0hax"] +MULTI_CONF = True teleinfo_ns = cg.esphome_ns.namespace("teleinfo") TeleInfo = teleinfo_ns.class_("TeleInfo", cg.PollingComponent, uart.UARTDevice) From 9576d246eec5fa4a5d0b0d0b82261ad0aa539275 Mon Sep 17 00:00:00 2001 From: James Duke Date: Tue, 19 Apr 2022 17:50:24 -0700 Subject: [PATCH 0403/1729] Add support for Mopeka Pro+ Residential sensor (#3393) * Add support for Pro+ Residential sensor (enum) The Mopeka Pro+ Residential sensor is very similar to the Pro sensor, but includes a longer range antenna, and maybe hardware? The Pro+ identifies itself with 0x08 sensor type. * Add logic to support Pro+ Residential sensor * Fix formatting --- esphome/components/mopeka_pro_check/mopeka_pro_check.cpp | 3 ++- esphome/components/mopeka_pro_check/mopeka_pro_check.h | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/esphome/components/mopeka_pro_check/mopeka_pro_check.cpp b/esphome/components/mopeka_pro_check/mopeka_pro_check.cpp index bcfe0a80ce..fc57318a81 100644 --- a/esphome/components/mopeka_pro_check/mopeka_pro_check.cpp +++ b/esphome/components/mopeka_pro_check/mopeka_pro_check.cpp @@ -52,7 +52,8 @@ bool MopekaProCheck::parse_device(const esp32_ble_tracker::ESPBTDevice &device) // Now parse the data - See Datasheet for definition - if (static_cast(manu_data.data[0]) != STANDARD_BOTTOM_UP) { + if (static_cast(manu_data.data[0]) != STANDARD_BOTTOM_UP && + static_cast(manu_data.data[0]) != PLUS_BOTTOM_UP) { ESP_LOGE(TAG, "Unsupported Sensor Type (0x%X)", manu_data.data[0]); return false; } diff --git a/esphome/components/mopeka_pro_check/mopeka_pro_check.h b/esphome/components/mopeka_pro_check/mopeka_pro_check.h index 59d33f7763..dfdce9353e 100644 --- a/esphome/components/mopeka_pro_check/mopeka_pro_check.h +++ b/esphome/components/mopeka_pro_check/mopeka_pro_check.h @@ -12,7 +12,8 @@ namespace mopeka_pro_check { enum SensorType { STANDARD_BOTTOM_UP = 0x03, TOP_DOWN_AIR_ABOVE = 0x04, - BOTTOM_UP_WATER = 0x05 + BOTTOM_UP_WATER = 0x05, + PLUS_BOTTOM_UP = 0x08 // all other values are reserved }; From ad41c07a1f704d938cbc6a5ffb2542a5a438f76e Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 20 Apr 2022 06:56:09 +1200 Subject: [PATCH 0404/1729] Dont require {} for wifi ap with defaults (#3404) --- esphome/components/wifi/__init__.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 20f43cb450..b56902df2f 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -126,6 +126,13 @@ WIFI_NETWORK_AP = WIFI_NETWORK_BASE.extend( } ) + +def wifi_network_ap(value): + if value is None: + value = {} + return WIFI_NETWORK_AP(value) + + WIFI_NETWORK_STA = WIFI_NETWORK_BASE.extend( { cv.Optional(CONF_BSSID): cv.mac_address, @@ -252,7 +259,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_PASSWORD): validate_password, cv.Optional(CONF_MANUAL_IP): STA_MANUAL_IP_SCHEMA, cv.Optional(CONF_EAP): EAP_AUTH_SCHEMA, - cv.Optional(CONF_AP): WIFI_NETWORK_AP, + cv.Optional(CONF_AP): wifi_network_ap, cv.Optional(CONF_DOMAIN, default=".local"): cv.domain_name, cv.Optional( CONF_REBOOT_TIMEOUT, default="15min" From e26e0d7c01d7cfd0844866a2ec97273e34125955 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 20 Apr 2022 16:35:43 +1200 Subject: [PATCH 0405/1729] Bump version to 2022.4.0b4 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index bca64e8175..66f8b26d17 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2022.4.0b3" +__version__ = "2022.4.0b4" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From a8c1b63edb20ab08895656d96f149ec4e1371c5d Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 20 Apr 2022 17:06:08 +1200 Subject: [PATCH 0406/1729] Bump version to 2022.4.0 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 66f8b26d17..161b60f7fa 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2022.4.0b4" +__version__ = "2022.4.0" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From 7a778f3f330ca607f2772501eac82a043d0346d3 Mon Sep 17 00:00:00 2001 From: "I. Tomita" Date: Thu, 21 Apr 2022 01:11:25 +0300 Subject: [PATCH 0407/1729] Add support for BL0939 (Sonoff Dual R3 V2 powermeter) (#3300) --- CODEOWNERS | 1 + esphome/components/bl0939/__init__.py | 1 + esphome/components/bl0939/bl0939.cpp | 144 ++++++++++++++++++++++++++ esphome/components/bl0939/bl0939.h | 107 +++++++++++++++++++ esphome/components/bl0939/sensor.py | 123 ++++++++++++++++++++++ tests/test3.yaml | 24 +++++ 6 files changed, 400 insertions(+) create mode 100644 esphome/components/bl0939/__init__.py create mode 100644 esphome/components/bl0939/bl0939.cpp create mode 100644 esphome/components/bl0939/bl0939.h create mode 100644 esphome/components/bl0939/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index c9df669f03..7fd049f46e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -31,6 +31,7 @@ esphome/components/bang_bang/* @OttoWinter esphome/components/bedjet/* @jhansche esphome/components/bh1750/* @OttoWinter esphome/components/binary_sensor/* @esphome/core +esphome/components/bl0939/* @ziceva esphome/components/bl0940/* @tobias- esphome/components/ble_client/* @buxtronix esphome/components/bme680_bsec/* @trvrnrth diff --git a/esphome/components/bl0939/__init__.py b/esphome/components/bl0939/__init__.py new file mode 100644 index 0000000000..9bd4598dd2 --- /dev/null +++ b/esphome/components/bl0939/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@ziceva"] diff --git a/esphome/components/bl0939/bl0939.cpp b/esphome/components/bl0939/bl0939.cpp new file mode 100644 index 0000000000..61d7835a4b --- /dev/null +++ b/esphome/components/bl0939/bl0939.cpp @@ -0,0 +1,144 @@ +#include "bl0939.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace bl0939 { + +static const char *const TAG = "bl0939"; + +// https://www.belling.com.cn/media/file_object/bel_product/BL0939/datasheet/BL0939_V1.2_cn.pdf +// (unfortunatelly chinese, but the protocol can be understood with some translation tool) +static const uint8_t BL0939_READ_COMMAND = 0x55; // 0x5{A4,A3,A2,A1} +static const uint8_t BL0939_FULL_PACKET = 0xAA; +static const uint8_t BL0939_PACKET_HEADER = 0x55; + +static const uint8_t BL0939_WRITE_COMMAND = 0xA5; // 0xA{A4,A3,A2,A1} +static const uint8_t BL0939_REG_IA_FAST_RMS_CTRL = 0x10; +static const uint8_t BL0939_REG_IB_FAST_RMS_CTRL = 0x1E; +static const uint8_t BL0939_REG_MODE = 0x18; +static const uint8_t BL0939_REG_SOFT_RESET = 0x19; +static const uint8_t BL0939_REG_USR_WRPROT = 0x1A; +static const uint8_t BL0939_REG_TPS_CTRL = 0x1B; + +const uint8_t BL0939_INIT[6][6] = { + // Reset to default + {BL0939_WRITE_COMMAND, BL0939_REG_SOFT_RESET, 0x5A, 0x5A, 0x5A, 0x33}, + // Enable User Operation Write + {BL0939_WRITE_COMMAND, BL0939_REG_USR_WRPROT, 0x55, 0x00, 0x00, 0xEB}, + // 0x0100 = CF_UNABLE energy pulse, AC_FREQ_SEL 50Hz, RMS_UPDATE_SEL 800mS + {BL0939_WRITE_COMMAND, BL0939_REG_MODE, 0x00, 0x10, 0x00, 0x32}, + // 0x47FF = Over-current and leakage alarm on, Automatic temperature measurement, Interval 100mS + {BL0939_WRITE_COMMAND, BL0939_REG_TPS_CTRL, 0xFF, 0x47, 0x00, 0xF9}, + // 0x181C = Half cycle, Fast RMS threshold 6172 + {BL0939_WRITE_COMMAND, BL0939_REG_IA_FAST_RMS_CTRL, 0x1C, 0x18, 0x00, 0x16}, + // 0x181C = Half cycle, Fast RMS threshold 6172 + {BL0939_WRITE_COMMAND, BL0939_REG_IB_FAST_RMS_CTRL, 0x1C, 0x18, 0x00, 0x08}}; + +void BL0939::loop() { + DataPacket buffer; + if (!this->available()) { + return; + } + if (read_array((uint8_t *) &buffer, sizeof(buffer))) { + if (validate_checksum(&buffer)) { + received_package_(&buffer); + } + } else { + ESP_LOGW(TAG, "Junk on wire. Throwing away partial message"); + while (read() >= 0) + ; + } +} + +bool BL0939::validate_checksum(const DataPacket *data) { + uint8_t checksum = BL0939_READ_COMMAND; + // Whole package but checksum + for (uint32_t i = 0; i < sizeof(data->raw) - 1; i++) { + checksum += data->raw[i]; + } + checksum ^= 0xFF; + if (checksum != data->checksum) { + ESP_LOGW(TAG, "BL0939 invalid checksum! 0x%02X != 0x%02X", checksum, data->checksum); + } + return checksum == data->checksum; +} + +void BL0939::update() { + this->flush(); + this->write_byte(BL0939_READ_COMMAND); + this->write_byte(BL0939_FULL_PACKET); +} + +void BL0939::setup() { + for (auto *i : BL0939_INIT) { + this->write_array(i, 6); + delay(1); + } + this->flush(); +} + +void BL0939::received_package_(const DataPacket *data) const { + // Bad header + if (data->frame_header != BL0939_PACKET_HEADER) { + ESP_LOGI("bl0939", "Invalid data. Header mismatch: %d", data->frame_header); + return; + } + + float v_rms = (float) to_uint32_t(data->v_rms) / voltage_reference_; + float ia_rms = (float) to_uint32_t(data->ia_rms) / current_reference_; + float ib_rms = (float) to_uint32_t(data->ib_rms) / current_reference_; + float a_watt = (float) to_int32_t(data->a_watt) / power_reference_; + float b_watt = (float) to_int32_t(data->b_watt) / power_reference_; + int32_t cfa_cnt = to_int32_t(data->cfa_cnt); + int32_t cfb_cnt = to_int32_t(data->cfb_cnt); + float a_energy_consumption = (float) cfa_cnt / energy_reference_; + float b_energy_consumption = (float) cfb_cnt / energy_reference_; + float total_energy_consumption = a_energy_consumption + b_energy_consumption; + + if (voltage_sensor_ != nullptr) { + voltage_sensor_->publish_state(v_rms); + } + if (current_sensor_1_ != nullptr) { + current_sensor_1_->publish_state(ia_rms); + } + if (current_sensor_2_ != nullptr) { + current_sensor_2_->publish_state(ib_rms); + } + if (power_sensor_1_ != nullptr) { + power_sensor_1_->publish_state(a_watt); + } + if (power_sensor_2_ != nullptr) { + power_sensor_2_->publish_state(b_watt); + } + if (energy_sensor_1_ != nullptr) { + energy_sensor_1_->publish_state(a_energy_consumption); + } + if (energy_sensor_2_ != nullptr) { + energy_sensor_2_->publish_state(b_energy_consumption); + } + if (energy_sensor_sum_ != nullptr) { + energy_sensor_sum_->publish_state(total_energy_consumption); + } + + ESP_LOGV("bl0939", "BL0939: U %fV, I1 %fA, I2 %fA, P1 %fW, P2 %fW, CntA %d, CntB %d, ∫P1 %fkWh, ∫P2 %fkWh", v_rms, + ia_rms, ib_rms, a_watt, b_watt, cfa_cnt, cfb_cnt, a_energy_consumption, b_energy_consumption); +} + +void BL0939::dump_config() { // NOLINT(readability-function-cognitive-complexity) + ESP_LOGCONFIG(TAG, "BL0939:"); + LOG_SENSOR("", "Voltage", this->voltage_sensor_); + LOG_SENSOR("", "Current 1", this->current_sensor_1_); + LOG_SENSOR("", "Current 2", this->current_sensor_2_); + LOG_SENSOR("", "Power 1", this->power_sensor_1_); + LOG_SENSOR("", "Power 2", this->power_sensor_2_); + LOG_SENSOR("", "Energy 1", this->energy_sensor_1_); + LOG_SENSOR("", "Energy 2", this->energy_sensor_2_); + LOG_SENSOR("", "Energy sum", this->energy_sensor_sum_); +} + +uint32_t BL0939::to_uint32_t(ube24_t input) { return input.h << 16 | input.m << 8 | input.l; } + +int32_t BL0939::to_int32_t(sbe24_t input) { return input.h << 16 | input.m << 8 | input.l; } + +} // namespace bl0939 +} // namespace esphome diff --git a/esphome/components/bl0939/bl0939.h b/esphome/components/bl0939/bl0939.h new file mode 100644 index 0000000000..5221ae26e7 --- /dev/null +++ b/esphome/components/bl0939/bl0939.h @@ -0,0 +1,107 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/uart/uart.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace bl0939 { + +// https://datasheet.lcsc.com/lcsc/2108071830_BL-Shanghai-Belling-BL0939_C2841044.pdf +// (unfortunatelly chinese, but the formulas can be easily understood) +// Sonoff Dual R3 V2 has the exact same resistor values for the current shunts (RL=1miliOhm) +// and for the voltage divider (R1=0.51kOhm, R2=5*390kOhm) +// as in the manufacturer's reference circuit, so the same formulas were used here (Vref=1.218V) +static const float BL0939_IREF = 324004 * 1 / 1.218; +static const float BL0939_UREF = 79931 * 0.51 * 1000 / (1.218 * (5 * 390 + 0.51)); +static const float BL0939_PREF = 4046 * 1 * 0.51 * 1000 / (1.218 * 1.218 * (5 * 390 + 0.51)); +static const float BL0939_EREF = 3.6e6 * 4046 * 1 * 0.51 * 1000 / (1638.4 * 256 * 1.218 * 1.218 * (5 * 390 + 0.51)); + +struct ube24_t { // NOLINT(readability-identifier-naming,altera-struct-pack-align) + uint8_t l; + uint8_t m; + uint8_t h; +} __attribute__((packed)); + +struct ube16_t { // NOLINT(readability-identifier-naming,altera-struct-pack-align) + uint8_t l; + uint8_t h; +} __attribute__((packed)); + +struct sbe24_t { // NOLINT(readability-identifier-naming,altera-struct-pack-align) + uint8_t l; + uint8_t m; + int8_t h; +} __attribute__((packed)); + +// Caveat: All these values are big endian (low - middle - high) + +union DataPacket { // NOLINT(altera-struct-pack-align) + uint8_t raw[35]; + struct { + uint8_t frame_header; // 0x55 according to docs + ube24_t ia_fast_rms; + ube24_t ia_rms; + ube24_t ib_rms; + ube24_t v_rms; + ube24_t ib_fast_rms; + sbe24_t a_watt; + sbe24_t b_watt; + sbe24_t cfa_cnt; + sbe24_t cfb_cnt; + ube16_t tps1; + uint8_t RESERVED1; // value of 0x00 + ube16_t tps2; + uint8_t RESERVED2; // value of 0x00 + uint8_t checksum; // checksum + }; +} __attribute__((packed)); + +class BL0939 : public PollingComponent, public uart::UARTDevice { + public: + void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; } + void set_current_sensor_1(sensor::Sensor *current_sensor_1) { current_sensor_1_ = current_sensor_1; } + void set_current_sensor_2(sensor::Sensor *current_sensor_2) { current_sensor_2_ = current_sensor_2; } + void set_power_sensor_1(sensor::Sensor *power_sensor_1) { power_sensor_1_ = power_sensor_1; } + void set_power_sensor_2(sensor::Sensor *power_sensor_2) { power_sensor_2_ = power_sensor_2; } + void set_energy_sensor_1(sensor::Sensor *energy_sensor_1) { energy_sensor_1_ = energy_sensor_1; } + void set_energy_sensor_2(sensor::Sensor *energy_sensor_2) { energy_sensor_2_ = energy_sensor_2; } + void set_energy_sensor_sum(sensor::Sensor *energy_sensor_sum) { energy_sensor_sum_ = energy_sensor_sum; } + + void loop() override; + + void update() override; + void setup() override; + void dump_config() override; + + protected: + sensor::Sensor *voltage_sensor_; + sensor::Sensor *current_sensor_1_; + sensor::Sensor *current_sensor_2_; + // NB This may be negative as the circuits is seemingly able to measure + // power in both directions + sensor::Sensor *power_sensor_1_; + sensor::Sensor *power_sensor_2_; + sensor::Sensor *energy_sensor_1_; + sensor::Sensor *energy_sensor_2_; + sensor::Sensor *energy_sensor_sum_; + + // Divide by this to turn into Watt + float power_reference_ = BL0939_PREF; + // Divide by this to turn into Volt + float voltage_reference_ = BL0939_UREF; + // Divide by this to turn into Ampere + float current_reference_ = BL0939_IREF; + // Divide by this to turn into kWh + float energy_reference_ = BL0939_EREF; + + static uint32_t to_uint32_t(ube24_t input); + + static int32_t to_int32_t(sbe24_t input); + + static bool validate_checksum(const DataPacket *data); + + void received_package_(const DataPacket *data) const; +}; +} // namespace bl0939 +} // namespace esphome diff --git a/esphome/components/bl0939/sensor.py b/esphome/components/bl0939/sensor.py new file mode 100644 index 0000000000..bcc72ad61a --- /dev/null +++ b/esphome/components/bl0939/sensor.py @@ -0,0 +1,123 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, uart +from esphome.const import ( + CONF_ID, + CONF_VOLTAGE, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_VOLTAGE, + STATE_CLASS_MEASUREMENT, + UNIT_AMPERE, + UNIT_KILOWATT_HOURS, + UNIT_VOLT, + UNIT_WATT, +) + +DEPENDENCIES = ["uart"] + +CONF_CURRENT_1 = "current_1" +CONF_CURRENT_2 = "current_2" +CONF_ACTIVE_POWER_1 = "active_power_1" +CONF_ACTIVE_POWER_2 = "active_power_2" +CONF_ENERGY_1 = "energy_1" +CONF_ENERGY_2 = "energy_2" +CONF_ENERGY_TOTAL = "energy_total" + +bl0939_ns = cg.esphome_ns.namespace("bl0939") +BL0939 = bl0939_ns.class_("BL0939", cg.PollingComponent, uart.UARTDevice) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(BL0939), + cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_CURRENT_1): sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_CURRENT_2): sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_ACTIVE_POWER_1): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_ACTIVE_POWER_2): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_ENERGY_1): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOWATT_HOURS, + accuracy_decimals=3, + device_class=DEVICE_CLASS_ENERGY, + ), + cv.Optional(CONF_ENERGY_2): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOWATT_HOURS, + accuracy_decimals=3, + device_class=DEVICE_CLASS_ENERGY, + ), + cv.Optional(CONF_ENERGY_TOTAL): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOWATT_HOURS, + accuracy_decimals=3, + device_class=DEVICE_CLASS_ENERGY, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(uart.UART_DEVICE_SCHEMA) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) + + if CONF_VOLTAGE in config: + conf = config[CONF_VOLTAGE] + sens = await sensor.new_sensor(conf) + cg.add(var.set_voltage_sensor(sens)) + if CONF_CURRENT_1 in config: + conf = config[CONF_CURRENT_1] + sens = await sensor.new_sensor(conf) + cg.add(var.set_current_sensor_1(sens)) + if CONF_CURRENT_2 in config: + conf = config[CONF_CURRENT_2] + sens = await sensor.new_sensor(conf) + cg.add(var.set_current_sensor_2(sens)) + if CONF_ACTIVE_POWER_1 in config: + conf = config[CONF_ACTIVE_POWER_1] + sens = await sensor.new_sensor(conf) + cg.add(var.set_power_sensor_1(sens)) + if CONF_ACTIVE_POWER_2 in config: + conf = config[CONF_ACTIVE_POWER_2] + sens = await sensor.new_sensor(conf) + cg.add(var.set_power_sensor_2(sens)) + if CONF_ENERGY_1 in config: + conf = config[CONF_ENERGY_1] + sens = await sensor.new_sensor(conf) + cg.add(var.set_energy_sensor_1(sens)) + if CONF_ENERGY_2 in config: + conf = config[CONF_ENERGY_2] + sens = await sensor.new_sensor(conf) + cg.add(var.set_energy_sensor_2(sens)) + if CONF_ENERGY_TOTAL in config: + conf = config[CONF_ENERGY_TOTAL] + sens = await sensor.new_sensor(conf) + cg.add(var.set_energy_sensor_sum(sens)) diff --git a/tests/test3.yaml b/tests/test3.yaml index 58cb14740f..29a70d3cc3 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -256,6 +256,12 @@ uart: tx_pin: GPIO4 rx_pin: GPIO5 baud_rate: 38400 + - id: uart8 + tx_pin: GPIO4 + rx_pin: GPIO5 + baud_rate: 4800 + parity: NONE + stop_bits: 2 # Specifically added for testing debug with no options at all. debug: @@ -477,6 +483,24 @@ sensor: active_power_b: name: ADE7953 Active Power B id: ade7953_active_power_b + - platform: bl0939 + uart_id: uart8 + voltage: + name: 'BL0939 Voltage' + current_1: + name: 'BL0939 Current 1' + current_2: + name: 'BL0939 Current 2' + active_power_1: + name: 'BL0939 Active Power 1' + active_power_2: + name: 'BL0939 Active Power 2' + energy_1: + name: 'BL0939 Energy 1' + energy_2: + name: 'BL0939 Energy 2' + energy_total: + name: 'BL0939 Total energy' - platform: bl0940 uart_id: uart3 voltage: From 757b98748b7e6d13cf21ecf379870eb4efe94fa4 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 22 Apr 2022 17:08:01 +1200 Subject: [PATCH 0408/1729] Add "esphome rename" command (#3403) * Add "esphome rename" command * Only open file once * Update esphome/__main__.py Co-authored-by: Paulus Schoutsen * Add final return * Use match.group consistently * Validate name characters * Add whitespace to regex so it is only replacing exact match * Validate yaml config file after manipulation Co-authored-by: Paulus Schoutsen --- esphome/__main__.py | 101 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/esphome/__main__.py b/esphome/__main__.py index 85cf4ede85..00770d6f05 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -2,6 +2,7 @@ import argparse import functools import logging import os +import re import sys from datetime import datetime @@ -9,15 +10,18 @@ from esphome import const, writer, yaml_util import esphome.codegen as cg from esphome.config import iter_components, read_config, strip_default_ids from esphome.const import ( + ALLOWED_NAME_CHARS, CONF_BAUD_RATE, CONF_BROKER, CONF_DEASSERT_RTS_DTR, CONF_LOGGER, + CONF_NAME, CONF_OTA, CONF_PASSWORD, CONF_PORT, CONF_ESPHOME, CONF_PLATFORMIO_OPTIONS, + CONF_SUBSTITUTIONS, SECRETS_FILES, ) from esphome.core import CORE, EsphomeError, coroutine @@ -481,6 +485,96 @@ def command_idedata(args, config): return 0 +def command_rename(args, config): + for c in args.name: + if c not in ALLOWED_NAME_CHARS: + print( + color( + Fore.BOLD_RED, + f"'{c}' is an invalid character for names. Valid characters are: " + f"{ALLOWED_NAME_CHARS} (lowercase, no spaces)", + ) + ) + return 1 + with open(CORE.config_path, mode="r+", encoding="utf-8") as raw_file: + raw_contents = raw_file.read() + yaml = yaml_util.load_yaml(CORE.config_path) + if CONF_ESPHOME not in yaml or CONF_NAME not in yaml[CONF_ESPHOME]: + print( + color( + Fore.BOLD_RED, "Complex YAML files cannot be automatically renamed." + ) + ) + return 1 + old_name = yaml[CONF_ESPHOME][CONF_NAME] + match = re.match(r"^\$\{?([a-zA-Z0-9_]+)\}?$", old_name) + if match is None: + new_raw = re.sub( + rf"name:\s+[\"']?{old_name}[\"']?", + f'name: "{args.name}"', + raw_contents, + ) + else: + old_name = yaml[CONF_SUBSTITUTIONS][match.group(1)] + if ( + len( + re.findall( + rf"^\s+{match.group(1)}:\s+[\"']?{old_name}[\"']?", + raw_contents, + flags=re.MULTILINE, + ) + ) + > 1 + ): + print(color(Fore.BOLD_RED, "Too many matches in YAML to safely rename")) + return 1 + + new_raw = re.sub( + rf"^(\s+{match.group(1)}):\s+[\"']?{old_name}[\"']?", + f'\\1: "{args.name}"', + raw_contents, + flags=re.MULTILINE, + ) + + raw_file.seek(0) + raw_file.write(new_raw) + raw_file.flush() + + print(f"Updating {color(Fore.CYAN, CORE.config_path)}") + print() + + rc = run_external_process("esphome", "config", CORE.config_path) + if rc != 0: + raw_file.seek(0) + raw_file.write(raw_contents) + print(color(Fore.BOLD_RED, "Rename failed. Reverting changes.")) + return 1 + + cli_args = [ + "run", + CORE.config_path, + "--no-logs", + "--device", + CORE.address, + ] + + if args.dashboard: + cli_args.insert(0, "--dashboard") + + try: + rc = run_external_process("esphome", *cli_args) + except KeyboardInterrupt: + rc = 1 + if rc != 0: + raw_file.seek(0) + raw_file.write(raw_contents) + return 1 + + print(color(Fore.BOLD_GREEN, "SUCCESS")) + print() + return 0 + + PRE_CONFIG_ACTIONS = { "wizard": command_wizard, "version": command_version, @@ -499,6 +593,7 @@ POST_CONFIG_ACTIONS = { "mqtt-fingerprint": command_mqtt_fingerprint, "clean": command_clean, "idedata": command_idedata, + "rename": command_rename, } @@ -681,6 +776,12 @@ def parse_args(argv): "configuration", help="Your YAML configuration file(s).", nargs=1 ) + parser_rename = subparsers.add_parser("rename") + parser_rename.add_argument( + "configuration", help="Your YAML configuration file.", nargs=1 + ) + parser_rename.add_argument("name", help="The new name for the device.", type=str) + # Keep backward compatibility with the old command line format of # esphome . # From 6fe22a7e629edf9243ba822d77a75f4587e6afd2 Mon Sep 17 00:00:00 2001 From: Martin <25747549+martgras@users.noreply.github.com> Date: Mon, 25 Apr 2022 23:50:36 +0200 Subject: [PATCH 0409/1729] SPS30: Add fan action (#3410) * Add fan action to SPS30 * add codeowner --- CODEOWNERS | 1 + esphome/components/sps30/automation.h | 21 +++++++++++++++++++ esphome/components/sps30/sensor.py | 27 ++++++++++++++++++++++++ esphome/components/sps30/sps30.cpp | 30 ++++++++++++++++++++++++++- esphome/components/sps30/sps30.h | 5 ++++- 5 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 esphome/components/sps30/automation.h diff --git a/CODEOWNERS b/CODEOWNERS index 7fd049f46e..e2a356360a 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -183,6 +183,7 @@ esphome/components/sm2135/* @BoukeHaarsma23 esphome/components/socket/* @esphome/core esphome/components/sonoff_d1/* @anatoly-savchenkov esphome/components/spi/* @esphome/core +esphome/components/sps30/* @martgras esphome/components/ssd1322_base/* @kbx81 esphome/components/ssd1322_spi/* @kbx81 esphome/components/ssd1325_base/* @kbx81 diff --git a/esphome/components/sps30/automation.h b/esphome/components/sps30/automation.h new file mode 100644 index 0000000000..443aafb575 --- /dev/null +++ b/esphome/components/sps30/automation.h @@ -0,0 +1,21 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "sps30.h" + +namespace esphome { +namespace sps30 { + +template class StartFanAction : public Action { + public: + explicit StartFanAction(SPS30Component *sps30) : sps30_(sps30) {} + + void play(Ts... x) override { this->sps30_->start_fan_cleaning(); } + + protected: + SPS30Component *sps30_; +}; + +} // namespace sps30 +} // namespace esphome diff --git a/esphome/components/sps30/sensor.py b/esphome/components/sps30/sensor.py index 89cb25c24f..ff8d5a3594 100644 --- a/esphome/components/sps30/sensor.py +++ b/esphome/components/sps30/sensor.py @@ -1,6 +1,8 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor, sensirion_common +from esphome import automation +from esphome.automation import maybe_simple_id from esphome.const import ( CONF_ID, CONF_PM_1_0, @@ -25,6 +27,7 @@ from esphome.const import ( ICON_RULER, ) +CODEOWNERS = ["@martgras"] DEPENDENCIES = ["i2c"] AUTO_LOAD = ["sensirion_common"] @@ -33,6 +36,11 @@ SPS30Component = sps30_ns.class_( "SPS30Component", cg.PollingComponent, sensirion_common.SensirionI2CDevice ) +# Actions +StartFanAction = sps30_ns.class_("StartFanAction", automation.Action) + +CONF_AUTO_CLEANING_INTERVAL = "auto_cleaning_interval" + CONFIG_SCHEMA = ( cv.Schema( { @@ -100,6 +108,7 @@ CONFIG_SCHEMA = ( accuracy_decimals=0, state_class=STATE_CLASS_MEASUREMENT, ), + cv.Optional(CONF_AUTO_CLEANING_INTERVAL): cv.update_interval, } ) .extend(cv.polling_component_schema("60s")) @@ -151,3 +160,21 @@ async def to_code(config): if CONF_PM_SIZE in config: sens = await sensor.new_sensor(config[CONF_PM_SIZE]) cg.add(var.set_pm_size_sensor(sens)) + + if CONF_AUTO_CLEANING_INTERVAL in config: + cg.add(var.set_auto_cleaning_interval(config[CONF_AUTO_CLEANING_INTERVAL])) + + +SPS30_ACTION_SCHEMA = maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(SPS30Component), + } +) + + +@automation.register_action( + "sps30.start_fan_autoclean", StartFanAction, SPS30_ACTION_SCHEMA +) +async def sps30_fan_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) diff --git a/esphome/components/sps30/sps30.cpp b/esphome/components/sps30/sps30.cpp index 2885125a8a..cdcd4a6a54 100644 --- a/esphome/components/sps30/sps30.cpp +++ b/esphome/components/sps30/sps30.cpp @@ -1,5 +1,6 @@ -#include "sps30.h" +#include "esphome/core/hal.h" #include "esphome/core/log.h" +#include "sps30.h" namespace esphome { namespace sps30 { @@ -44,6 +45,22 @@ void SPS30Component::setup() { this->serial_number_[i * 2 + 1] = uint16_t(uint16_t(raw_serial_number[i] & 0xFF)); } ESP_LOGD(TAG, " Serial Number: '%s'", this->serial_number_); + + bool result; + if (this->fan_interval_.has_value()) { + // override default value + result = write_command(SPS30_CMD_SET_AUTOMATIC_CLEANING_INTERVAL_SECONDS, this->fan_interval_.value()); + } else { + result = write_command(SPS30_CMD_SET_AUTOMATIC_CLEANING_INTERVAL_SECONDS); + } + if (result) { + delay(20); + uint16_t secs[2]; + if (this->read_data(secs, 2)) { + fan_interval_ = secs[0] << 16 | secs[1]; + } + } + this->status_clear_warning(); this->skipped_data_read_cycles_ = 0; this->start_continuous_measurement_(); @@ -206,5 +223,16 @@ bool SPS30Component::start_continuous_measurement_() { return true; } +bool SPS30Component::start_fan_cleaning() { + if (!write_command(SPS30_CMD_START_FAN_CLEANING)) { + this->status_set_warning(); + ESP_LOGE(TAG, "write error start fan (%d)", this->last_error_); + return false; + } else { + ESP_LOGD(TAG, "Fan auto clean started"); + } + return true; +} + } // namespace sps30 } // namespace esphome diff --git a/esphome/components/sps30/sps30.h b/esphome/components/sps30/sps30.h index 9a93df8597..cf2e7a7d4f 100644 --- a/esphome/components/sps30/sps30.h +++ b/esphome/components/sps30/sps30.h @@ -22,12 +22,14 @@ class SPS30Component : public PollingComponent, public sensirion_common::Sensiri void set_pmc_10_0_sensor(sensor::Sensor *pmc_10_0) { pmc_10_0_sensor_ = pmc_10_0; } void set_pm_size_sensor(sensor::Sensor *pm_size) { pm_size_sensor_ = pm_size; } - + void set_auto_cleaning_interval(uint32_t auto_cleaning_interval) { fan_interval_ = auto_cleaning_interval; } void setup() override; void update() override; void dump_config() override; float get_setup_priority() const override { return setup_priority::DATA; } + bool start_fan_cleaning(); + protected: char serial_number_[17] = {0}; /// Terminating NULL character uint16_t raw_firmware_version_; @@ -54,6 +56,7 @@ class SPS30Component : public PollingComponent, public sensirion_common::Sensiri sensor::Sensor *pmc_4_0_sensor_{nullptr}; sensor::Sensor *pmc_10_0_sensor_{nullptr}; sensor::Sensor *pm_size_sensor_{nullptr}; + optional fan_interval_; }; } // namespace sps30 From 3346bc8bba12add5050eb8b10078e7dc7872ed85 Mon Sep 17 00:00:00 2001 From: quentin9696 Date: Mon, 25 Apr 2022 18:09:49 -0400 Subject: [PATCH 0410/1729] feat: add openssh-client on docker image (#1681) (#3319) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- docker/Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/docker/Dockerfile b/docker/Dockerfile index 610b689298..dc8ba03f48 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -30,6 +30,7 @@ RUN \ iputils-ping=3:20210202-1 \ git=1:2.30.2-1 \ curl=7.74.0-1.3+deb11u1 \ + openssh-client=1:8.4p1-5 \ && rm -rf \ /tmp/* \ /var/{cache,log}/* \ From 256395c28d615197e129ebafccd4e530a32f8d5b Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 26 Apr 2022 21:02:08 +1200 Subject: [PATCH 0411/1729] Add duration device class for sensors (#3421) --- esphome/components/sensor/__init__.py | 2 ++ esphome/const.py | 1 + 2 files changed, 3 insertions(+) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 0c38ceeb37..d01a594889 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -29,6 +29,7 @@ from esphome.const import ( CONF_WINDOW_SIZE, CONF_MQTT_ID, CONF_FORCE_UPDATE, + DEVICE_CLASS_DURATION, DEVICE_CLASS_EMPTY, DEVICE_CLASS_AQI, DEVICE_CLASS_BATTERY, @@ -70,6 +71,7 @@ DEVICE_CLASSES = [ DEVICE_CLASS_CARBON_DIOXIDE, DEVICE_CLASS_CARBON_MONOXIDE, DEVICE_CLASS_CURRENT, + DEVICE_CLASS_DURATION, DEVICE_CLASS_ENERGY, DEVICE_CLASS_GAS, DEVICE_CLASS_HUMIDITY, diff --git a/esphome/const.py b/esphome/const.py index fa5baf4fe2..9f2bed28d1 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -905,6 +905,7 @@ DEVICE_CLASS_AQI = "aqi" DEVICE_CLASS_CARBON_DIOXIDE = "carbon_dioxide" DEVICE_CLASS_CARBON_MONOXIDE = "carbon_monoxide" DEVICE_CLASS_CURRENT = "current" +DEVICE_CLASS_DURATION = "duration" DEVICE_CLASS_ENERGY = "energy" DEVICE_CLASS_HUMIDITY = "humidity" DEVICE_CLASS_ILLUMINANCE = "illuminance" From 2bff9937b761f61821c804703b021f1c7c910fc4 Mon Sep 17 00:00:00 2001 From: code-review-doctor <72647856+code-review-doctor@users.noreply.github.com> Date: Tue, 26 Apr 2022 20:43:35 +0100 Subject: [PATCH 0412/1729] Fix issue probably-meant-fstring found at https://codereview.doctor (#3415) --- esphome/components/esp32/gpio.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/esp32/gpio.py b/esphome/components/esp32/gpio.py index 5819943f37..6c3fa92fcd 100644 --- a/esphome/components/esp32/gpio.py +++ b/esphome/components/esp32/gpio.py @@ -107,7 +107,7 @@ def validate_gpio_pin(value): value = _translate_pin(value) variant = CORE.data[KEY_ESP32][KEY_VARIANT] if variant not in _esp32_validations: - raise cv.Invalid("Unsupported ESP32 variant {variant}") + raise cv.Invalid(f"Unsupported ESP32 variant {variant}") return _esp32_validations[variant].pin_validation(value) @@ -121,7 +121,7 @@ def validate_supports(value): is_pulldown = mode[CONF_PULLDOWN] variant = CORE.data[KEY_ESP32][KEY_VARIANT] if variant not in _esp32_validations: - raise cv.Invalid("Unsupported ESP32 variant {variant}") + raise cv.Invalid(f"Unsupported ESP32 variant {variant}") if is_open_drain and not is_output: raise cv.Invalid( From ebf13a0ba0a48453c2cc80bfc942d6a588859233 Mon Sep 17 00:00:00 2001 From: Trevor North Date: Tue, 26 Apr 2022 20:51:22 +0100 Subject: [PATCH 0413/1729] Queue sensor publishes so we don't block for too long (#3422) --- .../components/bme680_bsec/bme680_bsec.cpp | 44 ++++++++++++------- esphome/components/bme680_bsec/bme680_bsec.h | 8 +++- 2 files changed, 33 insertions(+), 19 deletions(-) diff --git a/esphome/components/bme680_bsec/bme680_bsec.cpp b/esphome/components/bme680_bsec/bme680_bsec.cpp index 0a8ca7f3c3..b84ca3318b 100644 --- a/esphome/components/bme680_bsec/bme680_bsec.cpp +++ b/esphome/components/bme680_bsec/bme680_bsec.cpp @@ -169,6 +169,14 @@ void BME680BSECComponent::loop() { } else { this->status_clear_warning(); } + + // Process a single action from the queue. These are primarily sensor state publishes + // that in totality take too long to send in a single call. + if (this->queue_.size()) { + auto action = std::move(this->queue_.front()); + this->queue_.pop(); + action(); + } } void BME680BSECComponent::run_() { @@ -306,37 +314,39 @@ void BME680BSECComponent::read_(int64_t trigger_time_ns, bsec_bme_settings_t bme } void BME680BSECComponent::publish_(const bsec_output_t *outputs, uint8_t num_outputs) { - ESP_LOGV(TAG, "Publishing sensor states"); + ESP_LOGV(TAG, "Queuing sensor state publish actions"); for (uint8_t i = 0; i < num_outputs; i++) { + float signal = outputs[i].signal; switch (outputs[i].sensor_id) { case BSEC_OUTPUT_IAQ: - case BSEC_OUTPUT_STATIC_IAQ: - uint8_t accuracy; - accuracy = outputs[i].accuracy; - this->publish_sensor_state_(this->iaq_sensor_, outputs[i].signal); - this->publish_sensor_state_(this->iaq_accuracy_text_sensor_, IAQ_ACCURACY_STATES[accuracy]); - this->publish_sensor_state_(this->iaq_accuracy_sensor_, accuracy, true); + case BSEC_OUTPUT_STATIC_IAQ: { + uint8_t accuracy = outputs[i].accuracy; + this->queue_push_([this, signal]() { this->publish_sensor_(this->iaq_sensor_, signal); }); + this->queue_push_([this, accuracy]() { + this->publish_sensor_(this->iaq_accuracy_text_sensor_, IAQ_ACCURACY_STATES[accuracy]); + }); + this->queue_push_([this, accuracy]() { this->publish_sensor_(this->iaq_accuracy_sensor_, accuracy, true); }); // Queue up an opportunity to save state - this->defer("save_state", [this, accuracy]() { this->save_state_(accuracy); }); - break; + this->queue_push_([this, accuracy]() { this->save_state_(accuracy); }); + } break; case BSEC_OUTPUT_CO2_EQUIVALENT: - this->publish_sensor_state_(this->co2_equivalent_sensor_, outputs[i].signal); + this->queue_push_([this, signal]() { this->publish_sensor_(this->co2_equivalent_sensor_, signal); }); break; case BSEC_OUTPUT_BREATH_VOC_EQUIVALENT: - this->publish_sensor_state_(this->breath_voc_equivalent_sensor_, outputs[i].signal); + this->queue_push_([this, signal]() { this->publish_sensor_(this->breath_voc_equivalent_sensor_, signal); }); break; case BSEC_OUTPUT_RAW_PRESSURE: - this->publish_sensor_state_(this->pressure_sensor_, outputs[i].signal / 100.0f); + this->queue_push_([this, signal]() { this->publish_sensor_(this->pressure_sensor_, signal / 100.0f); }); break; case BSEC_OUTPUT_RAW_GAS: - this->publish_sensor_state_(this->gas_resistance_sensor_, outputs[i].signal); + this->queue_push_([this, signal]() { this->publish_sensor_(this->gas_resistance_sensor_, signal); }); break; case BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE: - this->publish_sensor_state_(this->temperature_sensor_, outputs[i].signal); + this->queue_push_([this, signal]() { this->publish_sensor_(this->temperature_sensor_, signal); }); break; case BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY: - this->publish_sensor_state_(this->humidity_sensor_, outputs[i].signal); + this->queue_push_([this, signal]() { this->publish_sensor_(this->humidity_sensor_, signal); }); break; } } @@ -352,14 +362,14 @@ int64_t BME680BSECComponent::get_time_ns_() { return (time_ms + ((int64_t) this->millis_overflow_counter_ << 32)) * INT64_C(1000000); } -void BME680BSECComponent::publish_sensor_state_(sensor::Sensor *sensor, float value, bool change_only) { +void BME680BSECComponent::publish_sensor_(sensor::Sensor *sensor, float value, bool change_only) { if (!sensor || (change_only && sensor->has_state() && sensor->state == value)) { return; } sensor->publish_state(value); } -void BME680BSECComponent::publish_sensor_state_(text_sensor::TextSensor *sensor, const std::string &value) { +void BME680BSECComponent::publish_sensor_(text_sensor::TextSensor *sensor, const std::string &value) { if (!sensor || (sensor->has_state() && sensor->state == value)) { return; } diff --git a/esphome/components/bme680_bsec/bme680_bsec.h b/esphome/components/bme680_bsec/bme680_bsec.h index 53bc5c3280..650b4d2413 100644 --- a/esphome/components/bme680_bsec/bme680_bsec.h +++ b/esphome/components/bme680_bsec/bme680_bsec.h @@ -70,12 +70,14 @@ class BME680BSECComponent : public Component, public i2c::I2CDevice { void publish_(const bsec_output_t *outputs, uint8_t num_outputs); int64_t get_time_ns_(); - void publish_sensor_state_(sensor::Sensor *sensor, float value, bool change_only = false); - void publish_sensor_state_(text_sensor::TextSensor *sensor, const std::string &value); + void publish_sensor_(sensor::Sensor *sensor, float value, bool change_only = false); + void publish_sensor_(text_sensor::TextSensor *sensor, const std::string &value); void load_state_(); void save_state_(uint8_t accuracy); + void queue_push_(std::function &&f) { this->queue_.push(std::move(f)); } + struct bme680_dev bme680_; bsec_library_return_t bsec_status_{BSEC_OK}; int8_t bme680_status_{BME680_OK}; @@ -84,6 +86,8 @@ class BME680BSECComponent : public Component, public i2c::I2CDevice { uint32_t millis_overflow_counter_{0}; int64_t next_call_ns_{0}; + std::queue> queue_; + ESPPreferenceObject bsec_state_; uint32_t state_save_interval_ms_{21600000}; // 6 hours - 4 times a day uint32_t last_state_save_ms_ = 0; From 68dfaf238b8d16f052d24086464fc5c4a476745d Mon Sep 17 00:00:00 2001 From: LuBeDa Date: Tue, 26 Apr 2022 22:41:10 +0200 Subject: [PATCH 0414/1729] added RGB565 image type (#3229) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/animation/__init__.py | 23 +++++++++++++ esphome/components/display/display_buffer.cpp | 32 +++++++++++++++++++ esphome/components/display/display_buffer.h | 3 ++ esphome/components/image/__init__.py | 16 ++++++++++ 4 files changed, 74 insertions(+) diff --git a/esphome/components/animation/__init__.py b/esphome/components/animation/__init__.py index 7c9ff07f97..4cf0b0ed7d 100644 --- a/esphome/components/animation/__init__.py +++ b/esphome/components/animation/__init__.py @@ -92,6 +92,29 @@ async def to_code(config): data[pos] = pix[2] pos += 1 + elif config[CONF_TYPE] == "RGB565": + data = [0 for _ in range(height * width * 2 * frames)] + pos = 0 + for frameIndex in range(frames): + image.seek(frameIndex) + frame = image.convert("RGB") + 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: + R = pix[0] >> 3 + G = pix[1] >> 2 + B = pix[2] >> 3 + rgb = (R << 11) | (G << 5) | B + data[pos] = rgb >> 8 + pos += 1 + data[pos] = rgb & 255 + pos += 1 + elif config[CONF_TYPE] == "BINARY": width8 = ((width + 7) // 8) * 8 data = [0 for _ in range((height * width8 // 8) * frames)] diff --git a/esphome/components/display/display_buffer.cpp b/esphome/components/display/display_buffer.cpp index 4ad353a254..d00fdd5240 100644 --- a/esphome/components/display/display_buffer.cpp +++ b/esphome/components/display/display_buffer.cpp @@ -242,6 +242,13 @@ void DisplayBuffer::image(int x, int y, Image *image, Color color_on, Color colo } } break; + case IMAGE_TYPE_RGB565: + for (int img_x = 0; img_x < image->get_width(); img_x++) { + for (int img_y = 0; img_y < image->get_height(); img_y++) { + this->draw_pixel_at(x + img_x, y + img_y, image->get_rgb565_pixel(img_x, img_y)); + } + } + break; } } @@ -497,6 +504,17 @@ Color Image::get_color_pixel(int x, int y) const { (progmem_read_byte(this->data_start_ + pos + 0) << 16); return Color(color32); } +Color Image::get_rgb565_pixel(int x, int y) const { + if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) + return Color::BLACK; + const uint32_t pos = (x + y * this->width_) * 2; + uint16_t rgb565 = + progmem_read_byte(this->data_start_ + pos + 0) << 8 | progmem_read_byte(this->data_start_ + pos + 1); + auto r = (rgb565 & 0xF800) >> 11; + auto g = (rgb565 & 0x07E0) >> 5; + auto b = rgb565 & 0x001F; + return Color((r << 3) | (r >> 2), (g << 2) | (g >> 4), (b << 3) | (b >> 2)); +} Color Image::get_grayscale_pixel(int x, int y) const { if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) return Color::BLACK; @@ -532,6 +550,20 @@ Color Animation::get_color_pixel(int x, int y) const { (progmem_read_byte(this->data_start_ + pos + 0) << 16); return Color(color32); } +Color Animation::get_rgb565_pixel(int x, int y) const { + if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) + return Color::BLACK; + const uint32_t frame_index = this->width_ * this->height_ * this->current_frame_; + if (frame_index >= (uint32_t)(this->width_ * this->height_ * this->animation_frame_count_)) + return Color::BLACK; + const uint32_t pos = (x + y * this->width_ + frame_index) * 2; + uint16_t rgb565 = + progmem_read_byte(this->data_start_ + pos + 0) << 8 | progmem_read_byte(this->data_start_ + pos + 1); + auto r = (rgb565 & 0xF800) >> 11; + auto g = (rgb565 & 0x07E0) >> 5; + auto b = rgb565 & 0x001F; + return Color((r << 3) | (r >> 2), (g << 2) | (g >> 4), (b << 3) | (b >> 2)); +} Color Animation::get_grayscale_pixel(int x, int y) const { if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) return Color::BLACK; diff --git a/esphome/components/display/display_buffer.h b/esphome/components/display/display_buffer.h index 8ee1cd8779..86221c5f96 100644 --- a/esphome/components/display/display_buffer.h +++ b/esphome/components/display/display_buffer.h @@ -82,6 +82,7 @@ enum ImageType { IMAGE_TYPE_GRAYSCALE = 1, IMAGE_TYPE_RGB24 = 2, IMAGE_TYPE_TRANSPARENT_BINARY = 3, + IMAGE_TYPE_RGB565 = 4, }; enum DisplayRotation { @@ -453,6 +454,7 @@ class Image { Image(const uint8_t *data_start, int width, int height, ImageType type); virtual bool get_pixel(int x, int y) const; virtual Color get_color_pixel(int x, int y) const; + virtual Color get_rgb565_pixel(int x, int y) const; virtual Color get_grayscale_pixel(int x, int y) const; int get_width() const; int get_height() const; @@ -470,6 +472,7 @@ class Animation : public Image { Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, ImageType type); bool get_pixel(int x, int y) const override; Color get_color_pixel(int x, int y) const override; + Color get_rgb565_pixel(int x, int y) const override; Color get_grayscale_pixel(int x, int y) const override; int get_animation_frame_count() const; diff --git a/esphome/components/image/__init__.py b/esphome/components/image/__init__.py index 70d77dfd14..0004391f20 100644 --- a/esphome/components/image/__init__.py +++ b/esphome/components/image/__init__.py @@ -25,6 +25,7 @@ IMAGE_TYPE = { "GRAYSCALE": ImageType.IMAGE_TYPE_GRAYSCALE, "RGB24": ImageType.IMAGE_TYPE_RGB24, "TRANSPARENT_BINARY": ImageType.IMAGE_TYPE_TRANSPARENT_BINARY, + "RGB565": ImageType.IMAGE_TYPE_RGB565, } Image_ = display.display_ns.class_("Image") @@ -89,6 +90,21 @@ async def to_code(config): data[pos] = pix[2] pos += 1 + elif config[CONF_TYPE] == "RGB565": + image = image.convert("RGB") + pixels = list(image.getdata()) + data = [0 for _ in range(height * width * 3)] + pos = 0 + for pix in pixels: + R = pix[0] >> 3 + G = pix[1] >> 2 + B = pix[2] >> 3 + rgb = (R << 11) | (G << 5) | B + data[pos] = rgb >> 8 + pos += 1 + data[pos] = rgb & 255 + pos += 1 + elif config[CONF_TYPE] == "BINARY": image = image.convert("1", dither=dither) width8 = ((width + 7) // 8) * 8 From 91895aa70c6f82866c182dee626b017ca2dd36a8 Mon Sep 17 00:00:00 2001 From: Dan Jackson Date: Tue, 3 May 2022 00:09:06 -0700 Subject: [PATCH 0415/1729] Allow wifi output_power down to 8.5dB (#3405) --- esphome/components/wifi/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index b56902df2f..c3f70506e2 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -270,7 +270,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_FAST_CONNECT, default=False): cv.boolean, cv.Optional(CONF_USE_ADDRESS): cv.string_strict, cv.SplitDefault(CONF_OUTPUT_POWER, esp8266=20.0): cv.All( - cv.decibel, cv.float_range(min=10.0, max=20.5) + cv.decibel, cv.float_range(min=8.5, max=20.5) ), cv.Optional("enable_mdns"): cv.invalid( "This option has been removed. Please use the [disabled] option under the " From 64fb39a653be8633942cce7733f8379b6b0550ca Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 9 May 2022 10:18:24 +1200 Subject: [PATCH 0416/1729] Add help text to rename command (#3442) --- esphome/__main__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index 00770d6f05..80e8455465 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -776,7 +776,10 @@ def parse_args(argv): "configuration", help="Your YAML configuration file(s).", nargs=1 ) - parser_rename = subparsers.add_parser("rename") + parser_rename = subparsers.add_parser( + "rename", + help="Rename a device in YAML, compile the binary and upload it.", + ) parser_rename.add_argument( "configuration", help="Your YAML configuration file.", nargs=1 ) From 7c30d6254e0ee2d9656525cd2345411522972ec4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 8 May 2022 18:53:34 -0700 Subject: [PATCH 0417/1729] Add rename command handler (#3443) --- esphome/dashboard/dashboard.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index af68f2ae08..b78d22cf7c 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -283,6 +283,18 @@ class EsphomeLogsHandler(EsphomeCommandWebSocket): ] +class EsphomeRenameHandler(EsphomeCommandWebSocket): + def build_command(self, json_message): + config_file = settings.rel_path(json_message["configuration"]) + return [ + "esphome", + "--dashboard", + "rename", + config_file, + json_message["newName"], + ] + + class EsphomeUploadHandler(EsphomeCommandWebSocket): def build_command(self, json_message): config_file = settings.rel_path(json_message["configuration"]) @@ -971,6 +983,7 @@ def make_app(debug=get_bool_env(ENV_DEV)): (f"{rel}devices", ListDevicesHandler), (f"{rel}import", ImportRequestHandler), (f"{rel}secret_keys", SecretKeysRequestHandler), + (f"{rel}rename", EsphomeRenameHandler), ], **app_settings, ) From d2f37cf3f9a87576f7a1b069cf5d9774e9caffaa Mon Sep 17 00:00:00 2001 From: Jens-Christian Skibakk Date: Mon, 9 May 2022 06:17:22 +0200 Subject: [PATCH 0418/1729] Support for Arduino 2 and serial port on ESP32-S2 and ESP32-C3 (#3436) --- .../improv_serial/improv_serial_component.h | 2 +- esphome/components/logger/logger.cpp | 22 ++++++++++--------- esphome/components/logger/logger.h | 4 ++-- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/esphome/components/improv_serial/improv_serial_component.h b/esphome/components/improv_serial/improv_serial_component.h index c6b980ab99..6be5704b71 100644 --- a/esphome/components/improv_serial/improv_serial_component.h +++ b/esphome/components/improv_serial/improv_serial_component.h @@ -51,7 +51,7 @@ class ImprovSerialComponent : public Component { void write_data_(std::vector &data); #ifdef USE_ARDUINO - HardwareSerial *hw_serial_{nullptr}; + Stream *hw_serial_{nullptr}; #endif #ifdef USE_ESP_IDF uart_port_t uart_num_; diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 11c0733701..3f4e4e7753 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -149,13 +149,25 @@ void Logger::pre_setup() { case UART_SELECTION_UART0_SWAP: #endif this->hw_serial_ = &Serial; + Serial.begin(this->baud_rate_); +#ifdef USE_ESP8266 + if (this->uart_ == UART_SELECTION_UART0_SWAP) { + Serial.swap(); + } + Serial.setDebugOutput(ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE); +#endif break; case UART_SELECTION_UART1: this->hw_serial_ = &Serial1; + Serial1.begin(this->baud_rate_); +#ifdef USE_ESP8266 + Serial1.setDebugOutput(ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE); +#endif break; #if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32S2) case UART_SELECTION_UART2: this->hw_serial_ = &Serial2; + Serial2.begin(this->baud_rate_); break; #endif } @@ -186,16 +198,6 @@ void Logger::pre_setup() { // Install UART driver using an event queue here uart_driver_install(uart_num_, uart_buffer_size, uart_buffer_size, 10, nullptr, 0); #endif - -#ifdef USE_ARDUINO - this->hw_serial_->begin(this->baud_rate_); -#ifdef USE_ESP8266 - if (this->uart_ == UART_SELECTION_UART0_SWAP) { - this->hw_serial_->swap(); - } - this->hw_serial_->setDebugOutput(ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE); -#endif -#endif // USE_ARDUINO } #ifdef USE_ESP8266 else { diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 8756bc2387..fa93972e19 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -40,7 +40,7 @@ class Logger : public Component { void set_baud_rate(uint32_t baud_rate); uint32_t get_baud_rate() const { return baud_rate_; } #ifdef USE_ARDUINO - HardwareSerial *get_hw_serial() const { return hw_serial_; } + Stream *get_hw_serial() const { return hw_serial_; } #endif #ifdef USE_ESP_IDF uart_port_t get_uart_num() const { return uart_num_; } @@ -119,7 +119,7 @@ class Logger : public Component { int tx_buffer_size_{0}; UARTSelection uart_{UART_SELECTION_UART0}; #ifdef USE_ARDUINO - HardwareSerial *hw_serial_{nullptr}; + Stream *hw_serial_{nullptr}; #endif #ifdef USE_ESP_IDF uart_port_t uart_num_; From 6f88f0ea3f229ef91652ba35d88d2d5116ca941c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 8 May 2022 22:17:21 -0700 Subject: [PATCH 0419/1729] Bump dashboard to 20220508.0 (#3448) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 465d961cb6..543999a9f3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ pyserial==3.5 platformio==5.2.5 # When updating platformio, also update Dockerfile esptool==3.3 click==8.1.2 -esphome-dashboard==20220309.0 +esphome-dashboard==20220508.0 aioesphomeapi==10.8.2 zeroconf==0.38.4 From 8e3af515c9941d4cd9535cb4388ce31c724fce18 Mon Sep 17 00:00:00 2001 From: Patrick van der Leer Date: Mon, 9 May 2022 07:17:36 +0200 Subject: [PATCH 0420/1729] Waveshare epaper 7in5 v2alt (#3276) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- .../components/waveshare_epaper/display.py | 4 + .../waveshare_epaper/waveshare_epaper.cpp | 137 ++++++++++++++++++ .../waveshare_epaper/waveshare_epaper.h | 19 +++ 3 files changed, 160 insertions(+) diff --git a/esphome/components/waveshare_epaper/display.py b/esphome/components/waveshare_epaper/display.py index fe5b51290e..a9d8350404 100644 --- a/esphome/components/waveshare_epaper/display.py +++ b/esphome/components/waveshare_epaper/display.py @@ -50,6 +50,9 @@ WaveshareEPaper7P5InBV2 = waveshare_epaper_ns.class_( WaveshareEPaper7P5InV2 = waveshare_epaper_ns.class_( "WaveshareEPaper7P5InV2", WaveshareEPaper ) +WaveshareEPaper7P5InV2alt = waveshare_epaper_ns.class_( + "WaveshareEPaper7P5InV2alt", WaveshareEPaper +) WaveshareEPaper7P5InHDB = waveshare_epaper_ns.class_( "WaveshareEPaper7P5InHDB", WaveshareEPaper ) @@ -79,6 +82,7 @@ MODELS = { "7.50in-bv2": ("b", WaveshareEPaper7P5InBV2), "7.50in-bc": ("b", WaveshareEPaper7P5InBC), "7.50inv2": ("b", WaveshareEPaper7P5InV2), + "7.50inv2alt": ("b", WaveshareEPaper7P5InV2alt), "7.50in-hd-b": ("b", WaveshareEPaper7P5InHDB), "2.13in-ttgo-dke": ("c", WaveshareEPaper2P13InDKE), } diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.cpp b/esphome/components/waveshare_epaper/waveshare_epaper.cpp index 59b3e90b03..5580674c34 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.cpp +++ b/esphome/components/waveshare_epaper/waveshare_epaper.cpp @@ -1137,6 +1137,143 @@ void HOT WaveshareEPaper7P5InV2::display() { int WaveshareEPaper7P5InV2::get_width_internal() { return 800; } int WaveshareEPaper7P5InV2::get_height_internal() { return 480; } void WaveshareEPaper7P5InV2::dump_config() { + LOG_DISPLAY("", "Waveshare E-Paper", this); + ESP_LOGCONFIG(TAG, " Model: 7.5inV2rev2"); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + LOG_PIN(" DC Pin: ", this->dc_pin_); + LOG_PIN(" Busy Pin: ", this->busy_pin_); + LOG_UPDATE_INTERVAL(this); +} + +/* 7.50inV2alt */ +bool WaveshareEPaper7P5InV2alt::wait_until_idle_() { + if (this->busy_pin_ == nullptr) { + return true; + } + + const uint32_t start = millis(); + while (this->busy_pin_->digital_read()) { + this->command(0x71); + if (millis() - start > this->idle_timeout_()) { + ESP_LOGI(TAG, "Timeout while displaying image!"); + return false; + } + delay(10); + } + return true; +} + +void WaveshareEPaper7P5InV2alt::initialize() { + this->reset_(); + + // COMMAND POWER SETTING + this->command(0x01); + + // 1-0=11: internal power + this->data(0x17); + + this->data(0x17); // VGH&VGL + this->data(0x3F); // VSH + this->data(0x3F); // VSL + this->data(0x11); // VSHR + + // 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); + + // OSC Setting + this->command(0x30); + this->data(0x06); // 2-0=100: N=4 ; 5-3=111: M=7 ; 3C=50Hz 3A=100HZ + + // POWER ON + this->command(0x04); + + delay(100); // NOLINT + this->wait_until_idle_(); + // COMMAND PANEL SETTING + this->command(0x00); + this->data(0x3F); // KW-3f KWR-2F BWROTP 0f BWOTP 1f + + // COMMAND RESOLUTION SETTING + this->command(0x61); + this->data(0x03); // source 800 + 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(0x10); + this->data(0x07); + // COMMAND TCON SETTING + this->command(0x60); + this->data(0x22); + // Resolution setting + this->command(0x65); + this->data(0x00); + this->data(0x00); // 800*480 + this->data(0x00); + this->data(0x00); + + this->wait_until_idle_(); + + uint8_t lut_vcom_7_i_n5_v2[] = { + 0x0, 0xF, 0xF, 0x0, 0x0, 0x1, 0x0, 0xF, 0x1, 0xF, 0x1, 0x2, 0x0, 0xF, 0xF, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + }; + + uint8_t lut_ww_7_i_n5_v2[] = { + 0x10, 0xF, 0xF, 0x0, 0x0, 0x1, 0x84, 0xF, 0x1, 0xF, 0x1, 0x2, 0x20, 0xF, 0xF, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + }; + + uint8_t lut_bw_7_i_n5_v2[] = { + 0x10, 0xF, 0xF, 0x0, 0x0, 0x1, 0x84, 0xF, 0x1, 0xF, 0x1, 0x2, 0x20, 0xF, 0xF, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + }; + + uint8_t lut_wb_7_i_n5_v2[] = { + 0x80, 0xF, 0xF, 0x0, 0x0, 0x3, 0x84, 0xF, 0x1, 0xF, 0x1, 0x4, 0x40, 0xF, 0xF, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + }; + + uint8_t lut_bb_7_i_n5_v2[] = { + 0x80, 0xF, 0xF, 0x0, 0x0, 0x1, 0x84, 0xF, 0x1, 0xF, 0x1, 0x2, 0x40, 0xF, 0xF, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + }; + + uint8_t count; + this->command(0x20); // VCOM + for (count = 0; count < 42; count++) + this->data(lut_vcom_7_i_n5_v2[count]); + + this->command(0x21); // LUTBW + for (count = 0; count < 42; count++) + this->data(lut_ww_7_i_n5_v2[count]); + + this->command(0x22); // LUTBW + for (count = 0; count < 42; count++) + this->data(lut_bw_7_i_n5_v2[count]); + + this->command(0x23); // LUTWB + for (count = 0; count < 42; count++) + this->data(lut_wb_7_i_n5_v2[count]); + + this->command(0x24); // LUTBB + for (count = 0; count < 42; count++) + this->data(lut_bb_7_i_n5_v2[count]); +} + +void WaveshareEPaper7P5InV2alt::dump_config() { LOG_DISPLAY("", "Waveshare E-Paper", this); ESP_LOGCONFIG(TAG, " Model: 7.5inV2"); LOG_PIN(" Reset Pin: ", this->reset_pin_); diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.h b/esphome/components/waveshare_epaper/waveshare_epaper.h index 41b93978ab..7a88fecbdb 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.h +++ b/esphome/components/waveshare_epaper/waveshare_epaper.h @@ -350,6 +350,25 @@ class WaveshareEPaper7P5InV2 : public WaveshareEPaper { int get_height_internal() override; }; +class WaveshareEPaper7P5InV2alt : public WaveshareEPaper7P5InV2 { + public: + bool wait_until_idle_(); + void initialize() override; + void dump_config() override; + + protected: + void reset_() { + if (this->reset_pin_ != nullptr) { + this->reset_pin_->digital_write(true); + delay(200); // NOLINT + this->reset_pin_->digital_write(false); + delay(2); + this->reset_pin_->digital_write(true); + delay(20); + } + }; +}; + class WaveshareEPaper7P5InHDB : public WaveshareEPaper { public: void initialize() override; From 2059283707fb0145dcf920d76e90afb6d80a20fb Mon Sep 17 00:00:00 2001 From: rainero84 Date: Mon, 9 May 2022 07:21:43 +0200 Subject: [PATCH 0421/1729] Early pin init (#3439) * Added early_pin_init configuration parameter for ESP8266 platform * Added #include to core * Updated test3.yaml to include early_pin_init parameter Co-authored-by: Rainer Oellermann --- esphome/components/esp8266/__init__.py | 5 +++++ esphome/components/esp8266/const.py | 1 + esphome/components/esp8266/core.cpp | 3 +++ tests/test3.yaml | 6 ++++-- 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index 7b1be32e38..41d7688d44 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -19,6 +19,7 @@ from esphome.helpers import copy_file_if_changed from .const import ( CONF_RESTORE_FROM_FLASH, + CONF_EARLY_PIN_INIT, KEY_BOARD, KEY_ESP8266, KEY_PIN_INITIAL_STATES, @@ -148,6 +149,7 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_BOARD): cv.string_strict, cv.Optional(CONF_FRAMEWORK, default={}): ARDUINO_FRAMEWORK_SCHEMA, cv.Optional(CONF_RESTORE_FROM_FLASH, default=False): cv.boolean, + cv.Optional(CONF_EARLY_PIN_INIT, default=True): cv.boolean, cv.Optional(CONF_BOARD_FLASH_MODE, default="dout"): cv.one_of( *BUILD_FLASH_MODES, lower=True ), @@ -197,6 +199,9 @@ async def to_code(config): if config[CONF_RESTORE_FROM_FLASH]: cg.add_define("USE_ESP8266_PREFERENCES_FLASH") + if config[CONF_EARLY_PIN_INIT]: + cg.add_define("USE_ESP8266_EARLY_PIN_INIT") + # Arduino 2 has a non-standards conformant new that returns a nullptr instead of failing when # out of memory and exceptions are disabled. Since Arduino 2.6.0, this flag can be used to make # new abort instead. Use it so that OOM fails early (on allocation) instead of on dereference of diff --git a/esphome/components/esp8266/const.py b/esphome/components/esp8266/const.py index 70429297e0..7740a97ff4 100644 --- a/esphome/components/esp8266/const.py +++ b/esphome/components/esp8266/const.py @@ -4,6 +4,7 @@ KEY_ESP8266 = "esp8266" KEY_BOARD = "board" KEY_PIN_INITIAL_STATES = "pin_initial_states" CONF_RESTORE_FROM_FLASH = "restore_from_flash" +CONF_EARLY_PIN_INIT = "early_pin_init" # esp8266 namespace is already defined by arduino, manually prefix esphome esp8266_ns = cg.global_ns.namespace("esphome").namespace("esp8266") diff --git a/esphome/components/esp8266/core.cpp b/esphome/components/esp8266/core.cpp index a9460f51f2..2d3959b031 100644 --- a/esphome/components/esp8266/core.cpp +++ b/esphome/components/esp8266/core.cpp @@ -1,6 +1,7 @@ #ifdef USE_ESP8266 #include "core.h" +#include "esphome/core/defines.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "preferences.h" @@ -55,6 +56,7 @@ extern "C" void resetPins() { // NOLINT // ourselves and this causes pins to toggle during reboot. force_link_symbols(); +#ifdef USE_ESP8266_EARLY_PIN_INIT for (int i = 0; i < 16; i++) { uint8_t mode = ESPHOME_ESP8266_GPIO_INITIAL_MODE[i]; uint8_t level = ESPHOME_ESP8266_GPIO_INITIAL_LEVEL[i]; @@ -63,6 +65,7 @@ extern "C" void resetPins() { // NOLINT if (level != 255) digitalWrite(i, level); // NOLINT } +#endif } } // namespace esphome diff --git a/tests/test3.yaml b/tests/test3.yaml index 29a70d3cc3..e3818d87ec 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -1,8 +1,6 @@ esphome: name: $device_name comment: $device_comment - platform: ESP8266 - board: d1_mini build_path: build/test3 on_boot: - if: @@ -15,6 +13,10 @@ esphome: includes: - custom.h +esp8266: + board: d1_mini + early_pin_init: True + substitutions: device_name: test3 device_comment: test3 device From 50a32b387e85f3f305b23a4d0c4d7e4fc1523045 Mon Sep 17 00:00:00 2001 From: Ingo Theiss Date: Mon, 9 May 2022 07:23:38 +0200 Subject: [PATCH 0422/1729] Add ENS210 Humidity & Temperature sensor component (#2942) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/ens210/__init__.py | 0 esphome/components/ens210/ens210.cpp | 230 ++++++++++++++++++++++++++ esphome/components/ens210/ens210.h | 39 +++++ esphome/components/ens210/sensor.py | 58 +++++++ tests/test1.yaml | 11 +- 6 files changed, 337 insertions(+), 2 deletions(-) create mode 100644 esphome/components/ens210/__init__.py create mode 100644 esphome/components/ens210/ens210.cpp create mode 100644 esphome/components/ens210/ens210.h create mode 100644 esphome/components/ens210/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index e2a356360a..51719ef1aa 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -60,6 +60,7 @@ esphome/components/dht/* @OttoWinter esphome/components/ds1307/* @badbadc0ffee esphome/components/dsmr/* @glmnet @zuidwijk esphome/components/ektf2232/* @jesserockz +esphome/components/ens210/* @itn3rd77 esphome/components/esp32/* @esphome/core esphome/components/esp32_ble/* @jesserockz esphome/components/esp32_ble_server/* @jesserockz diff --git a/esphome/components/ens210/__init__.py b/esphome/components/ens210/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/ens210/ens210.cpp b/esphome/components/ens210/ens210.cpp new file mode 100644 index 0000000000..9a89e85da2 --- /dev/null +++ b/esphome/components/ens210/ens210.cpp @@ -0,0 +1,230 @@ +// ENS210 relative humidity and temperature sensor with I2C interface from ScioSense +// +// Datasheet: https://www.sciosense.com/wp-content/uploads/2021/01/ENS210.pdf +// +// Implementation based on: +// https://github.com/maarten-pennings/ENS210 +// https://github.com/sciosense/ENS210_driver + +#include "ens210.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace ens210 { + +static const char *const TAG = "ens210"; + +// ENS210 chip constants +static const uint8_t ENS210_BOOTING_MS = 2; // Booting time in ms (also after reset, or going to high power) +static const uint8_t ENS210_SINGLE_MEASURMENT_CONVERSION_TIME_MS = + 130; // Conversion time in ms for single shot T/H measurement +static const uint16_t ENS210_PART_ID = 0x0210; // The expected part id of the ENS210 + +// Addresses of the ENS210 registers +static const uint8_t ENS210_REGISTER_PART_ID = 0x00; +static const uint8_t ENS210_REGISTER_UID = 0x04; +static const uint8_t ENS210_REGISTER_SYS_CTRL = 0x10; +static const uint8_t ENS210_REGISTER_SYS_STAT = 0x11; +static const uint8_t ENS210_REGISTER_SENS_RUN = 0x21; +static const uint8_t ENS210_REGISTER_SENS_START = 0x22; +static const uint8_t ENS210_REGISTER_SENS_STOP = 0x23; +static const uint8_t ENS210_REGISTER_SENS_STAT = 0x24; +static const uint8_t ENS210_REGISTER_T_VAL = 0x30; +static const uint8_t ENS210_REGISTER_H_VAL = 0x33; + +// CRC-7 constants +static const uint8_t CRC7_WIDTH = 7; // A 7 bits CRC has polynomial of 7th order, which has 8 terms +static const uint8_t CRC7_POLY = 0x89; // The 8 coefficients of the polynomial +static const uint8_t CRC7_IVEC = 0x7F; // Initial vector has all 7 bits high + +// Payload data constants +static const uint8_t DATA7_WIDTH = 17; +static const uint32_t DATA7_MASK = ((1UL << DATA7_WIDTH) - 1); // 0b 0 1111 1111 1111 1111 +static const uint32_t DATA7_MSB = (1UL << (DATA7_WIDTH - 1)); // 0b 1 0000 0000 0000 0000 + +// Converts a status to a human readable string +static const LogString *ens210_status_to_human(int status) { + switch (status) { + case ENS210Component::ENS210_STATUS_I2C_ERROR: + return LOG_STR("I2C error - communication with ENS210 failed!"); + case ENS210Component::ENS210_STATUS_CRC_ERROR: + return LOG_STR("CRC error"); + case ENS210Component::ENS210_STATUS_INVALID: + return LOG_STR("Invalid data"); + case ENS210Component::ENS210_STATUS_OK: + return LOG_STR("Status OK"); + case ENS210Component::ENS210_WRONG_CHIP_ID: + return LOG_STR("ENS210 has wrong chip ID! Is it a ENS210?"); + default: + return LOG_STR("Unknown"); + } +} + +// Compute the CRC-7 of 'value' (should only have 17 bits) +// https://en.wikipedia.org/wiki/Cyclic_redundancy_check#Computation +static uint32_t crc7(uint32_t value) { + // Setup polynomial + uint32_t polynomial = CRC7_POLY; + // Align polynomial with data + polynomial = polynomial << (DATA7_WIDTH - CRC7_WIDTH - 1); + // Loop variable (indicates which bit to test, start with highest) + uint32_t bit = DATA7_MSB; + // Make room for CRC value + value = value << CRC7_WIDTH; + bit = bit << CRC7_WIDTH; + polynomial = polynomial << CRC7_WIDTH; + // Insert initial vector + value |= CRC7_IVEC; + // Apply division until all bits done + while (bit & (DATA7_MASK << CRC7_WIDTH)) { + if (bit & value) + value ^= polynomial; + bit >>= 1; + polynomial >>= 1; + } + return value; +} + +void ENS210Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up ENS210..."); + uint8_t data[2]; + uint16_t part_id = 0; + // Reset + if (!this->write_byte(ENS210_REGISTER_SYS_CTRL, 0x80)) { + this->write_byte(ENS210_REGISTER_SYS_CTRL, 0x80); + this->error_code_ = ENS210_STATUS_I2C_ERROR; + this->mark_failed(); + return; + } + // Wait to boot after reset + delay(ENS210_BOOTING_MS); + // Must disable low power to read PART_ID + if (!set_low_power_(false)) { + // Try to go back to default mode (low power enabled) + set_low_power_(true); + this->error_code_ = ENS210_STATUS_I2C_ERROR; + this->mark_failed(); + return; + } + // Read the PART_ID + if (!this->read_bytes(ENS210_REGISTER_PART_ID, data, 2)) { + // Try to go back to default mode (low power enabled) + set_low_power_(true); + this->error_code_ = ENS210_STATUS_I2C_ERROR; + this->mark_failed(); + return; + } + // Pack bytes into partid + part_id = data[1] * 256U + data[0] * 1U; + // Check expected part id of the ENS210 + if (part_id != ENS210_PART_ID) { + this->error_code_ = ENS210_WRONG_CHIP_ID; + this->mark_failed(); + } + // Set default power mode (low power enabled) + set_low_power_(true); +} + +void ENS210Component::dump_config() { + ESP_LOGCONFIG(TAG, "ENS210:"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, "%s", LOG_STR_ARG(ens210_status_to_human(this->error_code_))); + } + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); +} + +float ENS210Component::get_setup_priority() const { return setup_priority::DATA; } + +void ENS210Component::update() { + // Execute a single measurement + if (!this->write_byte(ENS210_REGISTER_SENS_RUN, 0x00)) { + ESP_LOGE(TAG, "Starting single measurement failed!"); + this->status_set_warning(); + return; + } + // Trigger measurement + if (!this->write_byte(ENS210_REGISTER_SENS_START, 0x03)) { + ESP_LOGE(TAG, "Trigger of measurement failed!"); + this->status_set_warning(); + return; + } + // Wait for measurement to complete + this->set_timeout("data", uint32_t(ENS210_SINGLE_MEASURMENT_CONVERSION_TIME_MS), [this]() { + int temperature_data, temperature_status, humidity_data, humidity_status; + uint8_t data[6]; + uint32_t h_val_data, t_val_data; + // Set default status for early bail out + temperature_status = ENS210_STATUS_I2C_ERROR; + humidity_status = ENS210_STATUS_I2C_ERROR; + + // Read T_VAL and H_VAL + if (!this->read_bytes(ENS210_REGISTER_T_VAL, data, 6)) { + ESP_LOGE(TAG, "Communication with ENS210 failed!"); + this->status_set_warning(); + return; + } + // Pack bytes for humidity + h_val_data = (uint32_t)((uint32_t) data[5] << 16 | (uint32_t) data[4] << 8 | (uint32_t) data[3]); + // Extract humidity data and update the status + extract_measurement_(h_val_data, &humidity_data, &humidity_status); + + if (humidity_status == ENS210_STATUS_OK) { + if (this->humidity_sensor_ != nullptr) { + float humidity = (humidity_data & 0xFFFF) / 512.0; + this->humidity_sensor_->publish_state(humidity); + } + } else { + ESP_LOGW(TAG, "Humidity status failure: %s", LOG_STR_ARG(ens210_status_to_human(humidity_status))); + this->status_set_warning(); + return; + } + // Pack bytes for temperature + t_val_data = (uint32_t)((uint32_t) data[2] << 16 | (uint32_t) data[1] << 8 | (uint32_t) data[0]); + // Extract temperature data and update the status + extract_measurement_(t_val_data, &temperature_data, &temperature_status); + + if (temperature_status == ENS210_STATUS_OK) { + if (this->temperature_sensor_ != nullptr) { + // Temperature in Celsius + float temperature = (temperature_data & 0xFFFF) / 64.0 - 27315L / 100.0; + this->temperature_sensor_->publish_state(temperature); + } + } else { + ESP_LOGW(TAG, "Temperature status failure: %s", LOG_STR_ARG(ens210_status_to_human(temperature_status))); + } + }); +} + +// Extracts measurement 'data' and 'status' from a 'val' obtained from measurment. +void ENS210Component::extract_measurement_(uint32_t val, int *data, int *status) { + *data = (val >> 0) & 0xffff; + int valid = (val >> 16) & 0x1; + uint32_t crc = (val >> 17) & 0x7f; + uint32_t payload = (val >> 0) & 0x1ffff; + // Check CRC + uint8_t crc_ok = crc7(payload) == crc; + + if (!crc_ok) { + *status = ENS210_STATUS_CRC_ERROR; + } else if (!valid) { + *status = ENS210_STATUS_INVALID; + } else { + *status = ENS210_STATUS_OK; + } +} + +// Sets ENS210 to low (true) or high (false) power. Returns false on I2C problems. +bool ENS210Component::set_low_power_(bool enable) { + uint8_t low_power_cmd = enable ? 0x01 : 0x00; + ESP_LOGD(TAG, "Enable low power: %s", enable ? "true" : "false"); + bool result = this->write_byte(ENS210_REGISTER_SYS_CTRL, low_power_cmd); + delay(ENS210_BOOTING_MS); + return result; +} + +} // namespace ens210 +} // namespace esphome diff --git a/esphome/components/ens210/ens210.h b/esphome/components/ens210/ens210.h new file mode 100644 index 0000000000..342be04799 --- /dev/null +++ b/esphome/components/ens210/ens210.h @@ -0,0 +1,39 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace ens210 { + +/// This class implements support for the ENS210 relative humidity and temperature i2c sensor. +class ENS210Component : public PollingComponent, public i2c::I2CDevice { + public: + float get_setup_priority() const override; + void dump_config() override; + void setup() override; + void update() override; + + void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; } + void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } + + enum ErrorCode { + ENS210_STATUS_OK = 0, // The value was read, the CRC matches, and data is valid + ENS210_STATUS_INVALID, // The value was read, the CRC matches, but the data is invalid (e.g. the measurement was + // not yet finished) + ENS210_STATUS_CRC_ERROR, // The value was read, but the CRC over the payload (valid and data) does not match + ENS210_STATUS_I2C_ERROR, // There was an I2C communication error + ENS210_WRONG_CHIP_ID // The read PART_ID is not the expected part id of the ENS210 + } error_code_{ENS210_STATUS_OK}; + + protected: + bool set_low_power_(bool enable); + void extract_measurement_(uint32_t val, int *data, int *status); + + sensor::Sensor *temperature_sensor_; + sensor::Sensor *humidity_sensor_; +}; + +} // namespace ens210 +} // namespace esphome diff --git a/esphome/components/ens210/sensor.py b/esphome/components/ens210/sensor.py new file mode 100644 index 0000000000..3037156e01 --- /dev/null +++ b/esphome/components/ens210/sensor.py @@ -0,0 +1,58 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_HUMIDITY, + CONF_ID, + CONF_TEMPERATURE, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_PERCENT, +) + +CODEOWNERS = ["@itn3rd77"] +DEPENDENCIES = ["i2c"] + +ens210_ns = cg.esphome_ns.namespace("ens210") + +ENS210Component = ens210_ns.class_( + "ENS210Component", cg.PollingComponent, i2c.I2CDevice +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(ENS210Component), + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x43)) +) + + +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) + + if CONF_TEMPERATURE in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature_sensor(sens)) + + if CONF_HUMIDITY in config: + sens = await sensor.new_sensor(config[CONF_HUMIDITY]) + cg.add(var.set_humidity_sensor(sens)) diff --git a/tests/test1.yaml b/tests/test1.yaml index 375499942b..aba37976aa 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -547,11 +547,18 @@ sensor: - platform: esp32_hall name: "ESP32 Hall Sensor" update_interval: 15s - - platform: hdc1080 + - platform: ens210 temperature: name: "Living Room Temperature 5" humidity: - name: "Living Room Pressure 5" + name: 'Living Room Humidity 5' + update_interval: 15s + i2c_id: i2c_bus + - platform: hdc1080 + temperature: + name: 'Living Room Temperature 6' + humidity: + name: 'Living Room Humidity 5' update_interval: 15s i2c_id: i2c_bus - platform: hlw8012 From 2e4645310b2b8a893d75e508b82e3f0ba176754f Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 9 May 2022 19:16:46 +1200 Subject: [PATCH 0423/1729] Also rename yaml filename with rename command (#3447) --- esphome/__main__.py | 138 ++++++++++++++++++++++---------------------- 1 file changed, 70 insertions(+), 68 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index 80e8455465..c336336f18 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -496,83 +496,85 @@ def command_rename(args, config): ) ) return 1 + # Load existing yaml file with open(CORE.config_path, mode="r+", encoding="utf-8") as raw_file: raw_contents = raw_file.read() - yaml = yaml_util.load_yaml(CORE.config_path) - if CONF_ESPHOME not in yaml or CONF_NAME not in yaml[CONF_ESPHOME]: - print( - color( - Fore.BOLD_RED, "Complex YAML files cannot be automatically renamed." + + yaml = yaml_util.load_yaml(CORE.config_path) + if CONF_ESPHOME not in yaml or CONF_NAME not in yaml[CONF_ESPHOME]: + print( + color(Fore.BOLD_RED, "Complex YAML files cannot be automatically renamed.") + ) + return 1 + old_name = yaml[CONF_ESPHOME][CONF_NAME] + match = re.match(r"^\$\{?([a-zA-Z0-9_]+)\}?$", old_name) + if match is None: + new_raw = re.sub( + rf"name:\s+[\"']?{old_name}[\"']?", + f'name: "{args.name}"', + raw_contents, + ) + else: + old_name = yaml[CONF_SUBSTITUTIONS][match.group(1)] + if ( + len( + re.findall( + rf"^\s+{match.group(1)}:\s+[\"']?{old_name}[\"']?", + raw_contents, + flags=re.MULTILINE, ) ) - return 1 - old_name = yaml[CONF_ESPHOME][CONF_NAME] - match = re.match(r"^\$\{?([a-zA-Z0-9_]+)\}?$", old_name) - if match is None: - new_raw = re.sub( - rf"name:\s+[\"']?{old_name}[\"']?", - f'name: "{args.name}"', - raw_contents, - ) - else: - old_name = yaml[CONF_SUBSTITUTIONS][match.group(1)] - if ( - len( - re.findall( - rf"^\s+{match.group(1)}:\s+[\"']?{old_name}[\"']?", - raw_contents, - flags=re.MULTILINE, - ) - ) - > 1 - ): - print(color(Fore.BOLD_RED, "Too many matches in YAML to safely rename")) - return 1 - - new_raw = re.sub( - rf"^(\s+{match.group(1)}):\s+[\"']?{old_name}[\"']?", - f'\\1: "{args.name}"', - raw_contents, - flags=re.MULTILINE, - ) - - raw_file.seek(0) - raw_file.write(new_raw) - raw_file.flush() - - print(f"Updating {color(Fore.CYAN, CORE.config_path)}") - print() - - rc = run_external_process("esphome", "config", CORE.config_path) - if rc != 0: - raw_file.seek(0) - raw_file.write(raw_contents) - print(color(Fore.BOLD_RED, "Rename failed. Reverting changes.")) + > 1 + ): + print(color(Fore.BOLD_RED, "Too many matches in YAML to safely rename")) return 1 - cli_args = [ - "run", - CORE.config_path, - "--no-logs", - "--device", - CORE.address, - ] + new_raw = re.sub( + rf"^(\s+{match.group(1)}):\s+[\"']?{old_name}[\"']?", + f'\\1: "{args.name}"', + raw_contents, + flags=re.MULTILINE, + ) - if args.dashboard: - cli_args.insert(0, "--dashboard") + new_path = os.path.join(CORE.config_dir, args.name + ".yaml") + print( + f"Updating {color(Fore.CYAN, CORE.config_path)} to {color(Fore.CYAN, new_path)}" + ) + print() - try: - rc = run_external_process("esphome", *cli_args) - except KeyboardInterrupt: - rc = 1 - if rc != 0: - raw_file.seek(0) - raw_file.write(raw_contents) - return 1 + with open(new_path, mode="w", encoding="utf-8") as new_file: + new_file.write(new_raw) - print(color(Fore.BOLD_GREEN, "SUCCESS")) - print() - return 0 + rc = run_external_process("esphome", "config", new_path) + if rc != 0: + print(color(Fore.BOLD_RED, "Rename failed. Reverting changes.")) + os.remove(new_path) + return 1 + + cli_args = [ + "run", + new_path, + "--no-logs", + "--device", + CORE.address, + ] + + if args.dashboard: + cli_args.insert(0, "--dashboard") + + try: + rc = run_external_process("esphome", *cli_args) + except KeyboardInterrupt: + rc = 1 + if rc != 0: + os.remove(new_path) + return 1 + + os.remove(CORE.config_path) + + print(color(Fore.BOLD_GREEN, "SUCCESS")) + print() + return 0 PRE_CONFIG_ACTIONS = { From e5b3625f73e7be85342071897f6d52463de3e010 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 May 2022 19:22:47 +1200 Subject: [PATCH 0424/1729] Bump click from 8.1.2 to 8.1.3 (#3426) Bumps [click](https://github.com/pallets/click) from 8.1.2 to 8.1.3. - [Release notes](https://github.com/pallets/click/releases) - [Changelog](https://github.com/pallets/click/blob/main/CHANGES.rst) - [Commits](https://github.com/pallets/click/compare/8.1.2...8.1.3) --- updated-dependencies: - dependency-name: click dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 543999a9f3..e62ef86765 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ tzdata>=2021.1 # from time pyserial==3.5 platformio==5.2.5 # When updating platformio, also update Dockerfile esptool==3.3 -click==8.1.2 +click==8.1.3 esphome-dashboard==20220508.0 aioesphomeapi==10.8.2 zeroconf==0.38.4 From 8236e840a76fc94ac6ae3f023f8d26f619567033 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 9 May 2022 19:24:27 +1200 Subject: [PATCH 0425/1729] Fix spi transfer with miso pin defined on espidf (#3450) --- esphome/components/spi/spi.h | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/esphome/components/spi/spi.h b/esphome/components/spi/spi.h index 6c3fd17e56..7f0b0f481a 100644 --- a/esphome/components/spi/spi.h +++ b/esphome/components/spi/spi.h @@ -156,15 +156,17 @@ class SPIComponent : public Component { template uint8_t transfer_byte(uint8_t data) { -#ifdef USE_SPI_ARDUINO_BACKEND if (this->miso_ != nullptr) { +#ifdef USE_SPI_ARDUINO_BACKEND if (this->hw_spi_ != nullptr) { return this->hw_spi_->transfer(data); } else { - return this->transfer_(data); - } - } #endif // USE_SPI_ARDUINO_BACKEND + return this->transfer_(data); +#ifdef USE_SPI_ARDUINO_BACKEND + } +#endif // USE_SPI_ARDUINO_BACKEND + } this->write_byte(data); return 0; } From df999723f86b42697b8018fa860289134f048afd Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 9 May 2022 19:43:09 +1200 Subject: [PATCH 0426/1729] Force using name substitution when adopting a device (#3451) --- esphome/components/dashboard_import/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/esphome/components/dashboard_import/__init__.py b/esphome/components/dashboard_import/__init__.py index 6194a55205..41b4a8bed1 100644 --- a/esphome/components/dashboard_import/__init__.py +++ b/esphome/components/dashboard_import/__init__.py @@ -64,7 +64,10 @@ def import_config(path: str, name: str, project_name: str, import_url: str) -> N config = { "substitutions": {"name": name}, "packages": {project_name: import_url}, - "esphome": {"name_add_mac_suffix": False}, + "esphome": { + "name": "${name}", + "name_add_mac_suffix": False, + }, } p.write_text( dump(config) + WIFI_CONFIG, From d13a397f8ee2e13c834977238f6f7f01c3858ff8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 May 2022 19:44:54 +1200 Subject: [PATCH 0427/1729] Bump pyupgrade from 2.32.0 to 2.32.1 (#3452) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 083050252d..68da13aade 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,7 +1,7 @@ pylint==2.13.5 flake8==4.0.1 black==22.3.0 -pyupgrade==2.32.0 +pyupgrade==2.32.1 pre-commit # Unit tests From a35f36ad39ead5a9da7681fa76c1d3380b3eeab6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 May 2022 20:28:21 +1200 Subject: [PATCH 0428/1729] Bump pylint from 2.13.5 to 2.13.8 (#3432) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 68da13aade..4b5db8ce87 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,4 +1,4 @@ -pylint==2.13.5 +pylint==2.13.8 flake8==4.0.1 black==22.3.0 pyupgrade==2.32.1 From 47898b527cd974068af04723f253b16009c72735 Mon Sep 17 00:00:00 2001 From: MFlasskamp Date: Mon, 9 May 2022 10:32:14 +0200 Subject: [PATCH 0429/1729] Esp32c3 deepsleep fix (#3433) --- .../components/deep_sleep/deep_sleep_component.cpp | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/esphome/components/deep_sleep/deep_sleep_component.cpp b/esphome/components/deep_sleep/deep_sleep_component.cpp index 1bb70e0d7e..23f2a7a70c 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.cpp +++ b/esphome/components/deep_sleep/deep_sleep_component.cpp @@ -76,12 +76,14 @@ void DeepSleepComponent::set_sleep_duration(uint32_t time_ms) { this->sleep_dura void DeepSleepComponent::set_wakeup_pin_mode(WakeupPinMode wakeup_pin_mode) { this->wakeup_pin_mode_ = wakeup_pin_mode; } +#if !defined(USE_ESP32_VARIANT_ESP32C3) void DeepSleepComponent::set_ext1_wakeup(Ext1Wakeup ext1_wakeup) { this->ext1_wakeup_ = ext1_wakeup; } void DeepSleepComponent::set_touch_wakeup(bool touch_wakeup) { this->touch_wakeup_ = touch_wakeup; } void DeepSleepComponent::set_run_duration(WakeupCauseToRunDuration wakeup_cause_to_run_duration) { wakeup_cause_to_run_duration_ = wakeup_cause_to_run_duration; } #endif +#endif void DeepSleepComponent::set_run_duration(uint32_t time_ms) { this->run_duration_ = time_ms; } void DeepSleepComponent::begin_sleep(bool manual) { if (this->prevent_ && !manual) { @@ -107,7 +109,8 @@ void DeepSleepComponent::begin_sleep(bool manual) { App.run_safe_shutdown_hooks(); -#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3) +#if defined(USE_ESP32) +#if !defined(USE_ESP32_VARIANT_ESP32C3) if (this->sleep_duration_.has_value()) esp_sleep_enable_timer_wakeup(*this->sleep_duration_); if (this->wakeup_pin_ != nullptr) { @@ -125,10 +128,7 @@ void DeepSleepComponent::begin_sleep(bool manual) { esp_sleep_enable_touchpad_wakeup(); esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON); } - - esp_deep_sleep_start(); #endif - #ifdef USE_ESP32_VARIANT_ESP32C3 if (this->sleep_duration_.has_value()) esp_sleep_enable_timer_wakeup(*this->sleep_duration_); @@ -137,9 +137,12 @@ void DeepSleepComponent::begin_sleep(bool manual) { if (this->wakeup_pin_mode_ == WAKEUP_PIN_MODE_INVERT_WAKEUP && this->wakeup_pin_->digital_read()) { level = !level; } - esp_deep_sleep_enable_gpio_wakeup(gpio_num_t(this->wakeup_pin_->get_pin()), level); + esp_deep_sleep_enable_gpio_wakeup(gpio_num_t(this->wakeup_pin_->get_pin()), + static_cast(level)); } #endif + esp_deep_sleep_start(); +#endif #ifdef USE_ESP8266 ESP.deepSleep(*this->sleep_duration_); // NOLINT(readability-static-accessed-through-instance) From 3a3d97dfa79bd65b4eec7609d035299903baa717 Mon Sep 17 00:00:00 2001 From: Unai Date: Tue, 10 May 2022 03:28:22 +0200 Subject: [PATCH 0430/1729] Add SERIAL_JTAG/CDC logger option for ESP-IDF platform for ESP32-S2/S3/C3 (#3105) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/logger/__init__.py | 62 +++++++++++++++------ esphome/components/logger/logger.cpp | 79 ++++++++++++++++++++------- esphome/components/logger/logger.h | 12 +++- 3 files changed, 114 insertions(+), 39 deletions(-) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index d11b00405d..43d87bcefe 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -19,8 +19,13 @@ from esphome.const import ( CONF_TX_BUFFER_SIZE, ) from esphome.core import CORE, EsphomeError, Lambda, coroutine_with_priority -from esphome.components.esp32 import get_esp32_variant -from esphome.components.esp32.const import VARIANT_ESP32S2, VARIANT_ESP32C3 +from esphome.components.esp32 import add_idf_sdkconfig_option, get_esp32_variant +from esphome.components.esp32.const import ( + VARIANT_ESP32, + VARIANT_ESP32S2, + VARIANT_ESP32C3, + VARIANT_ESP32S3, +) CODEOWNERS = ["@esphome/core"] logger_ns = cg.esphome_ns.namespace("logger") @@ -54,36 +59,51 @@ LOG_LEVEL_SEVERITY = [ "VERY_VERBOSE", ] -ESP32_REDUCED_VARIANTS = [VARIANT_ESP32C3, VARIANT_ESP32S2] +UART0 = "UART0" +UART1 = "UART1" +UART2 = "UART2" +UART0_SWAP = "UART0_SWAP" +USB_SERIAL_JTAG = "USB_SERIAL_JTAG" +USB_CDC = "USB_CDC" -UART_SELECTION_ESP32_REDUCED = ["UART0", "UART1"] +UART_SELECTION_ESP32 = { + VARIANT_ESP32: [UART0, UART1, UART2], + VARIANT_ESP32S2: [UART0, UART1, USB_CDC], + VARIANT_ESP32S3: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG], + VARIANT_ESP32C3: [UART0, UART1, USB_SERIAL_JTAG], +} -UART_SELECTION_ESP32 = ["UART0", "UART1", "UART2"] +UART_SELECTION_ESP8266 = [UART0, UART0_SWAP, UART1] -UART_SELECTION_ESP8266 = ["UART0", "UART0_SWAP", "UART1"] +ESP_IDF_UARTS = [USB_CDC, USB_SERIAL_JTAG] HARDWARE_UART_TO_UART_SELECTION = { - "UART0": logger_ns.UART_SELECTION_UART0, - "UART0_SWAP": logger_ns.UART_SELECTION_UART0_SWAP, - "UART1": logger_ns.UART_SELECTION_UART1, - "UART2": logger_ns.UART_SELECTION_UART2, + UART0: logger_ns.UART_SELECTION_UART0, + UART0_SWAP: logger_ns.UART_SELECTION_UART0_SWAP, + UART1: logger_ns.UART_SELECTION_UART1, + UART2: logger_ns.UART_SELECTION_UART2, + USB_CDC: logger_ns.UART_SELECTION_USB_CDC, + USB_SERIAL_JTAG: logger_ns.UART_SELECTION_USB_SERIAL_JTAG, } HARDWARE_UART_TO_SERIAL = { - "UART0": cg.global_ns.Serial, - "UART0_SWAP": cg.global_ns.Serial, - "UART1": cg.global_ns.Serial1, - "UART2": cg.global_ns.Serial2, + UART0: cg.global_ns.Serial, + UART0_SWAP: cg.global_ns.Serial, + UART1: cg.global_ns.Serial1, + UART2: cg.global_ns.Serial2, } is_log_level = cv.one_of(*LOG_LEVELS, upper=True) def uart_selection(value): + if value.upper() in ESP_IDF_UARTS: + if not CORE.using_esp_idf: + raise cv.Invalid(f"Only esp-idf framework supports {value}.") if CORE.is_esp32: - if get_esp32_variant() in ESP32_REDUCED_VARIANTS: - return cv.one_of(*UART_SELECTION_ESP32_REDUCED, upper=True)(value) - return cv.one_of(*UART_SELECTION_ESP32, upper=True)(value) + variant = get_esp32_variant() + if variant in UART_SELECTION_ESP32: + return cv.one_of(*UART_SELECTION_ESP32[variant], upper=True)(value) if CORE.is_esp8266: return cv.one_of(*UART_SELECTION_ESP8266, upper=True)(value) raise NotImplementedError @@ -113,7 +133,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_BAUD_RATE, default=115200): cv.positive_int, cv.Optional(CONF_TX_BUFFER_SIZE, default=512): cv.validate_bytes, cv.Optional(CONF_DEASSERT_RTS_DTR, default=False): cv.boolean, - cv.Optional(CONF_HARDWARE_UART, default="UART0"): uart_selection, + cv.Optional(CONF_HARDWARE_UART, default=UART0): uart_selection, cv.Optional(CONF_LEVEL, default="DEBUG"): is_log_level, cv.Optional(CONF_LOGS, default={}): cv.Schema( { @@ -185,6 +205,12 @@ async def to_code(config): if config.get(CONF_ESP8266_STORE_LOG_STRINGS_IN_FLASH): cg.add_build_flag("-DUSE_STORE_LOG_STR_IN_FLASH") + if CORE.using_esp_idf: + if config[CONF_HARDWARE_UART] == USB_CDC: + add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_USB_CDC", True) + elif config[CONF_HARDWARE_UART] == USB_SERIAL_JTAG: + add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG", True) + # Register at end for safe mode await cg.register_component(log, config) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 3f4e4e7753..08c83035b6 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -116,8 +116,22 @@ void HOT Logger::log_message_(int level, const char *tag, int offset) { this->hw_serial_->println(msg); #endif // USE_ARDUINO #ifdef USE_ESP_IDF - uart_write_bytes(uart_num_, msg, strlen(msg)); - uart_write_bytes(uart_num_, "\n", 1); + if ( +#if defined(USE_ESP32_VARIANT_ESP32S2) + uart_ == UART_SELECTION_USB_CDC +#elif defined(USE_ESP32_VARIANT_ESP32C3) + uart_ == UART_SELECTION_USB_SERIAL_JTAG +#elif defined(USE_ESP32_VARIANT_ESP32S3) + uart_ == UART_SELECTION_USB_CDC || uart_ == UART_SELECTION_USB_SERIAL_JTAG +#else + /* DISABLES CODE */ (false) +#endif + ) { + puts(msg); + } else { + uart_write_bytes(uart_num_, msg, strlen(msg)); + uart_write_bytes(uart_num_, "\n", 1); + } #endif } @@ -181,29 +195,41 @@ void Logger::pre_setup() { case UART_SELECTION_UART1: uart_num_ = UART_NUM_1; break; -#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32S2) +#if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32S3) case UART_SELECTION_UART2: uart_num_ = UART_NUM_2; break; -#endif +#endif // !USE_ESP32_VARIANT_ESP32C3 && !USE_ESP32_VARIANT_ESP32S2 && !USE_ESP32_VARIANT_ESP32S3 +#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) + case UART_SELECTION_USB_CDC: + uart_num_ = -1; + break; +#endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 +#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S3) + case UART_SELECTION_USB_SERIAL_JTAG: + uart_num_ = -1; + break; +#endif // USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32S3 } - uart_config_t uart_config{}; - uart_config.baud_rate = (int) baud_rate_; - uart_config.data_bits = UART_DATA_8_BITS; - uart_config.parity = UART_PARITY_DISABLE; - uart_config.stop_bits = UART_STOP_BITS_1; - uart_config.flow_ctrl = UART_HW_FLOWCTRL_DISABLE; - uart_param_config(uart_num_, &uart_config); - const int uart_buffer_size = tx_buffer_size_; - // Install UART driver using an event queue here - uart_driver_install(uart_num_, uart_buffer_size, uart_buffer_size, 10, nullptr, 0); -#endif + if (uart_num_ >= 0) { + uart_config_t uart_config{}; + uart_config.baud_rate = (int) baud_rate_; + uart_config.data_bits = UART_DATA_8_BITS; + uart_config.parity = UART_PARITY_DISABLE; + uart_config.stop_bits = UART_STOP_BITS_1; + uart_config.flow_ctrl = UART_HW_FLOWCTRL_DISABLE; + uart_param_config(uart_num_, &uart_config); + const int uart_buffer_size = tx_buffer_size_; + // Install UART driver using an event queue here + uart_driver_install(uart_num_, uart_buffer_size, uart_buffer_size, 10, nullptr, 0); + } +#endif // USE_ESP_IDF } #ifdef USE_ESP8266 else { uart_set_debug(UART_NO); } -#endif +#endif // USE_ESP8266 global_logger = this; #if defined(USE_ESP_IDF) || defined(USE_ESP32_FRAMEWORK_ARDUINO) @@ -211,7 +237,7 @@ void Logger::pre_setup() { if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE) { esp_log_level_set("*", ESP_LOG_VERBOSE); } -#endif +#endif // USE_ESP_IDF || USE_ESP32_FRAMEWORK_ARDUINO ESP_LOGI(TAG, "Log initialized"); } @@ -226,11 +252,24 @@ void Logger::add_on_log_callback(std::function Date: Tue, 10 May 2022 06:41:16 +0200 Subject: [PATCH 0431/1729] Select enhancement (#3423) Co-authored-by: Maurice Makaay --- esphome/codegen.py | 1 + esphome/components/api/api_server.cpp | 2 +- esphome/components/api/api_server.h | 2 +- .../components/copy/select/copy_select.cpp | 2 +- esphome/components/mqtt/mqtt_select.cpp | 2 +- esphome/components/select/__init__.py | 126 +++++++++++++++++- esphome/components/select/automation.h | 40 +++++- esphome/components/select/select.cpp | 66 +++++---- esphome/components/select/select.h | 48 ++----- esphome/components/select/select_call.cpp | 122 +++++++++++++++++ esphome/components/select/select_call.h | 48 +++++++ esphome/components/select/select_traits.cpp | 11 ++ esphome/components/select/select_traits.h | 19 +++ esphome/components/web_server/web_server.cpp | 2 +- esphome/components/web_server/web_server.h | 2 +- esphome/const.py | 2 + esphome/core/controller.cpp | 6 +- esphome/core/controller.h | 2 +- esphome/cpp_types.py | 1 + tests/test5.yaml | 35 ++++- 20 files changed, 461 insertions(+), 78 deletions(-) create mode 100644 esphome/components/select/select_call.cpp create mode 100644 esphome/components/select/select_call.h create mode 100644 esphome/components/select/select_traits.cpp create mode 100644 esphome/components/select/select_traits.h diff --git a/esphome/codegen.py b/esphome/codegen.py index b862a8ce86..185e6599b1 100644 --- a/esphome/codegen.py +++ b/esphome/codegen.py @@ -64,6 +64,7 @@ from esphome.cpp_types import ( # noqa uint64, int32, int64, + size_t, const_char_ptr, NAN, esphome_ns, diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 4521cc5bfc..1f2800f298 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -255,7 +255,7 @@ void APIServer::on_number_update(number::Number *obj, float state) { #endif #ifdef USE_SELECT -void APIServer::on_select_update(select::Select *obj, const std::string &state) { +void APIServer::on_select_update(select::Select *obj, const std::string &state, size_t index) { if (obj->is_internal()) return; for (auto &c : this->clients_) diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index fdc46922ad..f03a83fc7b 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -64,7 +64,7 @@ class APIServer : public Component, public Controller { void on_number_update(number::Number *obj, float state) override; #endif #ifdef USE_SELECT - void on_select_update(select::Select *obj, const std::string &state) override; + void on_select_update(select::Select *obj, const std::string &state, size_t index) override; #endif #ifdef USE_LOCK void on_lock_update(lock::Lock *obj) override; diff --git a/esphome/components/copy/select/copy_select.cpp b/esphome/components/copy/select/copy_select.cpp index 0f01c2692c..bdcbd0b42c 100644 --- a/esphome/components/copy/select/copy_select.cpp +++ b/esphome/components/copy/select/copy_select.cpp @@ -7,7 +7,7 @@ namespace copy { static const char *const TAG = "copy.select"; void CopySelect::setup() { - source_->add_on_state_callback([this](const std::string &value) { this->publish_state(value); }); + source_->add_on_state_callback([this](const std::string &value, size_t index) { this->publish_state(value); }); traits.set_options(source_->traits.get_options()); diff --git a/esphome/components/mqtt/mqtt_select.cpp b/esphome/components/mqtt/mqtt_select.cpp index 7ecbf9425e..ea5130f823 100644 --- a/esphome/components/mqtt/mqtt_select.cpp +++ b/esphome/components/mqtt/mqtt_select.cpp @@ -21,7 +21,7 @@ void MQTTSelectComponent::setup() { call.set_option(state); call.perform(); }); - this->select_->add_on_state_callback([this](const std::string &state) { this->publish_state(state); }); + this->select_->add_on_state_callback([this](const std::string &state, size_t index) { this->publish_state(state); }); } void MQTTSelectComponent::dump_config() { diff --git a/esphome/components/select/__init__.py b/esphome/components/select/__init__.py index c15036e9f9..a1c73c385e 100644 --- a/esphome/components/select/__init__.py +++ b/esphome/components/select/__init__.py @@ -9,6 +9,10 @@ from esphome.const import ( CONF_OPTION, CONF_TRIGGER_ID, CONF_MQTT_ID, + CONF_CYCLE, + CONF_MODE, + CONF_OPERATION, + CONF_INDEX, ) from esphome.core import CORE, coroutine_with_priority from esphome.cpp_helpers import setup_entity @@ -22,14 +26,27 @@ SelectPtr = Select.operator("ptr") # Triggers SelectStateTrigger = select_ns.class_( - "SelectStateTrigger", automation.Trigger.template(cg.float_) + "SelectStateTrigger", + automation.Trigger.template(cg.std_string, cg.size_t), ) # Actions SelectSetAction = select_ns.class_("SelectSetAction", automation.Action) +SelectSetIndexAction = select_ns.class_("SelectSetIndexAction", automation.Action) +SelectOperationAction = select_ns.class_("SelectOperationAction", automation.Action) + +# Enums +SelectOperation = select_ns.enum("SelectOperation") +SELECT_OPERATION_OPTIONS = { + "NEXT": SelectOperation.SELECT_OP_NEXT, + "PREVIOUS": SelectOperation.SELECT_OP_PREVIOUS, + "FIRST": SelectOperation.SELECT_OP_FIRST, + "LAST": SelectOperation.SELECT_OP_LAST, +} icon = cv.icon + SELECT_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( { cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTSelectComponent), @@ -50,7 +67,9 @@ async def setup_select_core_(var, config, *, options: List[str]): for conf in config.get(CONF_ON_VALUE, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(cg.std_string, "x")], conf) + await automation.build_automation( + trigger, [(cg.std_string, "x"), (cg.size_t, "i")], conf + ) if CONF_MQTT_ID in config: mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var) @@ -76,12 +95,18 @@ async def to_code(config): cg.add_global(select_ns.using) +OPERATION_BASE_SCHEMA = cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(Select), + } +) + + @automation.register_action( "select.set", SelectSetAction, - cv.Schema( + OPERATION_BASE_SCHEMA.extend( { - cv.Required(CONF_ID): cv.use_id(Select), cv.Required(CONF_OPTION): cv.templatable(cv.string_strict), } ), @@ -92,3 +117,96 @@ async def select_set_to_code(config, action_id, template_arg, args): template_ = await cg.templatable(config[CONF_OPTION], args, cg.std_string) cg.add(var.set_option(template_)) return var + + +@automation.register_action( + "select.set_index", + SelectSetIndexAction, + OPERATION_BASE_SCHEMA.extend( + { + cv.Required(CONF_INDEX): cv.templatable(cv.positive_int), + } + ), +) +async def select_set_index_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_INDEX], args, cg.size_t) + cg.add(var.set_index(template_)) + return var + + +@automation.register_action( + "select.operation", + SelectOperationAction, + OPERATION_BASE_SCHEMA.extend( + { + cv.Required(CONF_OPERATION): cv.templatable( + cv.enum(SELECT_OPERATION_OPTIONS, upper=True) + ), + cv.Optional(CONF_CYCLE, default=True): cv.templatable(cv.boolean), + } + ), +) +@automation.register_action( + "select.next", + SelectOperationAction, + automation.maybe_simple_id( + OPERATION_BASE_SCHEMA.extend( + { + cv.Optional(CONF_MODE, default="NEXT"): cv.one_of("NEXT", upper=True), + cv.Optional(CONF_CYCLE, default=True): cv.boolean, + } + ) + ), +) +@automation.register_action( + "select.previous", + SelectOperationAction, + automation.maybe_simple_id( + OPERATION_BASE_SCHEMA.extend( + { + cv.Optional(CONF_MODE, default="PREVIOUS"): cv.one_of( + "PREVIOUS", upper=True + ), + cv.Optional(CONF_CYCLE, default=True): cv.boolean, + } + ) + ), +) +@automation.register_action( + "select.first", + SelectOperationAction, + automation.maybe_simple_id( + OPERATION_BASE_SCHEMA.extend( + { + cv.Optional(CONF_MODE, default="FIRST"): cv.one_of("FIRST", upper=True), + } + ) + ), +) +@automation.register_action( + "select.last", + SelectOperationAction, + automation.maybe_simple_id( + OPERATION_BASE_SCHEMA.extend( + { + cv.Optional(CONF_MODE, default="LAST"): cv.one_of("LAST", upper=True), + } + ) + ), +) +async def select_operation_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) + if CONF_OPERATION in config: + op_ = await cg.templatable(config[CONF_OPERATION], args, SelectOperation) + cg.add(var.set_operation(op_)) + if CONF_CYCLE in config: + cycle_ = await cg.templatable(config[CONF_CYCLE], args, bool) + cg.add(var.set_cycle(cycle_)) + if CONF_MODE in config: + cg.add(var.set_operation(SELECT_OPERATION_OPTIONS[config[CONF_MODE]])) + if CONF_CYCLE in config: + cg.add(var.set_cycle(config[CONF_CYCLE])) + return var diff --git a/esphome/components/select/automation.h b/esphome/components/select/automation.h index 1e0bfed63d..1250665188 100644 --- a/esphome/components/select/automation.h +++ b/esphome/components/select/automation.h @@ -7,16 +7,16 @@ namespace esphome { namespace select { -class SelectStateTrigger : public Trigger { +class SelectStateTrigger : public Trigger { public: explicit SelectStateTrigger(Select *parent) { - parent->add_on_state_callback([this](const std::string &value) { this->trigger(value); }); + parent->add_on_state_callback([this](const std::string &value, size_t index) { this->trigger(value, index); }); } }; template class SelectSetAction : public Action { public: - SelectSetAction(Select *select) : select_(select) {} + explicit SelectSetAction(Select *select) : select_(select) {} TEMPLATABLE_VALUE(std::string, option) void play(Ts... x) override { @@ -29,5 +29,39 @@ template class SelectSetAction : public Action { Select *select_; }; +template class SelectSetIndexAction : public Action { + public: + explicit SelectSetIndexAction(Select *select) : select_(select) {} + TEMPLATABLE_VALUE(size_t, index) + + void play(Ts... x) override { + auto call = this->select_->make_call(); + call.set_index(this->index_.value(x...)); + call.perform(); + } + + protected: + Select *select_; +}; + +template class SelectOperationAction : public Action { + public: + explicit SelectOperationAction(Select *select) : select_(select) {} + TEMPLATABLE_VALUE(bool, cycle) + TEMPLATABLE_VALUE(SelectOperation, operation) + + void play(Ts... x) override { + auto call = this->select_->make_call(); + call.with_operation(this->operation_.value(x...)); + if (this->cycle_.has_value()) { + call.with_cycle(this->cycle_.value(x...)); + } + call.perform(); + } + + protected: + Select *select_; +}; + } // namespace select } // namespace esphome diff --git a/esphome/components/select/select.cpp b/esphome/components/select/select.cpp index 14f4d9277d..75edb5c8ba 100644 --- a/esphome/components/select/select.cpp +++ b/esphome/components/select/select.cpp @@ -6,37 +6,53 @@ namespace select { static const char *const TAG = "select"; -void SelectCall::perform() { - ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); - if (!this->option_.has_value()) { - ESP_LOGW(TAG, "No value set for SelectCall"); - return; - } - - const auto &traits = this->parent_->traits; - auto value = *this->option_; - auto options = traits.get_options(); - - if (std::find(options.begin(), options.end(), value) == options.end()) { - ESP_LOGW(TAG, " Option %s is not a valid option.", value.c_str()); - return; - } - - ESP_LOGD(TAG, " Option: %s", (*this->option_).c_str()); - this->parent_->control(*this->option_); -} - void Select::publish_state(const std::string &state) { - this->has_state_ = true; - this->state = state; - ESP_LOGD(TAG, "'%s': Sending state %s", this->get_name().c_str(), state.c_str()); - this->state_callback_.call(state); + auto index = this->index_of(state); + const auto *name = this->get_name().c_str(); + if (index.has_value()) { + this->has_state_ = true; + this->state = state; + ESP_LOGD(TAG, "'%s': Sending state %s (index %d)", name, state.c_str(), index.value()); + this->state_callback_.call(state, index.value()); + } else { + ESP_LOGE(TAG, "'%s': invalid state for publish_state(): %s", name, state.c_str()); + } } -void Select::add_on_state_callback(std::function &&callback) { +void Select::add_on_state_callback(std::function &&callback) { this->state_callback_.add(std::move(callback)); } +size_t Select::size() const { + auto options = traits.get_options(); + return options.size(); +} + +optional Select::index_of(const std::string &option) const { + auto options = traits.get_options(); + auto it = std::find(options.begin(), options.end(), option); + if (it == options.end()) { + return {}; + } + return std::distance(options.begin(), it); +} + +optional Select::active_index() const { + if (this->has_state()) { + return this->index_of(this->state); + } else { + return {}; + } +} + +optional Select::at(size_t index) const { + auto options = traits.get_options(); + if (index >= options.size()) { + return {}; + } + return options.at(index); +} + uint32_t Select::hash_base() { return 2812997003UL; } } // namespace select diff --git a/esphome/components/select/select.h b/esphome/components/select/select.h index db655ea34e..64870fc9a3 100644 --- a/esphome/components/select/select.h +++ b/esphome/components/select/select.h @@ -1,10 +1,10 @@ #pragma once -#include -#include #include "esphome/core/component.h" #include "esphome/core/entity_base.h" #include "esphome/core/helpers.h" +#include "select_call.h" +#include "select_traits.h" namespace esphome { namespace select { @@ -17,33 +17,6 @@ namespace select { } \ } -class Select; - -class SelectCall { - public: - explicit SelectCall(Select *parent) : parent_(parent) {} - void perform(); - - SelectCall &set_option(const std::string &option) { - option_ = option; - return *this; - } - const optional &get_option() const { return option_; } - - protected: - Select *const parent_; - optional option_; -}; - -class SelectTraits { - public: - void set_options(std::vector options) { this->options_ = std::move(options); } - std::vector get_options() const { return this->options_; } - - protected: - std::vector options_; -}; - /** Base-class for all selects. * * A select can use publish_state to send out a new value. @@ -51,18 +24,23 @@ class SelectTraits { class Select : public EntityBase { public: std::string state; + SelectTraits traits; void publish_state(const std::string &state); + /// Return whether this select has gotten a full state yet. + bool has_state() const { return has_state_; } + SelectCall make_call() { return SelectCall(this); } void set(const std::string &value) { make_call().set_option(value).perform(); } - void add_on_state_callback(std::function &&callback); + // Methods that provide an API to index-based access. + size_t size() const; + optional index_of(const std::string &option) const; + optional active_index() const; + optional at(size_t index) const; - SelectTraits traits; - - /// Return whether this select has gotten a full state yet. - bool has_state() const { return has_state_; } + void add_on_state_callback(std::function &&callback); protected: friend class SelectCall; @@ -77,7 +55,7 @@ class Select : public EntityBase { uint32_t hash_base() override; - CallbackManager state_callback_; + CallbackManager state_callback_; bool has_state_{false}; }; diff --git a/esphome/components/select/select_call.cpp b/esphome/components/select/select_call.cpp new file mode 100644 index 0000000000..9442598740 --- /dev/null +++ b/esphome/components/select/select_call.cpp @@ -0,0 +1,122 @@ +#include "select_call.h" +#include "select.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace select { + +static const char *const TAG = "select"; + +SelectCall &SelectCall::set_option(const std::string &option) { + return with_operation(SELECT_OP_SET).with_option(option); +} + +SelectCall &SelectCall::set_index(size_t index) { return with_operation(SELECT_OP_SET_INDEX).with_index(index); } + +const optional &SelectCall::get_option() const { return option_; } + +SelectCall &SelectCall::select_next(bool cycle) { return with_operation(SELECT_OP_NEXT).with_cycle(cycle); } + +SelectCall &SelectCall::select_previous(bool cycle) { return with_operation(SELECT_OP_PREVIOUS).with_cycle(cycle); } + +SelectCall &SelectCall::select_first() { return with_operation(SELECT_OP_FIRST); } + +SelectCall &SelectCall::select_last() { return with_operation(SELECT_OP_LAST); } + +SelectCall &SelectCall::with_operation(SelectOperation operation) { + this->operation_ = operation; + return *this; +} + +SelectCall &SelectCall::with_cycle(bool cycle) { + this->cycle_ = cycle; + return *this; +} + +SelectCall &SelectCall::with_option(const std::string &option) { + this->option_ = option; + return *this; +} + +SelectCall &SelectCall::with_index(size_t index) { + this->index_ = index; + return *this; +} + +void SelectCall::perform() { + auto *parent = this->parent_; + const auto *name = parent->get_name().c_str(); + const auto &traits = parent->traits; + auto options = traits.get_options(); + + if (this->operation_ == SELECT_OP_NONE) { + ESP_LOGW(TAG, "'%s' - SelectCall performed without selecting an operation", name); + return; + } + if (options.empty()) { + ESP_LOGW(TAG, "'%s' - Cannot perform SelectCall, select has no options", name); + return; + } + + std::string target_value; + + if (this->operation_ == SELECT_OP_SET) { + ESP_LOGD(TAG, "'%s' - Setting", name); + if (!this->option_.has_value()) { + ESP_LOGW(TAG, "'%s' - No option value set for SelectCall", name); + return; + } + target_value = this->option_.value(); + } else if (this->operation_ == SELECT_OP_SET_INDEX) { + if (!this->index_.has_value()) { + ESP_LOGW(TAG, "'%s' - No index value set for SelectCall", name); + return; + } + if (this->index_.value() >= options.size()) { + ESP_LOGW(TAG, "'%s' - Index value %d out of bounds", name, this->index_.value()); + return; + } + target_value = options[this->index_.value()]; + } else if (this->operation_ == SELECT_OP_FIRST) { + target_value = options.front(); + } else if (this->operation_ == SELECT_OP_LAST) { + target_value = options.back(); + } else if (this->operation_ == SELECT_OP_NEXT || this->operation_ == SELECT_OP_PREVIOUS) { + auto cycle = this->cycle_; + ESP_LOGD(TAG, "'%s' - Selecting %s, with%s cycling", name, this->operation_ == SELECT_OP_NEXT ? "next" : "previous", + cycle ? "" : "out"); + if (!parent->has_state()) { + target_value = this->operation_ == SELECT_OP_NEXT ? options.front() : options.back(); + } else { + auto index = parent->index_of(parent->state); + if (index.has_value()) { + auto size = options.size(); + if (cycle) { + auto use_index = (size + index.value() + (this->operation_ == SELECT_OP_NEXT ? +1 : -1)) % size; + target_value = options[use_index]; + } else { + if (this->operation_ == SELECT_OP_PREVIOUS && index.value() > 0) { + target_value = options[index.value() - 1]; + } else if (this->operation_ == SELECT_OP_NEXT && index.value() < options.size() - 1) { + target_value = options[index.value() + 1]; + } else { + return; + } + } + } else { + target_value = this->operation_ == SELECT_OP_NEXT ? options.front() : options.back(); + } + } + } + + if (std::find(options.begin(), options.end(), target_value) == options.end()) { + ESP_LOGW(TAG, "'%s' - Option %s is not a valid option", name, target_value.c_str()); + return; + } + + ESP_LOGD(TAG, "'%s' - Set selected option to: %s", name, target_value.c_str()); + parent->control(target_value); +} + +} // namespace select +} // namespace esphome diff --git a/esphome/components/select/select_call.h b/esphome/components/select/select_call.h new file mode 100644 index 0000000000..ea4d34ab5f --- /dev/null +++ b/esphome/components/select/select_call.h @@ -0,0 +1,48 @@ +#pragma once + +#include "esphome/core/helpers.h" + +namespace esphome { +namespace select { + +class Select; + +enum SelectOperation { + SELECT_OP_NONE, + SELECT_OP_SET, + SELECT_OP_SET_INDEX, + SELECT_OP_NEXT, + SELECT_OP_PREVIOUS, + SELECT_OP_FIRST, + SELECT_OP_LAST +}; + +class SelectCall { + public: + explicit SelectCall(Select *parent) : parent_(parent) {} + void perform(); + + SelectCall &set_option(const std::string &option); + SelectCall &set_index(size_t index); + const optional &get_option() const; + + SelectCall &select_next(bool cycle); + SelectCall &select_previous(bool cycle); + SelectCall &select_first(); + SelectCall &select_last(); + + SelectCall &with_operation(SelectOperation operation); + SelectCall &with_cycle(bool cycle); + SelectCall &with_option(const std::string &option); + SelectCall &with_index(size_t index); + + protected: + Select *const parent_; + optional option_; + optional index_; + SelectOperation operation_{SELECT_OP_NONE}; + bool cycle_; +}; + +} // namespace select +} // namespace esphome diff --git a/esphome/components/select/select_traits.cpp b/esphome/components/select/select_traits.cpp new file mode 100644 index 0000000000..89da30c405 --- /dev/null +++ b/esphome/components/select/select_traits.cpp @@ -0,0 +1,11 @@ +#include "select_traits.h" + +namespace esphome { +namespace select { + +void SelectTraits::set_options(std::vector options) { this->options_ = std::move(options); } + +std::vector SelectTraits::get_options() const { return this->options_; } + +} // namespace select +} // namespace esphome diff --git a/esphome/components/select/select_traits.h b/esphome/components/select/select_traits.h new file mode 100644 index 0000000000..ccf23dc6d0 --- /dev/null +++ b/esphome/components/select/select_traits.h @@ -0,0 +1,19 @@ +#pragma once + +#include +#include + +namespace esphome { +namespace select { + +class SelectTraits { + public: + void set_options(std::vector options); + std::vector get_options() const; + + protected: + std::vector options_; +}; + +} // namespace select +} // namespace esphome diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 0dfd608661..6822ce9953 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -755,7 +755,7 @@ std::string WebServer::number_json(number::Number *obj, float value, JsonDetail #endif #ifdef USE_SELECT -void WebServer::on_select_update(select::Select *obj, const std::string &state) { +void WebServer::on_select_update(select::Select *obj, const std::string &state, size_t index) { this->events_.send(this->select_json(obj, state, DETAIL_STATE).c_str(), "state"); } void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlMatch &match) { diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index bd7acd91a0..78d0597e61 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -185,7 +185,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { #endif #ifdef USE_SELECT - void on_select_update(select::Select *obj, const std::string &state) override; + void on_select_update(select::Select *obj, const std::string &state, size_t index) override; /// Handle a select request under '/select/'. void handle_select_request(AsyncWebServerRequest *request, const UrlMatch &match); diff --git a/esphome/const.py b/esphome/const.py index 9f2bed28d1..fc928dc530 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -138,6 +138,7 @@ CONF_CUSTOM_FAN_MODE = "custom_fan_mode" CONF_CUSTOM_FAN_MODES = "custom_fan_modes" CONF_CUSTOM_PRESET = "custom_preset" CONF_CUSTOM_PRESETS = "custom_presets" +CONF_CYCLE = "cycle" CONF_DALLAS_ID = "dallas_id" CONF_DATA = "data" CONF_DATA_PIN = "data_pin" @@ -458,6 +459,7 @@ CONF_OPEN_DRAIN = "open_drain" CONF_OPEN_DRAIN_INTERRUPT = "open_drain_interrupt" CONF_OPEN_DURATION = "open_duration" CONF_OPEN_ENDSTOP = "open_endstop" +CONF_OPERATION = "operation" CONF_OPTIMISTIC = "optimistic" CONF_OPTION = "option" CONF_OPTIONS = "options" diff --git a/esphome/core/controller.cpp b/esphome/core/controller.cpp index dfcef5e4c1..7d63a3d143 100644 --- a/esphome/core/controller.cpp +++ b/esphome/core/controller.cpp @@ -61,8 +61,10 @@ void Controller::setup_controller(bool include_internal) { #endif #ifdef USE_SELECT for (auto *obj : App.get_selects()) { - if (include_internal || !obj->is_internal()) - obj->add_on_state_callback([this, obj](const std::string &state) { this->on_select_update(obj, state); }); + if (include_internal || !obj->is_internal()) { + obj->add_on_state_callback( + [this, obj](const std::string &state, size_t index) { this->on_select_update(obj, state, index); }); + } } #endif #ifdef USE_LOCK diff --git a/esphome/core/controller.h b/esphome/core/controller.h index 0be854828b..419624a2ae 100644 --- a/esphome/core/controller.h +++ b/esphome/core/controller.h @@ -71,7 +71,7 @@ class Controller { virtual void on_number_update(number::Number *obj, float state){}; #endif #ifdef USE_SELECT - virtual void on_select_update(select::Select *obj, const std::string &state){}; + virtual void on_select_update(select::Select *obj, const std::string &state, size_t index){}; #endif #ifdef USE_LOCK virtual void on_lock_update(lock::Lock *obj){}; diff --git a/esphome/cpp_types.py b/esphome/cpp_types.py index 2323b2578f..aafe765111 100644 --- a/esphome/cpp_types.py +++ b/esphome/cpp_types.py @@ -16,6 +16,7 @@ uint32 = global_ns.namespace("uint32_t") uint64 = global_ns.namespace("uint64_t") int32 = global_ns.namespace("int32_t") int64 = global_ns.namespace("int64_t") +size_t = global_ns.namespace("size_t") const_char_ptr = global_ns.namespace("const char *") NAN = global_ns.namespace("NAN") esphome_ns = global_ns # using namespace esphome; diff --git a/tests/test5.yaml b/tests/test5.yaml index ee90cc1149..e38f39eab6 100644 --- a/tests/test5.yaml +++ b/tests/test5.yaml @@ -154,8 +154,8 @@ select: restore_value: true on_value: - logger.log: - format: "Select changed to %s" - args: ["x.c_str()"] + format: "Select changed to %s (index %d)" + args: ["x.c_str()", "i"] set_action: - logger.log: format: "Template Select set to %s" @@ -163,11 +163,42 @@ select: - select.set: id: template_select_id option: two + - select.first: template_select_id + - select.last: + id: template_select_id + - select.previous: template_select_id + - select.next: + id: template_select_id + cycle: false + - select.operation: + id: template_select_id + operation: Previous + cycle: false + - select.operation: + id: template_select_id + operation: !lambda "return SELECT_OP_PREVIOUS;" + cycle: !lambda "return true;" + - select.set_index: + id: template_select_id + index: 1 + - select.set_index: + id: template_select_id + index: !lambda "return 1 + 1;" options: - one - two - three + - platform: modbus_controller + name: "Modbus Select Register 1000" + address: 1000 + value_type: U_WORD + optionsmap: + "Zero": 0 + "One": 1 + "Two": 2 + "Three": 3 + sensor: - platform: selec_meter total_active_energy: From d9caab41081f5230ac99252bc2bd645638c43f1b Mon Sep 17 00:00:00 2001 From: Maurice Makaay Date: Tue, 10 May 2022 06:58:56 +0200 Subject: [PATCH 0432/1729] Number enhancement (#3429) Co-authored-by: Maurice Makaay Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/number/__init__.py | 104 ++++++++++++++++- esphome/components/number/automation.h | 19 ++++ esphome/components/number/number.cpp | 33 ------ esphome/components/number/number.h | 50 +-------- esphome/components/number/number_call.cpp | 118 ++++++++++++++++++++ esphome/components/number/number_call.h | 45 ++++++++ esphome/components/number/number_traits.cpp | 20 ++++ esphome/components/number/number_traits.h | 44 ++++++++ tests/test5.yaml | 35 +++++- 9 files changed, 380 insertions(+), 88 deletions(-) create mode 100644 esphome/components/number/number_call.cpp create mode 100644 esphome/components/number/number_call.h create mode 100644 esphome/components/number/number_traits.cpp create mode 100644 esphome/components/number/number_traits.h diff --git a/esphome/components/number/__init__.py b/esphome/components/number/__init__.py index 89788f1e98..f809fff529 100644 --- a/esphome/components/number/__init__.py +++ b/esphome/components/number/__init__.py @@ -14,6 +14,8 @@ from esphome.const import ( CONF_UNIT_OF_MEASUREMENT, CONF_MQTT_ID, CONF_VALUE, + CONF_OPERATION, + CONF_CYCLE, ) from esphome.core import CORE, coroutine_with_priority from esphome.cpp_helpers import setup_entity @@ -35,6 +37,7 @@ ValueRangeTrigger = number_ns.class_( # Actions NumberSetAction = number_ns.class_("NumberSetAction", automation.Action) +NumberOperationAction = number_ns.class_("NumberOperationAction", automation.Action) # Conditions NumberInRangeCondition = number_ns.class_( @@ -49,6 +52,15 @@ NUMBER_MODES = { "SLIDER": NumberMode.NUMBER_MODE_SLIDER, } +NumberOperation = number_ns.enum("NumberOperation") + +NUMBER_OPERATION_OPTIONS = { + "INCREMENT": NumberOperation.NUMBER_OP_INCREMENT, + "DECREMENT": NumberOperation.NUMBER_OP_DECREMENT, + "TO_MIN": NumberOperation.NUMBER_OP_TO_MIN, + "TO_MAX": NumberOperation.NUMBER_OP_TO_MAX, +} + icon = cv.icon NUMBER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA).extend( @@ -159,12 +171,18 @@ async def to_code(config): cg.add_global(number_ns.using) +OPERATION_BASE_SCHEMA = cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(Number), + } +) + + @automation.register_action( "number.set", NumberSetAction, - cv.Schema( + OPERATION_BASE_SCHEMA.extend( { - cv.Required(CONF_ID): cv.use_id(Number), cv.Required(CONF_VALUE): cv.templatable(cv.float_), } ), @@ -175,3 +193,85 @@ async def number_set_to_code(config, action_id, template_arg, args): template_ = await cg.templatable(config[CONF_VALUE], args, float) cg.add(var.set_value(template_)) return var + + +@automation.register_action( + "number.increment", + NumberOperationAction, + automation.maybe_simple_id( + OPERATION_BASE_SCHEMA.extend( + { + cv.Optional(CONF_MODE, default="INCREMENT"): cv.one_of( + "INCREMENT", upper=True + ), + cv.Optional(CONF_CYCLE, default=True): cv.boolean, + } + ) + ), +) +@automation.register_action( + "number.decrement", + NumberOperationAction, + automation.maybe_simple_id( + OPERATION_BASE_SCHEMA.extend( + { + cv.Optional(CONF_MODE, default="DECREMENT"): cv.one_of( + "DECREMENT", upper=True + ), + cv.Optional(CONF_CYCLE, default=True): cv.boolean, + } + ) + ), +) +@automation.register_action( + "number.to_min", + NumberOperationAction, + automation.maybe_simple_id( + OPERATION_BASE_SCHEMA.extend( + { + cv.Optional(CONF_MODE, default="TO_MIN"): cv.one_of( + "TO_MIN", upper=True + ), + } + ) + ), +) +@automation.register_action( + "number.to_max", + NumberOperationAction, + automation.maybe_simple_id( + OPERATION_BASE_SCHEMA.extend( + { + cv.Optional(CONF_MODE, default="TO_MAX"): cv.one_of( + "TO_MAX", upper=True + ), + } + ) + ), +) +@automation.register_action( + "number.operation", + NumberOperationAction, + OPERATION_BASE_SCHEMA.extend( + { + cv.Required(CONF_OPERATION): cv.templatable( + cv.enum(NUMBER_OPERATION_OPTIONS, upper=True) + ), + cv.Optional(CONF_CYCLE, default=True): cv.templatable(cv.boolean), + } + ), +) +async def number_to_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) + if CONF_OPERATION in config: + to_ = await cg.templatable(config[CONF_OPERATION], args, NumberOperation) + cg.add(var.set_operation(to_)) + if CONF_CYCLE in config: + cycle_ = await cg.templatable(config[CONF_CYCLE], args, bool) + cg.add(var.set_cycle(cycle_)) + if CONF_MODE in config: + cg.add(var.set_operation(NUMBER_OPERATION_OPTIONS[config[CONF_MODE]])) + if CONF_CYCLE in config: + cg.add(var.set_cycle(config[CONF_CYCLE])) + return var diff --git a/esphome/components/number/automation.h b/esphome/components/number/automation.h index 98554a346a..33f0f9727e 100644 --- a/esphome/components/number/automation.h +++ b/esphome/components/number/automation.h @@ -29,6 +29,25 @@ template class NumberSetAction : public Action { Number *number_; }; +template class NumberOperationAction : public Action { + public: + explicit NumberOperationAction(Number *number) : number_(number) {} + TEMPLATABLE_VALUE(NumberOperation, operation) + TEMPLATABLE_VALUE(bool, cycle) + + void play(Ts... x) override { + auto call = this->number_->make_call(); + call.with_operation(this->operation_.value(x...)); + if (this->cycle_.has_value()) { + call.with_cycle(this->cycle_.value(x...)); + } + call.perform(); + } + + protected: + Number *number_; +}; + class ValueRangeTrigger : public Trigger, public Component { public: explicit ValueRangeTrigger(Number *parent) : parent_(parent) {} diff --git a/esphome/components/number/number.cpp b/esphome/components/number/number.cpp index 99a2c04a22..03a7cc6ce3 100644 --- a/esphome/components/number/number.cpp +++ b/esphome/components/number/number.cpp @@ -6,30 +6,6 @@ namespace number { static const char *const TAG = "number"; -void NumberCall::perform() { - ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); - if (!this->value_.has_value() || std::isnan(*this->value_)) { - ESP_LOGW(TAG, "No value set for NumberCall"); - return; - } - - const auto &traits = this->parent_->traits; - auto value = *this->value_; - - float min_value = traits.get_min_value(); - if (value < min_value) { - ESP_LOGW(TAG, " Value %f must not be less than minimum %f", value, min_value); - return; - } - float max_value = traits.get_max_value(); - if (value > max_value) { - ESP_LOGW(TAG, " Value %f must not be greater than maximum %f", value, max_value); - return; - } - ESP_LOGD(TAG, " Value: %f", *this->value_); - this->parent_->control(*this->value_); -} - void Number::publish_state(float state) { this->has_state_ = true; this->state = state; @@ -41,15 +17,6 @@ void Number::add_on_state_callback(std::function &&callback) { this->state_callback_.add(std::move(callback)); } -std::string NumberTraits::get_unit_of_measurement() { - if (this->unit_of_measurement_.has_value()) - return *this->unit_of_measurement_; - return ""; -} -void NumberTraits::set_unit_of_measurement(const std::string &unit_of_measurement) { - this->unit_of_measurement_ = unit_of_measurement; -} - uint32_t Number::hash_base() { return 2282307003UL; } } // namespace number diff --git a/esphome/components/number/number.h b/esphome/components/number/number.h index 40fdfceec1..8f9bf8c2e1 100644 --- a/esphome/components/number/number.h +++ b/esphome/components/number/number.h @@ -3,6 +3,8 @@ #include "esphome/core/component.h" #include "esphome/core/entity_base.h" #include "esphome/core/helpers.h" +#include "number_call.h" +#include "number_traits.h" namespace esphome { namespace number { @@ -20,54 +22,6 @@ namespace number { class Number; -class NumberCall { - public: - explicit NumberCall(Number *parent) : parent_(parent) {} - void perform(); - - NumberCall &set_value(float value) { - value_ = value; - return *this; - } - const optional &get_value() const { return value_; } - - protected: - Number *const parent_; - optional value_; -}; - -enum NumberMode : uint8_t { - NUMBER_MODE_AUTO = 0, - NUMBER_MODE_BOX = 1, - NUMBER_MODE_SLIDER = 2, -}; - -class NumberTraits { - public: - void set_min_value(float min_value) { min_value_ = min_value; } - float get_min_value() const { return min_value_; } - void set_max_value(float max_value) { max_value_ = max_value; } - float get_max_value() const { return max_value_; } - void set_step(float step) { step_ = step; } - float get_step() const { return step_; } - - /// Get the unit of measurement, using the manual override if set. - std::string get_unit_of_measurement(); - /// Manually set the unit of measurement. - void set_unit_of_measurement(const std::string &unit_of_measurement); - - // Get/set the frontend mode. - NumberMode get_mode() const { return this->mode_; } - void set_mode(NumberMode mode) { this->mode_ = mode; } - - protected: - float min_value_ = NAN; - float max_value_ = NAN; - float step_ = NAN; - optional unit_of_measurement_; ///< Unit of measurement override - NumberMode mode_{NUMBER_MODE_AUTO}; -}; - /** Base-class for all numbers. * * A number can use publish_state to send out a new value. diff --git a/esphome/components/number/number_call.cpp b/esphome/components/number/number_call.cpp new file mode 100644 index 0000000000..4219f85328 --- /dev/null +++ b/esphome/components/number/number_call.cpp @@ -0,0 +1,118 @@ +#include "number_call.h" +#include "number.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace number { + +static const char *const TAG = "number"; + +NumberCall &NumberCall::set_value(float value) { return this->with_operation(NUMBER_OP_SET).with_value(value); } + +NumberCall &NumberCall::number_increment(bool cycle) { + return this->with_operation(NUMBER_OP_INCREMENT).with_cycle(cycle); +} + +NumberCall &NumberCall::number_decrement(bool cycle) { + return this->with_operation(NUMBER_OP_DECREMENT).with_cycle(cycle); +} + +NumberCall &NumberCall::number_to_min() { return this->with_operation(NUMBER_OP_TO_MIN); } + +NumberCall &NumberCall::number_to_max() { return this->with_operation(NUMBER_OP_TO_MAX); } + +NumberCall &NumberCall::with_operation(NumberOperation operation) { + this->operation_ = operation; + return *this; +} + +NumberCall &NumberCall::with_value(float value) { + this->value_ = value; + return *this; +} + +NumberCall &NumberCall::with_cycle(bool cycle) { + this->cycle_ = cycle; + return *this; +} + +void NumberCall::perform() { + auto *parent = this->parent_; + const auto *name = parent->get_name().c_str(); + const auto &traits = parent->traits; + + if (this->operation_ == NUMBER_OP_NONE) { + ESP_LOGW(TAG, "'%s' - NumberCall performed without selecting an operation", name); + return; + } + + float target_value = NAN; + float min_value = traits.get_min_value(); + float max_value = traits.get_max_value(); + + if (this->operation_ == NUMBER_OP_SET) { + ESP_LOGD(TAG, "'%s' - Setting number value", name); + if (!this->value_.has_value() || std::isnan(*this->value_)) { + ESP_LOGW(TAG, "'%s' - No value set for NumberCall", name); + return; + } + target_value = this->value_.value(); + } else if (this->operation_ == NUMBER_OP_TO_MIN) { + if (std::isnan(min_value)) { + ESP_LOGW(TAG, "'%s' - Can't set to min value through NumberCall: no min_value defined", name); + } else { + target_value = min_value; + } + } else if (this->operation_ == NUMBER_OP_TO_MAX) { + if (std::isnan(max_value)) { + ESP_LOGW(TAG, "'%s' - Can't set to max value through NumberCall: no max_value defined", name); + } else { + target_value = max_value; + } + } else if (this->operation_ == NUMBER_OP_INCREMENT) { + ESP_LOGD(TAG, "'%s' - Increment number, with%s cycling", name, this->cycle_ ? "" : "out"); + if (!parent->has_state()) { + ESP_LOGW(TAG, "'%s' - Can't increment number through NumberCall: no active state to modify", name); + return; + } + auto step = traits.get_step(); + target_value = parent->state + (std::isnan(step) ? 1 : step); + if (target_value > max_value) { + if (this->cycle_ && !std::isnan(min_value)) { + target_value = min_value; + } else { + target_value = max_value; + } + } + } else if (this->operation_ == NUMBER_OP_DECREMENT) { + ESP_LOGD(TAG, "'%s' - Decrement number, with%s cycling", name, this->cycle_ ? "" : "out"); + if (!parent->has_state()) { + ESP_LOGW(TAG, "'%s' - Can't decrement number through NumberCall: no active state to modify", name); + return; + } + auto step = traits.get_step(); + target_value = parent->state - (std::isnan(step) ? 1 : step); + if (target_value < min_value) { + if (this->cycle_ && !std::isnan(max_value)) { + target_value = max_value; + } else { + target_value = min_value; + } + } + } + + if (target_value < min_value) { + ESP_LOGW(TAG, "'%s' - Value %f must not be less than minimum %f", name, target_value, min_value); + return; + } + if (target_value > max_value) { + ESP_LOGW(TAG, "'%s' - Value %f must not be greater than maximum %f", name, target_value, max_value); + return; + } + + ESP_LOGD(TAG, " New number value: %f", target_value); + this->parent_->control(target_value); +} + +} // namespace number +} // namespace esphome diff --git a/esphome/components/number/number_call.h b/esphome/components/number/number_call.h new file mode 100644 index 0000000000..9a3dad560f --- /dev/null +++ b/esphome/components/number/number_call.h @@ -0,0 +1,45 @@ +#pragma once + +#include "esphome/core/helpers.h" +#include "number_traits.h" + +namespace esphome { +namespace number { + +class Number; + +enum NumberOperation { + NUMBER_OP_NONE, + NUMBER_OP_SET, + NUMBER_OP_INCREMENT, + NUMBER_OP_DECREMENT, + NUMBER_OP_TO_MIN, + NUMBER_OP_TO_MAX, +}; + +class NumberCall { + public: + explicit NumberCall(Number *parent) : parent_(parent) {} + void perform(); + + NumberCall &set_value(float value); + const optional &get_value() const { return value_; } + + NumberCall &number_increment(bool cycle); + NumberCall &number_decrement(bool cycle); + NumberCall &number_to_min(); + NumberCall &number_to_max(); + + NumberCall &with_operation(NumberOperation operation); + NumberCall &with_value(float value); + NumberCall &with_cycle(bool cycle); + + protected: + Number *const parent_; + NumberOperation operation_{NUMBER_OP_NONE}; + optional value_; + bool cycle_; +}; + +} // namespace number +} // namespace esphome diff --git a/esphome/components/number/number_traits.cpp b/esphome/components/number/number_traits.cpp new file mode 100644 index 0000000000..dcd05daa2a --- /dev/null +++ b/esphome/components/number/number_traits.cpp @@ -0,0 +1,20 @@ +#include "esphome/core/log.h" +#include "number_traits.h" + +namespace esphome { +namespace number { + +static const char *const TAG = "number"; + +void NumberTraits::set_unit_of_measurement(const std::string &unit_of_measurement) { + this->unit_of_measurement_ = unit_of_measurement; +} + +std::string NumberTraits::get_unit_of_measurement() { + if (this->unit_of_measurement_.has_value()) + return *this->unit_of_measurement_; + return ""; +} + +} // namespace number +} // namespace esphome diff --git a/esphome/components/number/number_traits.h b/esphome/components/number/number_traits.h new file mode 100644 index 0000000000..47756ff66f --- /dev/null +++ b/esphome/components/number/number_traits.h @@ -0,0 +1,44 @@ +#pragma once + +#include "esphome/core/helpers.h" + +namespace esphome { +namespace number { + +enum NumberMode : uint8_t { + NUMBER_MODE_AUTO = 0, + NUMBER_MODE_BOX = 1, + NUMBER_MODE_SLIDER = 2, +}; + +class NumberTraits { + public: + // Set/get the number value boundaries. + void set_min_value(float min_value) { min_value_ = min_value; } + float get_min_value() const { return min_value_; } + void set_max_value(float max_value) { max_value_ = max_value; } + float get_max_value() const { return max_value_; } + + // Set/get the step size for incrementing or decrementing the number value. + void set_step(float step) { step_ = step; } + float get_step() const { return step_; } + + /// Manually set the unit of measurement. + void set_unit_of_measurement(const std::string &unit_of_measurement); + /// Get the unit of measurement, using the manual override if set. + std::string get_unit_of_measurement(); + + // Set/get the frontend mode. + void set_mode(NumberMode mode) { this->mode_ = mode; } + NumberMode get_mode() const { return this->mode_; } + + protected: + float min_value_ = NAN; + float max_value_ = NAN; + float step_ = NAN; + optional unit_of_measurement_; ///< Unit of measurement override + NumberMode mode_{NUMBER_MODE_AUTO}; +}; + +} // namespace number +} // namespace esphome diff --git a/tests/test5.yaml b/tests/test5.yaml index e38f39eab6..ffda860377 100644 --- a/tests/test5.yaml +++ b/tests/test5.yaml @@ -120,6 +120,11 @@ number: name: My template number id: template_number_id optimistic: true + max_value: 100 + min_value: 0 + step: 5 + unit_of_measurement: "%" + mode: slider on_value: - logger.log: format: "Number changed to %f" @@ -128,11 +133,31 @@ number: - logger.log: format: "Template Number set to %f" args: ["x"] - max_value: 100 - min_value: 0 - step: 5 - unit_of_measurement: "%" - mode: slider + - number.set: + id: template_number_id + value: 50 + - number.to_min: template_number_id + - number.to_min: + id: template_number_id + - number.to_max: template_number_id + - number.to_max: + id: template_number_id + - number.increment: template_number_id + - number.increment: + id: template_number_id + cycle: false + - number.decrement: template_number_id + - number.decrement: + id: template_number_id + cycle: false + - number.operation: + id: template_number_id + operation: Increment + cycle: false + - number.operation: + id: template_number_id + operation: !lambda "return NUMBER_OP_INCREMENT;" + cycle: !lambda "return false;" - id: modbus_numbertest platform: modbus_controller From d685fdf54a2cfbae32fa98eaf6b11ec1111e2912 Mon Sep 17 00:00:00 2001 From: MFlasskamp Date: Tue, 10 May 2022 07:16:16 +0200 Subject: [PATCH 0433/1729] mask deprecated adc_gpio_init() for esp32-s2 (#3445) --- esphome/components/adc/adc_sensor.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/adc/adc_sensor.cpp b/esphome/components/adc/adc_sensor.cpp index ad9cf29b6f..ca87c8d11f 100644 --- a/esphome/components/adc/adc_sensor.cpp +++ b/esphome/components/adc/adc_sensor.cpp @@ -51,8 +51,8 @@ void ADCSensor::setup() { } } - // adc_gpio_init doesn't exist on ESP32-C3 or ESP32-H2 -#if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32H2) + // adc_gpio_init doesn't exist on ESP32-S2, ESP32-C3 or ESP32-H2 +#if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32H2) && !defined(USE_ESP32_VARIANT_ESP32S2) adc_gpio_init(ADC_UNIT_1, (adc_channel_t) channel_); #endif #endif // USE_ESP32 From 86b52df839075e956f307c2666ae5719a992efbc Mon Sep 17 00:00:00 2001 From: Martin <25747549+martgras@users.noreply.github.com> Date: Tue, 10 May 2022 07:17:55 +0200 Subject: [PATCH 0434/1729] tca9548a fix channel selection (#3417) --- esphome/components/tca9548a/tca9548a.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/tca9548a/tca9548a.cpp b/esphome/components/tca9548a/tca9548a.cpp index de0d21b968..caa3dd0655 100644 --- a/esphome/components/tca9548a/tca9548a.cpp +++ b/esphome/components/tca9548a/tca9548a.cpp @@ -41,7 +41,7 @@ i2c::ErrorCode TCA9548AComponent::switch_to_channel(uint8_t channel) { return i2c::ERROR_OK; uint8_t channel_val = 1 << channel; - auto err = this->write_register(0x70, &channel_val, 1); + auto err = this->write(&channel_val, 1); if (err == i2c::ERROR_OK) { current_channel_ = channel; } From 0e547390da32bd773a2a6b0d575feaa981275ecf Mon Sep 17 00:00:00 2001 From: Martin <25747549+martgras@users.noreply.github.com> Date: Tue, 10 May 2022 10:15:02 +0200 Subject: [PATCH 0435/1729] add support for Sen5x sensor series (#3383) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/sen5x/__init__.py | 0 esphome/components/sen5x/automation.h | 21 ++ esphome/components/sen5x/sen5x.cpp | 413 ++++++++++++++++++++++++++ esphome/components/sen5x/sen5x.h | 128 ++++++++ esphome/components/sen5x/sensor.py | 241 +++++++++++++++ tests/test5.yaml | 43 +++ 7 files changed, 847 insertions(+) create mode 100644 esphome/components/sen5x/__init__.py create mode 100644 esphome/components/sen5x/automation.h create mode 100644 esphome/components/sen5x/sen5x.cpp create mode 100644 esphome/components/sen5x/sen5x.h create mode 100644 esphome/components/sen5x/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 51719ef1aa..77c9d30c5d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -173,6 +173,7 @@ esphome/components/sdm_meter/* @jesserockz @polyfaces esphome/components/sdp3x/* @Azimath esphome/components/selec_meter/* @sourabhjaiswal esphome/components/select/* @esphome/core +esphome/components/sen5x/* @martgras esphome/components/sensirion_common/* @martgras esphome/components/sensor/* @esphome/core esphome/components/sgp40/* @SenexCrenshaw diff --git a/esphome/components/sen5x/__init__.py b/esphome/components/sen5x/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/sen5x/automation.h b/esphome/components/sen5x/automation.h new file mode 100644 index 0000000000..423b942000 --- /dev/null +++ b/esphome/components/sen5x/automation.h @@ -0,0 +1,21 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "sen5x.h" + +namespace esphome { +namespace sen5x { + +template class StartFanAction : public Action { + public: + explicit StartFanAction(SEN5XComponent *sen5x) : sen5x_(sen5x) {} + + void play(Ts... x) override { this->sen5x_->start_fan_cleaning(); } + + protected: + SEN5XComponent *sen5x_; +}; + +} // namespace sen5x +} // namespace esphome diff --git a/esphome/components/sen5x/sen5x.cpp b/esphome/components/sen5x/sen5x.cpp new file mode 100644 index 0000000000..865fae373b --- /dev/null +++ b/esphome/components/sen5x/sen5x.cpp @@ -0,0 +1,413 @@ +#include "sen5x.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace sen5x { + +static const char *const TAG = "sen5x"; + +static const uint16_t SEN5X_CMD_AUTO_CLEANING_INTERVAL = 0x8004; +static const uint16_t SEN5X_CMD_GET_DATA_READY_STATUS = 0x0202; +static const uint16_t SEN5X_CMD_GET_FIRMWARE_VERSION = 0xD100; +static const uint16_t SEN5X_CMD_GET_PRODUCT_NAME = 0xD014; +static const uint16_t SEN5X_CMD_GET_SERIAL_NUMBER = 0xD033; +static const uint16_t SEN5X_CMD_NOX_ALGORITHM_TUNING = 0x60E1; +static const uint16_t SEN5X_CMD_READ_MEASUREMENT = 0x03C4; +static const uint16_t SEN5X_CMD_RHT_ACCELERATION_MODE = 0x60F7; +static const uint16_t SEN5X_CMD_START_CLEANING_FAN = 0x5607; +static const uint16_t SEN5X_CMD_START_MEASUREMENTS = 0x0021; +static const uint16_t SEN5X_CMD_START_MEASUREMENTS_RHT_ONLY = 0x0037; +static const uint16_t SEN5X_CMD_STOP_MEASUREMENTS = 0x3f86; +static const uint16_t SEN5X_CMD_TEMPERATURE_COMPENSATION = 0x60B2; +static const uint16_t SEN5X_CMD_VOC_ALGORITHM_STATE = 0x6181; +static const uint16_t SEN5X_CMD_VOC_ALGORITHM_TUNING = 0x60D0; + +void SEN5XComponent::setup() { + ESP_LOGCONFIG(TAG, "Setting up sen5x..."); + + // the sensor needs 1000 ms to enter the idle state + this->set_timeout(1000, [this]() { + // Check if measurement is ready before reading the value + if (!this->write_command(SEN5X_CMD_GET_DATA_READY_STATUS)) { + ESP_LOGE(TAG, "Failed to write data ready status command"); + this->mark_failed(); + return; + } + + uint16_t raw_read_status; + if (!this->read_data(raw_read_status)) { + ESP_LOGE(TAG, "Failed to read data ready status"); + this->mark_failed(); + return; + } + + uint32_t stop_measurement_delay = 0; + // In order to query the device periodic measurement must be ceased + if (raw_read_status) { + ESP_LOGD(TAG, "Sensor has data available, stopping periodic measurement"); + if (!this->write_command(SEN5X_CMD_STOP_MEASUREMENTS)) { + ESP_LOGE(TAG, "Failed to stop measurements"); + this->mark_failed(); + return; + } + // According to the SEN5x datasheet the sensor will only respond to other commands after waiting 200 ms after + // issuing the stop_periodic_measurement command + stop_measurement_delay = 200; + } + this->set_timeout(stop_measurement_delay, [this]() { + uint16_t raw_serial_number[3]; + if (!this->get_register(SEN5X_CMD_GET_SERIAL_NUMBER, raw_serial_number, 3, 20)) { + ESP_LOGE(TAG, "Failed to read serial number"); + this->error_code_ = SERIAL_NUMBER_IDENTIFICATION_FAILED; + this->mark_failed(); + return; + } + this->serial_number_[0] = static_cast(uint16_t(raw_serial_number[0]) & 0xFF); + this->serial_number_[1] = static_cast(raw_serial_number[0] & 0xFF); + this->serial_number_[2] = static_cast(raw_serial_number[1] >> 8); + ESP_LOGD(TAG, "Serial number %02d.%02d.%02d", serial_number_[0], serial_number_[1], serial_number_[2]); + + uint16_t raw_product_name[16]; + if (!this->get_register(SEN5X_CMD_GET_PRODUCT_NAME, raw_product_name, 16, 20)) { + ESP_LOGE(TAG, "Failed to read product name"); + this->error_code_ = PRODUCT_NAME_FAILED; + this->mark_failed(); + return; + } + // 2 ASCII bytes are encoded in an int + const uint16_t *current_int = raw_product_name; + char current_char; + uint8_t max = 16; + do { + // first char + current_char = *current_int >> 8; + if (current_char) { + product_name_.push_back(current_char); + // second char + current_char = *current_int & 0xFF; + if (current_char) + product_name_.push_back(current_char); + } + current_int++; + } while (current_char && --max); + + Sen5xType sen5x_type = UNKNOWN; + if (product_name_ == "SEN50") { + sen5x_type = SEN50; + } else { + if (product_name_ == "SEN54") { + sen5x_type = SEN54; + } else { + if (product_name_ == "SEN55") { + sen5x_type = SEN55; + } + } + ESP_LOGD(TAG, "Productname %s", product_name_.c_str()); + } + if (this->humidity_sensor_ && sen5x_type == SEN50) { + ESP_LOGE(TAG, "For Relative humidity a SEN54 OR SEN55 is required. You are using a <%s> sensor", + this->product_name_.c_str()); + this->humidity_sensor_ = nullptr; // mark as not used + } + if (this->temperature_sensor_ && sen5x_type == SEN50) { + ESP_LOGE(TAG, "For Temperature a SEN54 OR SEN55 is required. You are using a <%s> sensor", + this->product_name_.c_str()); + this->temperature_sensor_ = nullptr; // mark as not used + } + if (this->voc_sensor_ && sen5x_type == SEN50) { + ESP_LOGE(TAG, "For VOC a SEN54 OR SEN55 is required. You are using a <%s> sensor", this->product_name_.c_str()); + this->voc_sensor_ = nullptr; // mark as not used + } + if (this->nox_sensor_ && sen5x_type != SEN55) { + ESP_LOGE(TAG, "For NOx a SEN55 is required. You are using a <%s> sensor", this->product_name_.c_str()); + this->nox_sensor_ = nullptr; // mark as not used + } + + if (!this->get_register(SEN5X_CMD_GET_FIRMWARE_VERSION, this->firmware_version_, 20)) { + ESP_LOGE(TAG, "Failed to read firmware version"); + this->error_code_ = FIRMWARE_FAILED; + this->mark_failed(); + return; + } + this->firmware_version_ >>= 8; + ESP_LOGD(TAG, "Firmware version %d", this->firmware_version_); + + if (this->voc_sensor_ && this->store_baseline_) { + // Hash with compilation time + // This ensures the baseline storage is cleared after OTA + uint32_t hash = fnv1_hash(App.get_compilation_time()); + this->pref_ = global_preferences->make_preference(hash, true); + + if (this->pref_.load(&this->voc_baselines_storage_)) { + ESP_LOGI(TAG, "Loaded VOC baseline state0: 0x%04X, state1: 0x%04X", this->voc_baselines_storage_.state0, + voc_baselines_storage_.state1); + } + + // Initialize storage timestamp + this->seconds_since_last_store_ = 0; + + if (this->voc_baselines_storage_.state0 > 0 && this->voc_baselines_storage_.state1 > 0) { + ESP_LOGI(TAG, "Setting VOC baseline from save state0: 0x%04X, state1: 0x%04X", + this->voc_baselines_storage_.state0, voc_baselines_storage_.state1); + uint16_t states[4]; + + states[0] = voc_baselines_storage_.state0 >> 16; + states[1] = voc_baselines_storage_.state0 & 0xFFFF; + states[2] = voc_baselines_storage_.state1 >> 16; + states[3] = voc_baselines_storage_.state1 & 0xFFFF; + + if (!this->write_command(SEN5X_CMD_VOC_ALGORITHM_STATE, states, 4)) { + ESP_LOGE(TAG, "Failed to set VOC baseline from saved state"); + } + } + } + bool result; + if (this->auto_cleaning_interval_.has_value()) { + // override default value + result = write_command(SEN5X_CMD_AUTO_CLEANING_INTERVAL, this->auto_cleaning_interval_.value()); + } else { + result = write_command(SEN5X_CMD_AUTO_CLEANING_INTERVAL); + } + if (result) { + delay(20); + uint16_t secs[2]; + if (this->read_data(secs, 2)) { + auto_cleaning_interval_ = secs[0] << 16 | secs[1]; + } + } + if (acceleration_mode_.has_value()) { + result = this->write_command(SEN5X_CMD_RHT_ACCELERATION_MODE, acceleration_mode_.value()); + } else { + result = this->write_command(SEN5X_CMD_RHT_ACCELERATION_MODE); + } + if (!result) { + ESP_LOGE(TAG, "Failed to set rh/t acceleration mode"); + this->error_code_ = COMMUNICATION_FAILED; + this->mark_failed(); + return; + } + delay(20); + if (!acceleration_mode_.has_value()) { + uint16_t mode; + if (this->read_data(mode)) { + this->acceleration_mode_ = RhtAccelerationMode(mode); + } else { + ESP_LOGE(TAG, "Failed to read RHT Acceleration mode"); + } + } + if (this->voc_tuning_params_.has_value()) + this->write_tuning_parameters_(SEN5X_CMD_VOC_ALGORITHM_TUNING, this->voc_tuning_params_.value()); + if (this->nox_tuning_params_.has_value()) + this->write_tuning_parameters_(SEN5X_CMD_NOX_ALGORITHM_TUNING, this->nox_tuning_params_.value()); + + if (this->temperature_compensation_.has_value()) + this->write_temperature_compensation_(this->temperature_compensation_.value()); + + // Finally start sensor measurements + auto cmd = SEN5X_CMD_START_MEASUREMENTS_RHT_ONLY; + if (this->pm_1_0_sensor_ || this->pm_2_5_sensor_ || this->pm_4_0_sensor_ || this->pm_10_0_sensor_) { + // if any of the gas sensors are active we need a full measurement + cmd = SEN5X_CMD_START_MEASUREMENTS; + } + + if (!this->write_command(cmd)) { + ESP_LOGE(TAG, "Error starting continuous measurements."); + this->error_code_ = MEASUREMENT_INIT_FAILED; + this->mark_failed(); + return; + } + initialized_ = true; + ESP_LOGD(TAG, "Sensor initialized"); + }); + }); +} + +void SEN5XComponent::dump_config() { + ESP_LOGCONFIG(TAG, "sen5x:"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + switch (this->error_code_) { + case COMMUNICATION_FAILED: + ESP_LOGW(TAG, "Communication failed! Is the sensor connected?"); + break; + case MEASUREMENT_INIT_FAILED: + ESP_LOGW(TAG, "Measurement Initialization failed!"); + break; + case SERIAL_NUMBER_IDENTIFICATION_FAILED: + ESP_LOGW(TAG, "Unable to read sensor serial id"); + break; + case PRODUCT_NAME_FAILED: + ESP_LOGW(TAG, "Unable to read product name"); + break; + case FIRMWARE_FAILED: + ESP_LOGW(TAG, "Unable to read sensor firmware version"); + break; + default: + ESP_LOGW(TAG, "Unknown setup error!"); + break; + } + } + ESP_LOGCONFIG(TAG, " Productname: %s", this->product_name_.c_str()); + ESP_LOGCONFIG(TAG, " Firmware version: %d", this->firmware_version_); + ESP_LOGCONFIG(TAG, " Serial number %02d.%02d.%02d", serial_number_[0], serial_number_[1], serial_number_[2]); + if (this->auto_cleaning_interval_.has_value()) { + ESP_LOGCONFIG(TAG, " Auto auto cleaning interval %d seconds", auto_cleaning_interval_.value()); + } + if (this->acceleration_mode_.has_value()) { + switch (this->acceleration_mode_.value()) { + case LOW_ACCELERATION: + ESP_LOGCONFIG(TAG, " Low RH/T acceleration mode"); + break; + case MEDIUM_ACCELERATION: + ESP_LOGCONFIG(TAG, " Medium RH/T accelertion mode"); + break; + case HIGH_ACCELERATION: + ESP_LOGCONFIG(TAG, " High RH/T accelertion mode"); + break; + } + } + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "PM 1.0", this->pm_1_0_sensor_); + LOG_SENSOR(" ", "PM 2.5", this->pm_2_5_sensor_); + LOG_SENSOR(" ", "PM 4.0", this->pm_4_0_sensor_); + LOG_SENSOR(" ", "PM 10.0", this->pm_10_0_sensor_); + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); + LOG_SENSOR(" ", "VOC", this->voc_sensor_); // SEN54 and SEN55 only + LOG_SENSOR(" ", "NOx", this->nox_sensor_); // SEN55 only +} + +void SEN5XComponent::update() { + if (!initialized_) { + return; + } + + // Store baselines after defined interval or if the difference between current and stored baseline becomes too + // much + if (this->store_baseline_ && this->seconds_since_last_store_ > SHORTEST_BASELINE_STORE_INTERVAL) { + if (this->write_command(SEN5X_CMD_VOC_ALGORITHM_STATE)) { + // run it a bit later to avoid adding a delay here + this->set_timeout(550, [this]() { + uint16_t states[4]; + if (this->read_data(states, 4)) { + uint32_t state0 = states[0] << 16 | states[1]; + uint32_t state1 = states[2] << 16 | states[3]; + if ((uint32_t) std::abs(static_cast(this->voc_baselines_storage_.state0 - state0)) > + MAXIMUM_STORAGE_DIFF || + (uint32_t) std::abs(static_cast(this->voc_baselines_storage_.state1 - state1)) > + MAXIMUM_STORAGE_DIFF) { + this->seconds_since_last_store_ = 0; + this->voc_baselines_storage_.state0 = state0; + this->voc_baselines_storage_.state1 = state1; + + if (this->pref_.save(&this->voc_baselines_storage_)) { + ESP_LOGI(TAG, "Stored VOC baseline state0: 0x%04X ,state1: 0x%04X", this->voc_baselines_storage_.state0, + voc_baselines_storage_.state1); + } else { + ESP_LOGW(TAG, "Could not store VOC baselines"); + } + } + } + }); + } + } + + if (!this->write_command(SEN5X_CMD_READ_MEASUREMENT)) { + this->status_set_warning(); + ESP_LOGD(TAG, "write error read measurement (%d)", this->last_error_); + return; + } + this->set_timeout(20, [this]() { + uint16_t measurements[8]; + + if (!this->read_data(measurements, 8)) { + this->status_set_warning(); + ESP_LOGD(TAG, "read data error (%d)", this->last_error_); + return; + } + float pm_1_0 = measurements[0] / 10.0; + if (measurements[0] == 0xFFFF) + pm_1_0 = NAN; + float pm_2_5 = measurements[1] / 10.0; + if (measurements[1] == 0xFFFF) + pm_2_5 = NAN; + float pm_4_0 = measurements[2] / 10.0; + if (measurements[2] == 0xFFFF) + pm_4_0 = NAN; + float pm_10_0 = measurements[3] / 10.0; + if (measurements[3] == 0xFFFF) + pm_10_0 = NAN; + float humidity = measurements[4] / 100.0; + if (measurements[4] == 0xFFFF) + humidity = NAN; + float temperature = measurements[5] / 200.0; + if (measurements[5] == 0xFFFF) + temperature = NAN; + float voc = measurements[6] / 10.0; + if (measurements[6] == 0xFFFF) + voc = NAN; + float nox = measurements[7] / 10.0; + if (measurements[7] == 0xFFFF) + nox = NAN; + + if (this->pm_1_0_sensor_ != nullptr) + this->pm_1_0_sensor_->publish_state(pm_1_0); + if (this->pm_2_5_sensor_ != nullptr) + this->pm_2_5_sensor_->publish_state(pm_2_5); + if (this->pm_4_0_sensor_ != nullptr) + this->pm_4_0_sensor_->publish_state(pm_4_0); + if (this->pm_10_0_sensor_ != nullptr) + this->pm_10_0_sensor_->publish_state(pm_10_0); + if (this->temperature_sensor_ != nullptr) + this->temperature_sensor_->publish_state(temperature); + if (this->humidity_sensor_ != nullptr) + this->humidity_sensor_->publish_state(humidity); + if (this->voc_sensor_ != nullptr) + this->voc_sensor_->publish_state(voc); + if (this->nox_sensor_ != nullptr) + this->nox_sensor_->publish_state(nox); + this->status_clear_warning(); + }); +} + +bool SEN5XComponent::write_tuning_parameters_(uint16_t i2c_command, const GasTuning &tuning) { + uint16_t params[6]; + params[0] = tuning.index_offset; + params[1] = tuning.learning_time_offset_hours; + params[2] = tuning.learning_time_gain_hours; + params[3] = tuning.gating_max_duration_minutes; + params[4] = tuning.std_initial; + params[5] = tuning.gain_factor; + auto result = write_command(i2c_command, params, 6); + if (!result) { + ESP_LOGE(TAG, "set tuning parameters failed. i2c command=%0xX, err=%d", i2c_command, this->last_error_); + } + return result; +} + +bool SEN5XComponent::write_temperature_compensation_(const TemperatureCompensation &compensation) { + uint16_t params[3]; + params[0] = compensation.offset; + params[1] = compensation.normalized_offset_slope; + params[2] = compensation.time_constant; + if (!write_command(SEN5X_CMD_TEMPERATURE_COMPENSATION, params, 3)) { + ESP_LOGE(TAG, "set temperature_compensation failed. Err=%d", this->last_error_); + return false; + } + return true; +} + +bool SEN5XComponent::start_fan_cleaning() { + if (!write_command(SEN5X_CMD_START_CLEANING_FAN)) { + this->status_set_warning(); + ESP_LOGE(TAG, "write error start fan (%d)", this->last_error_); + return false; + } else { + ESP_LOGD(TAG, "Fan auto clean started"); + } + return true; +} + +} // namespace sen5x +} // namespace esphome diff --git a/esphome/components/sen5x/sen5x.h b/esphome/components/sen5x/sen5x.h new file mode 100644 index 0000000000..f306003a82 --- /dev/null +++ b/esphome/components/sen5x/sen5x.h @@ -0,0 +1,128 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/sensirion_common/i2c_sensirion.h" +#include "esphome/core/application.h" +#include "esphome/core/preferences.h" + +namespace esphome { +namespace sen5x { + +enum ERRORCODE { + COMMUNICATION_FAILED, + SERIAL_NUMBER_IDENTIFICATION_FAILED, + MEASUREMENT_INIT_FAILED, + PRODUCT_NAME_FAILED, + FIRMWARE_FAILED, + UNKNOWN +}; + +// Shortest time interval of 3H for storing baseline values. +// Prevents wear of the flash because of too many write operations +const uint32_t SHORTEST_BASELINE_STORE_INTERVAL = 10800; +// Store anyway if the baseline difference exceeds the max storage diff value +const uint32_t MAXIMUM_STORAGE_DIFF = 50; + +struct Sen5xBaselines { + int32_t state0; + int32_t state1; +} PACKED; // NOLINT + +enum RhtAccelerationMode : uint16_t { LOW_ACCELERATION = 0, MEDIUM_ACCELERATION = 1, HIGH_ACCELERATION = 2 }; + +struct GasTuning { + uint16_t index_offset; + uint16_t learning_time_offset_hours; + uint16_t learning_time_gain_hours; + uint16_t gating_max_duration_minutes; + uint16_t std_initial; + uint16_t gain_factor; +}; + +struct TemperatureCompensation { + uint16_t offset; + uint16_t normalized_offset_slope; + uint16_t time_constant; +}; + +class SEN5XComponent : public PollingComponent, public sensirion_common::SensirionI2CDevice { + public: + float get_setup_priority() const override { return setup_priority::DATA; } + void setup() override; + void dump_config() override; + void update() override; + + enum Sen5xType { SEN50, SEN54, SEN55, UNKNOWN }; + + void set_pm_1_0_sensor(sensor::Sensor *pm_1_0) { pm_1_0_sensor_ = pm_1_0; } + void set_pm_2_5_sensor(sensor::Sensor *pm_2_5) { pm_2_5_sensor_ = pm_2_5; } + void set_pm_4_0_sensor(sensor::Sensor *pm_4_0) { pm_4_0_sensor_ = pm_4_0; } + void set_pm_10_0_sensor(sensor::Sensor *pm_10_0) { pm_10_0_sensor_ = pm_10_0; } + + void set_voc_sensor(sensor::Sensor *voc_sensor) { voc_sensor_ = voc_sensor; } + void set_nox_sensor(sensor::Sensor *nox_sensor) { nox_sensor_ = nox_sensor; } + void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; } + void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } + void set_store_baseline(bool store_baseline) { store_baseline_ = store_baseline; } + void set_acceleration_mode(RhtAccelerationMode mode) { acceleration_mode_ = mode; } + void set_auto_cleaning_interval(uint32_t auto_cleaning_interval) { auto_cleaning_interval_ = auto_cleaning_interval; } + void set_voc_algorithm_tuning(uint16_t index_offset, uint16_t learning_time_offset_hours, + uint16_t learning_time_gain_hours, uint16_t gating_max_duration_minutes, + uint16_t std_initial, uint16_t gain_factor) { + voc_tuning_params_.value().index_offset = index_offset; + voc_tuning_params_.value().learning_time_offset_hours = learning_time_offset_hours; + voc_tuning_params_.value().learning_time_gain_hours = learning_time_gain_hours; + voc_tuning_params_.value().gating_max_duration_minutes = gating_max_duration_minutes; + voc_tuning_params_.value().std_initial = std_initial; + voc_tuning_params_.value().gain_factor = gain_factor; + } + void set_nox_algorithm_tuning(uint16_t index_offset, uint16_t learning_time_offset_hours, + uint16_t learning_time_gain_hours, uint16_t gating_max_duration_minutes, + uint16_t gain_factor) { + nox_tuning_params_.value().index_offset = index_offset; + nox_tuning_params_.value().learning_time_offset_hours = learning_time_offset_hours; + nox_tuning_params_.value().learning_time_gain_hours = learning_time_gain_hours; + nox_tuning_params_.value().gating_max_duration_minutes = gating_max_duration_minutes; + nox_tuning_params_.value().std_initial = 50; + nox_tuning_params_.value().gain_factor = gain_factor; + } + void set_temperature_compensation(float offset, float normalized_offset_slope, uint16_t time_constant) { + temperature_compensation_.value().offset = offset * 200; + temperature_compensation_.value().normalized_offset_slope = normalized_offset_slope * 100; + temperature_compensation_.value().time_constant = time_constant; + } + bool start_fan_cleaning(); + + protected: + bool write_tuning_parameters_(uint16_t i2c_command, const GasTuning &tuning); + bool write_temperature_compensation_(const TemperatureCompensation &compensation); + ERRORCODE error_code_; + bool initialized_{false}; + sensor::Sensor *pm_1_0_sensor_{nullptr}; + sensor::Sensor *pm_2_5_sensor_{nullptr}; + sensor::Sensor *pm_4_0_sensor_{nullptr}; + sensor::Sensor *pm_10_0_sensor_{nullptr}; + // SEN54 and SEN55 only + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *humidity_sensor_{nullptr}; + sensor::Sensor *voc_sensor_{nullptr}; + // SEN55 only + sensor::Sensor *nox_sensor_{nullptr}; + + std::string product_name_; + uint8_t serial_number_[4]; + uint16_t firmware_version_; + Sen5xBaselines voc_baselines_storage_; + bool store_baseline_; + uint32_t seconds_since_last_store_; + ESPPreferenceObject pref_; + optional acceleration_mode_; + optional auto_cleaning_interval_; + optional voc_tuning_params_; + optional nox_tuning_params_; + optional temperature_compensation_; +}; + +} // namespace sen5x +} // namespace esphome diff --git a/esphome/components/sen5x/sensor.py b/esphome/components/sen5x/sensor.py new file mode 100644 index 0000000000..489fda8335 --- /dev/null +++ b/esphome/components/sen5x/sensor.py @@ -0,0 +1,241 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor, sensirion_common +from esphome import automation +from esphome.automation import maybe_simple_id + +from esphome.const import ( + CONF_HUMIDITY, + CONF_ID, + CONF_OFFSET, + CONF_PM_1_0, + CONF_PM_10_0, + CONF_PM_2_5, + CONF_PM_4_0, + CONF_STORE_BASELINE, + CONF_TEMPERATURE, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_NITROUS_OXIDE, + DEVICE_CLASS_PM1, + DEVICE_CLASS_PM10, + DEVICE_CLASS_PM25, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, + ICON_CHEMICAL_WEAPON, + ICON_RADIATOR, + ICON_THERMOMETER, + ICON_WATER_PERCENT, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_MICROGRAMS_PER_CUBIC_METER, + UNIT_PERCENT, +) + +CODEOWNERS = ["@martgras"] +DEPENDENCIES = ["i2c"] +AUTO_LOAD = ["sensirion_common"] + +sen5x_ns = cg.esphome_ns.namespace("sen5x") +SEN5XComponent = sen5x_ns.class_( + "SEN5XComponent", cg.PollingComponent, sensirion_common.SensirionI2CDevice +) +RhtAccelerationMode = sen5x_ns.enum("RhtAccelerationMode") + +CONF_ACCELERATION_MODE = "acceleration_mode" +CONF_ALGORITHM_TUNING = "algorithm_tuning" +CONF_AUTO_CLEANING_INTERVAL = "auto_cleaning_interval" +CONF_GAIN_FACTOR = "gain_factor" +CONF_GATING_MAX_DURATION_MINUTES = "gating_max_duration_minutes" +CONF_INDEX_OFFSET = "index_offset" +CONF_LEARNING_TIME_GAIN_HOURS = "learning_time_gain_hours" +CONF_LEARNING_TIME_OFFSET_HOURS = "learning_time_offset_hours" +CONF_NORMALIZED_OFFSET_SLOPE = "normalized_offset_slope" +CONF_NOX = "nox" +CONF_STD_INITIAL = "std_initial" +CONF_TEMPERATURE_COMPENSATION = "temperature_compensation" +CONF_TIME_CONSTANT = "time_constant" +CONF_VOC = "voc" +CONF_VOC_BASELINE = "voc_baseline" + + +# Actions +StartFanAction = sen5x_ns.class_("StartFanAction", automation.Action) + +ACCELERATION_MODES = { + "low": RhtAccelerationMode.LOW_ACCELERATION, + "medium": RhtAccelerationMode.MEDIUM_ACCELERATION, + "high": RhtAccelerationMode.HIGH_ACCELERATION, +} + +GAS_SENSOR = cv.Schema( + { + cv.Optional(CONF_ALGORITHM_TUNING): cv.Schema( + { + cv.Optional(CONF_INDEX_OFFSET, default=100): cv.int_range(1, 250), + cv.Optional(CONF_LEARNING_TIME_OFFSET_HOURS, default=12): cv.int_range( + 1, 1000 + ), + cv.Optional(CONF_LEARNING_TIME_GAIN_HOURS, default=12): cv.int_range( + 1, 1000 + ), + cv.Optional( + CONF_GATING_MAX_DURATION_MINUTES, default=720 + ): cv.int_range(0, 3000), + cv.Optional(CONF_STD_INITIAL, default=50): cv.int_, + cv.Optional(CONF_GAIN_FACTOR, default=230): cv.int_range(1, 1000), + } + ) + } +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(SEN5XComponent), + cv.Optional(CONF_PM_1_0): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=2, + device_class=DEVICE_CLASS_PM1, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PM_2_5): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=2, + device_class=DEVICE_CLASS_PM25, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PM_4_0): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PM_10_0): sensor.sensor_schema( + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=2, + device_class=DEVICE_CLASS_PM10, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_AUTO_CLEANING_INTERVAL): cv.time_period_in_seconds_, + cv.Optional(CONF_VOC): sensor.sensor_schema( + icon=ICON_RADIATOR, + accuracy_decimals=0, + device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, + state_class=STATE_CLASS_MEASUREMENT, + ).extend(GAS_SENSOR), + cv.Optional(CONF_NOX): sensor.sensor_schema( + icon=ICON_RADIATOR, + accuracy_decimals=0, + device_class=DEVICE_CLASS_NITROUS_OXIDE, + state_class=STATE_CLASS_MEASUREMENT, + ).extend(GAS_SENSOR), + cv.Optional(CONF_STORE_BASELINE, default=True): cv.boolean, + cv.Optional(CONF_VOC_BASELINE): cv.hex_uint16_t, + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=2, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + icon=ICON_WATER_PERCENT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TEMPERATURE_COMPENSATION): cv.Schema( + { + cv.Optional(CONF_OFFSET, default=0): cv.float_, + cv.Optional(CONF_NORMALIZED_OFFSET_SLOPE, default=0): cv.percentage, + cv.Optional(CONF_TIME_CONSTANT, default=0): cv.int_, + } + ), + cv.Optional(CONF_ACCELERATION_MODE): cv.enum(ACCELERATION_MODES), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x69)) +) + +SENSOR_MAP = { + CONF_PM_1_0: "set_pm_1_0_sensor", + CONF_PM_2_5: "set_pm_2_5_sensor", + CONF_PM_4_0: "set_pm_4_0_sensor", + CONF_PM_10_0: "set_pm_10_0_sensor", + CONF_VOC: "set_voc_sensor", + CONF_NOX: "set_nox_sensor", + CONF_TEMPERATURE: "set_temperature_sensor", + CONF_HUMIDITY: "set_humidity_sensor", +} + +SETTING_MAP = { + CONF_AUTO_CLEANING_INTERVAL: "set_auto_cleaning_interval", + CONF_ACCELERATION_MODE: "set_acceleration_mode", +} + + +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) + + for key, funcName in SETTING_MAP.items(): + if key in config: + cg.add(getattr(var, funcName)(config[key])) + + for key, funcName in SENSOR_MAP.items(): + if key in config: + sens = await sensor.new_sensor(config[key]) + cg.add(getattr(var, funcName)(sens)) + + if CONF_VOC in config and CONF_ALGORITHM_TUNING in config[CONF_VOC]: + cfg = config[CONF_VOC][CONF_ALGORITHM_TUNING] + cg.add( + var.set_voc_algorithm_tuning( + cfg[CONF_INDEX_OFFSET], + cfg[CONF_LEARNING_TIME_OFFSET_HOURS], + cfg[CONF_LEARNING_TIME_GAIN_HOURS], + cfg[CONF_GATING_MAX_DURATION_MINUTES], + cfg[CONF_STD_INITIAL], + cfg[CONF_GAIN_FACTOR], + ) + ) + if CONF_NOX in config and CONF_ALGORITHM_TUNING in config[CONF_NOX]: + cfg = config[CONF_NOX][CONF_ALGORITHM_TUNING] + cg.add( + var.set_nox_algorithm_tuning( + cfg[CONF_INDEX_OFFSET], + cfg[CONF_LEARNING_TIME_OFFSET_HOURS], + cfg[CONF_LEARNING_TIME_GAIN_HOURS], + cfg[CONF_GATING_MAX_DURATION_MINUTES], + cfg[CONF_GAIN_FACTOR], + ) + ) + if CONF_TEMPERATURE_COMPENSATION in config: + cg.add( + var.set_temperature_compensation( + config[CONF_TEMPERATURE_COMPENSATION][CONF_OFFSET], + config[CONF_TEMPERATURE_COMPENSATION][CONF_NORMALIZED_OFFSET_SLOPE], + config[CONF_TEMPERATURE_COMPENSATION][CONF_TIME_CONSTANT], + ) + ) + + +SEN5X_ACTION_SCHEMA = maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(SEN5XComponent), + } +) + + +@automation.register_action( + "sen5x.start_fan_autoclean", StartFanAction, SEN5X_ACTION_SCHEMA +) +async def sen54_fan_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) diff --git a/tests/test5.yaml b/tests/test5.yaml index ffda860377..35f6b14f2a 100644 --- a/tests/test5.yaml +++ b/tests/test5.yaml @@ -285,6 +285,49 @@ sensor: address: 0x77 iir_filter: 2X + - platform: sen5x + id: sen54 + temperature: + name: "Temperature" + accuracy_decimals: 1 + humidity: + name: "Humidity" + accuracy_decimals: 0 + pm_1_0: + name: " PM <1µm Weight concentration" + id: pm_1_0 + accuracy_decimals: 1 + pm_2_5: + name: " PM <2.5µm Weight concentration" + id: pm_2_5 + accuracy_decimals: 1 + pm_4_0: + name: " PM <4µm Weight concentration" + id: pm_4_0 + accuracy_decimals: 1 + pm_10_0: + name: " PM <10µm Weight concentration" + id: pm_10_0 + accuracy_decimals: 1 + nox: + name: "NOx" + voc: + name: "VOC" + algorithm_tuning: + index_offset: 100 + learning_time_offset_hours: 12 + learning_time_gain_hours: 12 + gating_max_duration_minutes: 180 + std_initial: 50 + gain_factor: 230 + temperature_compensation: + offset: 0 + normalized_offset_slope: 0 + time_constant: 0 + acceleration_mode: low + store_baseline: true + address: 0x69 + script: - id: automation_test then: From 53e0fe8e51c4f357e7e9a5974fc6c95143e3200d Mon Sep 17 00:00:00 2001 From: Andre Lengwenus Date: Tue, 10 May 2022 11:05:49 +0200 Subject: [PATCH 0436/1729] Add SML (Smart Message Language) platform for energy meters (#2396) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/sml/__init__.py | 38 +++++ esphome/components/sml/constants.h | 48 ++++++ esphome/components/sml/sensor/__init__.py | 30 ++++ esphome/components/sml/sensor/sml_sensor.cpp | 41 +++++ esphome/components/sml/sensor/sml_sensor.h | 16 ++ esphome/components/sml/sml.cpp | 146 ++++++++++++++++++ esphome/components/sml/sml.h | 47 ++++++ esphome/components/sml/sml_parser.cpp | 131 ++++++++++++++++ esphome/components/sml/sml_parser.h | 54 +++++++ .../components/sml/text_sensor/__init__.py | 43 ++++++ .../sml/text_sensor/sml_text_sensor.cpp | 54 +++++++ .../sml/text_sensor/sml_text_sensor.h | 21 +++ 13 files changed, 670 insertions(+) create mode 100644 esphome/components/sml/__init__.py create mode 100644 esphome/components/sml/constants.h create mode 100644 esphome/components/sml/sensor/__init__.py create mode 100644 esphome/components/sml/sensor/sml_sensor.cpp create mode 100644 esphome/components/sml/sensor/sml_sensor.h create mode 100644 esphome/components/sml/sml.cpp create mode 100644 esphome/components/sml/sml.h create mode 100644 esphome/components/sml/sml_parser.cpp create mode 100644 esphome/components/sml/sml_parser.h create mode 100644 esphome/components/sml/text_sensor/__init__.py create mode 100644 esphome/components/sml/text_sensor/sml_text_sensor.cpp create mode 100644 esphome/components/sml/text_sensor/sml_text_sensor.h diff --git a/CODEOWNERS b/CODEOWNERS index 77c9d30c5d..3a511275e1 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -182,6 +182,7 @@ esphome/components/sht4x/* @sjtrny esphome/components/shutdown/* @esphome/core @jsuanet esphome/components/sim800l/* @glmnet esphome/components/sm2135/* @BoukeHaarsma23 +esphome/components/sml/* @alengwenus esphome/components/socket/* @esphome/core esphome/components/sonoff_d1/* @anatoly-savchenkov esphome/components/spi/* @esphome/core diff --git a/esphome/components/sml/__init__.py b/esphome/components/sml/__init__.py new file mode 100644 index 0000000000..f3b6dd95ef --- /dev/null +++ b/esphome/components/sml/__init__.py @@ -0,0 +1,38 @@ +import re + +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import uart +from esphome.const import CONF_ID + +CODEOWNERS = ["@alengwenus"] + +DEPENDENCIES = ["uart"] + +sml_ns = cg.esphome_ns.namespace("sml") +Sml = sml_ns.class_("Sml", cg.Component, uart.UARTDevice) +MULTI_CONF = True + +CONF_SML_ID = "sml_id" +CONF_OBIS_CODE = "obis_code" +CONF_SERVER_ID = "server_id" + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(Sml), + } +).extend(uart.UART_DEVICE_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) + + +def obis_code(value): + value = cv.string(value) + match = re.match(r"^\d{1,3}-\d{1,3}:\d{1,3}\.\d{1,3}\.\d{1,3}$", value) + if match is None: + raise cv.Invalid(f"{value} is not a valid OBIS code") + return value diff --git a/esphome/components/sml/constants.h b/esphome/components/sml/constants.h new file mode 100644 index 0000000000..22114fd233 --- /dev/null +++ b/esphome/components/sml/constants.h @@ -0,0 +1,48 @@ +#pragma once + +#include + +namespace esphome { +namespace sml { + +enum SmlType : uint8_t { + SML_OCTET = 0, + SML_BOOL = 4, + SML_INT = 5, + SML_UINT = 6, + SML_LIST = 7, + SML_HEX = 10, + SML_UNDEFINED = 255 +}; + +enum SmlMessageType : uint16_t { SML_PUBLIC_OPEN_RES = 0x0101, SML_GET_LIST_RES = 0x701 }; + +enum Crc16CheckResult : uint8_t { CHECK_CRC16_FAILED, CHECK_CRC16_X25_SUCCESS, CHECK_CRC16_KERMIT_SUCCESS }; + +// masks with two-bit mapping 0x1b -> 0b01; 0x01 -> 0b10; 0x1a -> 0b11 +const uint16_t START_MASK = 0x55aa; // 0x1b 1b 1b 1b 1b 01 01 01 01 +const uint16_t END_MASK = 0x0157; // 0x1b 1b 1b 1b 1a + +const uint16_t CRC16_X25_TABLE[256] = { + 0x0000, 0x1189, 0x2312, 0x329b, 0x4624, 0x57ad, 0x6536, 0x74bf, 0x8c48, 0x9dc1, 0xaf5a, 0xbed3, 0xca6c, 0xdbe5, + 0xe97e, 0xf8f7, 0x1081, 0x0108, 0x3393, 0x221a, 0x56a5, 0x472c, 0x75b7, 0x643e, 0x9cc9, 0x8d40, 0xbfdb, 0xae52, + 0xdaed, 0xcb64, 0xf9ff, 0xe876, 0x2102, 0x308b, 0x0210, 0x1399, 0x6726, 0x76af, 0x4434, 0x55bd, 0xad4a, 0xbcc3, + 0x8e58, 0x9fd1, 0xeb6e, 0xfae7, 0xc87c, 0xd9f5, 0x3183, 0x200a, 0x1291, 0x0318, 0x77a7, 0x662e, 0x54b5, 0x453c, + 0xbdcb, 0xac42, 0x9ed9, 0x8f50, 0xfbef, 0xea66, 0xd8fd, 0xc974, 0x4204, 0x538d, 0x6116, 0x709f, 0x0420, 0x15a9, + 0x2732, 0x36bb, 0xce4c, 0xdfc5, 0xed5e, 0xfcd7, 0x8868, 0x99e1, 0xab7a, 0xbaf3, 0x5285, 0x430c, 0x7197, 0x601e, + 0x14a1, 0x0528, 0x37b3, 0x263a, 0xdecd, 0xcf44, 0xfddf, 0xec56, 0x98e9, 0x8960, 0xbbfb, 0xaa72, 0x6306, 0x728f, + 0x4014, 0x519d, 0x2522, 0x34ab, 0x0630, 0x17b9, 0xef4e, 0xfec7, 0xcc5c, 0xddd5, 0xa96a, 0xb8e3, 0x8a78, 0x9bf1, + 0x7387, 0x620e, 0x5095, 0x411c, 0x35a3, 0x242a, 0x16b1, 0x0738, 0xffcf, 0xee46, 0xdcdd, 0xcd54, 0xb9eb, 0xa862, + 0x9af9, 0x8b70, 0x8408, 0x9581, 0xa71a, 0xb693, 0xc22c, 0xd3a5, 0xe13e, 0xf0b7, 0x0840, 0x19c9, 0x2b52, 0x3adb, + 0x4e64, 0x5fed, 0x6d76, 0x7cff, 0x9489, 0x8500, 0xb79b, 0xa612, 0xd2ad, 0xc324, 0xf1bf, 0xe036, 0x18c1, 0x0948, + 0x3bd3, 0x2a5a, 0x5ee5, 0x4f6c, 0x7df7, 0x6c7e, 0xa50a, 0xb483, 0x8618, 0x9791, 0xe32e, 0xf2a7, 0xc03c, 0xd1b5, + 0x2942, 0x38cb, 0x0a50, 0x1bd9, 0x6f66, 0x7eef, 0x4c74, 0x5dfd, 0xb58b, 0xa402, 0x9699, 0x8710, 0xf3af, 0xe226, + 0xd0bd, 0xc134, 0x39c3, 0x284a, 0x1ad1, 0x0b58, 0x7fe7, 0x6e6e, 0x5cf5, 0x4d7c, 0xc60c, 0xd785, 0xe51e, 0xf497, + 0x8028, 0x91a1, 0xa33a, 0xb2b3, 0x4a44, 0x5bcd, 0x6956, 0x78df, 0x0c60, 0x1de9, 0x2f72, 0x3efb, 0xd68d, 0xc704, + 0xf59f, 0xe416, 0x90a9, 0x8120, 0xb3bb, 0xa232, 0x5ac5, 0x4b4c, 0x79d7, 0x685e, 0x1ce1, 0x0d68, 0x3ff3, 0x2e7a, + 0xe70e, 0xf687, 0xc41c, 0xd595, 0xa12a, 0xb0a3, 0x8238, 0x93b1, 0x6b46, 0x7acf, 0x4854, 0x59dd, 0x2d62, 0x3ceb, + 0x0e70, 0x1ff9, 0xf78f, 0xe606, 0xd49d, 0xc514, 0xb1ab, 0xa022, 0x92b9, 0x8330, 0x7bc7, 0x6a4e, 0x58d5, 0x495c, + 0x3de3, 0x2c6a, 0x1ef1, 0x0f78}; + +} // namespace sml +} // namespace esphome diff --git a/esphome/components/sml/sensor/__init__.py b/esphome/components/sml/sensor/__init__.py new file mode 100644 index 0000000000..a1fcc5e7ae --- /dev/null +++ b/esphome/components/sml/sensor/__init__.py @@ -0,0 +1,30 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import CONF_ID + +from .. import CONF_OBIS_CODE, CONF_SERVER_ID, CONF_SML_ID, Sml, obis_code, sml_ns + +AUTO_LOAD = ["sml"] + +SmlSensor = sml_ns.class_("SmlSensor", sensor.Sensor, cg.Component) + + +CONFIG_SCHEMA = sensor.sensor_schema().extend( + { + cv.GenerateID(): cv.declare_id(SmlSensor), + cv.GenerateID(CONF_SML_ID): cv.use_id(Sml), + cv.Required(CONF_OBIS_CODE): obis_code, + cv.Optional(CONF_SERVER_ID, default=""): cv.string, + } +) + + +async def to_code(config): + var = cg.new_Pvariable( + config[CONF_ID], config[CONF_SERVER_ID], config[CONF_OBIS_CODE] + ) + await cg.register_component(var, config) + await sensor.register_sensor(var, config) + sml = await cg.get_variable(config[CONF_SML_ID]) + cg.add(sml.register_sml_listener(var)) diff --git a/esphome/components/sml/sensor/sml_sensor.cpp b/esphome/components/sml/sensor/sml_sensor.cpp new file mode 100644 index 0000000000..e9a384d275 --- /dev/null +++ b/esphome/components/sml/sensor/sml_sensor.cpp @@ -0,0 +1,41 @@ +#include "esphome/core/log.h" +#include "sml_sensor.h" +#include "../sml_parser.h" + +namespace esphome { +namespace sml { + +static const char *const TAG = "sml_sensor"; + +SmlSensor::SmlSensor(std::string server_id, std::string obis_code) + : SmlListener(std::move(server_id), std::move(obis_code)) {} + +void SmlSensor::publish_val(const ObisInfo &obis_info) { + switch (obis_info.value_type) { + case SML_INT: { + publish_state(bytes_to_int(obis_info.value)); + break; + } + case SML_BOOL: + case SML_UINT: { + publish_state(bytes_to_uint(obis_info.value)); + break; + } + case SML_OCTET: { + ESP_LOGW(TAG, "No number conversion for (%s) %s. Consider using SML TextSensor instead.", + bytes_repr(obis_info.server_id).c_str(), obis_info.code_repr().c_str()); + break; + } + } +} + +void SmlSensor::dump_config() { + LOG_SENSOR("", "SML", this); + if (!this->server_id.empty()) { + ESP_LOGCONFIG(TAG, " Server ID: %s", this->server_id.c_str()); + } + ESP_LOGCONFIG(TAG, " OBIS Code: %s", this->obis_code.c_str()); +} + +} // namespace sml +} // namespace esphome diff --git a/esphome/components/sml/sensor/sml_sensor.h b/esphome/components/sml/sensor/sml_sensor.h new file mode 100644 index 0000000000..eb7b108f94 --- /dev/null +++ b/esphome/components/sml/sensor/sml_sensor.h @@ -0,0 +1,16 @@ +#pragma once +#include "esphome/components/sml/sml.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace sml { + +class SmlSensor : public SmlListener, public sensor::Sensor, public Component { + public: + SmlSensor(std::string server_id, std::string obis_code); + void publish_val(const ObisInfo &obis_info) override; + void dump_config() override; +}; + +} // namespace sml +} // namespace esphome diff --git a/esphome/components/sml/sml.cpp b/esphome/components/sml/sml.cpp new file mode 100644 index 0000000000..c6fe7d64cd --- /dev/null +++ b/esphome/components/sml/sml.cpp @@ -0,0 +1,146 @@ +#include "sml.h" +#include "esphome/core/log.h" +#include "sml_parser.h" + +namespace esphome { +namespace sml { + +static const char *const TAG = "sml"; + +const char START_BYTES_DETECTED = 1; +const char END_BYTES_DETECTED = 2; + +SmlListener::SmlListener(std::string server_id, std::string obis_code) + : server_id(std::move(server_id)), obis_code(std::move(obis_code)) {} + +char Sml::check_start_end_bytes_(uint8_t byte) { + this->incoming_mask_ = (this->incoming_mask_ << 2) | get_code(byte); + + if (this->incoming_mask_ == START_MASK) + return START_BYTES_DETECTED; + if ((this->incoming_mask_ >> 6) == END_MASK) + return END_BYTES_DETECTED; + return 0; +} + +void Sml::loop() { + while (available()) { + const char c = read(); + + if (this->record_) + this->sml_data_.emplace_back(c); + + switch (this->check_start_end_bytes_(c)) { + case START_BYTES_DETECTED: { + this->record_ = true; + this->sml_data_.clear(); + break; + }; + case END_BYTES_DETECTED: { + if (this->record_) { + this->record_ = false; + + if (!check_sml_data(this->sml_data_)) + break; + + // remove footer bytes + this->sml_data_.resize(this->sml_data_.size() - 8); + this->process_sml_file_(this->sml_data_); + } + break; + }; + }; + } +} + +void Sml::process_sml_file_(const bytes &sml_data) { + SmlFile sml_file = SmlFile(sml_data); + std::vector obis_info = sml_file.get_obis_info(); + this->publish_obis_info_(obis_info); + + this->log_obis_info_(obis_info); +} + +void Sml::log_obis_info_(const std::vector &obis_info_vec) { + ESP_LOGD(TAG, "OBIS info:"); + for (auto const &obis_info : obis_info_vec) { + std::string info; + info += " (" + bytes_repr(obis_info.server_id) + ") "; + info += obis_info.code_repr(); + info += " [0x" + bytes_repr(obis_info.value) + "]"; + ESP_LOGD(TAG, "%s", info.c_str()); + } +} + +void Sml::publish_obis_info_(const std::vector &obis_info_vec) { + for (auto const &obis_info : obis_info_vec) { + this->publish_value_(obis_info); + } +} + +void Sml::publish_value_(const ObisInfo &obis_info) { + for (auto const &sml_listener : sml_listeners_) { + if ((!sml_listener->server_id.empty()) && (bytes_repr(obis_info.server_id) != sml_listener->server_id)) + continue; + if (obis_info.code_repr() != sml_listener->obis_code) + continue; + sml_listener->publish_val(obis_info); + } +} + +void Sml::dump_config() { ESP_LOGCONFIG(TAG, "SML:"); } + +void Sml::register_sml_listener(SmlListener *listener) { sml_listeners_.emplace_back(listener); } + +bool check_sml_data(const bytes &buffer) { + if (buffer.size() < 2) { + ESP_LOGW(TAG, "Checksum error in received SML data."); + return false; + } + + uint16_t crc_received = (buffer.at(buffer.size() - 2) << 8) | buffer.at(buffer.size() - 1); + if (crc_received == calc_crc16_x25(buffer.begin(), buffer.end() - 2, 0x6e23)) { + ESP_LOGV(TAG, "Checksum verification successful with CRC16/X25."); + return true; + } + + if (crc_received == calc_crc16_kermit(buffer.begin(), buffer.end() - 2, 0xed50)) { + ESP_LOGV(TAG, "Checksum verification successful with CRC16/KERMIT."); + return true; + } + + ESP_LOGW(TAG, "Checksum error in received SML data."); + return false; +} + +uint16_t calc_crc16_p1021(bytes::const_iterator begin, bytes::const_iterator end, uint16_t crcsum) { + for (auto it = begin; it != end; it++) { + crcsum = (crcsum >> 8) ^ CRC16_X25_TABLE[(crcsum & 0xff) ^ *it]; + } + return crcsum; +} + +uint16_t calc_crc16_x25(bytes::const_iterator begin, bytes::const_iterator end, uint16_t crcsum = 0) { + crcsum = calc_crc16_p1021(begin, end, crcsum ^ 0xffff) ^ 0xffff; + return (crcsum >> 8) | ((crcsum & 0xff) << 8); +} + +uint16_t calc_crc16_kermit(bytes::const_iterator begin, bytes::const_iterator end, uint16_t crcsum = 0) { + return calc_crc16_p1021(begin, end, crcsum); +} + +uint8_t get_code(uint8_t byte) { + switch (byte) { + case 0x1b: + return 1; + case 0x01: + return 2; + case 0x1a: + return 3; + default: + return 0; + } +} + +} // namespace sml +} // namespace esphome diff --git a/esphome/components/sml/sml.h b/esphome/components/sml/sml.h new file mode 100644 index 0000000000..ac7befb043 --- /dev/null +++ b/esphome/components/sml/sml.h @@ -0,0 +1,47 @@ +#pragma once + +#include +#include +#include "esphome/core/component.h" +#include "esphome/components/uart/uart.h" +#include "sml_parser.h" + +namespace esphome { +namespace sml { + +class SmlListener { + public: + std::string server_id; + std::string obis_code; + SmlListener(std::string server_id, std::string obis_code); + virtual void publish_val(const ObisInfo &obis_info){}; +}; + +class Sml : public Component, public uart::UARTDevice { + public: + void register_sml_listener(SmlListener *listener); + void loop() override; + void dump_config() override; + std::vector sml_listeners_{}; + + protected: + void process_sml_file_(const bytes &sml_data); + void log_obis_info_(const std::vector &obis_info_vec); + void publish_obis_info_(const std::vector &obis_info_vec); + char check_start_end_bytes_(uint8_t byte); + void publish_value_(const ObisInfo &obis_info); + + // Serial parser + bool record_ = false; + uint16_t incoming_mask_ = 0; + bytes sml_data_; +}; + +bool check_sml_data(const bytes &buffer); +uint16_t calc_crc16_p1021(bytes::const_iterator begin, bytes::const_iterator end, uint16_t crcsum); +uint16_t calc_crc16_x25(bytes::const_iterator begin, bytes::const_iterator end, uint16_t crcsum); +uint16_t calc_crc16_kermit(bytes::const_iterator begin, bytes::const_iterator end, uint16_t crcsum); + +uint8_t get_code(uint8_t byte); +} // namespace sml +} // namespace esphome diff --git a/esphome/components/sml/sml_parser.cpp b/esphome/components/sml/sml_parser.cpp new file mode 100644 index 0000000000..ff7da4cabd --- /dev/null +++ b/esphome/components/sml/sml_parser.cpp @@ -0,0 +1,131 @@ +#include "esphome/core/helpers.h" +#include "constants.h" +#include "sml_parser.h" + +namespace esphome { +namespace sml { + +SmlFile::SmlFile(bytes buffer) : buffer_(std::move(buffer)) { + // extract messages + this->pos_ = 0; + while (this->pos_ < this->buffer_.size()) { + if (this->buffer_[this->pos_] == 0x00) + break; // fill byte detected -> no more messages + + SmlNode message = SmlNode(); + if (!this->setup_node(&message)) + break; + this->messages.emplace_back(message); + } +} + +bool SmlFile::setup_node(SmlNode *node) { + uint8_t type = this->buffer_[this->pos_] >> 4; // type including overlength info + uint8_t length = this->buffer_[this->pos_] & 0x0f; // length including TL bytes + bool is_list = (type & 0x07) == SML_LIST; + bool has_extended_length = type & 0x08; // we have a long list/value (>15 entries) + uint8_t parse_length = length; + if (has_extended_length) { + length = (length << 4) + (this->buffer_[this->pos_ + 1] & 0x0f); + parse_length = length - 1; + this->pos_ += 1; + } + + if (this->pos_ + parse_length >= this->buffer_.size()) + return false; + + node->type = type & 0x07; + node->nodes.clear(); + node->value_bytes.clear(); + if (this->buffer_[this->pos_] == 0x00) { // end of message + this->pos_ += 1; + } else if (is_list) { // list + this->pos_ += 1; + node->nodes.reserve(parse_length); + for (size_t i = 0; i != parse_length; i++) { + SmlNode child_node = SmlNode(); + if (!this->setup_node(&child_node)) + return false; + node->nodes.emplace_back(child_node); + } + } else { // value + node->value_bytes = + bytes(this->buffer_.begin() + this->pos_ + 1, this->buffer_.begin() + this->pos_ + parse_length); + this->pos_ += parse_length; + } + return true; +} + +std::vector SmlFile::get_obis_info() { + std::vector obis_info; + for (auto const &message : messages) { + SmlNode message_body = message.nodes[3]; + uint16_t message_type = bytes_to_uint(message_body.nodes[0].value_bytes); + if (message_type != SML_GET_LIST_RES) + continue; + + SmlNode get_list_response = message_body.nodes[1]; + bytes server_id = get_list_response.nodes[1].value_bytes; + SmlNode val_list = get_list_response.nodes[4]; + + for (auto const &val_list_entry : val_list.nodes) { + obis_info.emplace_back(server_id, val_list_entry); + } + } + return obis_info; +} + +std::string bytes_repr(const bytes &buffer) { + std::string repr; + for (auto const value : buffer) { + repr += str_sprintf("%02x", value & 0xff); + } + return repr; +} + +uint64_t bytes_to_uint(const bytes &buffer) { + uint64_t val = 0; + for (auto const value : buffer) { + val = (val << 8) + value; + } + return val; +} + +int64_t bytes_to_int(const bytes &buffer) { + uint64_t tmp = bytes_to_uint(buffer); + int64_t val; + + switch (buffer.size()) { + case 1: // int8 + val = (int8_t) tmp; + break; + case 2: // int16 + val = (int16_t) tmp; + break; + case 4: // int32 + val = (int32_t) tmp; + break; + default: // int64 + val = (int64_t) tmp; + } + return val; +} + +std::string bytes_to_string(const bytes &buffer) { return std::string(buffer.begin(), buffer.end()); } + +ObisInfo::ObisInfo(bytes server_id, SmlNode val_list_entry) : server_id(std::move(server_id)) { + this->code = val_list_entry.nodes[0].value_bytes; + this->status = val_list_entry.nodes[1].value_bytes; + this->unit = bytes_to_uint(val_list_entry.nodes[3].value_bytes); + this->scaler = bytes_to_int(val_list_entry.nodes[4].value_bytes); + SmlNode value_node = val_list_entry.nodes[5]; + this->value = value_node.value_bytes; + this->value_type = value_node.type; +} + +std::string ObisInfo::code_repr() const { + return str_sprintf("%d-%d:%d.%d.%d", this->code[0], this->code[1], this->code[2], this->code[3], this->code[4]); +} + +} // namespace sml +} // namespace esphome diff --git a/esphome/components/sml/sml_parser.h b/esphome/components/sml/sml_parser.h new file mode 100644 index 0000000000..fca859d4b8 --- /dev/null +++ b/esphome/components/sml/sml_parser.h @@ -0,0 +1,54 @@ +#pragma once + +#include +#include +#include +#include +#include "constants.h" + +namespace esphome { +namespace sml { + +using bytes = std::vector; + +class SmlNode { + public: + uint8_t type; + bytes value_bytes; + std::vector nodes; +}; + +class ObisInfo { + public: + ObisInfo(bytes server_id, SmlNode val_list_entry); + bytes server_id; + bytes code; + bytes status; + char unit; + char scaler; + bytes value; + uint16_t value_type; + std::string code_repr() const; +}; + +class SmlFile { + public: + SmlFile(bytes buffer); + bool setup_node(SmlNode *node); + std::vector messages; + std::vector get_obis_info(); + + protected: + const bytes buffer_; + size_t pos_; +}; + +std::string bytes_repr(const bytes &buffer); + +uint64_t bytes_to_uint(const bytes &buffer); + +int64_t bytes_to_int(const bytes &buffer); + +std::string bytes_to_string(const bytes &buffer); +} // namespace sml +} // namespace esphome diff --git a/esphome/components/sml/text_sensor/__init__.py b/esphome/components/sml/text_sensor/__init__.py new file mode 100644 index 0000000000..81564bf65b --- /dev/null +++ b/esphome/components/sml/text_sensor/__init__.py @@ -0,0 +1,43 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import text_sensor +from esphome.const import CONF_FORMAT, CONF_ID + +from .. import CONF_OBIS_CODE, CONF_SERVER_ID, CONF_SML_ID, Sml, obis_code, sml_ns + +AUTO_LOAD = ["sml"] + +SmlType = sml_ns.enum("SmlType") +SML_TYPES = { + "text": SmlType.SML_OCTET, + "bool": SmlType.SML_BOOL, + "int": SmlType.SML_INT, + "uint": SmlType.SML_UINT, + "hex": SmlType.SML_HEX, + "": SmlType.SML_UNDEFINED, +} + +SmlTextSensor = sml_ns.class_("SmlTextSensor", text_sensor.TextSensor, cg.Component) + +CONFIG_SCHEMA = text_sensor.TEXT_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(SmlTextSensor), + cv.GenerateID(CONF_SML_ID): cv.use_id(Sml), + cv.Required(CONF_OBIS_CODE): obis_code, + cv.Optional(CONF_SERVER_ID, default=""): cv.string, + cv.Optional(CONF_FORMAT, default=""): cv.enum(SML_TYPES, lower=True), + } +) + + +async def to_code(config): + var = cg.new_Pvariable( + config[CONF_ID], + config[CONF_SERVER_ID], + config[CONF_OBIS_CODE], + config[CONF_FORMAT], + ) + await cg.register_component(var, config) + await text_sensor.register_text_sensor(var, config) + sml = await cg.get_variable(config[CONF_SML_ID]) + cg.add(sml.register_sml_listener(var)) diff --git a/esphome/components/sml/text_sensor/sml_text_sensor.cpp b/esphome/components/sml/text_sensor/sml_text_sensor.cpp new file mode 100644 index 0000000000..64f10698f0 --- /dev/null +++ b/esphome/components/sml/text_sensor/sml_text_sensor.cpp @@ -0,0 +1,54 @@ +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" +#include "sml_text_sensor.h" +#include "../sml_parser.h" + +namespace esphome { +namespace sml { + +static const char *const TAG = "sml_text_sensor"; + +SmlTextSensor::SmlTextSensor(std::string server_id, std::string obis_code, SmlType format) + : SmlListener(std::move(server_id), std::move(obis_code)), format_(format) {} + +void SmlTextSensor::publish_val(const ObisInfo &obis_info) { + uint8_t value_type; + if (this->format_ == SML_UNDEFINED) { + value_type = obis_info.value_type; + } else { + value_type = this->format_; + } + + switch (value_type) { + case SML_HEX: { + publish_state("0x" + bytes_repr(obis_info.value)); + break; + } + case SML_INT: { + publish_state(to_string(bytes_to_int(obis_info.value))); + break; + } + case SML_BOOL: + publish_state(bytes_to_uint(obis_info.value) ? "True" : "False"); + break; + case SML_UINT: { + publish_state(to_string(bytes_to_uint(obis_info.value))); + break; + } + case SML_OCTET: { + publish_state(std::string(obis_info.value.begin(), obis_info.value.end())); + break; + } + } +} + +void SmlTextSensor::dump_config() { + LOG_TEXT_SENSOR("", "SML", this); + if (!this->server_id.empty()) { + ESP_LOGCONFIG(TAG, " Server ID: %s", this->server_id.c_str()); + } + ESP_LOGCONFIG(TAG, " OBIS Code: %s", this->obis_code.c_str()); +} + +} // namespace sml +} // namespace esphome diff --git a/esphome/components/sml/text_sensor/sml_text_sensor.h b/esphome/components/sml/text_sensor/sml_text_sensor.h new file mode 100644 index 0000000000..20d27c9f71 --- /dev/null +++ b/esphome/components/sml/text_sensor/sml_text_sensor.h @@ -0,0 +1,21 @@ +#pragma once + +#include "esphome/components/sml/sml.h" +#include "esphome/components/text_sensor/text_sensor.h" +#include "../constants.h" + +namespace esphome { +namespace sml { + +class SmlTextSensor : public SmlListener, public text_sensor::TextSensor, public Component { + public: + SmlTextSensor(std::string server_id, std::string obis_code, SmlType format); + void publish_val(const ObisInfo &obis_info) override; + void dump_config() override; + + protected: + SmlType format_; +}; + +} // namespace sml +} // namespace esphome From 4e1f6518e829bcd80422cdf76b9d359603f23ae9 Mon Sep 17 00:00:00 2001 From: George Date: Tue, 10 May 2022 19:22:22 +1000 Subject: [PATCH 0437/1729] Delonghi Penguino PAC W120HP ir support (#3124) --- CODEOWNERS | 1 + esphome/components/delonghi/__init__.py | 1 + esphome/components/delonghi/climate.py | 20 +++ esphome/components/delonghi/delonghi.cpp | 186 +++++++++++++++++++++++ esphome/components/delonghi/delonghi.h | 64 ++++++++ tests/test1.yaml | 2 + 6 files changed, 274 insertions(+) create mode 100644 esphome/components/delonghi/__init__.py create mode 100644 esphome/components/delonghi/climate.py create mode 100644 esphome/components/delonghi/delonghi.cpp create mode 100644 esphome/components/delonghi/delonghi.h diff --git a/CODEOWNERS b/CODEOWNERS index 3a511275e1..16b9008379 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -55,6 +55,7 @@ esphome/components/current_based/* @djwmarcx esphome/components/daly_bms/* @s1lvi0 esphome/components/dashboard_import/* @esphome/core esphome/components/debug/* @OttoWinter +esphome/components/delonghi/* @grob6000 esphome/components/dfplayer/* @glmnet esphome/components/dht/* @OttoWinter esphome/components/ds1307/* @badbadc0ffee diff --git a/esphome/components/delonghi/__init__.py b/esphome/components/delonghi/__init__.py new file mode 100644 index 0000000000..0a81eb2da7 --- /dev/null +++ b/esphome/components/delonghi/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@grob6000"] diff --git a/esphome/components/delonghi/climate.py b/esphome/components/delonghi/climate.py new file mode 100644 index 0000000000..614706defe --- /dev/null +++ b/esphome/components/delonghi/climate.py @@ -0,0 +1,20 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import climate_ir +from esphome.const import CONF_ID + +AUTO_LOAD = ["climate_ir"] + +delonghi_ns = cg.esphome_ns.namespace("delonghi") +DelonghiClimate = delonghi_ns.class_("DelonghiClimate", climate_ir.ClimateIR) + +CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(DelonghiClimate), + } +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await climate_ir.register_climate_ir(var, config) diff --git a/esphome/components/delonghi/delonghi.cpp b/esphome/components/delonghi/delonghi.cpp new file mode 100644 index 0000000000..9bc0b5753d --- /dev/null +++ b/esphome/components/delonghi/delonghi.cpp @@ -0,0 +1,186 @@ +#include "delonghi.h" +#include "esphome/components/remote_base/remote_base.h" + +namespace esphome { +namespace delonghi { + +static const char *const TAG = "delonghi.climate"; + +void DelonghiClimate::transmit_state() { + uint8_t remote_state[DELONGHI_STATE_FRAME_SIZE] = {0}; + remote_state[0] = DELONGHI_ADDRESS; + remote_state[1] = this->temperature_(); + remote_state[1] |= (this->fan_speed_()) << 5; + remote_state[2] = this->operation_mode_(); + // Calculate checksum + for (int i = 0; i < DELONGHI_STATE_FRAME_SIZE - 1; i++) { + remote_state[DELONGHI_STATE_FRAME_SIZE - 1] += remote_state[i]; + } + + auto transmit = this->transmitter_->transmit(); + auto *data = transmit.get_data(); + data->set_carrier_frequency(DELONGHI_IR_FREQUENCY); + + data->mark(DELONGHI_HEADER_MARK); + data->space(DELONGHI_HEADER_SPACE); + for (unsigned char b : remote_state) { + for (uint8_t mask = 1; mask > 0; mask <<= 1) { // iterate through bit mask + data->mark(DELONGHI_BIT_MARK); + bool bit = b & mask; + data->space(bit ? DELONGHI_ONE_SPACE : DELONGHI_ZERO_SPACE); + } + } + data->mark(DELONGHI_BIT_MARK); + data->space(0); + + transmit.perform(); +} + +uint8_t DelonghiClimate::operation_mode_() { + uint8_t operating_mode = DELONGHI_MODE_ON; + switch (this->mode) { + case climate::CLIMATE_MODE_COOL: + operating_mode |= DELONGHI_MODE_COOL; + break; + case climate::CLIMATE_MODE_DRY: + operating_mode |= DELONGHI_MODE_DRY; + break; + case climate::CLIMATE_MODE_HEAT: + operating_mode |= DELONGHI_MODE_HEAT; + break; + case climate::CLIMATE_MODE_HEAT_COOL: + operating_mode |= DELONGHI_MODE_AUTO; + break; + case climate::CLIMATE_MODE_FAN_ONLY: + operating_mode |= DELONGHI_MODE_FAN; + break; + case climate::CLIMATE_MODE_OFF: + default: + operating_mode = DELONGHI_MODE_OFF; + break; + } + return operating_mode; +} + +uint16_t DelonghiClimate::fan_speed_() { + uint16_t fan_speed; + switch (this->fan_mode.value()) { + case climate::CLIMATE_FAN_LOW: + fan_speed = DELONGHI_FAN_LOW; + break; + case climate::CLIMATE_FAN_MEDIUM: + fan_speed = DELONGHI_FAN_MEDIUM; + break; + case climate::CLIMATE_FAN_HIGH: + fan_speed = DELONGHI_FAN_HIGH; + break; + case climate::CLIMATE_FAN_AUTO: + default: + fan_speed = DELONGHI_FAN_AUTO; + } + return fan_speed; +} + +uint8_t DelonghiClimate::temperature_() { + // Force special temperatures depending on the mode + uint8_t temperature = 0b0001; + switch (this->mode) { + case climate::CLIMATE_MODE_HEAT: + temperature = (uint8_t) roundf(this->target_temperature) - DELONGHI_TEMP_OFFSET_HEAT; + break; + case climate::CLIMATE_MODE_COOL: + case climate::CLIMATE_MODE_DRY: + case climate::CLIMATE_MODE_HEAT_COOL: + case climate::CLIMATE_MODE_FAN_ONLY: + case climate::CLIMATE_MODE_OFF: + default: + temperature = (uint8_t) roundf(this->target_temperature) - DELONGHI_TEMP_OFFSET_COOL; + } + if (temperature > 0x0F) { + temperature = 0x0F; // clamp maximum + } + return temperature; +} + +bool DelonghiClimate::parse_state_frame_(const uint8_t frame[]) { + uint8_t checksum = 0; + for (int i = 0; i < (DELONGHI_STATE_FRAME_SIZE - 1); i++) { + checksum += frame[i]; + } + if (frame[DELONGHI_STATE_FRAME_SIZE - 1] != checksum) { + return false; + } + uint8_t mode = frame[2] & 0x0F; + if (mode & DELONGHI_MODE_ON) { + switch (mode & 0x0E) { + case DELONGHI_MODE_COOL: + this->mode = climate::CLIMATE_MODE_COOL; + break; + case DELONGHI_MODE_DRY: + this->mode = climate::CLIMATE_MODE_DRY; + break; + case DELONGHI_MODE_HEAT: + this->mode = climate::CLIMATE_MODE_HEAT; + break; + case DELONGHI_MODE_AUTO: + this->mode = climate::CLIMATE_MODE_HEAT_COOL; + break; + case DELONGHI_MODE_FAN: + this->mode = climate::CLIMATE_MODE_FAN_ONLY; + break; + } + } else { + this->mode = climate::CLIMATE_MODE_OFF; + } + uint8_t temperature = frame[1] & 0x0F; + if (this->mode == climate::CLIMATE_MODE_HEAT) { + this->target_temperature = temperature + DELONGHI_TEMP_OFFSET_HEAT; + } else { + this->target_temperature = temperature + DELONGHI_TEMP_OFFSET_COOL; + } + uint8_t fan_mode = frame[1] >> 5; + switch (fan_mode) { + case DELONGHI_FAN_LOW: + this->fan_mode = climate::CLIMATE_FAN_LOW; + break; + case DELONGHI_FAN_MEDIUM: + this->fan_mode = climate::CLIMATE_FAN_MEDIUM; + break; + case DELONGHI_FAN_HIGH: + this->fan_mode = climate::CLIMATE_FAN_HIGH; + break; + case DELONGHI_FAN_AUTO: + this->fan_mode = climate::CLIMATE_FAN_AUTO; + break; + } + this->publish_state(); + return true; +} + +bool DelonghiClimate::on_receive(remote_base::RemoteReceiveData data) { + uint8_t state_frame[DELONGHI_STATE_FRAME_SIZE] = {}; + if (!data.expect_item(DELONGHI_HEADER_MARK, DELONGHI_HEADER_SPACE)) { + return false; + } + for (uint8_t pos = 0; pos < DELONGHI_STATE_FRAME_SIZE; pos++) { + uint8_t byte = 0; + for (int8_t bit = 0; bit < 8; bit++) { + if (data.expect_item(DELONGHI_BIT_MARK, DELONGHI_ONE_SPACE)) { + byte |= 1 << bit; + } else if (!data.expect_item(DELONGHI_BIT_MARK, DELONGHI_ZERO_SPACE)) { + return false; + } + } + state_frame[pos] = byte; + if (pos == 0) { + // frame header + if (byte != DELONGHI_ADDRESS) { + return false; + } + } + } + return this->parse_state_frame_(state_frame); +} + +} // namespace delonghi +} // namespace esphome diff --git a/esphome/components/delonghi/delonghi.h b/esphome/components/delonghi/delonghi.h new file mode 100644 index 0000000000..d310a58aee --- /dev/null +++ b/esphome/components/delonghi/delonghi.h @@ -0,0 +1,64 @@ +#pragma once + +#include "esphome/components/climate_ir/climate_ir.h" + +namespace esphome { +namespace delonghi { + +// Values for DELONGHI ARC43XXX IR Controllers +const uint8_t DELONGHI_ADDRESS = 83; + +// Temperature +const uint8_t DELONGHI_TEMP_MIN = 13; // Celsius +const uint8_t DELONGHI_TEMP_MAX = 32; // Celsius +const uint8_t DELONGHI_TEMP_OFFSET_COOL = 17; // Celsius +const uint8_t DELONGHI_TEMP_OFFSET_HEAT = 12; // Celsius + +// Modes +const uint8_t DELONGHI_MODE_AUTO = 0b1000; +const uint8_t DELONGHI_MODE_COOL = 0b0000; +const uint8_t DELONGHI_MODE_HEAT = 0b0110; +const uint8_t DELONGHI_MODE_DRY = 0b0010; +const uint8_t DELONGHI_MODE_FAN = 0b0100; +const uint8_t DELONGHI_MODE_OFF = 0b0000; +const uint8_t DELONGHI_MODE_ON = 0b0001; + +// Fan Speed +const uint8_t DELONGHI_FAN_AUTO = 0b00; +const uint8_t DELONGHI_FAN_HIGH = 0b01; +const uint8_t DELONGHI_FAN_MEDIUM = 0b10; +const uint8_t DELONGHI_FAN_LOW = 0b11; + +// IR Transmission - similar to NEC1 +const uint32_t DELONGHI_IR_FREQUENCY = 38000; +const uint32_t DELONGHI_HEADER_MARK = 9000; +const uint32_t DELONGHI_HEADER_SPACE = 4500; +const uint32_t DELONGHI_BIT_MARK = 465; +const uint32_t DELONGHI_ONE_SPACE = 1750; +const uint32_t DELONGHI_ZERO_SPACE = 670; + +// State Frame size +const uint8_t DELONGHI_STATE_FRAME_SIZE = 8; + +class DelonghiClimate : public climate_ir::ClimateIR { + public: + DelonghiClimate() + : climate_ir::ClimateIR(DELONGHI_TEMP_MIN, DELONGHI_TEMP_MAX, 1.0f, true, true, + {climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, + climate::CLIMATE_FAN_HIGH}, + {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL, + climate::CLIMATE_SWING_HORIZONTAL, climate::CLIMATE_SWING_BOTH}) {} + + protected: + // Transmit via IR the state of this climate controller. + void transmit_state() override; + uint8_t operation_mode_(); + uint16_t fan_speed_(); + uint8_t temperature_(); + // Handle received IR Buffer + bool on_receive(remote_base::RemoteReceiveData data) override; + bool parse_state_frame_(const uint8_t frame[]); +}; + +} // namespace delonghi +} // namespace esphome diff --git a/tests/test1.yaml b/tests/test1.yaml index aba37976aa..deaf1c237e 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1811,6 +1811,8 @@ climate: name: Fujitsu General Climate - platform: daikin name: Daikin Climate + - platform: delonghi + name: Delonghi Climate - platform: yashima name: Yashima Climate - platform: mitsubishi From 782186e13d98cf87e99fce261d92663feae17393 Mon Sep 17 00:00:00 2001 From: Martin <25747549+martgras@users.noreply.github.com> Date: Tue, 10 May 2022 11:25:44 +0200 Subject: [PATCH 0438/1729] extend scd4x (#3409) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 2 +- esphome/components/scd4x/automation.h | 28 ++++ esphome/components/scd4x/scd4x.cpp | 233 ++++++++++++++++++-------- esphome/components/scd4x/scd4x.h | 19 ++- esphome/components/scd4x/sensor.py | 69 +++++++- tests/test1.yaml | 12 ++ 6 files changed, 287 insertions(+), 76 deletions(-) create mode 100644 esphome/components/scd4x/automation.h diff --git a/CODEOWNERS b/CODEOWNERS index 16b9008379..e2b29547cb 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -168,7 +168,7 @@ esphome/components/rf_bridge/* @jesserockz esphome/components/rgbct/* @jesserockz esphome/components/rtttl/* @glmnet esphome/components/safe_mode/* @jsuanet @paulmonigatti -esphome/components/scd4x/* @sjtrny +esphome/components/scd4x/* @martgras @sjtrny esphome/components/script/* @esphome/core esphome/components/sdm_meter/* @jesserockz @polyfaces esphome/components/sdp3x/* @Azimath diff --git a/esphome/components/scd4x/automation.h b/esphome/components/scd4x/automation.h new file mode 100644 index 0000000000..21ecb2ea4c --- /dev/null +++ b/esphome/components/scd4x/automation.h @@ -0,0 +1,28 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "scd4x.h" + +namespace esphome { +namespace scd4x { + +template class PerformForcedCalibrationAction : public Action, public Parented { + public: + void play(Ts... x) override { + if (this->value_.has_value()) { + this->parent_->perform_forced_calibration(value_.value()); + } + } + + protected: + TEMPLATABLE_VALUE(uint16_t, value) +}; + +template class FactoryResetAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->factory_reset(); } +}; + +} // namespace scd4x +} // namespace esphome diff --git a/esphome/components/scd4x/scd4x.cpp b/esphome/components/scd4x/scd4x.cpp index 559c95df32..cbda996a4c 100644 --- a/esphome/components/scd4x/scd4x.cpp +++ b/esphome/components/scd4x/scd4x.cpp @@ -13,39 +13,32 @@ static const uint16_t SCD4X_CMD_ALTITUDE_COMPENSATION = 0x2427; static const uint16_t SCD4X_CMD_AMBIENT_PRESSURE_COMPENSATION = 0xe000; static const uint16_t SCD4X_CMD_AUTOMATIC_SELF_CALIBRATION = 0x2416; static const uint16_t SCD4X_CMD_START_CONTINUOUS_MEASUREMENTS = 0x21b1; +static const uint16_t SCD4X_CMD_START_LOW_POWER_CONTINUOUS_MEASUREMENTS = 0x21ac; +static const uint16_t SCD4X_CMD_START_LOW_POWER_SINGLE_SHOT = 0x219d; // SCD41 only +static const uint16_t SCD4X_CMD_START_LOW_POWER_SINGLE_SHOT_RHT_ONLY = 0x2196; static const uint16_t SCD4X_CMD_GET_DATA_READY_STATUS = 0xe4b8; static const uint16_t SCD4X_CMD_READ_MEASUREMENT = 0xec05; static const uint16_t SCD4X_CMD_PERFORM_FORCED_CALIBRATION = 0x362f; static const uint16_t SCD4X_CMD_STOP_MEASUREMENTS = 0x3f86; - +static const uint16_t SCD4X_CMD_FACTORY_RESET = 0x3632; +static const uint16_t SCD4X_CMD_GET_FEATURESET = 0x202f; static const float SCD4X_TEMPERATURE_OFFSET_MULTIPLIER = (1 << 16) / 175.0f; +static const uint16_t SCD41_ID = 0x1408; +static const uint16_t SCD40_ID = 0x440; void SCD4XComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up scd4x..."); - // the sensor needs 1000 ms to enter the idle state this->set_timeout(1000, [this]() { - uint16_t raw_read_status; - if (!this->get_register(SCD4X_CMD_GET_DATA_READY_STATUS, raw_read_status)) { - ESP_LOGE(TAG, "Failed to read data ready status"); + this->status_clear_error(); + if (!this->write_command(SCD4X_CMD_STOP_MEASUREMENTS)) { + ESP_LOGE(TAG, "Failed to stop measurements"); this->mark_failed(); return; } - - uint32_t stop_measurement_delay = 0; - // In order to query the device periodic measurement must be ceased - if (raw_read_status) { - ESP_LOGD(TAG, "Sensor has data available, stopping periodic measurement"); - if (!this->write_command(SCD4X_CMD_STOP_MEASUREMENTS)) { - ESP_LOGE(TAG, "Failed to stop measurements"); - this->mark_failed(); - return; - } - // According to the SCD4x datasheet the sensor will only respond to other commands after waiting 500 ms after - // issuing the stop_periodic_measurement command - stop_measurement_delay = 500; - } - this->set_timeout(stop_measurement_delay, [this]() { + // According to the SCD4x datasheet the sensor will only respond to other commands after waiting 500 ms after + // issuing the stop_periodic_measurement command + this->set_timeout(500, [this]() { uint16_t raw_serial_number[3]; if (!this->get_register(SCD4X_CMD_GET_SERIAL_NUMBER, raw_serial_number, 3, 1)) { ESP_LOGE(TAG, "Failed to read serial number"); @@ -89,15 +82,9 @@ void SCD4XComponent::setup() { return; } - // Finally start sensor measurements - if (!this->write_command(SCD4X_CMD_START_CONTINUOUS_MEASUREMENTS)) { - ESP_LOGE(TAG, "Error starting continuous measurements."); - this->error_code_ = MEASUREMENT_INIT_FAILED; - this->mark_failed(); - return; - } - initialized_ = true; + // Finally start sensor measurements + this->start_measurement_(); ESP_LOGD(TAG, "Sensor initialized"); }); }); @@ -123,12 +110,31 @@ void SCD4XComponent::dump_config() { } } ESP_LOGCONFIG(TAG, " Automatic self calibration: %s", ONOFF(this->enable_asc_)); - if (this->ambient_pressure_compensation_) { - ESP_LOGCONFIG(TAG, " Altitude compensation disabled"); - ESP_LOGCONFIG(TAG, " Ambient pressure compensation: %dmBar", this->ambient_pressure_); + if (this->ambient_pressure_source_ != nullptr) { + ESP_LOGCONFIG(TAG, " Dynamic ambient pressure compensation using sensor '%s'", + this->ambient_pressure_source_->get_name().c_str()); } else { - ESP_LOGCONFIG(TAG, " Ambient pressure compensation disabled"); - ESP_LOGCONFIG(TAG, " Altitude compensation: %dm", this->altitude_compensation_); + if (this->ambient_pressure_compensation_) { + ESP_LOGCONFIG(TAG, " Altitude compensation disabled"); + ESP_LOGCONFIG(TAG, " Ambient pressure compensation: %dmBar", this->ambient_pressure_); + } else { + ESP_LOGCONFIG(TAG, " Ambient pressure compensation disabled"); + ESP_LOGCONFIG(TAG, " Altitude compensation: %dm", this->altitude_compensation_); + } + } + switch (this->measurement_mode_) { + case PERIODIC: + ESP_LOGCONFIG(TAG, " Measurement mode: periodic (5s)"); + break; + case LOW_POWER_PERIODIC: + ESP_LOGCONFIG(TAG, " Measurement mode: low power periodic (30s)"); + break; + case SINGLE_SHOT: + ESP_LOGCONFIG(TAG, " Measurement mode: single shot"); + break; + case SINGLE_SHOT_RHT_ONLY: + ESP_LOGCONFIG(TAG, " Measurement mode: single shot rht only"); + break; } ESP_LOGCONFIG(TAG, " Temperature offset: %.2f °C", this->temperature_offset_); LOG_UPDATE_INTERVAL(this); @@ -149,47 +155,105 @@ void SCD4XComponent::update() { } } - // Check if data is ready - if (!this->write_command(SCD4X_CMD_GET_DATA_READY_STATUS)) { - this->status_set_warning(); - return; + uint32_t wait_time = 0; + if (this->measurement_mode_ == SINGLE_SHOT || this->measurement_mode_ == SINGLE_SHOT_RHT_ONLY) { + start_measurement_(); + wait_time = + this->measurement_mode_ == SINGLE_SHOT ? 5000 : 50; // Single shot measurement takes 5 secs rht mode 50 ms } + this->set_timeout(wait_time, [this]() { + // Check if data is ready + if (!this->write_command(SCD4X_CMD_GET_DATA_READY_STATUS)) { + this->status_set_warning(); + return; + } - uint16_t raw_read_status; - if (!this->read_data(raw_read_status) || raw_read_status == 0x00) { - this->status_set_warning(); - ESP_LOGW(TAG, "Data not ready yet!"); - return; - } + uint16_t raw_read_status; - if (!this->write_command(SCD4X_CMD_READ_MEASUREMENT)) { - ESP_LOGW(TAG, "Error reading measurement!"); - this->status_set_warning(); - return; - } + if (!this->read_data(raw_read_status) || raw_read_status == 0x00) { + this->status_set_warning(); + ESP_LOGW(TAG, "Data not ready yet!"); + return; + } - // Read off sensor data - uint16_t raw_data[3]; - if (!this->read_data(raw_data, 3)) { - this->status_set_warning(); - return; - } + if (!this->write_command(SCD4X_CMD_READ_MEASUREMENT)) { + ESP_LOGW(TAG, "Error reading measurement!"); + this->status_set_warning(); + return; // NO RETRY + } + // Read off sensor data + uint16_t raw_data[3]; + if (!this->read_data(raw_data, 3)) { + this->status_set_warning(); + return; + } + if (this->co2_sensor_ != nullptr) + this->co2_sensor_->publish_state(raw_data[0]); - if (this->co2_sensor_ != nullptr) - this->co2_sensor_->publish_state(raw_data[0]); - - if (this->temperature_sensor_ != nullptr) { - const float temperature = -45.0f + (175.0f * (raw_data[1])) / (1 << 16); - this->temperature_sensor_->publish_state(temperature); - } - - if (this->humidity_sensor_ != nullptr) { - const float humidity = (100.0f * raw_data[2]) / (1 << 16); - this->humidity_sensor_->publish_state(humidity); - } - - this->status_clear_warning(); + if (this->temperature_sensor_ != nullptr) { + const float temperature = -45.0f + (175.0f * (raw_data[1])) / (1 << 16); + this->temperature_sensor_->publish_state(temperature); + } + if (this->humidity_sensor_ != nullptr) { + const float humidity = (100.0f * raw_data[2]) / (1 << 16); + this->humidity_sensor_->publish_state(humidity); + } + this->status_clear_warning(); + }); // set_timeout } + +bool SCD4XComponent::perform_forced_calibration(uint16_t current_co2_concentration) { + /* + Operate the SCD4x in the operation mode later used in normal sensor operation (periodic measurement, low power + periodic measurement or single shot) for > 3 minutes in an environment with homogenous and constant CO2 + concentration before performing a forced recalibration. + */ + if (!this->write_command(SCD4X_CMD_STOP_MEASUREMENTS)) { + ESP_LOGE(TAG, "Failed to stop measurements"); + this->status_set_warning(); + } + this->set_timeout(500, [this, current_co2_concentration]() { + if (this->write_command(SCD4X_CMD_PERFORM_FORCED_CALIBRATION, current_co2_concentration)) { + ESP_LOGD(TAG, "setting forced calibration Co2 level %d ppm", current_co2_concentration); + // frc takes 400 ms + // because this method will be used very rarly + // the simple aproach with delay is ok + delay(400); // NOLINT' + if (!this->start_measurement_()) { + return false; + } else { + ESP_LOGD(TAG, "forced calibration complete"); + } + return true; + } else { + ESP_LOGE(TAG, "force calibration failed"); + this->error_code_ = FRC_FAILED; + this->status_set_warning(); + return false; + } + }); + return true; +} + +bool SCD4XComponent::factory_reset() { + if (!this->write_command(SCD4X_CMD_STOP_MEASUREMENTS)) { + ESP_LOGE(TAG, "Failed to stop measurements"); + this->status_set_warning(); + return false; + } + + this->set_timeout(500, [this]() { + if (!this->write_command(SCD4X_CMD_FACTORY_RESET)) { + ESP_LOGE(TAG, "Failed to send factory reset command"); + this->status_set_warning(); + return false; + } + ESP_LOGD(TAG, "Factory reset complete"); + return true; + }); + return true; +} + // Note pressure in bar here. Convert to hPa void SCD4XComponent::set_ambient_pressure_compensation(float pressure_in_bar) { ambient_pressure_compensation_ = true; @@ -213,5 +277,38 @@ bool SCD4XComponent::update_ambient_pressure_compensation_(uint16_t pressure_in_ } } +bool SCD4XComponent::start_measurement_() { + uint16_t measurement_command = SCD4X_CMD_START_CONTINUOUS_MEASUREMENTS; + switch (this->measurement_mode_) { + case PERIODIC: + measurement_command = SCD4X_CMD_START_CONTINUOUS_MEASUREMENTS; + break; + case LOW_POWER_PERIODIC: + measurement_command = SCD4X_CMD_START_LOW_POWER_CONTINUOUS_MEASUREMENTS; + break; + case SINGLE_SHOT: + measurement_command = SCD4X_CMD_START_LOW_POWER_SINGLE_SHOT; + break; + case SINGLE_SHOT_RHT_ONLY: + measurement_command = SCD4X_CMD_START_LOW_POWER_SINGLE_SHOT_RHT_ONLY; + break; + } + + static uint8_t remaining_retries = 3; + while (remaining_retries) { + if (!this->write_command(measurement_command)) { + ESP_LOGE(TAG, "Error starting measurements."); + this->error_code_ = MEASUREMENT_INIT_FAILED; + this->status_set_warning(); + if (--remaining_retries == 0) + return false; + delay(50); // NOLINT wait 50 ms and try again + } + this->status_clear_warning(); + return true; + } + return false; +} + } // namespace scd4x } // namespace esphome diff --git a/esphome/components/scd4x/scd4x.h b/esphome/components/scd4x/scd4x.h index 3e534bcf98..23c3766e60 100644 --- a/esphome/components/scd4x/scd4x.h +++ b/esphome/components/scd4x/scd4x.h @@ -1,5 +1,6 @@ #pragma once - +#include +#include "esphome/core/application.h" #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" #include "esphome/components/sensirion_common/i2c_sensirion.h" @@ -7,7 +8,14 @@ namespace esphome { namespace scd4x { -enum ERRORCODE { COMMUNICATION_FAILED, SERIAL_NUMBER_IDENTIFICATION_FAILED, MEASUREMENT_INIT_FAILED, UNKNOWN }; +enum ERRORCODE { + COMMUNICATION_FAILED, + SERIAL_NUMBER_IDENTIFICATION_FAILED, + MEASUREMENT_INIT_FAILED, + FRC_FAILED, + UNKNOWN +}; +enum MeasurementMode { PERIODIC, LOW_POWER_PERIODIC, SINGLE_SHOT, SINGLE_SHOT_RHT_ONLY }; class SCD4XComponent : public PollingComponent, public sensirion_common::SensirionI2CDevice { public: @@ -25,10 +33,13 @@ class SCD4XComponent : public PollingComponent, public sensirion_common::Sensiri void set_co2_sensor(sensor::Sensor *co2) { co2_sensor_ = co2; } void set_temperature_sensor(sensor::Sensor *temperature) { temperature_sensor_ = temperature; }; void set_humidity_sensor(sensor::Sensor *humidity) { humidity_sensor_ = humidity; } + void set_measurement_mode(MeasurementMode mode) { measurement_mode_ = mode; } + bool perform_forced_calibration(uint16_t current_co2_concentration); + bool factory_reset(); protected: bool update_ambient_pressure_compensation_(uint16_t pressure_in_hpa); - + bool start_measurement_(); ERRORCODE error_code_; bool initialized_{false}; @@ -38,7 +49,7 @@ class SCD4XComponent : public PollingComponent, public sensirion_common::Sensiri bool ambient_pressure_compensation_; uint16_t ambient_pressure_; bool enable_asc_; - + MeasurementMode measurement_mode_{PERIODIC}; sensor::Sensor *co2_sensor_{nullptr}; sensor::Sensor *temperature_sensor_{nullptr}; sensor::Sensor *humidity_sensor_{nullptr}; diff --git a/esphome/components/scd4x/sensor.py b/esphome/components/scd4x/sensor.py index 6ab0e1ba99..4c94d4257f 100644 --- a/esphome/components/scd4x/sensor.py +++ b/esphome/components/scd4x/sensor.py @@ -2,11 +2,15 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor from esphome.components import sensirion_common +from esphome import automation +from esphome.automation import maybe_simple_id + from esphome.const import ( CONF_ID, CONF_CO2, CONF_HUMIDITY, CONF_TEMPERATURE, + CONF_VALUE, DEVICE_CLASS_CARBON_DIOXIDE, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, @@ -19,7 +23,7 @@ from esphome.const import ( UNIT_PERCENT, ) -CODEOWNERS = ["@sjtrny"] +CODEOWNERS = ["@sjtrny", "@martgras"] DEPENDENCIES = ["i2c"] AUTO_LOAD = ["sensirion_common"] @@ -27,12 +31,29 @@ scd4x_ns = cg.esphome_ns.namespace("scd4x") SCD4XComponent = scd4x_ns.class_( "SCD4XComponent", cg.PollingComponent, sensirion_common.SensirionI2CDevice ) +MeasurementMode = scd4x_ns.enum("MEASUREMENT_MODE") +MEASUREMENT_MODE_OPTIONS = { + "periodic": MeasurementMode.PERIODIC, + "low_power_periodic": MeasurementMode.LOW_POWER_PERIODIC, + "single_shot": MeasurementMode.SINGLE_SHOT, + "single_shot_rht_only": MeasurementMode.SINGLE_SHOT_RHT_ONLY, +} + + +# Actions +PerformForcedCalibrationAction = scd4x_ns.class_( + "PerformForcedCalibrationAction", automation.Action +) +FactoryResetAction = scd4x_ns.class_("FactoryResetAction", automation.Action) + -CONF_AUTOMATIC_SELF_CALIBRATION = "automatic_self_calibration" CONF_ALTITUDE_COMPENSATION = "altitude_compensation" CONF_AMBIENT_PRESSURE_COMPENSATION = "ambient_pressure_compensation" -CONF_TEMPERATURE_OFFSET = "temperature_offset" CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE = "ambient_pressure_compensation_source" +CONF_AUTOMATIC_SELF_CALIBRATION = "automatic_self_calibration" +CONF_MEASUREMENT_MODE = "measurement_mode" +CONF_TEMPERATURE_OFFSET = "temperature_offset" + CONFIG_SCHEMA = ( cv.Schema( @@ -69,6 +90,9 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE): cv.use_id( sensor.Sensor ), + cv.Optional(CONF_MEASUREMENT_MODE, default="periodic"): cv.enum( + MEASUREMENT_MODE_OPTIONS, lower=True + ), } ) .extend(cv.polling_component_schema("60s")) @@ -106,3 +130,42 @@ async def to_code(config): if CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE in config: sens = await cg.get_variable(config[CONF_AMBIENT_PRESSURE_COMPENSATION_SOURCE]) cg.add(var.set_ambient_pressure_source(sens)) + + cg.add(var.set_measurement_mode(config[CONF_MEASUREMENT_MODE])) + + +SCD4X_ACTION_SCHEMA = maybe_simple_id( + { + cv.GenerateID(): cv.use_id(SCD4XComponent), + cv.Required(CONF_VALUE): cv.templatable(cv.positive_int), + } +) + + +@automation.register_action( + "scd4x.perform_forced_calibration", + PerformForcedCalibrationAction, + SCD4X_ACTION_SCHEMA, +) +async def scd4x_frc_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + template_ = await cg.templatable(config[CONF_VALUE], args, cg.uint16) + cg.add(var.set_value(template_)) + return var + + +SCD4X_RESET_ACTION_SCHEMA = maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(SCD4XComponent), + } +) + + +@automation.register_action( + "scd4x.factory_reset", FactoryResetAction, SCD4X_RESET_ACTION_SCHEMA +) +async def scd4x_reset_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var diff --git a/tests/test1.yaml b/tests/test1.yaml index deaf1c237e..e15b2cf6b7 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -890,6 +890,7 @@ sensor: temperature_offset: 4.2C i2c_id: i2c_bus - platform: scd4x + id: scd40 co2: name: "SCD4X CO2" temperature: @@ -2822,3 +2823,14 @@ lock: - platform: copy source_id: test_lock2 name: Generic Output Lock Copy + +button: + - platform: template + name: "Start calibration" + on_press: + - scd4x.perform_forced_calibration: + value: 419 + id: scd40 + - scd4x.factory_reset: + id: scd40 + From 98c733108e430b7828dc7f884942ab063f5e616f Mon Sep 17 00:00:00 2001 From: Matthew Garrett Date: Tue, 10 May 2022 02:35:43 -0700 Subject: [PATCH 0439/1729] PMSX003: Add support for specifying the update interval and spinning down (#3053) Co-authored-by: Otto Winter --- esphome/components/pmsx003/pmsx003.cpp | 68 ++++++++++++++++++++++++++ esphome/components/pmsx003/pmsx003.h | 21 ++++++++ esphome/components/pmsx003/sensor.py | 28 +++++++++++ tests/test3.yaml | 11 +++-- 4 files changed, 125 insertions(+), 3 deletions(-) diff --git a/esphome/components/pmsx003/pmsx003.cpp b/esphome/components/pmsx003/pmsx003.cpp index 5de94699f0..43f2e12f55 100644 --- a/esphome/components/pmsx003/pmsx003.cpp +++ b/esphome/components/pmsx003/pmsx003.cpp @@ -49,6 +49,47 @@ void PMSX003Component::set_formaldehyde_sensor(sensor::Sensor *formaldehyde_sens void PMSX003Component::loop() { const uint32_t now = millis(); + + // If we update less often than it takes the device to stabilise, spin the fan down + // rather than running it constantly. It does take some time to stabilise, so we + // need to keep track of what state we're in. + if (this->update_interval_ > PMS_STABILISING_MS) { + if (this->initialised_ == 0) { + this->send_command_(PMS_CMD_AUTO_MANUAL, 0); + this->send_command_(PMS_CMD_ON_STANDBY, 1); + this->initialised_ = 1; + } + switch (this->state_) { + case PMSX003_STATE_IDLE: + // Power on the sensor now so it'll be ready when we hit the update time + if (now - this->last_update_ < (this->update_interval_ - PMS_STABILISING_MS)) + return; + + this->state_ = PMSX003_STATE_STABILISING; + this->send_command_(PMS_CMD_ON_STANDBY, 1); + this->fan_on_time_ = now; + return; + case PMSX003_STATE_STABILISING: + // wait for the sensor to be stable + if (now - this->fan_on_time_ < PMS_STABILISING_MS) + return; + // consume any command responses that are in the serial buffer + while (this->available()) + this->read_byte(&this->data_[0]); + // Trigger a new read + this->send_command_(PMS_CMD_TRIG_MANUAL, 0); + this->state_ = PMSX003_STATE_WAITING; + break; + case PMSX003_STATE_WAITING: + // Just go ahead and read stuff + break; + } + } else if (now - this->last_update_ < this->update_interval_) { + // Otherwise just leave the sensor powered up and come back when we hit the update + // time + return; + } + if (now - this->last_transmission_ >= 500) { // last transmission too long ago. Reset RX index. this->data_index_ = 0; @@ -65,6 +106,7 @@ void PMSX003Component::loop() { // finished this->parse_data_(); this->data_index_ = 0; + this->last_update_ = now; } else if (!*check) { // wrong data this->data_index_ = 0; @@ -131,6 +173,25 @@ optional PMSX003Component::check_byte_() { return {}; } +void PMSX003Component::send_command_(uint8_t cmd, uint16_t data) { + this->data_index_ = 0; + this->data_[data_index_++] = 0x42; + this->data_[data_index_++] = 0x4D; + this->data_[data_index_++] = cmd; + this->data_[data_index_++] = (data >> 8) & 0xFF; + this->data_[data_index_++] = (data >> 0) & 0xFF; + int sum = 0; + for (int i = 0; i < data_index_; i++) { + sum += this->data_[i]; + } + this->data_[data_index_++] = (sum >> 8) & 0xFF; + this->data_[data_index_++] = (sum >> 0) & 0xFF; + for (int i = 0; i < data_index_; i++) { + this->write_byte(this->data_[i]); + } + this->data_index_ = 0; +} + void PMSX003Component::parse_data_() { switch (this->type_) { case PMSX003_TYPE_5003ST: { @@ -218,6 +279,13 @@ void PMSX003Component::parse_data_() { } } + // Spin down the sensor again if we aren't going to need it until more time has + // passed than it takes to stabilise + if (this->update_interval_ > PMS_STABILISING_MS) { + this->send_command_(PMS_CMD_ON_STANDBY, 0); + this->state_ = PMSX003_STATE_IDLE; + } + this->status_clear_warning(); } uint16_t PMSX003Component::get_16_bit_uint_(uint8_t start_index) { diff --git a/esphome/components/pmsx003/pmsx003.h b/esphome/components/pmsx003/pmsx003.h index fd6364c70c..eb33f66909 100644 --- a/esphome/components/pmsx003/pmsx003.h +++ b/esphome/components/pmsx003/pmsx003.h @@ -7,6 +7,13 @@ namespace esphome { namespace pmsx003 { +// known command bytes +#define PMS_CMD_AUTO_MANUAL 0xE1 // data=0: perform measurement manually, data=1: perform measurement automatically +#define PMS_CMD_TRIG_MANUAL 0xE2 // trigger a manual measurement +#define PMS_CMD_ON_STANDBY 0xE4 // data=0: go to standby mode, data=1: go to normal mode + +static const uint16_t PMS_STABILISING_MS = 30000; // time taken for the sensor to become stable after power on + enum PMSX003Type { PMSX003_TYPE_X003 = 0, PMSX003_TYPE_5003T, @@ -14,6 +21,12 @@ enum PMSX003Type { PMSX003_TYPE_5003S, }; +enum PMSX003State { + PMSX003_STATE_IDLE = 0, + PMSX003_STATE_STABILISING, + PMSX003_STATE_WAITING, +}; + class PMSX003Component : public uart::UARTDevice, public Component { public: PMSX003Component() = default; @@ -23,6 +36,8 @@ class PMSX003Component : public uart::UARTDevice, public Component { void set_type(PMSX003Type type) { type_ = type; } + void set_update_interval(uint32_t val) { update_interval_ = val; }; + void set_pm_1_0_std_sensor(sensor::Sensor *pm_1_0_std_sensor); void set_pm_2_5_std_sensor(sensor::Sensor *pm_2_5_std_sensor); void set_pm_10_0_std_sensor(sensor::Sensor *pm_10_0_std_sensor); @@ -45,11 +60,17 @@ class PMSX003Component : public uart::UARTDevice, public Component { protected: optional check_byte_(); void parse_data_(); + void send_command_(uint8_t cmd, uint16_t data); uint16_t get_16_bit_uint_(uint8_t start_index); uint8_t data_[64]; uint8_t data_index_{0}; + uint8_t initialised_{0}; + uint32_t fan_on_time_{0}; + uint32_t last_update_{0}; uint32_t last_transmission_{0}; + uint32_t update_interval_{0}; + PMSX003State state_{PMSX003_STATE_IDLE}; PMSX003Type type_; // "Standard Particle" diff --git a/esphome/components/pmsx003/sensor.py b/esphome/components/pmsx003/sensor.py index b731e48e31..f3552f4081 100644 --- a/esphome/components/pmsx003/sensor.py +++ b/esphome/components/pmsx003/sensor.py @@ -1,6 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor, uart + from esphome.const import ( CONF_FORMALDEHYDE, CONF_HUMIDITY, @@ -17,6 +18,7 @@ from esphome.const import ( CONF_PM_2_5UM, CONF_PM_5_0UM, CONF_PM_10_0UM, + CONF_UPDATE_INTERVAL, CONF_TEMPERATURE, CONF_TYPE, DEVICE_CLASS_PM1, @@ -44,6 +46,7 @@ TYPE_PMS5003ST = "PMS5003ST" TYPE_PMS5003S = "PMS5003S" PMSX003Type = pmsx003_ns.enum("PMSX003Type") + PMSX003_TYPES = { TYPE_PMSX003: PMSX003Type.PMSX003_TYPE_X003, TYPE_PMS5003T: PMSX003Type.PMSX003_TYPE_5003T, @@ -68,6 +71,17 @@ def validate_pmsx003_sensors(value): return value +def validate_update_interval(value): + value = cv.positive_time_period_milliseconds(value) + if value == cv.time_period("0s"): + return value + if value < cv.time_period("30s"): + raise cv.Invalid( + "Update interval must be greater than or equal to 30 seconds if set." + ) + return value + + CONFIG_SCHEMA = ( cv.Schema( { @@ -157,6 +171,7 @@ CONFIG_SCHEMA = ( accuracy_decimals=0, state_class=STATE_CLASS_MEASUREMENT, ), + cv.Optional(CONF_UPDATE_INTERVAL, default="0s"): validate_update_interval, } ) .extend(cv.COMPONENT_SCHEMA) @@ -164,6 +179,17 @@ CONFIG_SCHEMA = ( ) +def final_validate(config): + require_tx = config[CONF_UPDATE_INTERVAL] > cv.time_period("0s") + schema = uart.final_validate_device_schema( + "pmsx003", baud_rate=9600, require_rx=True, require_tx=require_tx + ) + schema(config) + + +FINAL_VALIDATE_SCHEMA = final_validate + + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) @@ -230,3 +256,5 @@ async def to_code(config): if CONF_FORMALDEHYDE in config: sens = await sensor.new_sensor(config[CONF_FORMALDEHYDE]) cg.add(var.set_formaldehyde_sensor(sens)) + + cg.add(var.set_update_interval(config[CONF_UPDATE_INTERVAL])) diff --git a/tests/test3.yaml b/tests/test3.yaml index e3818d87ec..ec768e17e5 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -266,6 +266,10 @@ uart: stop_bits: 2 # Specifically added for testing debug with no options at all. debug: + - id: uart8 + tx_pin: GPIO4 + rx_pin: GPIO5 + baud_rate: 9600 modbus: uart_id: uart1 @@ -559,7 +563,7 @@ sensor: name: 'AQI' calculation_type: 'AQI' - platform: pmsx003 - uart_id: uart2 + uart_id: uart8 type: PMSX003 pm_1_0: name: 'PM 1.0 Concentration' @@ -585,8 +589,9 @@ sensor: name: 'Particulate Count >5.0um' pm_10_0um: name: 'Particulate Count >10.0um' + update_interval: 30s - platform: pmsx003 - uart_id: uart2 + uart_id: uart5 type: PMS5003T pm_2_5: name: 'PM 2.5 Concentration' @@ -595,7 +600,7 @@ sensor: humidity: name: 'PMS Humidity' - platform: pmsx003 - uart_id: uart2 + uart_id: uart6 type: PMS5003ST pm_1_0: name: 'PM 1.0 Concentration' From 5fac67ce15dbbd8c9fc1da89264a71fcfe87258c Mon Sep 17 00:00:00 2001 From: Felix Storm Date: Tue, 10 May 2022 11:39:18 +0200 Subject: [PATCH 0440/1729] CAN bus: on_frame remote_transmission_request (#3376) --- esphome/components/canbus/__init__.py | 13 ++++++++++++- esphome/components/canbus/canbus.cpp | 6 ++++-- esphome/components/canbus/canbus.h | 8 +++++++- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/esphome/components/canbus/__init__.py b/esphome/components/canbus/__init__.py index 20f2642144..1dbd743c75 100644 --- a/esphome/components/canbus/__init__.py +++ b/esphome/components/canbus/__init__.py @@ -78,6 +78,7 @@ CANBUS_SCHEMA = cv.Schema( min=0, max=0x1FFFFFFF ), cv.Optional(CONF_USE_EXTENDED_ID, default=False): cv.boolean, + cv.Optional(CONF_REMOTE_TRANSMISSION_REQUEST): cv.boolean, }, validate_id, ), @@ -100,10 +101,20 @@ async def setup_canbus_core_(var, config): trigger = cg.new_Pvariable( conf[CONF_TRIGGER_ID], var, can_id, can_id_mask, ext_id ) + if CONF_REMOTE_TRANSMISSION_REQUEST in conf: + cg.add( + trigger.set_remote_transmission_request( + conf[CONF_REMOTE_TRANSMISSION_REQUEST] + ) + ) await cg.register_component(trigger, conf) await automation.build_automation( trigger, - [(cg.std_vector.template(cg.uint8), "x"), (cg.uint32, "can_id")], + [ + (cg.std_vector.template(cg.uint8), "x"), + (cg.uint32, "can_id"), + (cg.bool_, "remote_transmission_request"), + ], conf, ) diff --git a/esphome/components/canbus/canbus.cpp b/esphome/components/canbus/canbus.cpp index 5d9084706b..3fe0d50f06 100644 --- a/esphome/components/canbus/canbus.cpp +++ b/esphome/components/canbus/canbus.cpp @@ -81,8 +81,10 @@ void Canbus::loop() { // fire all triggers for (auto *trigger : this->triggers_) { if ((trigger->can_id_ == (can_message.can_id & trigger->can_id_mask_)) && - (trigger->use_extended_id_ == can_message.use_extended_id)) { - trigger->trigger(data, can_message.can_id); + (trigger->use_extended_id_ == can_message.use_extended_id) && + (!trigger->remote_transmission_request_.has_value() || + trigger->remote_transmission_request_.value() == can_message.remote_transmission_request)) { + trigger->trigger(data, can_message.can_id, can_message.remote_transmission_request); } } } diff --git a/esphome/components/canbus/canbus.h b/esphome/components/canbus/canbus.h index 20c490c083..06b15c0db5 100644 --- a/esphome/components/canbus/canbus.h +++ b/esphome/components/canbus/canbus.h @@ -126,13 +126,18 @@ template class CanbusSendAction : public Action, public P std::vector data_static_{}; }; -class CanbusTrigger : public Trigger, uint32_t>, public Component { +class CanbusTrigger : public Trigger, uint32_t, bool>, public Component { friend class Canbus; public: explicit CanbusTrigger(Canbus *parent, const std::uint32_t can_id, const std::uint32_t can_id_mask, const bool use_extended_id) : parent_(parent), can_id_(can_id), can_id_mask_(can_id_mask), use_extended_id_(use_extended_id){}; + + void set_remote_transmission_request(bool remote_transmission_request) { + this->remote_transmission_request_ = remote_transmission_request; + } + void setup() override { this->parent_->add_trigger(this); } protected: @@ -140,6 +145,7 @@ class CanbusTrigger : public Trigger, uint32_t>, public Com uint32_t can_id_; uint32_t can_id_mask_; bool use_extended_id_; + optional remote_transmission_request_{}; }; } // namespace canbus From 7cba0c6fb0a95573f21e192921f130933b79162a Mon Sep 17 00:00:00 2001 From: Dennis <51165557+dennisvbussel@users.noreply.github.com> Date: Tue, 10 May 2022 11:42:31 +0200 Subject: [PATCH 0441/1729] =?UTF-8?q?Fix=20cover=20set=20position=20by=20f?= =?UTF-8?q?orce=20pushing=20position=5Fid=20datapoint=20(simila=E2=80=A6?= =?UTF-8?q?=20(#3435)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- esphome/components/tuya/cover/tuya_cover.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/tuya/cover/tuya_cover.cpp b/esphome/components/tuya/cover/tuya_cover.cpp index b63eb9109d..b55873c3c1 100644 --- a/esphome/components/tuya/cover/tuya_cover.cpp +++ b/esphome/components/tuya/cover/tuya_cover.cpp @@ -66,7 +66,7 @@ void TuyaCover::control(const cover::CoverCall &call) { auto position_int = static_cast(pos * this->value_range_); position_int = position_int + this->min_value_; - parent_->set_integer_datapoint_value(*this->position_id_, position_int); + parent_->force_set_integer_datapoint_value(*this->position_id_, position_int); } } if (call.get_position().has_value()) { @@ -82,7 +82,7 @@ void TuyaCover::control(const cover::CoverCall &call) { auto position_int = static_cast(pos * this->value_range_); position_int = position_int + this->min_value_; - parent_->set_integer_datapoint_value(*this->position_id_, position_int); + parent_->force_set_integer_datapoint_value(*this->position_id_, position_int); } } From 69118120d9145f24a40c70675de8d5dfda5483a8 Mon Sep 17 00:00:00 2001 From: LuBeDa Date: Tue, 10 May 2022 11:56:29 +0200 Subject: [PATCH 0442/1729] added prev_frame for animation (#3427) --- esphome/components/display/display_buffer.cpp | 6 ++++++ esphome/components/display/display_buffer.h | 1 + 2 files changed, 7 insertions(+) diff --git a/esphome/components/display/display_buffer.cpp b/esphome/components/display/display_buffer.cpp index d00fdd5240..ca866a43d2 100644 --- a/esphome/components/display/display_buffer.cpp +++ b/esphome/components/display/display_buffer.cpp @@ -584,6 +584,12 @@ void Animation::next_frame() { this->current_frame_ = 0; } } +void Animation::prev_frame() { + this->current_frame_--; + if (this->current_frame_ < 0) { + this->current_frame_ = this->animation_frame_count_ - 1; + } +} DisplayPage::DisplayPage(display_writer_t writer) : writer_(std::move(writer)) {} void DisplayPage::show() { this->parent_->show_page(this); } diff --git a/esphome/components/display/display_buffer.h b/esphome/components/display/display_buffer.h index 86221c5f96..33f55aa3d1 100644 --- a/esphome/components/display/display_buffer.h +++ b/esphome/components/display/display_buffer.h @@ -478,6 +478,7 @@ class Animation : public Image { int get_animation_frame_count() const; int get_current_frame() const; void next_frame(); + void prev_frame(); protected: int current_frame_; From b7e52812f8d713827c25299a76bdb4c7195a72ca Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 10 May 2022 22:02:58 +1200 Subject: [PATCH 0443/1729] Fix tests (#3455) --- tests/test3.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test3.yaml b/tests/test3.yaml index ec768e17e5..af0e784465 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -266,7 +266,7 @@ uart: stop_bits: 2 # Specifically added for testing debug with no options at all. debug: - - id: uart8 + - id: uart9 tx_pin: GPIO4 rx_pin: GPIO5 baud_rate: 9600 @@ -563,7 +563,7 @@ sensor: name: 'AQI' calculation_type: 'AQI' - platform: pmsx003 - uart_id: uart8 + uart_id: uart9 type: PMSX003 pm_1_0: name: 'PM 1.0 Concentration' From 4822abde860670ada7733338548be4ccf6e4c4f2 Mon Sep 17 00:00:00 2001 From: Massimo Cetra Date: Tue, 10 May 2022 12:03:40 +0200 Subject: [PATCH 0444/1729] Fix BLE280 setup when the sensor is marked as failed. (#3396) --- esphome/components/bme280/bme280.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/esphome/components/bme280/bme280.cpp b/esphome/components/bme280/bme280.cpp index fcb293afa0..a4ea8d608e 100644 --- a/esphome/components/bme280/bme280.cpp +++ b/esphome/components/bme280/bme280.cpp @@ -81,6 +81,11 @@ static const char *iir_filter_to_str(BME280IIRFilter filter) { void BME280Component::setup() { ESP_LOGCONFIG(TAG, "Setting up BME280..."); uint8_t chip_id = 0; + + // Mark as not failed before initializing. Some devices will turn off sensors to save on batteries + // and when they come back on, the COMPONENT_STATE_FAILED bit must be unset on the component. + this->component_state_ &= ~COMPONENT_STATE_FAILED; + if (!this->read_byte(BME280_REGISTER_CHIPID, &chip_id)) { this->error_code_ = COMMUNICATION_FAILED; this->mark_failed(); From e541ae400c3a5d43c2e3d621ac4c7c99799ecb36 Mon Sep 17 00:00:00 2001 From: MFlasskamp Date: Tue, 10 May 2022 12:03:59 +0200 Subject: [PATCH 0445/1729] Esp32c3 deepsleep fix (#3454) --- .../components/deep_sleep/deep_sleep_component.cpp | 14 ++++++++++++-- .../components/deep_sleep/deep_sleep_component.h | 6 ++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/esphome/components/deep_sleep/deep_sleep_component.cpp b/esphome/components/deep_sleep/deep_sleep_component.cpp index 23f2a7a70c..1e31cef33e 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.cpp +++ b/esphome/components/deep_sleep/deep_sleep_component.cpp @@ -21,6 +21,7 @@ optional DeepSleepComponent::get_run_duration_() const { switch (wakeup_cause) { case ESP_SLEEP_WAKEUP_EXT0: case ESP_SLEEP_WAKEUP_EXT1: + case ESP_SLEEP_WAKEUP_GPIO: return this->wakeup_cause_to_run_duration_->gpio_cause; case ESP_SLEEP_WAKEUP_TOUCHPAD: return this->wakeup_cause_to_run_duration_->touch_cause; @@ -72,18 +73,27 @@ float DeepSleepComponent::get_loop_priority() const { return -100.0f; // run after everything else is ready } void DeepSleepComponent::set_sleep_duration(uint32_t time_ms) { this->sleep_duration_ = uint64_t(time_ms) * 1000; } -#ifdef USE_ESP32 +#if defined(USE_ESP32) void DeepSleepComponent::set_wakeup_pin_mode(WakeupPinMode wakeup_pin_mode) { this->wakeup_pin_mode_ = wakeup_pin_mode; } +#endif + +#if defined(USE_ESP32) #if !defined(USE_ESP32_VARIANT_ESP32C3) + void DeepSleepComponent::set_ext1_wakeup(Ext1Wakeup ext1_wakeup) { this->ext1_wakeup_ = ext1_wakeup; } + void DeepSleepComponent::set_touch_wakeup(bool touch_wakeup) { this->touch_wakeup_ = touch_wakeup; } + +#endif + void DeepSleepComponent::set_run_duration(WakeupCauseToRunDuration wakeup_cause_to_run_duration) { wakeup_cause_to_run_duration_ = wakeup_cause_to_run_duration; } + #endif -#endif + void DeepSleepComponent::set_run_duration(uint32_t time_ms) { this->run_duration_ = time_ms; } void DeepSleepComponent::begin_sleep(bool manual) { if (this->prevent_ && !manual) { diff --git a/esphome/components/deep_sleep/deep_sleep_component.h b/esphome/components/deep_sleep/deep_sleep_component.h index 5e90d4b89d..1ff681693f 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.h +++ b/esphome/components/deep_sleep/deep_sleep_component.h @@ -70,17 +70,19 @@ class DeepSleepComponent : public Component { void set_wakeup_pin_mode(WakeupPinMode wakeup_pin_mode); #endif -#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3) +#if defined(USE_ESP32) +#if !defined(USE_ESP32_VARIANT_ESP32C3) void set_ext1_wakeup(Ext1Wakeup ext1_wakeup); void set_touch_wakeup(bool touch_wakeup); +#endif // Set the duration in ms for how long the code should run before entering // deep sleep mode, according to the cause the ESP32 has woken. void set_run_duration(WakeupCauseToRunDuration wakeup_cause_to_run_duration); - #endif + /// Set a duration in ms for how long the code should run before entering deep sleep mode. void set_run_duration(uint32_t time_ms); From 235a97ea1014e13454db4693e57db64ae153c4bc Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Tue, 10 May 2022 21:54:00 +0200 Subject: [PATCH 0446/1729] Make retry scheduler efficient (#3225) --- esphome/core/component.h | 2 +- esphome/core/scheduler.cpp | 71 ++++++++++++++++++++------------------ esphome/core/scheduler.h | 19 +++------- 3 files changed, 43 insertions(+), 49 deletions(-) diff --git a/esphome/core/component.h b/esphome/core/component.h index c3a4ac3782..e394736653 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -61,7 +61,7 @@ extern const uint32_t STATUS_LED_OK; extern const uint32_t STATUS_LED_WARNING; extern const uint32_t STATUS_LED_ERROR; -enum RetryResult { DONE, RETRY }; +enum class RetryResult { DONE, RETRY }; class Component { public: diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 56f823556b..cc4074b94d 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -14,7 +14,7 @@ static const uint32_t MAX_LOGICALLY_DELETED_ITEMS = 10; // #define ESPHOME_DEBUG_SCHEDULER void HOT Scheduler::set_timeout(Component *component, const std::string &name, uint32_t timeout, - std::function &&func) { + std::function func) { const uint32_t now = this->millis_(); if (!name.empty()) @@ -32,7 +32,7 @@ void HOT Scheduler::set_timeout(Component *component, const std::string &name, u item->timeout = timeout; item->last_execution = now; item->last_execution_major = this->millis_major_; - item->void_callback = std::move(func); + item->callback = std::move(func); item->remove = false; this->push_(std::move(item)); } @@ -40,7 +40,7 @@ bool HOT Scheduler::cancel_timeout(Component *component, const std::string &name return this->cancel_item_(component, name, SchedulerItem::TIMEOUT); } void HOT Scheduler::set_interval(Component *component, const std::string &name, uint32_t interval, - std::function &&func) { + std::function func) { const uint32_t now = this->millis_(); if (!name.empty()) @@ -65,7 +65,7 @@ void HOT Scheduler::set_interval(Component *component, const std::string &name, item->last_execution_major = this->millis_major_; if (item->last_execution > now) item->last_execution_major--; - item->void_callback = std::move(func); + item->callback = std::move(func); item->remove = false; this->push_(std::move(item)); } @@ -73,37 +73,48 @@ bool HOT Scheduler::cancel_interval(Component *component, const std::string &nam return this->cancel_item_(component, name, SchedulerItem::INTERVAL); } -void HOT Scheduler::set_retry(Component *component, const std::string &name, uint32_t initial_wait_time, - uint8_t max_attempts, std::function &&func, - float backoff_increase_factor) { - const uint32_t now = this->millis_(); +struct RetryArgs { + std::function func; + uint8_t retry_countdown; + uint32_t current_interval; + Component *component; + std::string name; + float backoff_increase_factor; + Scheduler *scheduler; +}; +static void retry_handler(const std::shared_ptr &args) { + RetryResult retry_result = args->func(); + if (retry_result == RetryResult::DONE || --args->retry_countdown <= 0) + return; + args->current_interval *= args->backoff_increase_factor; + args->scheduler->set_timeout(args->component, args->name, args->current_interval, [args]() { retry_handler(args); }); +} + +void HOT Scheduler::set_retry(Component *component, const std::string &name, uint32_t initial_wait_time, + uint8_t max_attempts, std::function func, float backoff_increase_factor) { if (!name.empty()) this->cancel_retry(component, name); if (initial_wait_time == SCHEDULER_DONT_RUN) return; - ESP_LOGVV(TAG, "set_retry(name='%s', initial_wait_time=%u,max_attempts=%u, backoff_factor=%0.1f)", name.c_str(), + ESP_LOGVV(TAG, "set_retry(name='%s', initial_wait_time=%u, max_attempts=%u, backoff_factor=%0.1f)", name.c_str(), initial_wait_time, max_attempts, backoff_increase_factor); - auto item = make_unique(); - item->component = component; - item->name = name; - item->type = SchedulerItem::RETRY; - item->interval = initial_wait_time; - item->retry_countdown = max_attempts; - item->backoff_multiplier = backoff_increase_factor; - item->last_execution = now - initial_wait_time; - item->last_execution_major = this->millis_major_; - if (item->last_execution > now) - item->last_execution_major--; - item->retry_callback = std::move(func); - item->remove = false; - this->push_(std::move(item)); + auto args = std::make_shared(); + args->func = std::move(func); + args->retry_countdown = max_attempts; + args->current_interval = initial_wait_time; + args->component = component; + args->name = "retry$" + name; + args->backoff_increase_factor = backoff_increase_factor; + args->scheduler = this; + + this->set_timeout(component, args->name, initial_wait_time, [args]() { retry_handler(args); }); } bool HOT Scheduler::cancel_retry(Component *component, const std::string &name) { - return this->cancel_item_(component, name, SchedulerItem::RETRY); + return this->cancel_timeout(component, "retry$" + name); } optional HOT Scheduler::next_schedule_in() { @@ -162,7 +173,6 @@ void HOT Scheduler::call() { } while (!this->empty_()) { - RetryResult retry_result = RETRY; // use scoping to indicate visibility of `item` variable { // Don't copy-by value yet @@ -191,11 +201,7 @@ void HOT Scheduler::call() { // - timeouts/intervals get cancelled { WarnIfComponentBlockingGuard guard{item->component}; - if (item->type == SchedulerItem::RETRY) { - retry_result = item->retry_callback(); - } else { - item->void_callback(); - } + item->callback(); } } @@ -213,16 +219,13 @@ void HOT Scheduler::call() { continue; } - if (item->type == SchedulerItem::INTERVAL || - (item->type == SchedulerItem::RETRY && (--item->retry_countdown > 0 && retry_result != RetryResult::DONE))) { + if (item->type == SchedulerItem::INTERVAL) { if (item->interval != 0) { const uint32_t before = item->last_execution; const uint32_t amount = (now - item->last_execution) / item->interval; item->last_execution += amount * item->interval; if (item->last_execution < before) item->last_execution_major++; - if (item->type == SchedulerItem::RETRY) - item->interval *= item->backoff_multiplier; } this->push_(std::move(item)); } diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index dc96d58329..111bee1df2 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -10,13 +10,13 @@ class Component; class Scheduler { public: - void set_timeout(Component *component, const std::string &name, uint32_t timeout, std::function &&func); + void set_timeout(Component *component, const std::string &name, uint32_t timeout, std::function func); bool cancel_timeout(Component *component, const std::string &name); - void set_interval(Component *component, const std::string &name, uint32_t interval, std::function &&func); + void set_interval(Component *component, const std::string &name, uint32_t interval, std::function func); bool cancel_interval(Component *component, const std::string &name); void set_retry(Component *component, const std::string &name, uint32_t initial_wait_time, uint8_t max_attempts, - std::function &&func, float backoff_increase_factor = 1.0f); + std::function func, float backoff_increase_factor = 1.0f); bool cancel_retry(Component *component, const std::string &name); optional next_schedule_in(); @@ -29,20 +29,13 @@ class Scheduler { struct SchedulerItem { Component *component; std::string name; - enum Type { TIMEOUT, INTERVAL, RETRY } type; + enum Type { TIMEOUT, INTERVAL } type; union { uint32_t interval; uint32_t timeout; }; uint32_t last_execution; - // Ideally this should be a union or std::variant - // but unions don't work with object like std::function - // union CallBack_{ - std::function void_callback; - std::function retry_callback; - // }; - uint8_t retry_countdown{3}; - float backoff_multiplier{1.0f}; + std::function callback; bool remove; uint8_t last_execution_major; @@ -60,8 +53,6 @@ class Scheduler { switch (this->type) { case SchedulerItem::INTERVAL: return "interval"; - case SchedulerItem::RETRY: - return "retry"; case SchedulerItem::TIMEOUT: return "timeout"; default: From 62f9e181e03e6f3ae1aea18833b686b069fb0680 Mon Sep 17 00:00:00 2001 From: Maurice Makaay Date: Wed, 11 May 2022 00:58:28 +0200 Subject: [PATCH 0447/1729] Code cleanup fixes for the select component (#3457) Co-authored-by: Maurice Makaay --- esphome/components/select/select.cpp | 11 ++++++++--- esphome/components/select/select.h | 18 +++++++++++++++--- esphome/components/select/select_call.cpp | 2 -- esphome/components/select/select_call.h | 1 - .../template/select/template_select.cpp | 19 ++++++++++--------- 5 files changed, 33 insertions(+), 18 deletions(-) diff --git a/esphome/components/select/select.cpp b/esphome/components/select/select.cpp index 75edb5c8ba..35f1cfba46 100644 --- a/esphome/components/select/select.cpp +++ b/esphome/components/select/select.cpp @@ -23,6 +23,10 @@ void Select::add_on_state_callback(std::function &&ca this->state_callback_.add(std::move(callback)); } +bool Select::has_option(const std::string &option) const { return this->index_of(option).has_value(); } + +bool Select::has_index(size_t index) const { return index < this->size(); } + size_t Select::size() const { auto options = traits.get_options(); return options.size(); @@ -46,11 +50,12 @@ optional Select::active_index() const { } optional Select::at(size_t index) const { - auto options = traits.get_options(); - if (index >= options.size()) { + if (this->has_index(index)) { + auto options = traits.get_options(); + return options.at(index); + } else { return {}; } - return options.at(index); } uint32_t Select::hash_base() { return 2812997003UL; } diff --git a/esphome/components/select/select.h b/esphome/components/select/select.h index 64870fc9a3..b11c6404a0 100644 --- a/esphome/components/select/select.h +++ b/esphome/components/select/select.h @@ -28,16 +28,28 @@ class Select : public EntityBase { void publish_state(const std::string &state); - /// Return whether this select has gotten a full state yet. + /// Return whether this select component has gotten a full state yet. bool has_state() const { return has_state_; } + /// Instantiate a SelectCall object to modify this select component's state. SelectCall make_call() { return SelectCall(this); } - void set(const std::string &value) { make_call().set_option(value).perform(); } - // Methods that provide an API to index-based access. + /// Return whether this select component contains the provided option. + bool has_option(const std::string &option) const; + + /// Return whether this select component contains the provided index offset. + bool has_index(size_t index) const; + + /// Return the number of options in this select component. size_t size() const; + + /// Find the (optional) index offset of the provided option value. optional index_of(const std::string &option) const; + + /// Return the (optional) index offset of the currently active option. optional active_index() const; + + /// Return the (optional) option value at the provided index offset. optional at(size_t index) const; void add_on_state_callback(std::function &&callback); diff --git a/esphome/components/select/select_call.cpp b/esphome/components/select/select_call.cpp index 9442598740..6ee41b1029 100644 --- a/esphome/components/select/select_call.cpp +++ b/esphome/components/select/select_call.cpp @@ -13,8 +13,6 @@ SelectCall &SelectCall::set_option(const std::string &option) { SelectCall &SelectCall::set_index(size_t index) { return with_operation(SELECT_OP_SET_INDEX).with_index(index); } -const optional &SelectCall::get_option() const { return option_; } - SelectCall &SelectCall::select_next(bool cycle) { return with_operation(SELECT_OP_NEXT).with_cycle(cycle); } SelectCall &SelectCall::select_previous(bool cycle) { return with_operation(SELECT_OP_PREVIOUS).with_cycle(cycle); } diff --git a/esphome/components/select/select_call.h b/esphome/components/select/select_call.h index ea4d34ab5f..efc9a982ec 100644 --- a/esphome/components/select/select_call.h +++ b/esphome/components/select/select_call.h @@ -24,7 +24,6 @@ class SelectCall { SelectCall &set_option(const std::string &option); SelectCall &set_index(size_t index); - const optional &get_option() const; SelectCall &select_next(bool cycle); SelectCall &select_previous(bool cycle); diff --git a/esphome/components/template/select/template_select.cpp b/esphome/components/template/select/template_select.cpp index 219c341ec9..88a0d66ed6 100644 --- a/esphome/components/template/select/template_select.cpp +++ b/esphome/components/template/select/template_select.cpp @@ -20,9 +20,12 @@ void TemplateSelect::setup() { this->pref_ = global_preferences->make_preference(this->get_object_id_hash()); if (!this->pref_.load(&index)) { value = this->initial_option_; - ESP_LOGD(TAG, "State from initial (could not load): %s", value.c_str()); + ESP_LOGD(TAG, "State from initial (could not load stored index): %s", value.c_str()); + } else if (!this->has_index(index)) { + value = this->initial_option_; + ESP_LOGD(TAG, "State from initial (restored index %d out of bounds): %s", index, value.c_str()); } else { - value = this->traits.get_options().at(index); + value = this->at(index).value(); ESP_LOGD(TAG, "State from restore: %s", value.c_str()); } } @@ -38,9 +41,8 @@ void TemplateSelect::update() { if (!val.has_value()) return; - auto options = this->traits.get_options(); - if (std::find(options.begin(), options.end(), *val) == options.end()) { - ESP_LOGE(TAG, "lambda returned an invalid option %s", (*val).c_str()); + if (!this->has_option(*val)) { + ESP_LOGE(TAG, "Lambda returned an invalid option: %s", (*val).c_str()); return; } @@ -54,12 +56,11 @@ void TemplateSelect::control(const std::string &value) { this->publish_state(value); if (this->restore_value_) { - auto options = this->traits.get_options(); - size_t index = std::find(options.begin(), options.end(), value) - options.begin(); - - this->pref_.save(&index); + auto index = this->index_of(value); + this->pref_.save(&index.value()); } } + void TemplateSelect::dump_config() { LOG_SELECT("", "Template Select", this); LOG_UPDATE_INTERVAL(this); From c569f5ddcfbb301424860055f1ef14e7bf7c8850 Mon Sep 17 00:00:00 2001 From: Maurice Makaay Date: Wed, 11 May 2022 01:02:49 +0200 Subject: [PATCH 0448/1729] Code cleanup fixes for the number component (#3458) Co-authored-by: Maurice Makaay --- esphome/components/number/number.h | 1 - esphome/components/number/number_call.h | 2 -- 2 files changed, 3 deletions(-) diff --git a/esphome/components/number/number.h b/esphome/components/number/number.h index 8f9bf8c2e1..ad058e3a0e 100644 --- a/esphome/components/number/number.h +++ b/esphome/components/number/number.h @@ -33,7 +33,6 @@ class Number : public EntityBase { void publish_state(float state); NumberCall make_call() { return NumberCall(this); } - void set(float value) { make_call().set_value(value).perform(); } void add_on_state_callback(std::function &&callback); diff --git a/esphome/components/number/number_call.h b/esphome/components/number/number_call.h index 9a3dad560f..bd50170be5 100644 --- a/esphome/components/number/number_call.h +++ b/esphome/components/number/number_call.h @@ -23,8 +23,6 @@ class NumberCall { void perform(); NumberCall &set_value(float value); - const optional &get_value() const { return value_; } - NumberCall &number_increment(bool cycle); NumberCall &number_decrement(bool cycle); NumberCall &number_to_min(); From 0b69f7231585cd8d110efeb08a992647b1bbb6a3 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Wed, 11 May 2022 01:38:05 +0200 Subject: [PATCH 0449/1729] Enable api transport encryption for new projects (#3142) * Enable api transport encryption for new projects * Format --- esphome/dashboard/dashboard.py | 3 +++ esphome/wizard.py | 2 ++ 2 files changed, 5 insertions(+) diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index b78d22cf7c..1fadac968d 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -1,5 +1,6 @@ # pylint: disable=wrong-import-position +import base64 import codecs import collections import functools @@ -378,6 +379,8 @@ class WizardRequestHandler(BaseHandler): if k in ("name", "platform", "board", "ssid", "psk", "password") } kwargs["ota_password"] = secrets.token_hex(16) + noise_psk = secrets.token_bytes(32) + kwargs["api_encryption_key"] = base64.b64encode(noise_psk).decode() destination = settings.rel_path(f"{kwargs['name']}.yaml") wizard.wizard_write(path=destination, **kwargs) self.set_status(200) diff --git a/esphome/wizard.py b/esphome/wizard.py index 34930ff66f..469219300b 100644 --- a/esphome/wizard.py +++ b/esphome/wizard.py @@ -111,6 +111,8 @@ def wizard_file(**kwargs): # Configure API if "password" in kwargs: config += f" password: \"{kwargs['password']}\"\n" + if "api_encryption_key" in kwargs: + config += f" encryption:\n key: \"{kwargs['api_encryption_key']}\"\n" # Configure OTA config += "\nota:\n" From 4116caff6ae1262502dd7c4d6952a140105146f8 Mon Sep 17 00:00:00 2001 From: Ruben De Smet Date: Wed, 11 May 2022 01:44:52 +0200 Subject: [PATCH 0450/1729] Implement allow_deep_sleep (#3282) --- esphome/components/deep_sleep/deep_sleep_component.cpp | 1 + esphome/components/deep_sleep/deep_sleep_component.h | 1 + 2 files changed, 2 insertions(+) diff --git a/esphome/components/deep_sleep/deep_sleep_component.cpp b/esphome/components/deep_sleep/deep_sleep_component.cpp index 1e31cef33e..8db100f236 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.cpp +++ b/esphome/components/deep_sleep/deep_sleep_component.cpp @@ -160,6 +160,7 @@ void DeepSleepComponent::begin_sleep(bool manual) { } float DeepSleepComponent::get_setup_priority() const { return setup_priority::LATE; } void DeepSleepComponent::prevent_deep_sleep() { this->prevent_ = true; } +void DeepSleepComponent::allow_deep_sleep() { this->prevent_ = false; } } // namespace deep_sleep } // namespace esphome diff --git a/esphome/components/deep_sleep/deep_sleep_component.h b/esphome/components/deep_sleep/deep_sleep_component.h index 1ff681693f..f384dee01d 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.h +++ b/esphome/components/deep_sleep/deep_sleep_component.h @@ -96,6 +96,7 @@ class DeepSleepComponent : public Component { void begin_sleep(bool manual = false); void prevent_deep_sleep(); + void allow_deep_sleep(); protected: // Returns nullopt if no run duration is set. Otherwise, returns the run From 40ad9f491182c58453bc94b49d9a934d0c25d46c Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 11 May 2022 12:47:50 +1200 Subject: [PATCH 0451/1729] Add deep_sleep.allow YAML action (#3459) --- esphome/components/deep_sleep/__init__.py | 63 ++++++++++++------- .../deep_sleep/deep_sleep_component.h | 12 ++-- tests/test1.yaml | 2 +- 3 files changed, 48 insertions(+), 29 deletions(-) diff --git a/esphome/components/deep_sleep/__init__.py b/esphome/components/deep_sleep/__init__.py index 058358fa67..8b60b4eb5f 100644 --- a/esphome/components/deep_sleep/__init__.py +++ b/esphome/components/deep_sleep/__init__.py @@ -93,7 +93,14 @@ deep_sleep_ns = cg.esphome_ns.namespace("deep_sleep") DeepSleepComponent = deep_sleep_ns.class_("DeepSleepComponent", cg.Component) EnterDeepSleepAction = deep_sleep_ns.class_("EnterDeepSleepAction", automation.Action) PreventDeepSleepAction = deep_sleep_ns.class_( - "PreventDeepSleepAction", automation.Action + "PreventDeepSleepAction", + automation.Action, + cg.Parented.template(DeepSleepComponent), +) +AllowDeepSleepAction = deep_sleep_ns.class_( + "AllowDeepSleepAction", + automation.Action, + cg.Parented.template(DeepSleepComponent), ) WakeupPinMode = deep_sleep_ns.enum("WakeupPinMode") @@ -208,28 +215,32 @@ async def to_code(config): cg.add_define("USE_DEEP_SLEEP") -DEEP_SLEEP_ENTER_SCHEMA = cv.All( - automation.maybe_simple_id( - { - cv.GenerateID(): cv.use_id(DeepSleepComponent), - cv.Exclusive(CONF_SLEEP_DURATION, "time"): cv.templatable( - cv.positive_time_period_milliseconds - ), - # Only on ESP32 due to how long the RTC on ESP8266 can stay asleep - cv.Exclusive(CONF_UNTIL, "time"): cv.All(cv.only_on_esp32, cv.time_of_day), - cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock), - } - ), - cv.has_none_or_all_keys(CONF_UNTIL, CONF_TIME_ID), -) - - -DEEP_SLEEP_PREVENT_SCHEMA = automation.maybe_simple_id( +DEEP_SLEEP_ACTION_SCHEMA = cv.Schema( { cv.GenerateID(): cv.use_id(DeepSleepComponent), } ) +DEEP_SLEEP_ENTER_SCHEMA = cv.All( + automation.maybe_simple_id( + DEEP_SLEEP_ACTION_SCHEMA.extend( + cv.Schema( + { + cv.Exclusive(CONF_SLEEP_DURATION, "time"): cv.templatable( + cv.positive_time_period_milliseconds + ), + # Only on ESP32 due to how long the RTC on ESP8266 can stay asleep + cv.Exclusive(CONF_UNTIL, "time"): cv.All( + cv.only_on_esp32, cv.time_of_day + ), + cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock), + } + ) + ) + ), + cv.has_none_or_all_keys(CONF_UNTIL, CONF_TIME_ID), +) + @automation.register_action( "deep_sleep.enter", EnterDeepSleepAction, DEEP_SLEEP_ENTER_SCHEMA @@ -252,8 +263,16 @@ async def deep_sleep_enter_to_code(config, action_id, template_arg, args): @automation.register_action( - "deep_sleep.prevent", PreventDeepSleepAction, DEEP_SLEEP_PREVENT_SCHEMA + "deep_sleep.prevent", + PreventDeepSleepAction, + automation.maybe_simple_id(DEEP_SLEEP_ACTION_SCHEMA), ) -async def deep_sleep_prevent_to_code(config, action_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, paren) +@automation.register_action( + "deep_sleep.allow", + AllowDeepSleepAction, + automation.maybe_simple_id(DEEP_SLEEP_ACTION_SCHEMA), +) +async def deep_sleep_action_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var diff --git a/esphome/components/deep_sleep/deep_sleep_component.h b/esphome/components/deep_sleep/deep_sleep_component.h index f384dee01d..8dc87cece8 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.h +++ b/esphome/components/deep_sleep/deep_sleep_component.h @@ -190,14 +190,14 @@ template class EnterDeepSleepAction : public Action { #endif }; -template class PreventDeepSleepAction : public Action { +template class PreventDeepSleepAction : public Action, public Parented { public: - PreventDeepSleepAction(DeepSleepComponent *deep_sleep) : deep_sleep_(deep_sleep) {} + void play(Ts... x) override { this->parent_->prevent_deep_sleep(); } +}; - void play(Ts... x) override { this->deep_sleep_->prevent_deep_sleep(); } - - protected: - DeepSleepComponent *deep_sleep_; +template class AllowDeepSleepAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->allow_deep_sleep(); } }; } // namespace deep_sleep diff --git a/tests/test1.yaml b/tests/test1.yaml index e15b2cf6b7..7bb1fbe954 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -121,6 +121,7 @@ mqtt: - topic: livingroom/ota_mode then: - deep_sleep.prevent + - deep_sleep.allow - topic: livingroom/ota_mode then: - deep_sleep.enter: @@ -2833,4 +2834,3 @@ button: id: scd40 - scd4x.factory_reset: id: scd40 - From d6e039a1d14ab6473321d2fc6d1a47158accd672 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 11 May 2022 12:50:42 +1200 Subject: [PATCH 0452/1729] Bump version to 2022.5.0b1 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index fc928dc530..c254edf1ee 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2022.5.0-dev" +__version__ = "2022.5.0b1" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From f8a1bd4e79449a07420f09b5a88321dbf1ca6d35 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 11 May 2022 12:50:42 +1200 Subject: [PATCH 0453/1729] Bump version to 2022.6.0-dev --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index fc928dc530..9a2e41d69f 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2022.5.0-dev" +__version__ = "2022.6.0-dev" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From 3f678e218d22194de8f2c19dd2a9fa5b009d5d77 Mon Sep 17 00:00:00 2001 From: Maurice Makaay Date: Wed, 11 May 2022 23:25:00 +0200 Subject: [PATCH 0454/1729] On epoch sync, restore local TZ (#3462) Co-authored-by: Maurice Makaay --- esphome/components/time/real_time_clock.cpp | 12 ++++++++++-- esphome/components/time/real_time_clock.h | 1 + 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/esphome/components/time/real_time_clock.cpp b/esphome/components/time/real_time_clock.cpp index 36c5f4161d..7b5f0aa49b 100644 --- a/esphome/components/time/real_time_clock.cpp +++ b/esphome/components/time/real_time_clock.cpp @@ -13,11 +13,11 @@ static const char *const TAG = "time"; RealTimeClock::RealTimeClock() = default; void RealTimeClock::call_setup() { - setenv("TZ", this->timezone_.c_str(), 1); - tzset(); + this->apply_timezone_(); PollingComponent::call_setup(); } void RealTimeClock::synchronize_epoch_(uint32_t epoch) { + // Update UTC epoch time. struct timeval timev { .tv_sec = static_cast(epoch), .tv_usec = 0, }; @@ -30,6 +30,9 @@ void RealTimeClock::synchronize_epoch_(uint32_t epoch) { ret = settimeofday(&timev, nullptr); } + // Move timezone back to local timezone. + this->apply_timezone_(); + if (ret != 0) { ESP_LOGW(TAG, "setimeofday() failed with code %d", ret); } @@ -41,6 +44,11 @@ void RealTimeClock::synchronize_epoch_(uint32_t epoch) { this->time_sync_callback_.call(); } +void RealTimeClock::apply_timezone_() { + setenv("TZ", this->timezone_.c_str(), 1); + tzset(); +} + size_t ESPTime::strftime(char *buffer, size_t buffer_len, const char *format) { struct tm c_tm = this->to_c_tm(); return ::strftime(buffer, buffer_len, format, &c_tm); diff --git a/esphome/components/time/real_time_clock.h b/esphome/components/time/real_time_clock.h index b22c6f04d7..7f4afee306 100644 --- a/esphome/components/time/real_time_clock.h +++ b/esphome/components/time/real_time_clock.h @@ -137,6 +137,7 @@ class RealTimeClock : public PollingComponent { void synchronize_epoch_(uint32_t epoch); std::string timezone_{}; + void apply_timezone_(); CallbackManager time_sync_callback_; }; From c2aaae4818266bf82cfb8650650e9929ad954331 Mon Sep 17 00:00:00 2001 From: Niclas Larsson Date: Thu, 12 May 2022 00:26:51 +0200 Subject: [PATCH 0455/1729] Shelly dimmer: Use unique_ptr to handle the lifetime of stm32_t (#3400) Co-authored-by: Martin <25747549+martgras@users.noreply.github.com> Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- .../shelly_dimmer/shelly_dimmer.cpp | 9 +- .../components/shelly_dimmer/stm32flash.cpp | 119 +++++++++--------- esphome/components/shelly_dimmer/stm32flash.h | 28 +++-- 3 files changed, 79 insertions(+), 77 deletions(-) diff --git a/esphome/components/shelly_dimmer/shelly_dimmer.cpp b/esphome/components/shelly_dimmer/shelly_dimmer.cpp index 3b79d0bf57..32c556da5e 100644 --- a/esphome/components/shelly_dimmer/shelly_dimmer.cpp +++ b/esphome/components/shelly_dimmer/shelly_dimmer.cpp @@ -158,11 +158,8 @@ bool ShellyDimmer::upgrade_firmware_() { ESP_LOGW(TAG, "Starting STM32 firmware upgrade"); this->reset_dfu_boot_(); - // Could be constexpr in c++17 - static const auto CLOSE = [](stm32_t *stm32) { stm32_close(stm32); }; - // Cleanup with RAII - std::unique_ptr stm32{stm32_init(this, STREAM_SERIAL, 1), CLOSE}; + auto stm32 = stm32_init(this, STREAM_SERIAL, 1); if (!stm32) { ESP_LOGW(TAG, "Failed to initialize STM32"); @@ -170,7 +167,7 @@ bool ShellyDimmer::upgrade_firmware_() { } // Erase STM32 flash. - if (stm32_erase_memory(stm32.get(), 0, STM32_MASS_ERASE) != STM32_ERR_OK) { + if (stm32_erase_memory(stm32, 0, STM32_MASS_ERASE) != STM32_ERR_OK) { ESP_LOGW(TAG, "Failed to erase STM32 flash memory"); return false; } @@ -196,7 +193,7 @@ bool ShellyDimmer::upgrade_firmware_() { std::memcpy(buffer, p, BUFFER_SIZE); p += BUFFER_SIZE; - if (stm32_write_memory(stm32.get(), addr, buffer, len) != STM32_ERR_OK) { + if (stm32_write_memory(stm32, addr, buffer, len) != STM32_ERR_OK) { ESP_LOGW(TAG, "Failed to write to STM32 flash memory"); return false; } diff --git a/esphome/components/shelly_dimmer/stm32flash.cpp b/esphome/components/shelly_dimmer/stm32flash.cpp index 4c777776fb..e688f2de36 100644 --- a/esphome/components/shelly_dimmer/stm32flash.cpp +++ b/esphome/components/shelly_dimmer/stm32flash.cpp @@ -117,7 +117,7 @@ namespace shelly_dimmer { namespace { -int flash_addr_to_page_ceil(const stm32_t *stm, uint32_t addr) { +int flash_addr_to_page_ceil(const stm32_unique_ptr &stm, uint32_t addr) { if (!(addr >= stm->dev->fl_start && addr <= stm->dev->fl_end)) return 0; @@ -135,7 +135,7 @@ int flash_addr_to_page_ceil(const stm32_t *stm, uint32_t addr) { return addr ? page + 1 : page; } -stm32_err_t stm32_get_ack_timeout(const stm32_t *stm, uint32_t timeout) { +stm32_err_t stm32_get_ack_timeout(const stm32_unique_ptr &stm, uint32_t timeout) { auto *stream = stm->stream; uint8_t rxbyte; @@ -168,9 +168,9 @@ stm32_err_t stm32_get_ack_timeout(const stm32_t *stm, uint32_t timeout) { } while (true); } -stm32_err_t stm32_get_ack(const stm32_t *stm) { return stm32_get_ack_timeout(stm, 0); } +stm32_err_t stm32_get_ack(const stm32_unique_ptr &stm) { return stm32_get_ack_timeout(stm, 0); } -stm32_err_t stm32_send_command_timeout(const stm32_t *stm, const uint8_t cmd, const uint32_t timeout) { +stm32_err_t stm32_send_command_timeout(const stm32_unique_ptr &stm, const uint8_t cmd, const uint32_t timeout) { auto *const stream = stm->stream; static constexpr auto BUFFER_SIZE = 2; @@ -194,12 +194,12 @@ stm32_err_t stm32_send_command_timeout(const stm32_t *stm, const uint8_t cmd, co return STM32_ERR_UNKNOWN; } -stm32_err_t stm32_send_command(const stm32_t *stm, const uint8_t cmd) { +stm32_err_t stm32_send_command(const stm32_unique_ptr &stm, const uint8_t cmd) { return stm32_send_command_timeout(stm, cmd, 0); } /* if we have lost sync, send a wrong command and expect a NACK */ -stm32_err_t stm32_resync(const stm32_t *stm) { +stm32_err_t stm32_resync(const stm32_unique_ptr &stm) { auto *const stream = stm->stream; uint32_t t0 = millis(); auto t1 = t0; @@ -238,7 +238,7 @@ stm32_err_t stm32_resync(const stm32_t *stm) { * * len is value of the first byte in the frame. */ -stm32_err_t stm32_guess_len_cmd(const stm32_t *stm, const uint8_t cmd, uint8_t *const data, unsigned int len) { +stm32_err_t stm32_guess_len_cmd(const stm32_unique_ptr &stm, const uint8_t cmd, uint8_t *const data, unsigned int len) { auto *const stream = stm->stream; if (stm32_send_command(stm, cmd) != STM32_ERR_OK) @@ -286,7 +286,7 @@ stm32_err_t stm32_guess_len_cmd(const stm32_t *stm, const uint8_t cmd, uint8_t * * This function sends the init sequence and, in case of timeout, recovers * the interface. */ -stm32_err_t stm32_send_init_seq(const stm32_t *stm) { +stm32_err_t stm32_send_init_seq(const stm32_unique_ptr &stm) { auto *const stream = stm->stream; stream->write_array(&STM32_CMD_INIT, 1); @@ -320,7 +320,7 @@ stm32_err_t stm32_send_init_seq(const stm32_t *stm) { return STM32_ERR_UNKNOWN; } -stm32_err_t stm32_mass_erase(const stm32_t *stm) { +stm32_err_t stm32_mass_erase(const stm32_unique_ptr &stm) { auto *const stream = stm->stream; if (stm32_send_command(stm, stm->cmd->er) != STM32_ERR_OK) { @@ -364,7 +364,7 @@ template std::unique_ptr malloc_array_raii DELETOR}; } -stm32_err_t stm32_pages_erase(const stm32_t *stm, const uint32_t spage, const uint32_t pages) { +stm32_err_t stm32_pages_erase(const stm32_unique_ptr &stm, const uint32_t spage, const uint32_t pages) { auto *const stream = stm->stream; uint8_t cs = 0; int i = 0; @@ -474,6 +474,18 @@ template void populate_buffer_with_address(uint8_t (&buffer)[N], uint3 buffer[4] = static_cast(buffer[0] ^ buffer[1] ^ buffer[2] ^ buffer[3]); } +template stm32_unique_ptr make_stm32_with_deletor(T ptr) { + static const auto CLOSE = [](stm32_t *stm32) { + if (stm32) { + free(stm32->cmd); // NOLINT + } + free(stm32); // NOLINT + }; + + // Cleanup with RAII + return std::unique_ptr{ptr, CLOSE}; +} + } // Anonymous namespace } // namespace shelly_dimmer @@ -485,48 +497,44 @@ namespace shelly_dimmer { /* find newer command by higher code */ #define newer(prev, a) (((prev) == STM32_CMD_ERR) ? (a) : (((prev) > (a)) ? (prev) : (a))) -stm32_t *stm32_init(uart::UARTDevice *stream, const uint8_t flags, const char init) { +stm32_unique_ptr stm32_init(uart::UARTDevice *stream, const uint8_t flags, const char init) { uint8_t buf[257]; - // Could be constexpr in c++17 - static const auto CLOSE = [](stm32_t *stm32) { stm32_close(stm32); }; - - // Cleanup with RAII - std::unique_ptr stm{static_cast(calloc(sizeof(stm32_t), 1)), // NOLINT - CLOSE}; + auto stm = make_stm32_with_deletor(static_cast(calloc(sizeof(stm32_t), 1))); // NOLINT if (!stm) { - return nullptr; + return make_stm32_with_deletor(nullptr); } stm->stream = stream; stm->flags = flags; stm->cmd = static_cast(malloc(sizeof(stm32_cmd_t))); // NOLINT if (!stm->cmd) { - return nullptr; + return make_stm32_with_deletor(nullptr); } memset(stm->cmd, STM32_CMD_ERR, sizeof(stm32_cmd_t)); if ((stm->flags & STREAM_OPT_CMD_INIT) && init) { - if (stm32_send_init_seq(stm.get()) != STM32_ERR_OK) - return nullptr; // NOLINT + if (stm32_send_init_seq(stm) != STM32_ERR_OK) + return make_stm32_with_deletor(nullptr); } /* get the version and read protection status */ - if (stm32_send_command(stm.get(), STM32_CMD_GVR) != STM32_ERR_OK) { - return nullptr; // NOLINT + if (stm32_send_command(stm, STM32_CMD_GVR) != STM32_ERR_OK) { + return make_stm32_with_deletor(nullptr); } /* From AN, only UART bootloader returns 3 bytes */ { const auto len = (stm->flags & STREAM_OPT_GVR_ETX) ? 3 : 1; if (!stream->read_array(buf, len)) - return nullptr; // NOLINT + return make_stm32_with_deletor(nullptr); + stm->version = buf[0]; stm->option1 = (stm->flags & STREAM_OPT_GVR_ETX) ? buf[1] : 0; stm->option2 = (stm->flags & STREAM_OPT_GVR_ETX) ? buf[2] : 0; - if (stm32_get_ack(stm.get()) != STM32_ERR_OK) { - return nullptr; + if (stm32_get_ack(stm) != STM32_ERR_OK) { + return make_stm32_with_deletor(nullptr); } } @@ -544,8 +552,8 @@ stm32_t *stm32_init(uart::UARTDevice *stream, const uint8_t flags, const char in return STM32_CMD_GET_LENGTH; })(); - if (stm32_guess_len_cmd(stm.get(), STM32_CMD_GET, buf, len) != STM32_ERR_OK) - return nullptr; + if (stm32_guess_len_cmd(stm, STM32_CMD_GET, buf, len) != STM32_ERR_OK) + return make_stm32_with_deletor(nullptr); } const auto stop = buf[0] + 1; @@ -607,23 +615,23 @@ stm32_t *stm32_init(uart::UARTDevice *stream, const uint8_t flags, const char in } if (new_cmds) ESP_LOGD(TAG, ")"); - if (stm32_get_ack(stm.get()) != STM32_ERR_OK) { - return nullptr; + if (stm32_get_ack(stm) != STM32_ERR_OK) { + return make_stm32_with_deletor(nullptr); } if (stm->cmd->get == STM32_CMD_ERR || stm->cmd->gvr == STM32_CMD_ERR || stm->cmd->gid == STM32_CMD_ERR) { ESP_LOGD(TAG, "Error: bootloader did not returned correct information from GET command"); - return nullptr; + return make_stm32_with_deletor(nullptr); } /* get the device ID */ - if (stm32_guess_len_cmd(stm.get(), stm->cmd->gid, buf, 1) != STM32_ERR_OK) { - return nullptr; + if (stm32_guess_len_cmd(stm, stm->cmd->gid, buf, 1) != STM32_ERR_OK) { + return make_stm32_with_deletor(nullptr); } const auto returned = buf[0] + 1; if (returned < 2) { ESP_LOGD(TAG, "Only %d bytes sent in the PID, unknown/unsupported device", returned); - return nullptr; + return make_stm32_with_deletor(nullptr); } stm->pid = (buf[1] << 8) | buf[2]; if (returned > 2) { @@ -631,8 +639,8 @@ stm32_t *stm32_init(uart::UARTDevice *stream, const uint8_t flags, const char in for (auto i = 2; i <= returned; i++) ESP_LOGD(TAG, " %02x", buf[i]); } - if (stm32_get_ack(stm.get()) != STM32_ERR_OK) { - return nullptr; + if (stm32_get_ack(stm) != STM32_ERR_OK) { + return make_stm32_with_deletor(nullptr); } stm->dev = DEVICES; @@ -641,21 +649,14 @@ stm32_t *stm32_init(uart::UARTDevice *stream, const uint8_t flags, const char in if (!stm->dev->id) { ESP_LOGD(TAG, "Unknown/unsupported device (Device ID: 0x%03x)", stm->pid); - return nullptr; + return make_stm32_with_deletor(nullptr); } - // TODO: Would be much better if the unique_ptr was returned from this function - // Release ownership of unique_ptr - return stm.release(); // NOLINT + return stm; } -void stm32_close(stm32_t *stm) { - if (stm) - free(stm->cmd); // NOLINT - free(stm); // NOLINT -} - -stm32_err_t stm32_read_memory(const stm32_t *stm, const uint32_t address, uint8_t *data, const unsigned int len) { +stm32_err_t stm32_read_memory(const stm32_unique_ptr &stm, const uint32_t address, uint8_t *data, + const unsigned int len) { auto *const stream = stm->stream; if (!len) @@ -693,7 +694,8 @@ stm32_err_t stm32_read_memory(const stm32_t *stm, const uint32_t address, uint8_ return STM32_ERR_OK; } -stm32_err_t stm32_write_memory(const stm32_t *stm, uint32_t address, const uint8_t *data, const unsigned int len) { +stm32_err_t stm32_write_memory(const stm32_unique_ptr &stm, uint32_t address, const uint8_t *data, + const unsigned int len) { auto *const stream = stm->stream; if (!len) @@ -753,7 +755,7 @@ stm32_err_t stm32_write_memory(const stm32_t *stm, uint32_t address, const uint8 return STM32_ERR_OK; } -stm32_err_t stm32_wunprot_memory(const stm32_t *stm) { +stm32_err_t stm32_wunprot_memory(const stm32_unique_ptr &stm) { if (stm->cmd->uw == STM32_CMD_ERR) { ESP_LOGD(TAG, "Error: WRITE UNPROTECT command not implemented in bootloader."); return STM32_ERR_NO_CMD; @@ -766,7 +768,7 @@ stm32_err_t stm32_wunprot_memory(const stm32_t *stm) { []() { ESP_LOGD(TAG, "Error: Failed to WRITE UNPROTECT"); }); } -stm32_err_t stm32_wprot_memory(const stm32_t *stm) { +stm32_err_t stm32_wprot_memory(const stm32_unique_ptr &stm) { if (stm->cmd->wp == STM32_CMD_ERR) { ESP_LOGD(TAG, "Error: WRITE PROTECT command not implemented in bootloader."); return STM32_ERR_NO_CMD; @@ -779,7 +781,7 @@ stm32_err_t stm32_wprot_memory(const stm32_t *stm) { []() { ESP_LOGD(TAG, "Error: Failed to WRITE PROTECT"); }); } -stm32_err_t stm32_runprot_memory(const stm32_t *stm) { +stm32_err_t stm32_runprot_memory(const stm32_unique_ptr &stm) { if (stm->cmd->ur == STM32_CMD_ERR) { ESP_LOGD(TAG, "Error: READOUT UNPROTECT command not implemented in bootloader."); return STM32_ERR_NO_CMD; @@ -792,7 +794,7 @@ stm32_err_t stm32_runprot_memory(const stm32_t *stm) { []() { ESP_LOGD(TAG, "Error: Failed to READOUT UNPROTECT"); }); } -stm32_err_t stm32_readprot_memory(const stm32_t *stm) { +stm32_err_t stm32_readprot_memory(const stm32_unique_ptr &stm) { if (stm->cmd->rp == STM32_CMD_ERR) { ESP_LOGD(TAG, "Error: READOUT PROTECT command not implemented in bootloader."); return STM32_ERR_NO_CMD; @@ -805,7 +807,7 @@ stm32_err_t stm32_readprot_memory(const stm32_t *stm) { []() { ESP_LOGD(TAG, "Error: Failed to READOUT PROTECT"); }); } -stm32_err_t stm32_erase_memory(const stm32_t *stm, uint32_t spage, uint32_t pages) { +stm32_err_t stm32_erase_memory(const stm32_unique_ptr &stm, uint32_t spage, uint32_t pages) { if (!pages || spage > STM32_MAX_PAGES || ((pages != STM32_MASS_ERASE) && ((spage + pages) > STM32_MAX_PAGES))) return STM32_ERR_OK; @@ -847,7 +849,7 @@ stm32_err_t stm32_erase_memory(const stm32_t *stm, uint32_t spage, uint32_t page return STM32_ERR_OK; } -static stm32_err_t stm32_run_raw_code(const stm32_t *stm, uint32_t target_address, const uint8_t *code, +static stm32_err_t stm32_run_raw_code(const stm32_unique_ptr &stm, uint32_t target_address, const uint8_t *code, uint32_t code_size) { static constexpr uint32_t BUFFER_SIZE = 256; @@ -893,7 +895,7 @@ static stm32_err_t stm32_run_raw_code(const stm32_t *stm, uint32_t target_addres return stm32_go(stm, target_address); } -stm32_err_t stm32_go(const stm32_t *stm, const uint32_t address) { +stm32_err_t stm32_go(const stm32_unique_ptr &stm, const uint32_t address) { auto *const stream = stm->stream; if (stm->cmd->go == STM32_CMD_ERR) { @@ -916,7 +918,7 @@ stm32_err_t stm32_go(const stm32_t *stm, const uint32_t address) { return STM32_ERR_OK; } -stm32_err_t stm32_reset_device(const stm32_t *stm) { +stm32_err_t stm32_reset_device(const stm32_unique_ptr &stm) { const auto target_address = stm->dev->ram_start; if (stm->dev->flags & F_OBLL) { @@ -927,7 +929,8 @@ stm32_err_t stm32_reset_device(const stm32_t *stm) { } } -stm32_err_t stm32_crc_memory(const stm32_t *stm, const uint32_t address, const uint32_t length, uint32_t *const crc) { +stm32_err_t stm32_crc_memory(const stm32_unique_ptr &stm, const uint32_t address, const uint32_t length, + uint32_t *const crc) { static constexpr auto BUFFER_SIZE = 5; auto *const stream = stm->stream; @@ -1022,7 +1025,7 @@ uint32_t stm32_sw_crc(uint32_t crc, uint8_t *buf, unsigned int len) { return crc; } -stm32_err_t stm32_crc_wrapper(const stm32_t *stm, uint32_t address, uint32_t length, uint32_t *crc) { +stm32_err_t stm32_crc_wrapper(const stm32_unique_ptr &stm, uint32_t address, uint32_t length, uint32_t *crc) { static constexpr uint32_t CRC_INIT_VALUE = 0xFFFFFFFF; static constexpr uint32_t BUFFER_SIZE = 256; diff --git a/esphome/components/shelly_dimmer/stm32flash.h b/esphome/components/shelly_dimmer/stm32flash.h index c561375c38..d973b35222 100644 --- a/esphome/components/shelly_dimmer/stm32flash.h +++ b/esphome/components/shelly_dimmer/stm32flash.h @@ -23,6 +23,7 @@ #ifdef USE_SHD_FIRMWARE_DATA #include +#include #include "esphome/components/uart/uart.h" namespace esphome { @@ -108,19 +109,20 @@ struct VarlenCmd { uint8_t length; }; -stm32_t *stm32_init(uart::UARTDevice *stream, uint8_t flags, char init); -void stm32_close(stm32_t *stm); -stm32_err_t stm32_read_memory(const stm32_t *stm, uint32_t address, uint8_t *data, unsigned int len); -stm32_err_t stm32_write_memory(const stm32_t *stm, uint32_t address, const uint8_t *data, unsigned int len); -stm32_err_t stm32_wunprot_memory(const stm32_t *stm); -stm32_err_t stm32_wprot_memory(const stm32_t *stm); -stm32_err_t stm32_erase_memory(const stm32_t *stm, uint32_t spage, uint32_t pages); -stm32_err_t stm32_go(const stm32_t *stm, uint32_t address); -stm32_err_t stm32_reset_device(const stm32_t *stm); -stm32_err_t stm32_readprot_memory(const stm32_t *stm); -stm32_err_t stm32_runprot_memory(const stm32_t *stm); -stm32_err_t stm32_crc_memory(const stm32_t *stm, uint32_t address, uint32_t length, uint32_t *crc); -stm32_err_t stm32_crc_wrapper(const stm32_t *stm, uint32_t address, uint32_t length, uint32_t *crc); +using stm32_unique_ptr = std::unique_ptr; + +stm32_unique_ptr stm32_init(uart::UARTDevice *stream, uint8_t flags, char init); +stm32_err_t stm32_read_memory(const stm32_unique_ptr &stm, uint32_t address, uint8_t *data, unsigned int len); +stm32_err_t stm32_write_memory(const stm32_unique_ptr &stm, uint32_t address, const uint8_t *data, unsigned int len); +stm32_err_t stm32_wunprot_memory(const stm32_unique_ptr &stm); +stm32_err_t stm32_wprot_memory(const stm32_unique_ptr &stm); +stm32_err_t stm32_erase_memory(const stm32_unique_ptr &stm, uint32_t spage, uint32_t pages); +stm32_err_t stm32_go(const stm32_unique_ptr &stm, uint32_t address); +stm32_err_t stm32_reset_device(const stm32_unique_ptr &stm); +stm32_err_t stm32_readprot_memory(const stm32_unique_ptr &stm); +stm32_err_t stm32_runprot_memory(const stm32_unique_ptr &stm); +stm32_err_t stm32_crc_memory(const stm32_unique_ptr &stm, uint32_t address, uint32_t length, uint32_t *crc); +stm32_err_t stm32_crc_wrapper(const stm32_unique_ptr &stm, uint32_t address, uint32_t length, uint32_t *crc); uint32_t stm32_sw_crc(uint32_t crc, uint8_t *buf, unsigned int len); } // namespace shelly_dimmer From 01c4d3c225fca32bc62a39ec7126637262e8957d Mon Sep 17 00:00:00 2001 From: James Szalay Date: Wed, 11 May 2022 23:26:14 -0400 Subject: [PATCH 0456/1729] Use heat mode for heat. Move EXT HT to custom presets. (#3437) * Use heat mode for heat. Move EXT HT to custom presets. * Fix syntax error. --- esphome/components/bedjet/bedjet.cpp | 6 ++++-- esphome/components/bedjet/bedjet.h | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/esphome/components/bedjet/bedjet.cpp b/esphome/components/bedjet/bedjet.cpp index 1a932da0c5..38ed6206a8 100644 --- a/esphome/components/bedjet/bedjet.cpp +++ b/esphome/components/bedjet/bedjet.cpp @@ -117,7 +117,7 @@ void Bedjet::control(const ClimateCall &call) { pkt = this->codec_->get_button_request(BTN_OFF); break; case climate::CLIMATE_MODE_HEAT: - pkt = this->codec_->get_button_request(BTN_EXTHT); + pkt = this->codec_->get_button_request(BTN_HEAT); break; case climate::CLIMATE_MODE_FAN_ONLY: pkt = this->codec_->get_button_request(BTN_COOL); @@ -137,7 +137,7 @@ void Bedjet::control(const ClimateCall &call) { } else { this->force_refresh_ = true; this->mode = mode; - // We're using (custom) preset for Turbo & M1-3 presets, so changing climate mode will clear those + // We're using (custom) preset for Turbo, EXT HT, & M1-3 presets, so changing climate mode will clear those this->custom_preset.reset(); this->preset.reset(); } @@ -186,6 +186,8 @@ void Bedjet::control(const ClimateCall &call) { pkt = this->codec_->get_button_request(BTN_M2); } else if (preset == "M3") { pkt = this->codec_->get_button_request(BTN_M3); + } else if (preset == "EXT HT") { + pkt = this->codec_->get_button_request(BTN_EXTHT); } else { ESP_LOGW(TAG, "Unsupported preset: %s", preset.c_str()); return; diff --git a/esphome/components/bedjet/bedjet.h b/esphome/components/bedjet/bedjet.h index b061d2b5ec..0565be6045 100644 --- a/esphome/components/bedjet/bedjet.h +++ b/esphome/components/bedjet/bedjet.h @@ -67,6 +67,8 @@ class Bedjet : public climate::Climate, public esphome::ble_client::BLEClientNod // We could fetch biodata from bedjet and set these names that way. // But then we have to invert the lookup in order to send the right preset. // For now, we can leave them as M1-3 to match the remote buttons. + // EXT HT added to match remote button. + "EXT HT", "M1", "M2", "M3", From bcb47c306c996340fd19a2e66cbca5985fe059e2 Mon Sep 17 00:00:00 2001 From: swifty99 Date: Thu, 12 May 2022 06:53:33 +0200 Subject: [PATCH 0457/1729] Tcs34725 automatic sampling settings for improved dynamics and accuracy (#3258) Co-authored-by: Daniel Cousens <413395+dcousens@users.noreply.github.com> Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/tcs34725/sensor.py | 3 +- esphome/components/tcs34725/tcs34725.cpp | 109 ++++++++++++++++++++--- esphome/components/tcs34725/tcs34725.h | 4 +- 3 files changed, 103 insertions(+), 13 deletions(-) diff --git a/esphome/components/tcs34725/sensor.py b/esphome/components/tcs34725/sensor.py index fcc56e395f..d47e9a34c8 100644 --- a/esphome/components/tcs34725/sensor.py +++ b/esphome/components/tcs34725/sensor.py @@ -31,6 +31,7 @@ TCS34725Component = tcs34725_ns.class_( TCS34725IntegrationTime = tcs34725_ns.enum("TCS34725IntegrationTime") TCS34725_INTEGRATION_TIMES = { + "auto": TCS34725IntegrationTime.TCS34725_INTEGRATION_TIME_AUTO, "2.4ms": TCS34725IntegrationTime.TCS34725_INTEGRATION_TIME_2_4MS, "24ms": TCS34725IntegrationTime.TCS34725_INTEGRATION_TIME_24MS, "50ms": TCS34725IntegrationTime.TCS34725_INTEGRATION_TIME_50MS, @@ -88,7 +89,7 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_CLEAR_CHANNEL): color_channel_schema, cv.Optional(CONF_ILLUMINANCE): illuminance_schema, cv.Optional(CONF_COLOR_TEMPERATURE): color_temperature_schema, - cv.Optional(CONF_INTEGRATION_TIME, default="2.4ms"): cv.enum( + cv.Optional(CONF_INTEGRATION_TIME, default="auto"): cv.enum( TCS34725_INTEGRATION_TIMES, lower=True ), cv.Optional(CONF_GAIN, default="1X"): cv.enum(TCS34725_GAINS, upper=True), diff --git a/esphome/components/tcs34725/tcs34725.cpp b/esphome/components/tcs34725/tcs34725.cpp index 825f7da4cc..276bf65ebf 100644 --- a/esphome/components/tcs34725/tcs34725.cpp +++ b/esphome/components/tcs34725/tcs34725.cpp @@ -136,8 +136,14 @@ void TCS34725Component::calculate_temperature_and_lux_(uint16_t r, uint16_t g, u } /* Check for saturation and mark the sample as invalid if true */ if (c >= sat) { - ESP_LOGW(TAG, "Saturation too high, discarding sample with saturation %.1f and clear %d", sat, c); - return; + if (this->integration_time_auto_) { + ESP_LOGI(TAG, "Saturation too high, sample discarded, autogain ongoing"); + } else { + ESP_LOGW( + TAG, + "Saturation too high, sample with saturation %.1f and clear %d treat values carefully or use grey filter", + sat, c); + } } /* AMS RGB sensors have no IR channel, so the IR content must be */ @@ -149,8 +155,14 @@ void TCS34725Component::calculate_temperature_and_lux_(uint16_t r, uint16_t g, u g2 = g - ir; b2 = b - ir; + // discarding super low values? not recemmonded, and avoided by using auto gain. if (r2 == 0) { - return; + // legacy code + if (!this->integration_time_auto_) { + ESP_LOGW(TAG, + "No light detected on red channel, switch to auto gain or adjust timing, values will be unreliable"); + return; + } } // Lux Calculation (DN40 3.2) @@ -189,7 +201,7 @@ void TCS34725Component::update() { this->status_set_warning(); return; } - ESP_LOGV(TAG, "Raw values clear=%x red=%x green=%x blue=%x", raw_c, raw_r, raw_g, raw_b); + ESP_LOGV(TAG, "Raw values clear=%d red=%d green=%d blue=%d", raw_c, raw_r, raw_g, raw_b); float channel_c; float channel_r; @@ -220,20 +232,95 @@ void TCS34725Component::update() { calculate_temperature_and_lux_(raw_r, raw_g, raw_b, raw_c); } - if (this->illuminance_sensor_ != nullptr) - this->illuminance_sensor_->publish_state(this->illuminance_); + // do not publish values if auto gain finding ongoing, and oversaturated + // so: publish when: + // - not auto mode + // - clear not oversaturated + // - clear oversaturated but gain and timing cannot go lower + if (!this->integration_time_auto_ || raw_c < 65530 || (this->gain_reg_ == 0 && this->integration_time_ < 200)) { + if (this->illuminance_sensor_ != nullptr) + this->illuminance_sensor_->publish_state(this->illuminance_); - if (this->color_temperature_sensor_ != nullptr) - this->color_temperature_sensor_->publish_state(this->color_temperature_); + if (this->color_temperature_sensor_ != nullptr) + this->color_temperature_sensor_->publish_state(this->color_temperature_); + } - ESP_LOGD(TAG, "Got Red=%.1f%%,Green=%.1f%%,Blue=%.1f%%,Clear=%.1f%% Illuminance=%.1flx Color Temperature=%.1fK", + ESP_LOGD(TAG, + "Got Red=%.1f%%,Green=%.1f%%,Blue=%.1f%%,Clear=%.1f%% Illuminance=%.1flx Color " + "Temperature=%.1fK", channel_r, channel_g, channel_b, channel_c, this->illuminance_, this->color_temperature_); + if (this->integration_time_auto_) { + // change integration time an gain to achieve maximum resolution an dynamic range + // calculate optimal integration time to achieve 70% satuaration + float integration_time_ideal; + integration_time_ideal = 60 / ((float) raw_c / 655.35) * this->integration_time_; + + uint8_t gain_reg_val_new = this->gain_reg_; + // increase gain if less than 20% of white channel used and high integration time + // increase only if not already maximum + // do not use max gain, as ist will not get better + if (this->gain_reg_ < 3) { + if (((float) raw_c / 655.35 < 20.f) && (this->integration_time_ > 600.f)) { + gain_reg_val_new = this->gain_reg_ + 1; + // update integration time to new situation + integration_time_ideal = integration_time_ideal / 4; + } + } + + // decrease gain, if very high clear values and integration times alreadey low + if (this->gain_reg_ > 0) { + if (70 < ((float) raw_c / 655.35) && (this->integration_time_ < 200)) { + gain_reg_val_new = this->gain_reg_ - 1; + // update integration time to new situation + integration_time_ideal = integration_time_ideal * 4; + } + } + + // saturate integration times + float integration_time_next = integration_time_ideal; + if (integration_time_ideal > 2.4f * 256) { + integration_time_next = 2.4f * 256; + } + if (integration_time_ideal < 154) { + integration_time_next = 154; + } + + // calculate register value from timing + uint8_t regval_atime = (uint8_t)(256.f - integration_time_next / 2.4f); + ESP_LOGD(TAG, "Integration time: %.1fms, ideal: %.1fms regval_new %d Gain: %.f Clear channel raw: %d gain reg: %d", + this->integration_time_, integration_time_next, regval_atime, this->gain_, raw_c, this->gain_reg_); + + if (this->integration_reg_ != regval_atime || gain_reg_val_new != this->gain_reg_) { + this->integration_reg_ = regval_atime; + this->gain_reg_ = gain_reg_val_new; + set_gain((TCS34725Gain) gain_reg_val_new); + if (this->write_config_register_(TCS34725_REGISTER_ATIME, this->integration_reg_) != i2c::ERROR_OK || + this->write_config_register_(TCS34725_REGISTER_CONTROL, this->gain_reg_) != i2c::ERROR_OK) { + this->mark_failed(); + ESP_LOGW(TAG, "TCS34725I update timing failed!"); + } else { + this->integration_time_ = integration_time_next; + } + } + } this->status_clear_warning(); } void TCS34725Component::set_integration_time(TCS34725IntegrationTime integration_time) { - this->integration_reg_ = integration_time; - this->integration_time_ = (256.f - integration_time) * 2.4f; + // if an integration time is 0x100, this is auto start with 154ms as this gives best starting point + TCS34725IntegrationTime my_integration_time_regval; + + if (integration_time == TCS34725_INTEGRATION_TIME_AUTO) { + this->integration_time_auto_ = true; + this->integration_reg_ = TCS34725_INTEGRATION_TIME_154MS; + my_integration_time_regval = TCS34725_INTEGRATION_TIME_154MS; + } else { + this->integration_reg_ = integration_time; + my_integration_time_regval = integration_time; + this->integration_time_auto_ = false; + } + this->integration_time_ = (256.f - my_integration_time_regval) * 2.4f; + ESP_LOGI(TAG, "TCS34725I Integration time set to: %.1fms", this->integration_time_); } void TCS34725Component::set_gain(TCS34725Gain gain) { this->gain_reg_ = gain; diff --git a/esphome/components/tcs34725/tcs34725.h b/esphome/components/tcs34725/tcs34725.h index 04565d948e..23985e8221 100644 --- a/esphome/components/tcs34725/tcs34725.h +++ b/esphome/components/tcs34725/tcs34725.h @@ -26,6 +26,7 @@ enum TCS34725IntegrationTime { TCS34725_INTEGRATION_TIME_540MS = 0x1F, TCS34725_INTEGRATION_TIME_600MS = 0x06, TCS34725_INTEGRATION_TIME_614MS = 0x00, + TCS34725_INTEGRATION_TIME_AUTO = 0x100, }; enum TCS34725Gain { @@ -77,10 +78,11 @@ class TCS34725Component : public PollingComponent, public i2c::I2CDevice { float glass_attenuation_{1.0}; float illuminance_; float color_temperature_; + bool integration_time_auto_{true}; private: void calculate_temperature_and_lux_(uint16_t r, uint16_t g, uint16_t b, uint16_t c); - uint8_t integration_reg_{TCS34725_INTEGRATION_TIME_2_4MS}; + uint16_t integration_reg_; uint8_t gain_reg_{TCS34725_GAIN_1X}; }; From 1c873e003431562737d45271ab2fc5fe3409f5b8 Mon Sep 17 00:00:00 2001 From: Michael Davidson Date: Thu, 12 May 2022 14:54:45 +1000 Subject: [PATCH 0458/1729] Make custom_fan and custom_preset templatable as per documentation (#3330) --- esphome/components/climate/__init__.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py index 87b9a4b3e2..1de9aa3f3a 100644 --- a/esphome/components/climate/__init__.py +++ b/esphome/components/climate/__init__.py @@ -287,9 +287,11 @@ CLIMATE_CONTROL_ACTION_SCHEMA = cv.Schema( cv.Exclusive(CONF_FAN_MODE, "fan_mode"): cv.templatable( validate_climate_fan_mode ), - cv.Exclusive(CONF_CUSTOM_FAN_MODE, "fan_mode"): cv.string_strict, + cv.Exclusive(CONF_CUSTOM_FAN_MODE, "fan_mode"): cv.templatable( + cv.string_strict + ), cv.Exclusive(CONF_PRESET, "preset"): cv.templatable(validate_climate_preset), - cv.Exclusive(CONF_CUSTOM_PRESET, "preset"): cv.string_strict, + cv.Exclusive(CONF_CUSTOM_PRESET, "preset"): cv.templatable(cv.string_strict), cv.Optional(CONF_SWING_MODE): cv.templatable(validate_climate_swing_mode), } ) @@ -324,13 +326,17 @@ async def climate_control_to_code(config, action_id, template_arg, args): template_ = await cg.templatable(config[CONF_FAN_MODE], args, ClimateFanMode) cg.add(var.set_fan_mode(template_)) if CONF_CUSTOM_FAN_MODE in config: - template_ = await cg.templatable(config[CONF_CUSTOM_FAN_MODE], args, str) + template_ = await cg.templatable( + config[CONF_CUSTOM_FAN_MODE], args, cg.std_string + ) cg.add(var.set_custom_fan_mode(template_)) if CONF_PRESET in config: template_ = await cg.templatable(config[CONF_PRESET], args, ClimatePreset) cg.add(var.set_preset(template_)) if CONF_CUSTOM_PRESET in config: - template_ = await cg.templatable(config[CONF_CUSTOM_PRESET], args, str) + template_ = await cg.templatable( + config[CONF_CUSTOM_PRESET], args, cg.std_string + ) cg.add(var.set_custom_preset(template_)) if CONF_SWING_MODE in config: template_ = await cg.templatable( From 03d5a0ec1d1d251db9ae8871a80e6960591827fc Mon Sep 17 00:00:00 2001 From: Brian Kaufman Date: Wed, 11 May 2022 21:57:50 -0700 Subject: [PATCH 0459/1729] Update captive portal canHandle function (#3360) --- esphome/components/captive_portal/captive_portal.h | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/esphome/components/captive_portal/captive_portal.h b/esphome/components/captive_portal/captive_portal.h index 0e68bc9cef..c2aada171f 100644 --- a/esphome/components/captive_portal/captive_portal.h +++ b/esphome/components/captive_portal/captive_portal.h @@ -39,17 +39,7 @@ class CaptivePortal : public AsyncWebHandler, public Component { if (request->method() == HTTP_GET) { if (request->url() == "/") return true; - if (request->url() == "/stylesheet.css") - return true; - if (request->url() == "/wifi-strength-1.svg") - return true; - if (request->url() == "/wifi-strength-2.svg") - return true; - if (request->url() == "/wifi-strength-3.svg") - return true; - if (request->url() == "/wifi-strength-4.svg") - return true; - if (request->url() == "/lock.svg") + if (request->url() == "/config.json") return true; if (request->url() == "/wifisave") return true; From 63096ac2bcc03c48e7a076e8efaa8ad19411b828 Mon Sep 17 00:00:00 2001 From: Maurice Makaay Date: Wed, 11 May 2022 23:25:00 +0200 Subject: [PATCH 0460/1729] On epoch sync, restore local TZ (#3462) Co-authored-by: Maurice Makaay --- esphome/components/time/real_time_clock.cpp | 12 ++++++++++-- esphome/components/time/real_time_clock.h | 1 + 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/esphome/components/time/real_time_clock.cpp b/esphome/components/time/real_time_clock.cpp index 36c5f4161d..7b5f0aa49b 100644 --- a/esphome/components/time/real_time_clock.cpp +++ b/esphome/components/time/real_time_clock.cpp @@ -13,11 +13,11 @@ static const char *const TAG = "time"; RealTimeClock::RealTimeClock() = default; void RealTimeClock::call_setup() { - setenv("TZ", this->timezone_.c_str(), 1); - tzset(); + this->apply_timezone_(); PollingComponent::call_setup(); } void RealTimeClock::synchronize_epoch_(uint32_t epoch) { + // Update UTC epoch time. struct timeval timev { .tv_sec = static_cast(epoch), .tv_usec = 0, }; @@ -30,6 +30,9 @@ void RealTimeClock::synchronize_epoch_(uint32_t epoch) { ret = settimeofday(&timev, nullptr); } + // Move timezone back to local timezone. + this->apply_timezone_(); + if (ret != 0) { ESP_LOGW(TAG, "setimeofday() failed with code %d", ret); } @@ -41,6 +44,11 @@ void RealTimeClock::synchronize_epoch_(uint32_t epoch) { this->time_sync_callback_.call(); } +void RealTimeClock::apply_timezone_() { + setenv("TZ", this->timezone_.c_str(), 1); + tzset(); +} + size_t ESPTime::strftime(char *buffer, size_t buffer_len, const char *format) { struct tm c_tm = this->to_c_tm(); return ::strftime(buffer, buffer_len, format, &c_tm); diff --git a/esphome/components/time/real_time_clock.h b/esphome/components/time/real_time_clock.h index b22c6f04d7..7f4afee306 100644 --- a/esphome/components/time/real_time_clock.h +++ b/esphome/components/time/real_time_clock.h @@ -137,6 +137,7 @@ class RealTimeClock : public PollingComponent { void synchronize_epoch_(uint32_t epoch); std::string timezone_{}; + void apply_timezone_(); CallbackManager time_sync_callback_; }; From 40f622949e2b2d3b21ebb419324c70cf9605cc30 Mon Sep 17 00:00:00 2001 From: Niclas Larsson Date: Thu, 12 May 2022 00:26:51 +0200 Subject: [PATCH 0461/1729] Shelly dimmer: Use unique_ptr to handle the lifetime of stm32_t (#3400) Co-authored-by: Martin <25747549+martgras@users.noreply.github.com> Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- .../shelly_dimmer/shelly_dimmer.cpp | 9 +- .../components/shelly_dimmer/stm32flash.cpp | 119 +++++++++--------- esphome/components/shelly_dimmer/stm32flash.h | 28 +++-- 3 files changed, 79 insertions(+), 77 deletions(-) diff --git a/esphome/components/shelly_dimmer/shelly_dimmer.cpp b/esphome/components/shelly_dimmer/shelly_dimmer.cpp index 3b79d0bf57..32c556da5e 100644 --- a/esphome/components/shelly_dimmer/shelly_dimmer.cpp +++ b/esphome/components/shelly_dimmer/shelly_dimmer.cpp @@ -158,11 +158,8 @@ bool ShellyDimmer::upgrade_firmware_() { ESP_LOGW(TAG, "Starting STM32 firmware upgrade"); this->reset_dfu_boot_(); - // Could be constexpr in c++17 - static const auto CLOSE = [](stm32_t *stm32) { stm32_close(stm32); }; - // Cleanup with RAII - std::unique_ptr stm32{stm32_init(this, STREAM_SERIAL, 1), CLOSE}; + auto stm32 = stm32_init(this, STREAM_SERIAL, 1); if (!stm32) { ESP_LOGW(TAG, "Failed to initialize STM32"); @@ -170,7 +167,7 @@ bool ShellyDimmer::upgrade_firmware_() { } // Erase STM32 flash. - if (stm32_erase_memory(stm32.get(), 0, STM32_MASS_ERASE) != STM32_ERR_OK) { + if (stm32_erase_memory(stm32, 0, STM32_MASS_ERASE) != STM32_ERR_OK) { ESP_LOGW(TAG, "Failed to erase STM32 flash memory"); return false; } @@ -196,7 +193,7 @@ bool ShellyDimmer::upgrade_firmware_() { std::memcpy(buffer, p, BUFFER_SIZE); p += BUFFER_SIZE; - if (stm32_write_memory(stm32.get(), addr, buffer, len) != STM32_ERR_OK) { + if (stm32_write_memory(stm32, addr, buffer, len) != STM32_ERR_OK) { ESP_LOGW(TAG, "Failed to write to STM32 flash memory"); return false; } diff --git a/esphome/components/shelly_dimmer/stm32flash.cpp b/esphome/components/shelly_dimmer/stm32flash.cpp index 4c777776fb..e688f2de36 100644 --- a/esphome/components/shelly_dimmer/stm32flash.cpp +++ b/esphome/components/shelly_dimmer/stm32flash.cpp @@ -117,7 +117,7 @@ namespace shelly_dimmer { namespace { -int flash_addr_to_page_ceil(const stm32_t *stm, uint32_t addr) { +int flash_addr_to_page_ceil(const stm32_unique_ptr &stm, uint32_t addr) { if (!(addr >= stm->dev->fl_start && addr <= stm->dev->fl_end)) return 0; @@ -135,7 +135,7 @@ int flash_addr_to_page_ceil(const stm32_t *stm, uint32_t addr) { return addr ? page + 1 : page; } -stm32_err_t stm32_get_ack_timeout(const stm32_t *stm, uint32_t timeout) { +stm32_err_t stm32_get_ack_timeout(const stm32_unique_ptr &stm, uint32_t timeout) { auto *stream = stm->stream; uint8_t rxbyte; @@ -168,9 +168,9 @@ stm32_err_t stm32_get_ack_timeout(const stm32_t *stm, uint32_t timeout) { } while (true); } -stm32_err_t stm32_get_ack(const stm32_t *stm) { return stm32_get_ack_timeout(stm, 0); } +stm32_err_t stm32_get_ack(const stm32_unique_ptr &stm) { return stm32_get_ack_timeout(stm, 0); } -stm32_err_t stm32_send_command_timeout(const stm32_t *stm, const uint8_t cmd, const uint32_t timeout) { +stm32_err_t stm32_send_command_timeout(const stm32_unique_ptr &stm, const uint8_t cmd, const uint32_t timeout) { auto *const stream = stm->stream; static constexpr auto BUFFER_SIZE = 2; @@ -194,12 +194,12 @@ stm32_err_t stm32_send_command_timeout(const stm32_t *stm, const uint8_t cmd, co return STM32_ERR_UNKNOWN; } -stm32_err_t stm32_send_command(const stm32_t *stm, const uint8_t cmd) { +stm32_err_t stm32_send_command(const stm32_unique_ptr &stm, const uint8_t cmd) { return stm32_send_command_timeout(stm, cmd, 0); } /* if we have lost sync, send a wrong command and expect a NACK */ -stm32_err_t stm32_resync(const stm32_t *stm) { +stm32_err_t stm32_resync(const stm32_unique_ptr &stm) { auto *const stream = stm->stream; uint32_t t0 = millis(); auto t1 = t0; @@ -238,7 +238,7 @@ stm32_err_t stm32_resync(const stm32_t *stm) { * * len is value of the first byte in the frame. */ -stm32_err_t stm32_guess_len_cmd(const stm32_t *stm, const uint8_t cmd, uint8_t *const data, unsigned int len) { +stm32_err_t stm32_guess_len_cmd(const stm32_unique_ptr &stm, const uint8_t cmd, uint8_t *const data, unsigned int len) { auto *const stream = stm->stream; if (stm32_send_command(stm, cmd) != STM32_ERR_OK) @@ -286,7 +286,7 @@ stm32_err_t stm32_guess_len_cmd(const stm32_t *stm, const uint8_t cmd, uint8_t * * This function sends the init sequence and, in case of timeout, recovers * the interface. */ -stm32_err_t stm32_send_init_seq(const stm32_t *stm) { +stm32_err_t stm32_send_init_seq(const stm32_unique_ptr &stm) { auto *const stream = stm->stream; stream->write_array(&STM32_CMD_INIT, 1); @@ -320,7 +320,7 @@ stm32_err_t stm32_send_init_seq(const stm32_t *stm) { return STM32_ERR_UNKNOWN; } -stm32_err_t stm32_mass_erase(const stm32_t *stm) { +stm32_err_t stm32_mass_erase(const stm32_unique_ptr &stm) { auto *const stream = stm->stream; if (stm32_send_command(stm, stm->cmd->er) != STM32_ERR_OK) { @@ -364,7 +364,7 @@ template std::unique_ptr malloc_array_raii DELETOR}; } -stm32_err_t stm32_pages_erase(const stm32_t *stm, const uint32_t spage, const uint32_t pages) { +stm32_err_t stm32_pages_erase(const stm32_unique_ptr &stm, const uint32_t spage, const uint32_t pages) { auto *const stream = stm->stream; uint8_t cs = 0; int i = 0; @@ -474,6 +474,18 @@ template void populate_buffer_with_address(uint8_t (&buffer)[N], uint3 buffer[4] = static_cast(buffer[0] ^ buffer[1] ^ buffer[2] ^ buffer[3]); } +template stm32_unique_ptr make_stm32_with_deletor(T ptr) { + static const auto CLOSE = [](stm32_t *stm32) { + if (stm32) { + free(stm32->cmd); // NOLINT + } + free(stm32); // NOLINT + }; + + // Cleanup with RAII + return std::unique_ptr{ptr, CLOSE}; +} + } // Anonymous namespace } // namespace shelly_dimmer @@ -485,48 +497,44 @@ namespace shelly_dimmer { /* find newer command by higher code */ #define newer(prev, a) (((prev) == STM32_CMD_ERR) ? (a) : (((prev) > (a)) ? (prev) : (a))) -stm32_t *stm32_init(uart::UARTDevice *stream, const uint8_t flags, const char init) { +stm32_unique_ptr stm32_init(uart::UARTDevice *stream, const uint8_t flags, const char init) { uint8_t buf[257]; - // Could be constexpr in c++17 - static const auto CLOSE = [](stm32_t *stm32) { stm32_close(stm32); }; - - // Cleanup with RAII - std::unique_ptr stm{static_cast(calloc(sizeof(stm32_t), 1)), // NOLINT - CLOSE}; + auto stm = make_stm32_with_deletor(static_cast(calloc(sizeof(stm32_t), 1))); // NOLINT if (!stm) { - return nullptr; + return make_stm32_with_deletor(nullptr); } stm->stream = stream; stm->flags = flags; stm->cmd = static_cast(malloc(sizeof(stm32_cmd_t))); // NOLINT if (!stm->cmd) { - return nullptr; + return make_stm32_with_deletor(nullptr); } memset(stm->cmd, STM32_CMD_ERR, sizeof(stm32_cmd_t)); if ((stm->flags & STREAM_OPT_CMD_INIT) && init) { - if (stm32_send_init_seq(stm.get()) != STM32_ERR_OK) - return nullptr; // NOLINT + if (stm32_send_init_seq(stm) != STM32_ERR_OK) + return make_stm32_with_deletor(nullptr); } /* get the version and read protection status */ - if (stm32_send_command(stm.get(), STM32_CMD_GVR) != STM32_ERR_OK) { - return nullptr; // NOLINT + if (stm32_send_command(stm, STM32_CMD_GVR) != STM32_ERR_OK) { + return make_stm32_with_deletor(nullptr); } /* From AN, only UART bootloader returns 3 bytes */ { const auto len = (stm->flags & STREAM_OPT_GVR_ETX) ? 3 : 1; if (!stream->read_array(buf, len)) - return nullptr; // NOLINT + return make_stm32_with_deletor(nullptr); + stm->version = buf[0]; stm->option1 = (stm->flags & STREAM_OPT_GVR_ETX) ? buf[1] : 0; stm->option2 = (stm->flags & STREAM_OPT_GVR_ETX) ? buf[2] : 0; - if (stm32_get_ack(stm.get()) != STM32_ERR_OK) { - return nullptr; + if (stm32_get_ack(stm) != STM32_ERR_OK) { + return make_stm32_with_deletor(nullptr); } } @@ -544,8 +552,8 @@ stm32_t *stm32_init(uart::UARTDevice *stream, const uint8_t flags, const char in return STM32_CMD_GET_LENGTH; })(); - if (stm32_guess_len_cmd(stm.get(), STM32_CMD_GET, buf, len) != STM32_ERR_OK) - return nullptr; + if (stm32_guess_len_cmd(stm, STM32_CMD_GET, buf, len) != STM32_ERR_OK) + return make_stm32_with_deletor(nullptr); } const auto stop = buf[0] + 1; @@ -607,23 +615,23 @@ stm32_t *stm32_init(uart::UARTDevice *stream, const uint8_t flags, const char in } if (new_cmds) ESP_LOGD(TAG, ")"); - if (stm32_get_ack(stm.get()) != STM32_ERR_OK) { - return nullptr; + if (stm32_get_ack(stm) != STM32_ERR_OK) { + return make_stm32_with_deletor(nullptr); } if (stm->cmd->get == STM32_CMD_ERR || stm->cmd->gvr == STM32_CMD_ERR || stm->cmd->gid == STM32_CMD_ERR) { ESP_LOGD(TAG, "Error: bootloader did not returned correct information from GET command"); - return nullptr; + return make_stm32_with_deletor(nullptr); } /* get the device ID */ - if (stm32_guess_len_cmd(stm.get(), stm->cmd->gid, buf, 1) != STM32_ERR_OK) { - return nullptr; + if (stm32_guess_len_cmd(stm, stm->cmd->gid, buf, 1) != STM32_ERR_OK) { + return make_stm32_with_deletor(nullptr); } const auto returned = buf[0] + 1; if (returned < 2) { ESP_LOGD(TAG, "Only %d bytes sent in the PID, unknown/unsupported device", returned); - return nullptr; + return make_stm32_with_deletor(nullptr); } stm->pid = (buf[1] << 8) | buf[2]; if (returned > 2) { @@ -631,8 +639,8 @@ stm32_t *stm32_init(uart::UARTDevice *stream, const uint8_t flags, const char in for (auto i = 2; i <= returned; i++) ESP_LOGD(TAG, " %02x", buf[i]); } - if (stm32_get_ack(stm.get()) != STM32_ERR_OK) { - return nullptr; + if (stm32_get_ack(stm) != STM32_ERR_OK) { + return make_stm32_with_deletor(nullptr); } stm->dev = DEVICES; @@ -641,21 +649,14 @@ stm32_t *stm32_init(uart::UARTDevice *stream, const uint8_t flags, const char in if (!stm->dev->id) { ESP_LOGD(TAG, "Unknown/unsupported device (Device ID: 0x%03x)", stm->pid); - return nullptr; + return make_stm32_with_deletor(nullptr); } - // TODO: Would be much better if the unique_ptr was returned from this function - // Release ownership of unique_ptr - return stm.release(); // NOLINT + return stm; } -void stm32_close(stm32_t *stm) { - if (stm) - free(stm->cmd); // NOLINT - free(stm); // NOLINT -} - -stm32_err_t stm32_read_memory(const stm32_t *stm, const uint32_t address, uint8_t *data, const unsigned int len) { +stm32_err_t stm32_read_memory(const stm32_unique_ptr &stm, const uint32_t address, uint8_t *data, + const unsigned int len) { auto *const stream = stm->stream; if (!len) @@ -693,7 +694,8 @@ stm32_err_t stm32_read_memory(const stm32_t *stm, const uint32_t address, uint8_ return STM32_ERR_OK; } -stm32_err_t stm32_write_memory(const stm32_t *stm, uint32_t address, const uint8_t *data, const unsigned int len) { +stm32_err_t stm32_write_memory(const stm32_unique_ptr &stm, uint32_t address, const uint8_t *data, + const unsigned int len) { auto *const stream = stm->stream; if (!len) @@ -753,7 +755,7 @@ stm32_err_t stm32_write_memory(const stm32_t *stm, uint32_t address, const uint8 return STM32_ERR_OK; } -stm32_err_t stm32_wunprot_memory(const stm32_t *stm) { +stm32_err_t stm32_wunprot_memory(const stm32_unique_ptr &stm) { if (stm->cmd->uw == STM32_CMD_ERR) { ESP_LOGD(TAG, "Error: WRITE UNPROTECT command not implemented in bootloader."); return STM32_ERR_NO_CMD; @@ -766,7 +768,7 @@ stm32_err_t stm32_wunprot_memory(const stm32_t *stm) { []() { ESP_LOGD(TAG, "Error: Failed to WRITE UNPROTECT"); }); } -stm32_err_t stm32_wprot_memory(const stm32_t *stm) { +stm32_err_t stm32_wprot_memory(const stm32_unique_ptr &stm) { if (stm->cmd->wp == STM32_CMD_ERR) { ESP_LOGD(TAG, "Error: WRITE PROTECT command not implemented in bootloader."); return STM32_ERR_NO_CMD; @@ -779,7 +781,7 @@ stm32_err_t stm32_wprot_memory(const stm32_t *stm) { []() { ESP_LOGD(TAG, "Error: Failed to WRITE PROTECT"); }); } -stm32_err_t stm32_runprot_memory(const stm32_t *stm) { +stm32_err_t stm32_runprot_memory(const stm32_unique_ptr &stm) { if (stm->cmd->ur == STM32_CMD_ERR) { ESP_LOGD(TAG, "Error: READOUT UNPROTECT command not implemented in bootloader."); return STM32_ERR_NO_CMD; @@ -792,7 +794,7 @@ stm32_err_t stm32_runprot_memory(const stm32_t *stm) { []() { ESP_LOGD(TAG, "Error: Failed to READOUT UNPROTECT"); }); } -stm32_err_t stm32_readprot_memory(const stm32_t *stm) { +stm32_err_t stm32_readprot_memory(const stm32_unique_ptr &stm) { if (stm->cmd->rp == STM32_CMD_ERR) { ESP_LOGD(TAG, "Error: READOUT PROTECT command not implemented in bootloader."); return STM32_ERR_NO_CMD; @@ -805,7 +807,7 @@ stm32_err_t stm32_readprot_memory(const stm32_t *stm) { []() { ESP_LOGD(TAG, "Error: Failed to READOUT PROTECT"); }); } -stm32_err_t stm32_erase_memory(const stm32_t *stm, uint32_t spage, uint32_t pages) { +stm32_err_t stm32_erase_memory(const stm32_unique_ptr &stm, uint32_t spage, uint32_t pages) { if (!pages || spage > STM32_MAX_PAGES || ((pages != STM32_MASS_ERASE) && ((spage + pages) > STM32_MAX_PAGES))) return STM32_ERR_OK; @@ -847,7 +849,7 @@ stm32_err_t stm32_erase_memory(const stm32_t *stm, uint32_t spage, uint32_t page return STM32_ERR_OK; } -static stm32_err_t stm32_run_raw_code(const stm32_t *stm, uint32_t target_address, const uint8_t *code, +static stm32_err_t stm32_run_raw_code(const stm32_unique_ptr &stm, uint32_t target_address, const uint8_t *code, uint32_t code_size) { static constexpr uint32_t BUFFER_SIZE = 256; @@ -893,7 +895,7 @@ static stm32_err_t stm32_run_raw_code(const stm32_t *stm, uint32_t target_addres return stm32_go(stm, target_address); } -stm32_err_t stm32_go(const stm32_t *stm, const uint32_t address) { +stm32_err_t stm32_go(const stm32_unique_ptr &stm, const uint32_t address) { auto *const stream = stm->stream; if (stm->cmd->go == STM32_CMD_ERR) { @@ -916,7 +918,7 @@ stm32_err_t stm32_go(const stm32_t *stm, const uint32_t address) { return STM32_ERR_OK; } -stm32_err_t stm32_reset_device(const stm32_t *stm) { +stm32_err_t stm32_reset_device(const stm32_unique_ptr &stm) { const auto target_address = stm->dev->ram_start; if (stm->dev->flags & F_OBLL) { @@ -927,7 +929,8 @@ stm32_err_t stm32_reset_device(const stm32_t *stm) { } } -stm32_err_t stm32_crc_memory(const stm32_t *stm, const uint32_t address, const uint32_t length, uint32_t *const crc) { +stm32_err_t stm32_crc_memory(const stm32_unique_ptr &stm, const uint32_t address, const uint32_t length, + uint32_t *const crc) { static constexpr auto BUFFER_SIZE = 5; auto *const stream = stm->stream; @@ -1022,7 +1025,7 @@ uint32_t stm32_sw_crc(uint32_t crc, uint8_t *buf, unsigned int len) { return crc; } -stm32_err_t stm32_crc_wrapper(const stm32_t *stm, uint32_t address, uint32_t length, uint32_t *crc) { +stm32_err_t stm32_crc_wrapper(const stm32_unique_ptr &stm, uint32_t address, uint32_t length, uint32_t *crc) { static constexpr uint32_t CRC_INIT_VALUE = 0xFFFFFFFF; static constexpr uint32_t BUFFER_SIZE = 256; diff --git a/esphome/components/shelly_dimmer/stm32flash.h b/esphome/components/shelly_dimmer/stm32flash.h index c561375c38..d973b35222 100644 --- a/esphome/components/shelly_dimmer/stm32flash.h +++ b/esphome/components/shelly_dimmer/stm32flash.h @@ -23,6 +23,7 @@ #ifdef USE_SHD_FIRMWARE_DATA #include +#include #include "esphome/components/uart/uart.h" namespace esphome { @@ -108,19 +109,20 @@ struct VarlenCmd { uint8_t length; }; -stm32_t *stm32_init(uart::UARTDevice *stream, uint8_t flags, char init); -void stm32_close(stm32_t *stm); -stm32_err_t stm32_read_memory(const stm32_t *stm, uint32_t address, uint8_t *data, unsigned int len); -stm32_err_t stm32_write_memory(const stm32_t *stm, uint32_t address, const uint8_t *data, unsigned int len); -stm32_err_t stm32_wunprot_memory(const stm32_t *stm); -stm32_err_t stm32_wprot_memory(const stm32_t *stm); -stm32_err_t stm32_erase_memory(const stm32_t *stm, uint32_t spage, uint32_t pages); -stm32_err_t stm32_go(const stm32_t *stm, uint32_t address); -stm32_err_t stm32_reset_device(const stm32_t *stm); -stm32_err_t stm32_readprot_memory(const stm32_t *stm); -stm32_err_t stm32_runprot_memory(const stm32_t *stm); -stm32_err_t stm32_crc_memory(const stm32_t *stm, uint32_t address, uint32_t length, uint32_t *crc); -stm32_err_t stm32_crc_wrapper(const stm32_t *stm, uint32_t address, uint32_t length, uint32_t *crc); +using stm32_unique_ptr = std::unique_ptr; + +stm32_unique_ptr stm32_init(uart::UARTDevice *stream, uint8_t flags, char init); +stm32_err_t stm32_read_memory(const stm32_unique_ptr &stm, uint32_t address, uint8_t *data, unsigned int len); +stm32_err_t stm32_write_memory(const stm32_unique_ptr &stm, uint32_t address, const uint8_t *data, unsigned int len); +stm32_err_t stm32_wunprot_memory(const stm32_unique_ptr &stm); +stm32_err_t stm32_wprot_memory(const stm32_unique_ptr &stm); +stm32_err_t stm32_erase_memory(const stm32_unique_ptr &stm, uint32_t spage, uint32_t pages); +stm32_err_t stm32_go(const stm32_unique_ptr &stm, uint32_t address); +stm32_err_t stm32_reset_device(const stm32_unique_ptr &stm); +stm32_err_t stm32_readprot_memory(const stm32_unique_ptr &stm); +stm32_err_t stm32_runprot_memory(const stm32_unique_ptr &stm); +stm32_err_t stm32_crc_memory(const stm32_unique_ptr &stm, uint32_t address, uint32_t length, uint32_t *crc); +stm32_err_t stm32_crc_wrapper(const stm32_unique_ptr &stm, uint32_t address, uint32_t length, uint32_t *crc); uint32_t stm32_sw_crc(uint32_t crc, uint8_t *buf, unsigned int len); } // namespace shelly_dimmer From c1480029fb1cb94c2aa445d33247c4e49e18cf23 Mon Sep 17 00:00:00 2001 From: James Szalay Date: Wed, 11 May 2022 23:26:14 -0400 Subject: [PATCH 0462/1729] Use heat mode for heat. Move EXT HT to custom presets. (#3437) * Use heat mode for heat. Move EXT HT to custom presets. * Fix syntax error. --- esphome/components/bedjet/bedjet.cpp | 6 ++++-- esphome/components/bedjet/bedjet.h | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/esphome/components/bedjet/bedjet.cpp b/esphome/components/bedjet/bedjet.cpp index 1a932da0c5..38ed6206a8 100644 --- a/esphome/components/bedjet/bedjet.cpp +++ b/esphome/components/bedjet/bedjet.cpp @@ -117,7 +117,7 @@ void Bedjet::control(const ClimateCall &call) { pkt = this->codec_->get_button_request(BTN_OFF); break; case climate::CLIMATE_MODE_HEAT: - pkt = this->codec_->get_button_request(BTN_EXTHT); + pkt = this->codec_->get_button_request(BTN_HEAT); break; case climate::CLIMATE_MODE_FAN_ONLY: pkt = this->codec_->get_button_request(BTN_COOL); @@ -137,7 +137,7 @@ void Bedjet::control(const ClimateCall &call) { } else { this->force_refresh_ = true; this->mode = mode; - // We're using (custom) preset for Turbo & M1-3 presets, so changing climate mode will clear those + // We're using (custom) preset for Turbo, EXT HT, & M1-3 presets, so changing climate mode will clear those this->custom_preset.reset(); this->preset.reset(); } @@ -186,6 +186,8 @@ void Bedjet::control(const ClimateCall &call) { pkt = this->codec_->get_button_request(BTN_M2); } else if (preset == "M3") { pkt = this->codec_->get_button_request(BTN_M3); + } else if (preset == "EXT HT") { + pkt = this->codec_->get_button_request(BTN_EXTHT); } else { ESP_LOGW(TAG, "Unsupported preset: %s", preset.c_str()); return; diff --git a/esphome/components/bedjet/bedjet.h b/esphome/components/bedjet/bedjet.h index b061d2b5ec..0565be6045 100644 --- a/esphome/components/bedjet/bedjet.h +++ b/esphome/components/bedjet/bedjet.h @@ -67,6 +67,8 @@ class Bedjet : public climate::Climate, public esphome::ble_client::BLEClientNod // We could fetch biodata from bedjet and set these names that way. // But then we have to invert the lookup in order to send the right preset. // For now, we can leave them as M1-3 to match the remote buttons. + // EXT HT added to match remote button. + "EXT HT", "M1", "M2", "M3", From e914828add0d6e41ac22c8d671184a73294560a8 Mon Sep 17 00:00:00 2001 From: Michael Davidson Date: Thu, 12 May 2022 14:54:45 +1000 Subject: [PATCH 0463/1729] Make custom_fan and custom_preset templatable as per documentation (#3330) --- esphome/components/climate/__init__.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py index 87b9a4b3e2..1de9aa3f3a 100644 --- a/esphome/components/climate/__init__.py +++ b/esphome/components/climate/__init__.py @@ -287,9 +287,11 @@ CLIMATE_CONTROL_ACTION_SCHEMA = cv.Schema( cv.Exclusive(CONF_FAN_MODE, "fan_mode"): cv.templatable( validate_climate_fan_mode ), - cv.Exclusive(CONF_CUSTOM_FAN_MODE, "fan_mode"): cv.string_strict, + cv.Exclusive(CONF_CUSTOM_FAN_MODE, "fan_mode"): cv.templatable( + cv.string_strict + ), cv.Exclusive(CONF_PRESET, "preset"): cv.templatable(validate_climate_preset), - cv.Exclusive(CONF_CUSTOM_PRESET, "preset"): cv.string_strict, + cv.Exclusive(CONF_CUSTOM_PRESET, "preset"): cv.templatable(cv.string_strict), cv.Optional(CONF_SWING_MODE): cv.templatable(validate_climate_swing_mode), } ) @@ -324,13 +326,17 @@ async def climate_control_to_code(config, action_id, template_arg, args): template_ = await cg.templatable(config[CONF_FAN_MODE], args, ClimateFanMode) cg.add(var.set_fan_mode(template_)) if CONF_CUSTOM_FAN_MODE in config: - template_ = await cg.templatable(config[CONF_CUSTOM_FAN_MODE], args, str) + template_ = await cg.templatable( + config[CONF_CUSTOM_FAN_MODE], args, cg.std_string + ) cg.add(var.set_custom_fan_mode(template_)) if CONF_PRESET in config: template_ = await cg.templatable(config[CONF_PRESET], args, ClimatePreset) cg.add(var.set_preset(template_)) if CONF_CUSTOM_PRESET in config: - template_ = await cg.templatable(config[CONF_CUSTOM_PRESET], args, str) + template_ = await cg.templatable( + config[CONF_CUSTOM_PRESET], args, cg.std_string + ) cg.add(var.set_custom_preset(template_)) if CONF_SWING_MODE in config: template_ = await cg.templatable( From 28883f711b6aa53db54ab16289de28baea890d15 Mon Sep 17 00:00:00 2001 From: Brian Kaufman Date: Wed, 11 May 2022 21:57:50 -0700 Subject: [PATCH 0464/1729] Update captive portal canHandle function (#3360) --- esphome/components/captive_portal/captive_portal.h | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/esphome/components/captive_portal/captive_portal.h b/esphome/components/captive_portal/captive_portal.h index 0e68bc9cef..c2aada171f 100644 --- a/esphome/components/captive_portal/captive_portal.h +++ b/esphome/components/captive_portal/captive_portal.h @@ -39,17 +39,7 @@ class CaptivePortal : public AsyncWebHandler, public Component { if (request->method() == HTTP_GET) { if (request->url() == "/") return true; - if (request->url() == "/stylesheet.css") - return true; - if (request->url() == "/wifi-strength-1.svg") - return true; - if (request->url() == "/wifi-strength-2.svg") - return true; - if (request->url() == "/wifi-strength-3.svg") - return true; - if (request->url() == "/wifi-strength-4.svg") - return true; - if (request->url() == "/lock.svg") + if (request->url() == "/config.json") return true; if (request->url() == "/wifisave") return true; From 603d0d0c7c1462166bcfeda0fe6cec87b779acf1 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 12 May 2022 17:00:14 +1200 Subject: [PATCH 0465/1729] Bump version to 2022.5.0b2 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index c254edf1ee..7717f709ec 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2022.5.0b1" +__version__ = "2022.5.0b2" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From 39c6c2417ac2b3b20f4dbc2457aa380b330a7fb9 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Thu, 12 May 2022 11:18:51 +0100 Subject: [PATCH 0466/1729] Remove duplicate convert_to_8bit_color_ function. (#2469) Co-authored-by: Oxan van Leeuwen --- .../components/display/display_color_utils.h | 4 ++- .../components/ili9341/ili9341_display.cpp | 30 ++++--------------- esphome/components/ili9341/ili9341_display.h | 2 -- 3 files changed, 8 insertions(+), 28 deletions(-) diff --git a/esphome/components/display/display_color_utils.h b/esphome/components/display/display_color_utils.h index 202de912de..7f29586932 100644 --- a/esphome/components/display/display_color_utils.h +++ b/esphome/components/display/display_color_utils.h @@ -66,6 +66,9 @@ class ColorUtil { } return color_return; } + static inline Color rgb332_to_color(uint8_t rgb332_color) { + return to_color((uint32_t) rgb332_color, COLOR_ORDER_RGB, COLOR_BITNESS_332); + } static uint8_t color_to_332(Color color, ColorOrder color_order = ColorOrder::COLOR_ORDER_RGB) { uint16_t red_color, green_color, blue_color; @@ -100,7 +103,6 @@ class ColorUtil { } return 0; } - static uint32_t color_to_grayscale4(Color color) { uint32_t gs4 = esp_scale8(color.white, 15); return gs4; diff --git a/esphome/components/ili9341/ili9341_display.cpp b/esphome/components/ili9341/ili9341_display.cpp index a24f0bbb64..09524ba787 100644 --- a/esphome/components/ili9341/ili9341_display.cpp +++ b/esphome/components/ili9341/ili9341_display.cpp @@ -112,29 +112,9 @@ void ILI9341Display::display_() { this->y_high_ = 0; } -uint16_t ILI9341Display::convert_to_16bit_color_(uint8_t color_8bit) { - int r = color_8bit >> 5; - int g = (color_8bit >> 2) & 0x07; - int b = color_8bit & 0x03; - uint16_t color = (r * 0x04) << 11; - color |= (g * 0x09) << 5; - color |= (b * 0x0A); - - return color; -} - -uint8_t ILI9341Display::convert_to_8bit_color_(uint16_t color_16bit) { - // convert 16bit color to 8 bit buffer - uint8_t r = color_16bit >> 11; - uint8_t g = (color_16bit >> 5) & 0x3F; - uint8_t b = color_16bit & 0x1F; - - return ((b / 0x0A) | ((g / 0x09) << 2) | ((r / 0x04) << 5)); -} - void ILI9341Display::fill(Color color) { - auto color565 = display::ColorUtil::color_to_565(color); - memset(this->buffer_, convert_to_8bit_color_(color565), this->get_buffer_length_()); + uint8_t color332 = display::ColorUtil::color_to_332(color, display::ColorOrder::COLOR_ORDER_RGB); + memset(this->buffer_, color332, this->get_buffer_length_()); this->x_low_ = 0; this->y_low_ = 0; this->x_high_ = this->get_width_internal() - 1; @@ -181,8 +161,8 @@ void HOT ILI9341Display::draw_absolute_pixel_internal(int x, int y, Color color) this->y_high_ = (y > this->y_high_) ? y : this->y_high_; uint32_t pos = (y * width_) + x; - auto color565 = display::ColorUtil::color_to_565(color); - buffer_[pos] = convert_to_8bit_color_(color565); + uint8_t color332 = display::ColorUtil::color_to_332(color, display::ColorOrder::COLOR_ORDER_RGB); + buffer_[pos] = color332; } // should return the total size: return this->get_width_internal() * this->get_height_internal() * 2 // 16bit color @@ -247,7 +227,7 @@ uint32_t ILI9341Display::buffer_to_transfer_(uint32_t pos, uint32_t sz) { } for (uint32_t i = 0; i < sz; ++i) { - uint16_t color = convert_to_16bit_color_(*src++); + uint16_t color = display::ColorUtil::color_to_565(display::ColorUtil::rgb332_to_color(*src++)); *dst++ = (uint8_t)(color >> 8); *dst++ = (uint8_t) color; } diff --git a/esphome/components/ili9341/ili9341_display.h b/esphome/components/ili9341/ili9341_display.h index d8c90c9d33..eeff688f4f 100644 --- a/esphome/components/ili9341/ili9341_display.h +++ b/esphome/components/ili9341/ili9341_display.h @@ -51,8 +51,6 @@ class ILI9341Display : public PollingComponent, void reset_(); void fill_internal_(Color color); void display_(); - uint16_t convert_to_16bit_color_(uint8_t color_8bit); - uint8_t convert_to_8bit_color_(uint16_t color_16bit); ILI9341Model model_; int16_t width_{320}; ///< Display width as modified by current rotation From 2dc2aec954b6c5dbc65d5898bc65dcce2efd0e1a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 13 May 2022 13:44:24 +1200 Subject: [PATCH 0467/1729] Bump esptool from 3.3 to 3.3.1 (#3468) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e62ef86765..dfe69cd33a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ tzlocal==4.2 # from time tzdata>=2021.1 # from time pyserial==3.5 platformio==5.2.5 # When updating platformio, also update Dockerfile -esptool==3.3 +esptool==3.3.1 click==8.1.3 esphome-dashboard==20220508.0 aioesphomeapi==10.8.2 From 7a03c7d56fd0b4075c6d3494e404fb4201452872 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 15 May 2022 19:46:36 +1200 Subject: [PATCH 0468/1729] Bump pylint from 2.13.8 to 2.13.9 (#3470) Bumps [pylint](https://github.com/PyCQA/pylint) from 2.13.8 to 2.13.9. - [Release notes](https://github.com/PyCQA/pylint/releases) - [Changelog](https://github.com/PyCQA/pylint/blob/main/ChangeLog) - [Commits](https://github.com/PyCQA/pylint/compare/v2.13.8...v2.13.9) --- updated-dependencies: - dependency-name: pylint dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 4b5db8ce87..7c21979647 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,4 +1,4 @@ -pylint==2.13.8 +pylint==2.13.9 flake8==4.0.1 black==22.3.0 pyupgrade==2.32.1 From fea05e9d33f13a5435f035bcf10d0ea0ef23abc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=5Bp=CA=B2=C9=B5s=5D?= Date: Sun, 15 May 2022 09:53:43 +0200 Subject: [PATCH 0469/1729] Increase JSON buffer size on overflow (#3475) --- esphome/components/json/json_util.cpp | 40 +++++++++++++++++---------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/esphome/components/json/json_util.cpp b/esphome/components/json/json_util.cpp index 2bd8112255..7e701af48b 100644 --- a/esphome/components/json/json_util.cpp +++ b/esphome/components/json/json_util.cpp @@ -26,21 +26,33 @@ std::string build_json(const json_build_t &f) { const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT); #endif - const size_t request_size = std::min(free_heap, (size_t) 512); - - 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", - request_size, free_heap); - return "{}"; + 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); + 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", + request_size, free_heap); + return "{}"; + } + JsonObject root = json_document.to(); + 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", + free_heap); + return "{}"; + } + request_size = std::min(request_size * 2, free_heap); + continue; + } + json_document.shrinkToFit(); + ESP_LOGV(TAG, "Size after shrink %u bytes", json_document.capacity()); + std::string output; + serializeJson(json_document, output); + return output; } - JsonObject root = json_document.to(); - f(root); - json_document.shrinkToFit(); - ESP_LOGV(TAG, "Size after shrink %u bytes", json_document.capacity()); - std::string output; - serializeJson(json_document, output); - return output; } void parse_json(const std::string &data, const json_parse_t &f) { From 0665acd1901f8ea1b644d89ab9933d032b288ba7 Mon Sep 17 00:00:00 2001 From: Maxim Ocheretianko Date: Sun, 15 May 2022 22:44:14 +0300 Subject: [PATCH 0470/1729] Tuya status gpio support (#3466) --- esphome/components/tuya/__init__.py | 6 +++++ esphome/components/tuya/tuya.cpp | 41 ++++++++++++++++++++++------- esphome/components/tuya/tuya.h | 7 +++-- tests/test4.yaml | 3 +++ 4 files changed, 45 insertions(+), 12 deletions(-) diff --git a/esphome/components/tuya/__init__.py b/esphome/components/tuya/__init__.py index 965893e012..2eaaa2a625 100644 --- a/esphome/components/tuya/__init__.py +++ b/esphome/components/tuya/__init__.py @@ -1,5 +1,6 @@ from esphome.components import time from esphome import automation +from esphome import pins import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import uart @@ -11,6 +12,7 @@ CONF_IGNORE_MCU_UPDATE_ON_DATAPOINTS = "ignore_mcu_update_on_datapoints" CONF_ON_DATAPOINT_UPDATE = "on_datapoint_update" CONF_DATAPOINT_TYPE = "datapoint_type" +CONF_STATUS_PIN = "status_pin" tuya_ns = cg.esphome_ns.namespace("tuya") Tuya = tuya_ns.class_("Tuya", cg.Component, uart.UARTDevice) @@ -88,6 +90,7 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_IGNORE_MCU_UPDATE_ON_DATAPOINTS): cv.ensure_list( cv.uint8_t ), + cv.Optional(CONF_STATUS_PIN): pins.gpio_output_pin_schema, cv.Optional(CONF_ON_DATAPOINT_UPDATE): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( @@ -114,6 +117,9 @@ async def to_code(config): if CONF_TIME_ID in config: time_ = await cg.get_variable(config[CONF_TIME_ID]) cg.add(var.set_time_id(time_)) + if CONF_STATUS_PIN in config: + status_pin_ = await cg.gpio_pin_expression(config[CONF_STATUS_PIN]) + cg.add(var.set_status_pin(status_pin_)) if CONF_IGNORE_MCU_UPDATE_ON_DATAPOINTS in config: for dp in config[CONF_IGNORE_MCU_UPDATE_ON_DATAPOINTS]: cg.add(var.add_ignore_mcu_update_on_datapoints(dp)) diff --git a/esphome/components/tuya/tuya.cpp b/esphome/components/tuya/tuya.cpp index 1fbca7796d..1b35121c57 100644 --- a/esphome/components/tuya/tuya.cpp +++ b/esphome/components/tuya/tuya.cpp @@ -3,6 +3,7 @@ #include "esphome/components/network/util.h" #include "esphome/core/helpers.h" #include "esphome/core/util.h" +#include "esphome/core/gpio.h" namespace esphome { namespace tuya { @@ -13,6 +14,9 @@ static const int RECEIVE_TIMEOUT = 300; void Tuya::setup() { this->set_interval("heartbeat", 15000, [this] { this->send_empty_command_(TuyaCommandType::HEARTBEAT); }); + if (this->status_pin_.has_value()) { + this->status_pin_.value()->digital_write(false); + } } void Tuya::loop() { @@ -49,9 +53,12 @@ void Tuya::dump_config() { ESP_LOGCONFIG(TAG, " Datapoint %u: unknown", info.id); } } - if ((this->gpio_status_ != -1) || (this->gpio_reset_ != -1)) { - ESP_LOGCONFIG(TAG, " GPIO Configuration: status: pin %d, reset: pin %d (not supported)", this->gpio_status_, - this->gpio_reset_); + if ((this->status_pin_reported_ != -1) || (this->reset_pin_reported_ != -1)) { + ESP_LOGCONFIG(TAG, " GPIO Configuration: status: pin %d, reset: pin %d (not supported)", + this->status_pin_reported_, this->reset_pin_reported_); + } + if (this->status_pin_.has_value()) { + LOG_PIN(" Status Pin: ", this->status_pin_.value()); } ESP_LOGCONFIG(TAG, " Product: '%s'", this->product_.c_str()); this->check_uart_settings(9600); @@ -164,16 +171,27 @@ void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buff } case TuyaCommandType::CONF_QUERY: { if (len >= 2) { - this->gpio_status_ = buffer[0]; - this->gpio_reset_ = buffer[1]; + this->status_pin_reported_ = buffer[0]; + this->reset_pin_reported_ = buffer[1]; } if (this->init_state_ == TuyaInitState::INIT_CONF) { // If mcu returned status gpio, then we can omit sending wifi state - if (this->gpio_status_ != -1) { + if (this->status_pin_reported_ != -1) { this->init_state_ = TuyaInitState::INIT_DATAPOINT; this->send_empty_command_(TuyaCommandType::DATAPOINT_QUERY); + bool is_pin_equals = + this->status_pin_.has_value() && this->status_pin_.value()->get_pin() == this->status_pin_reported_; + // Configure status pin toggling (if reported and configured) or WIFI_STATE periodic send + if (is_pin_equals) { + ESP_LOGV(TAG, "Configured status pin %i", this->status_pin_reported_); + this->set_interval("wifi", 1000, [this] { this->set_status_pin_(); }); + } else { + ESP_LOGW(TAG, "Supplied status_pin does not equals the reported pin %i. TuyaMcu will work in limited mode.", + this->status_pin_reported_); + } } else { this->init_state_ = TuyaInitState::INIT_WIFI; + ESP_LOGV(TAG, "Configured WIFI_STATE periodic send"); this->set_interval("wifi", 1000, [this] { this->send_wifi_status_(); }); } } @@ -397,16 +415,19 @@ void Tuya::send_empty_command_(TuyaCommandType command) { send_command_(TuyaCommand{.cmd = command, .payload = std::vector{}}); } +void Tuya::set_status_pin_() { + bool is_network_ready = network::is_connected() && remote_is_connected(); + this->status_pin_.value()->digital_write(is_network_ready); +} + void Tuya::send_wifi_status_() { uint8_t status = 0x02; if (network::is_connected()) { status = 0x03; // Protocol version 3 also supports specifying when connected to "the cloud" - if (this->protocol_version_ >= 0x03) { - if (remote_is_connected()) { - status = 0x04; - } + if (this->protocol_version_ >= 0x03 && remote_is_connected()) { + status = 0x04; } } diff --git a/esphome/components/tuya/tuya.h b/esphome/components/tuya/tuya.h index 3828c49b48..3a267d75a7 100644 --- a/esphome/components/tuya/tuya.h +++ b/esphome/components/tuya/tuya.h @@ -79,6 +79,7 @@ class Tuya : public Component, public uart::UARTDevice { void set_raw_datapoint_value(uint8_t datapoint_id, const std::vector &value); void set_boolean_datapoint_value(uint8_t datapoint_id, bool value); void set_integer_datapoint_value(uint8_t datapoint_id, uint32_t value); + void set_status_pin(InternalGPIOPin *status_pin) { this->status_pin_ = status_pin; } void set_string_datapoint_value(uint8_t datapoint_id, const std::string &value); void set_enum_datapoint_value(uint8_t datapoint_id, uint8_t value); void set_bitmask_datapoint_value(uint8_t datapoint_id, uint32_t value, uint8_t length); @@ -115,6 +116,7 @@ class Tuya : public Component, public uart::UARTDevice { void set_string_datapoint_value_(uint8_t datapoint_id, const std::string &value, bool forced); void set_raw_datapoint_value_(uint8_t datapoint_id, const std::vector &value, bool forced); void send_datapoint_command_(uint8_t datapoint_id, TuyaDatapointType datapoint_type, std::vector data); + void set_status_pin_(); void send_wifi_status_(); #ifdef USE_TIME @@ -123,8 +125,9 @@ class Tuya : public Component, public uart::UARTDevice { #endif TuyaInitState init_state_ = TuyaInitState::INIT_HEARTBEAT; uint8_t protocol_version_ = -1; - int gpio_status_ = -1; - int gpio_reset_ = -1; + optional status_pin_{}; + int status_pin_reported_ = -1; + int reset_pin_reported_ = -1; uint32_t last_command_timestamp_ = 0; uint32_t last_rx_char_timestamp_ = 0; std::string product_ = ""; diff --git a/tests/test4.yaml b/tests/test4.yaml index 54412222b5..82bb9e2f85 100644 --- a/tests/test4.yaml +++ b/tests/test4.yaml @@ -57,6 +57,9 @@ time: tuya: time_id: sntp_time + status_pin: + number: 14 + inverted: true pipsolar: id: inverter0 From f62d5d3b9d150891a4f80ed9ea24b3a65e6c2981 Mon Sep 17 00:00:00 2001 From: Maxim Ocheretianko Date: Sun, 15 May 2022 22:49:40 +0300 Subject: [PATCH 0471/1729] Add Tuya select (#3469) --- CODEOWNERS | 1 + esphome/components/tuya/select/__init__.py | 47 +++++++++++++++++ .../components/tuya/select/tuya_select.cpp | 52 +++++++++++++++++++ esphome/components/tuya/select/tuya_select.h | 30 +++++++++++ esphome/const.py | 1 + tests/test4.yaml | 9 ++++ 6 files changed, 140 insertions(+) create mode 100644 esphome/components/tuya/select/__init__.py create mode 100644 esphome/components/tuya/select/tuya_select.cpp create mode 100644 esphome/components/tuya/select/tuya_select.h diff --git a/CODEOWNERS b/CODEOWNERS index e2b29547cb..be6e8be3f7 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -222,6 +222,7 @@ esphome/components/tsl2591/* @wjcarpenter esphome/components/tuya/binary_sensor/* @jesserockz esphome/components/tuya/climate/* @jesserockz esphome/components/tuya/number/* @frankiboy1 +esphome/components/tuya/select/* @bearpawmaxim esphome/components/tuya/sensor/* @jesserockz esphome/components/tuya/switch/* @jesserockz esphome/components/tuya/text_sensor/* @dentra diff --git a/esphome/components/tuya/select/__init__.py b/esphome/components/tuya/select/__init__.py new file mode 100644 index 0000000000..3d65eda301 --- /dev/null +++ b/esphome/components/tuya/select/__init__.py @@ -0,0 +1,47 @@ +from esphome.components import select +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome.const import CONF_OPTIONS, CONF_OPTIMISTIC, CONF_ENUM_DATAPOINT +from .. import tuya_ns, CONF_TUYA_ID, Tuya + +DEPENDENCIES = ["tuya"] +CODEOWNERS = ["@bearpawmaxim"] + +TuyaSelect = tuya_ns.class_("TuyaSelect", select.Select, cg.Component) + + +def ensure_option_map(value): + cv.check_not_templatable(value) + option = cv.All(cv.int_range(0, 2**8 - 1)) + mapping = cv.All(cv.string_strict) + options_map_schema = cv.Schema({option: mapping}) + value = options_map_schema(value) + + all_values = list(value.keys()) + unique_values = set(value.keys()) + if len(all_values) != len(unique_values): + raise cv.Invalid("Mapping values must be unique.") + + return value + + +CONFIG_SCHEMA = select.SELECT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(TuyaSelect), + cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya), + cv.Required(CONF_ENUM_DATAPOINT): cv.uint8_t, + cv.Required(CONF_OPTIONS): ensure_option_map, + cv.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + options_map = config[CONF_OPTIONS] + var = await select.new_select(config, options=list(options_map.values())) + await cg.register_component(var, config) + cg.add(var.set_select_mappings(list(options_map.keys()))) + parent = await cg.get_variable(config[CONF_TUYA_ID]) + cg.add(var.set_tuya_parent(parent)) + cg.add(var.set_select_id(config[CONF_ENUM_DATAPOINT])) + cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) diff --git a/esphome/components/tuya/select/tuya_select.cpp b/esphome/components/tuya/select/tuya_select.cpp new file mode 100644 index 0000000000..a4df0873b0 --- /dev/null +++ b/esphome/components/tuya/select/tuya_select.cpp @@ -0,0 +1,52 @@ +#include "esphome/core/log.h" +#include "tuya_select.h" + +namespace esphome { +namespace tuya { + +static const char *const TAG = "tuya.select"; + +void TuyaSelect::setup() { + this->parent_->register_listener(this->select_id_, [this](const TuyaDatapoint &datapoint) { + uint8_t enum_value = datapoint.value_enum; + ESP_LOGV(TAG, "MCU reported select %u value %u", this->select_id_, enum_value); + auto options = this->traits.get_options(); + auto mappings = this->mappings_; + auto it = std::find(mappings.cbegin(), mappings.cend(), enum_value); + if (it == mappings.end()) { + ESP_LOGW(TAG, "Invalid value %u", enum_value); + return; + } + size_t mapping_idx = std::distance(mappings.cbegin(), it); + auto value = this->at(mapping_idx); + this->publish_state(value.value()); + }); +} + +void TuyaSelect::control(const std::string &value) { + if (this->optimistic_) + this->publish_state(value); + + auto idx = this->index_of(value); + if (idx.has_value()) { + uint8_t mapping = this->mappings_.at(idx.value()); + ESP_LOGV(TAG, "Setting %u datapoint value to %u:%s", this->select_id_, mapping, value.c_str()); + this->parent_->set_enum_datapoint_value(this->select_id_, mapping); + return; + } + + ESP_LOGW(TAG, "Invalid value %s", value.c_str()); +} + +void TuyaSelect::dump_config() { + LOG_SELECT("", "Tuya Select", this); + ESP_LOGCONFIG(TAG, " Select has datapoint ID %u", this->select_id_); + ESP_LOGCONFIG(TAG, " Options are:"); + auto options = this->traits.get_options(); + for (auto i = 0; i < this->mappings_.size(); i++) { + ESP_LOGCONFIG(TAG, " %i: %s", this->mappings_.at(i), options.at(i).c_str()); + } +} + +} // namespace tuya +} // namespace esphome diff --git a/esphome/components/tuya/select/tuya_select.h b/esphome/components/tuya/select/tuya_select.h new file mode 100644 index 0000000000..ab233dc501 --- /dev/null +++ b/esphome/components/tuya/select/tuya_select.h @@ -0,0 +1,30 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/tuya/tuya.h" +#include "esphome/components/select/select.h" + +namespace esphome { +namespace tuya { + +class TuyaSelect : public select::Select, public Component { + public: + void setup() override; + void dump_config() override; + + void set_tuya_parent(Tuya *parent) { this->parent_ = parent; } + void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } + void set_select_id(uint8_t select_id) { this->select_id_ = select_id; } + void set_select_mappings(std::vector mappings) { this->mappings_ = std::move(mappings); } + + protected: + void control(const std::string &value) override; + + Tuya *parent_; + bool optimistic_ = false; + uint8_t select_id_; + std::vector mappings_; +}; + +} // namespace tuya +} // namespace esphome diff --git a/esphome/const.py b/esphome/const.py index 9a2e41d69f..c2aa53be70 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -198,6 +198,7 @@ CONF_ENABLE_TIME = "enable_time" CONF_ENERGY = "energy" CONF_ENTITY_CATEGORY = "entity_category" CONF_ENTITY_ID = "entity_id" +CONF_ENUM_DATAPOINT = "enum_datapoint" CONF_ESP8266_DISABLE_SSL_SUPPORT = "esp8266_disable_ssl_support" CONF_ESPHOME = "esphome" CONF_ETHERNET = "ethernet" diff --git a/tests/test4.yaml b/tests/test4.yaml index 82bb9e2f85..0e9f14e0d6 100644 --- a/tests/test4.yaml +++ b/tests/test4.yaml @@ -61,6 +61,15 @@ tuya: number: 14 inverted: true +select: + - platform: tuya + id: tuya_select + enum_datapoint: 42 + options: + 0: Internal + 1: Floor + 2: Both + pipsolar: id: inverter0 From 93e2506279239a3d09511bcfc0db44ee00fa3489 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 16 May 2022 13:05:20 +1200 Subject: [PATCH 0472/1729] Mark improv_serial and ESP-IDF usb based serial on c3/s2/s3 unsupported (#3477) --- esphome/components/improv_serial/__init__.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/esphome/components/improv_serial/__init__.py b/esphome/components/improv_serial/__init__.py index 21073a8ab3..67a0f7f4ed 100644 --- a/esphome/components/improv_serial/__init__.py +++ b/esphome/components/improv_serial/__init__.py @@ -1,6 +1,8 @@ -from esphome.const import CONF_BAUD_RATE, CONF_ID, CONF_LOGGER +from esphome.components.logger import USB_CDC, USB_SERIAL_JTAG +from esphome.const import CONF_BAUD_RATE, CONF_HARDWARE_UART, CONF_ID, CONF_LOGGER import esphome.codegen as cg import esphome.config_validation as cv +from esphome.core import CORE import esphome.final_validate as fv CODEOWNERS = ["@esphome/core"] @@ -17,14 +19,19 @@ CONFIG_SCHEMA = cv.Schema( ).extend(cv.COMPONENT_SCHEMA) -def validate_logger_baud_rate(config): +def validate_logger(config): logger_conf = fv.full_config.get()[CONF_LOGGER] if logger_conf[CONF_BAUD_RATE] == 0: raise cv.Invalid("improv_serial requires the logger baud_rate to be not 0") + if CORE.using_esp_idf: + if logger_conf[CONF_HARDWARE_UART] in [USB_SERIAL_JTAG, USB_CDC]: + raise cv.Invalid( + "improv_serial does not support the selected logger hardware_uart" + ) return config -FINAL_VALIDATE_SCHEMA = validate_logger_baud_rate +FINAL_VALIDATE_SCHEMA = validate_logger async def to_code(config): From 01222dbab75a376c2be07cdca0b4c50b98aa15fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=5Bp=CA=B2=C9=B5s=5D?= Date: Sun, 15 May 2022 09:53:43 +0200 Subject: [PATCH 0473/1729] Increase JSON buffer size on overflow (#3475) --- esphome/components/json/json_util.cpp | 40 +++++++++++++++++---------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/esphome/components/json/json_util.cpp b/esphome/components/json/json_util.cpp index 2bd8112255..7e701af48b 100644 --- a/esphome/components/json/json_util.cpp +++ b/esphome/components/json/json_util.cpp @@ -26,21 +26,33 @@ std::string build_json(const json_build_t &f) { const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT); #endif - const size_t request_size = std::min(free_heap, (size_t) 512); - - 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", - request_size, free_heap); - return "{}"; + 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); + 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", + request_size, free_heap); + return "{}"; + } + JsonObject root = json_document.to(); + 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", + free_heap); + return "{}"; + } + request_size = std::min(request_size * 2, free_heap); + continue; + } + json_document.shrinkToFit(); + ESP_LOGV(TAG, "Size after shrink %u bytes", json_document.capacity()); + std::string output; + serializeJson(json_document, output); + return output; } - JsonObject root = json_document.to(); - f(root); - json_document.shrinkToFit(); - ESP_LOGV(TAG, "Size after shrink %u bytes", json_document.capacity()); - std::string output; - serializeJson(json_document, output); - return output; } void parse_json(const std::string &data, const json_parse_t &f) { From a639690716d3a598025cbd6fc18a69415bf4dee0 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 16 May 2022 13:05:20 +1200 Subject: [PATCH 0474/1729] Mark improv_serial and ESP-IDF usb based serial on c3/s2/s3 unsupported (#3477) --- esphome/components/improv_serial/__init__.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/esphome/components/improv_serial/__init__.py b/esphome/components/improv_serial/__init__.py index 21073a8ab3..67a0f7f4ed 100644 --- a/esphome/components/improv_serial/__init__.py +++ b/esphome/components/improv_serial/__init__.py @@ -1,6 +1,8 @@ -from esphome.const import CONF_BAUD_RATE, CONF_ID, CONF_LOGGER +from esphome.components.logger import USB_CDC, USB_SERIAL_JTAG +from esphome.const import CONF_BAUD_RATE, CONF_HARDWARE_UART, CONF_ID, CONF_LOGGER import esphome.codegen as cg import esphome.config_validation as cv +from esphome.core import CORE import esphome.final_validate as fv CODEOWNERS = ["@esphome/core"] @@ -17,14 +19,19 @@ CONFIG_SCHEMA = cv.Schema( ).extend(cv.COMPONENT_SCHEMA) -def validate_logger_baud_rate(config): +def validate_logger(config): logger_conf = fv.full_config.get()[CONF_LOGGER] if logger_conf[CONF_BAUD_RATE] == 0: raise cv.Invalid("improv_serial requires the logger baud_rate to be not 0") + if CORE.using_esp_idf: + if logger_conf[CONF_HARDWARE_UART] in [USB_SERIAL_JTAG, USB_CDC]: + raise cv.Invalid( + "improv_serial does not support the selected logger hardware_uart" + ) return config -FINAL_VALIDATE_SCHEMA = validate_logger_baud_rate +FINAL_VALIDATE_SCHEMA = validate_logger async def to_code(config): From c707e646854f04bf9463ca050ae373570c36c0b3 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 16 May 2022 13:07:12 +1200 Subject: [PATCH 0475/1729] Bump version to 2022.5.0b3 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 7717f709ec..03d8c98712 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2022.5.0b2" +__version__ = "2022.5.0b3" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From 6dabf24bf35681e98fe01950dd9c77bd28fe4c81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=5Bp=CA=B2=C9=B5s=5D?= Date: Mon, 16 May 2022 05:35:27 +0200 Subject: [PATCH 0476/1729] MQTT cover: send state even if position is available (#3473) --- esphome/components/mqtt/mqtt_cover.cpp | 28 +++++++++++--------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/esphome/components/mqtt/mqtt_cover.cpp b/esphome/components/mqtt/mqtt_cover.cpp index e5525bc0f7..0718a24828 100644 --- a/esphome/components/mqtt/mqtt_cover.cpp +++ b/esphome/components/mqtt/mqtt_cover.cpp @@ -51,10 +51,9 @@ void MQTTCoverComponent::setup() { void MQTTCoverComponent::dump_config() { ESP_LOGCONFIG(TAG, "MQTT cover '%s':", this->cover_->get_name().c_str()); auto traits = this->cover_->get_traits(); - // no state topic for position - bool state_topic = !traits.get_supports_position(); - LOG_MQTT_COMPONENT(state_topic, true) - if (!state_topic) { + bool has_command_topic = traits.get_supports_position() || !traits.get_supports_tilt(); + LOG_MQTT_COMPONENT(true, has_command_topic) + if (traits.get_supports_position()) { ESP_LOGCONFIG(TAG, " Position State Topic: '%s'", this->get_position_state_topic().c_str()); ESP_LOGCONFIG(TAG, " Position Command Topic: '%s'", this->get_position_command_topic().c_str()); } @@ -72,7 +71,6 @@ void MQTTCoverComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConf root[MQTT_OPTIMISTIC] = true; } if (traits.get_supports_position()) { - config.state_topic = false; root[MQTT_POSITION_TOPIC] = this->get_position_state_topic(); root[MQTT_SET_POSITION_TOPIC] = this->get_position_command_topic(); } @@ -92,17 +90,7 @@ bool MQTTCoverComponent::send_initial_state() { return this->publish_state(); } bool MQTTCoverComponent::publish_state() { auto traits = this->cover_->get_traits(); bool success = true; - if (!traits.get_supports_position()) { - const char *state_s = "unknown"; - if (this->cover_->position == COVER_OPEN) { - state_s = "open"; - } else if (this->cover_->position == COVER_CLOSED) { - state_s = "closed"; - } - - if (!this->publish(this->get_state_topic_(), state_s)) - success = false; - } else { + if (traits.get_supports_position()) { std::string pos = value_accuracy_to_string(roundf(this->cover_->position * 100), 0); if (!this->publish(this->get_position_state_topic(), pos)) success = false; @@ -112,6 +100,14 @@ bool MQTTCoverComponent::publish_state() { if (!this->publish(this->get_tilt_state_topic(), pos)) success = false; } + const char *state_s = this->cover_->current_operation == COVER_OPERATION_OPENING ? "opening" + : this->cover_->current_operation == COVER_OPERATION_CLOSING ? "closing" + : this->cover_->position == COVER_CLOSED ? "closed" + : this->cover_->position == COVER_OPEN ? "open" + : traits.get_supports_position() ? "open" + : "unknown"; + if (!this->publish(this->get_state_topic_(), state_s)) + success = false; return success; } From 609a2ca5926b1213f242cac02a324821530612c6 Mon Sep 17 00:00:00 2001 From: Martin <25747549+martgras@users.noreply.github.com> Date: Tue, 17 May 2022 00:59:36 +0200 Subject: [PATCH 0477/1729] ESP32: Only save to NVS if data was changed (#3479) --- esphome/components/esp32/preferences.cpp | 33 +++++++++++++++++++----- 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/esphome/components/esp32/preferences.cpp b/esphome/components/esp32/preferences.cpp index 8c2b67a942..a78159825e 100644 --- a/esphome/components/esp32/preferences.cpp +++ b/esphome/components/esp32/preferences.cpp @@ -118,12 +118,17 @@ class ESP32Preferences : public ESPPreferences { // go through vector from back to front (makes erase easier/more efficient) for (ssize_t i = s_pending_save.size() - 1; i >= 0; i--) { const auto &save = s_pending_save[i]; - esp_err_t err = nvs_set_blob(nvs_handle, save.key.c_str(), save.data.data(), save.data.size()); - if (err != 0) { - ESP_LOGV(TAG, "nvs_set_blob('%s', len=%u) failed: %s", save.key.c_str(), save.data.size(), - esp_err_to_name(err)); - any_failed = true; - continue; + ESP_LOGVV(TAG, "Checking if NVS data %s has changed", save.key.c_str()); + if (is_changed(nvs_handle, save)) { + esp_err_t err = nvs_set_blob(nvs_handle, save.key.c_str(), save.data.data(), save.data.size()); + if (err != 0) { + ESP_LOGV(TAG, "nvs_set_blob('%s', len=%u) failed: %s", save.key.c_str(), save.data.size(), + esp_err_to_name(err)); + any_failed = true; + continue; + } + } else { + ESP_LOGD(TAG, "NVS data not changed skipping %s len=%u", save.key.c_str(), save.data.size()); } s_pending_save.erase(s_pending_save.begin() + i); } @@ -137,6 +142,22 @@ class ESP32Preferences : public ESPPreferences { return !any_failed; } + bool is_changed(const uint32_t nvs_handle, const NVSData &to_save) { + NVSData stored_data{}; + size_t actual_len; + esp_err_t err = nvs_get_blob(nvs_handle, to_save.key.c_str(), nullptr, &actual_len); + if (err != 0) { + ESP_LOGV(TAG, "nvs_get_blob('%s'): %s - the key might not be set yet", to_save.key.c_str(), esp_err_to_name(err)); + return true; + } + stored_data.data.reserve(actual_len); + err = nvs_get_blob(nvs_handle, to_save.key.c_str(), stored_data.data.data(), &actual_len); + if (err != 0) { + ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", to_save.key.c_str(), esp_err_to_name(err)); + return true; + } + return to_save.data == stored_data.data; + } }; void setup_preferences() { From 9b6b9c1fa24b272c87232611126dbbbbcbac6a33 Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Tue, 17 May 2022 01:15:02 -0700 Subject: [PATCH 0478/1729] Retry Tuya init commands (#3482) Co-authored-by: Samuel Sieb --- esphome/components/tuya/tuya.cpp | 28 +++++++++++++++++++++++----- esphome/components/tuya/tuya.h | 2 ++ 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/esphome/components/tuya/tuya.cpp b/esphome/components/tuya/tuya.cpp index 1b35121c57..f4744064e3 100644 --- a/esphome/components/tuya/tuya.cpp +++ b/esphome/components/tuya/tuya.cpp @@ -11,6 +11,7 @@ namespace tuya { static const char *const TAG = "tuya"; static const int COMMAND_DELAY = 10; static const int RECEIVE_TIMEOUT = 300; +static const int MAX_RETRIES = 5; void Tuya::setup() { this->set_interval("heartbeat", 15000, [this] { this->send_empty_command_(TuyaCommandType::HEARTBEAT); }); @@ -31,8 +32,12 @@ void Tuya::loop() { void Tuya::dump_config() { ESP_LOGCONFIG(TAG, "Tuya:"); if (this->init_state_ != TuyaInitState::INIT_DONE) { - ESP_LOGCONFIG(TAG, " Configuration will be reported when setup is complete. Current init_state: %u", - static_cast(this->init_state_)); + if (this->init_failed_) { + ESP_LOGCONFIG(TAG, " Initialization failed. Current init_state: %u", static_cast(this->init_state_)); + } else { + ESP_LOGCONFIG(TAG, " Configuration will be reported when setup is complete. Current init_state: %u", + static_cast(this->init_state_)); + } ESP_LOGCONFIG(TAG, " If no further output is received, confirm that this is a supported Tuya device."); return; } @@ -54,8 +59,8 @@ void Tuya::dump_config() { } } if ((this->status_pin_reported_ != -1) || (this->reset_pin_reported_ != -1)) { - ESP_LOGCONFIG(TAG, " GPIO Configuration: status: pin %d, reset: pin %d (not supported)", - this->status_pin_reported_, this->reset_pin_reported_); + ESP_LOGCONFIG(TAG, " GPIO Configuration: status: pin %d, reset: pin %d", this->status_pin_reported_, + this->reset_pin_reported_); } if (this->status_pin_.has_value()) { LOG_PIN(" Status Pin: ", this->status_pin_.value()); @@ -134,6 +139,8 @@ void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buff if (this->expected_response_.has_value() && this->expected_response_ == command_type) { this->expected_response_.reset(); + this->command_queue_.erase(command_queue_.begin()); + this->init_retries_ = 0; } switch (command_type) { @@ -396,13 +403,24 @@ void Tuya::process_command_queue_() { if (this->expected_response_.has_value() && delay > RECEIVE_TIMEOUT) { this->expected_response_.reset(); + if (init_state_ != TuyaInitState::INIT_DONE) { + if (++this->init_retries_ >= MAX_RETRIES) { + this->init_failed_ = true; + ESP_LOGE(TAG, "Initialization failed at init_state %u", static_cast(this->init_state_)); + this->command_queue_.erase(command_queue_.begin()); + this->init_retries_ = 0; + } + } else { + this->command_queue_.erase(command_queue_.begin()); + } } // Left check of delay since last command in case there's ever a command sent by calling send_raw_command_ directly if (delay > COMMAND_DELAY && !this->command_queue_.empty() && this->rx_message_.empty() && !this->expected_response_.has_value()) { this->send_raw_command_(command_queue_.front()); - this->command_queue_.erase(command_queue_.begin()); + if (!this->expected_response_.has_value()) + this->command_queue_.erase(command_queue_.begin()); } } diff --git a/esphome/components/tuya/tuya.h b/esphome/components/tuya/tuya.h index 3a267d75a7..1f21b09c0c 100644 --- a/esphome/components/tuya/tuya.h +++ b/esphome/components/tuya/tuya.h @@ -124,6 +124,8 @@ class Tuya : public Component, public uart::UARTDevice { optional time_id_{}; #endif TuyaInitState init_state_ = TuyaInitState::INIT_HEARTBEAT; + bool init_failed_{false}; + int init_retries_{0}; uint8_t protocol_version_ = -1; optional status_pin_{}; int status_pin_reported_ = -1; From 17b8bd83161fa9e2fddd1bbb91cdd7dcddc591aa Mon Sep 17 00:00:00 2001 From: Martin <25747549+martgras@users.noreply.github.com> Date: Tue, 17 May 2022 00:59:36 +0200 Subject: [PATCH 0479/1729] ESP32: Only save to NVS if data was changed (#3479) --- esphome/components/esp32/preferences.cpp | 33 +++++++++++++++++++----- 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/esphome/components/esp32/preferences.cpp b/esphome/components/esp32/preferences.cpp index 8c2b67a942..a78159825e 100644 --- a/esphome/components/esp32/preferences.cpp +++ b/esphome/components/esp32/preferences.cpp @@ -118,12 +118,17 @@ class ESP32Preferences : public ESPPreferences { // go through vector from back to front (makes erase easier/more efficient) for (ssize_t i = s_pending_save.size() - 1; i >= 0; i--) { const auto &save = s_pending_save[i]; - esp_err_t err = nvs_set_blob(nvs_handle, save.key.c_str(), save.data.data(), save.data.size()); - if (err != 0) { - ESP_LOGV(TAG, "nvs_set_blob('%s', len=%u) failed: %s", save.key.c_str(), save.data.size(), - esp_err_to_name(err)); - any_failed = true; - continue; + ESP_LOGVV(TAG, "Checking if NVS data %s has changed", save.key.c_str()); + if (is_changed(nvs_handle, save)) { + esp_err_t err = nvs_set_blob(nvs_handle, save.key.c_str(), save.data.data(), save.data.size()); + if (err != 0) { + ESP_LOGV(TAG, "nvs_set_blob('%s', len=%u) failed: %s", save.key.c_str(), save.data.size(), + esp_err_to_name(err)); + any_failed = true; + continue; + } + } else { + ESP_LOGD(TAG, "NVS data not changed skipping %s len=%u", save.key.c_str(), save.data.size()); } s_pending_save.erase(s_pending_save.begin() + i); } @@ -137,6 +142,22 @@ class ESP32Preferences : public ESPPreferences { return !any_failed; } + bool is_changed(const uint32_t nvs_handle, const NVSData &to_save) { + NVSData stored_data{}; + size_t actual_len; + esp_err_t err = nvs_get_blob(nvs_handle, to_save.key.c_str(), nullptr, &actual_len); + if (err != 0) { + ESP_LOGV(TAG, "nvs_get_blob('%s'): %s - the key might not be set yet", to_save.key.c_str(), esp_err_to_name(err)); + return true; + } + stored_data.data.reserve(actual_len); + err = nvs_get_blob(nvs_handle, to_save.key.c_str(), stored_data.data.data(), &actual_len); + if (err != 0) { + ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", to_save.key.c_str(), esp_err_to_name(err)); + return true; + } + return to_save.data == stored_data.data; + } }; void setup_preferences() { From 6f49f5465b61581d5f357c891e3c4aa134fec477 Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Tue, 17 May 2022 01:15:02 -0700 Subject: [PATCH 0480/1729] Retry Tuya init commands (#3482) Co-authored-by: Samuel Sieb --- esphome/components/tuya/tuya.cpp | 30 ++++++++++++++++++++++++------ esphome/components/tuya/tuya.h | 2 ++ 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/esphome/components/tuya/tuya.cpp b/esphome/components/tuya/tuya.cpp index 1fbca7796d..78e9d9e568 100644 --- a/esphome/components/tuya/tuya.cpp +++ b/esphome/components/tuya/tuya.cpp @@ -1,7 +1,7 @@ #include "tuya.h" -#include "esphome/core/log.h" #include "esphome/components/network/util.h" #include "esphome/core/helpers.h" +#include "esphome/core/log.h" #include "esphome/core/util.h" namespace esphome { @@ -10,6 +10,7 @@ namespace tuya { static const char *const TAG = "tuya"; static const int COMMAND_DELAY = 10; static const int RECEIVE_TIMEOUT = 300; +static const int MAX_RETRIES = 5; void Tuya::setup() { this->set_interval("heartbeat", 15000, [this] { this->send_empty_command_(TuyaCommandType::HEARTBEAT); }); @@ -27,8 +28,12 @@ void Tuya::loop() { void Tuya::dump_config() { ESP_LOGCONFIG(TAG, "Tuya:"); if (this->init_state_ != TuyaInitState::INIT_DONE) { - ESP_LOGCONFIG(TAG, " Configuration will be reported when setup is complete. Current init_state: %u", - static_cast(this->init_state_)); + if (this->init_failed_) { + ESP_LOGCONFIG(TAG, " Initialization failed. Current init_state: %u", static_cast(this->init_state_)); + } else { + ESP_LOGCONFIG(TAG, " Configuration will be reported when setup is complete. Current init_state: %u", + static_cast(this->init_state_)); + } ESP_LOGCONFIG(TAG, " If no further output is received, confirm that this is a supported Tuya device."); return; } @@ -127,6 +132,8 @@ void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buff if (this->expected_response_.has_value() && this->expected_response_ == command_type) { this->expected_response_.reset(); + this->command_queue_.erase(command_queue_.begin()); + this->init_retries_ = 0; } switch (command_type) { @@ -332,8 +339,8 @@ void Tuya::handle_datapoints_(const uint8_t *buffer, size_t len) { } void Tuya::send_raw_command_(TuyaCommand command) { - uint8_t len_hi = (uint8_t)(command.payload.size() >> 8); - uint8_t len_lo = (uint8_t)(command.payload.size() & 0xFF); + uint8_t len_hi = (uint8_t) (command.payload.size() >> 8); + uint8_t len_lo = (uint8_t) (command.payload.size() & 0xFF); uint8_t version = 0; this->last_command_timestamp_ = millis(); @@ -378,13 +385,24 @@ void Tuya::process_command_queue_() { if (this->expected_response_.has_value() && delay > RECEIVE_TIMEOUT) { this->expected_response_.reset(); + if (init_state_ != TuyaInitState::INIT_DONE) { + if (++this->init_retries_ >= MAX_RETRIES) { + this->init_failed_ = true; + ESP_LOGE(TAG, "Initialization failed at init_state %u", static_cast(this->init_state_)); + this->command_queue_.erase(command_queue_.begin()); + this->init_retries_ = 0; + } + } else { + this->command_queue_.erase(command_queue_.begin()); + } } // Left check of delay since last command in case there's ever a command sent by calling send_raw_command_ directly if (delay > COMMAND_DELAY && !this->command_queue_.empty() && this->rx_message_.empty() && !this->expected_response_.has_value()) { this->send_raw_command_(command_queue_.front()); - this->command_queue_.erase(command_queue_.begin()); + if (!this->expected_response_.has_value()) + this->command_queue_.erase(command_queue_.begin()); } } diff --git a/esphome/components/tuya/tuya.h b/esphome/components/tuya/tuya.h index 3828c49b48..cdff523f90 100644 --- a/esphome/components/tuya/tuya.h +++ b/esphome/components/tuya/tuya.h @@ -122,6 +122,8 @@ class Tuya : public Component, public uart::UARTDevice { optional time_id_{}; #endif TuyaInitState init_state_ = TuyaInitState::INIT_HEARTBEAT; + bool init_failed_{false}; + int init_retries_{0}; uint8_t protocol_version_ = -1; int gpio_status_ = -1; int gpio_reset_ = -1; From 72fcf2cbe1f58113eca966bd62969c20a391f8ab Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 17 May 2022 23:23:37 +1200 Subject: [PATCH 0481/1729] Bump version to 2022.5.0b4 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 03d8c98712..3e5630c470 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2022.5.0b3" +__version__ = "2022.5.0b4" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From 282d9e138caf4cdbd8769703b29ff3c04ea71a46 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 17 May 2022 23:31:55 +1200 Subject: [PATCH 0482/1729] Revert adding spaces --- esphome/components/tuya/tuya.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/tuya/tuya.cpp b/esphome/components/tuya/tuya.cpp index 78e9d9e568..f8379a93f2 100644 --- a/esphome/components/tuya/tuya.cpp +++ b/esphome/components/tuya/tuya.cpp @@ -339,8 +339,8 @@ void Tuya::handle_datapoints_(const uint8_t *buffer, size_t len) { } void Tuya::send_raw_command_(TuyaCommand command) { - uint8_t len_hi = (uint8_t) (command.payload.size() >> 8); - uint8_t len_lo = (uint8_t) (command.payload.size() & 0xFF); + uint8_t len_hi = (uint8_t)(command.payload.size() >> 8); + uint8_t len_lo = (uint8_t)(command.payload.size() & 0xFF); uint8_t version = 0; this->last_command_timestamp_ = millis(); From ae2f6ad4d12d2f6959218e490c7525ac4497c724 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 18 May 2022 16:30:20 +1200 Subject: [PATCH 0483/1729] Bump version to 2022.5.0 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 3e5630c470..913f0fd0dc 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2022.5.0b4" +__version__ = "2022.5.0" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From c000e1d6dda3f0b07b2df46b7440a2ba79d33c19 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Wed, 18 May 2022 23:23:00 +0100 Subject: [PATCH 0484/1729] Ili9341 8bit indexed mode pt1 (#2490) --- .../components/display/display_color_utils.h | 47 +++++++++++++++++++ esphome/components/ili9341/display.py | 18 +++++++ .../components/ili9341/ili9341_display.cpp | 17 +++++-- esphome/components/ili9341/ili9341_display.h | 10 ++++ esphome/const.py | 1 + 5 files changed, 90 insertions(+), 3 deletions(-) diff --git a/esphome/components/display/display_color_utils.h b/esphome/components/display/display_color_utils.h index 7f29586932..bf6d5445f1 100644 --- a/esphome/components/display/display_color_utils.h +++ b/esphome/components/display/display_color_utils.h @@ -107,6 +107,53 @@ class ColorUtil { uint32_t gs4 = esp_scale8(color.white, 15); return gs4; } + /*** + * Converts a Color value to an 8bit index using a 24bit 888 palette. + * Uses euclidiean distance to calculate the linear distance between + * two points in an RGB cube, then iterates through the full palette + * returning the closest match. + * @param[in] color The target color. + * @param[in] palette The 256*3 byte RGB palette. + * @return The 8 bit index of the closest color (e.g. for display buffer). + */ + // static uint8_t color_to_index8_palette888(Color color, uint8_t *palette) { + static uint8_t color_to_index8_palette888(Color color, const uint8_t *palette) { + uint8_t closest_index = 0; + uint32_t minimum_dist2 = UINT32_MAX; // Smallest distance^2 to the target + // so far + // int8_t(*plt)[][3] = palette; + int16_t tgt_r = color.r; + int16_t tgt_g = color.g; + int16_t tgt_b = color.b; + uint16_t x, y, z; + // Loop through each row of the palette + for (uint16_t i = 0; i < 256; i++) { + // Get the pallet rgb color + int16_t plt_r = (int16_t) palette[i * 3 + 0]; + int16_t plt_g = (int16_t) palette[i * 3 + 1]; + int16_t plt_b = (int16_t) palette[i * 3 + 2]; + // Calculate euclidian distance (linear distance in rgb cube). + x = (uint32_t) std::abs(tgt_r - plt_r); + y = (uint32_t) std::abs(tgt_g - plt_g); + z = (uint32_t) std::abs(tgt_b - plt_b); + uint32_t dist2 = x * x + y * y + z * z; + if (dist2 < minimum_dist2) { + minimum_dist2 = dist2; + closest_index = (uint8_t) i; + } + } + return closest_index; + } + /*** + * Converts an 8bit palette index (e.g. from a display buffer) to a color. + * @param[in] index The index to look up. + * @param[in] palette The 256*3 byte RGB palette. + * @return The RGBW Color object looked up by the palette. + */ + static Color index8_to_color_palette888(uint8_t index, const uint8_t *palette) { + Color color = Color(palette[index * 3 + 0], palette[index * 3 + 1], palette[index * 3 + 2], 0); + return color; + } }; } // namespace display } // namespace esphome diff --git a/esphome/components/ili9341/display.py b/esphome/components/ili9341/display.py index 157e8212bd..6b18196ef1 100644 --- a/esphome/components/ili9341/display.py +++ b/esphome/components/ili9341/display.py @@ -3,13 +3,16 @@ import esphome.config_validation as cv from esphome import pins from esphome.components import display, spi from esphome.const import ( + CONF_COLOR_PALETTE, CONF_DC_PIN, CONF_ID, CONF_LAMBDA, CONF_MODEL, CONF_PAGES, + CONF_RAW_DATA_ID, CONF_RESET_PIN, ) +from esphome.core import HexInt DEPENDENCIES = ["spi"] @@ -23,6 +26,7 @@ ILI9341M5Stack = ili9341_ns.class_("ILI9341M5Stack", ili9341) ILI9341TFT24 = ili9341_ns.class_("ILI9341TFT24", ili9341) ILI9341Model = ili9341_ns.enum("ILI9341Model") +ILI9341ColorMode = ili9341_ns.enum("ILI9341ColorMode") MODELS = { "M5STACK": ILI9341Model.M5STACK, @@ -31,6 +35,8 @@ MODELS = { ILI9341_MODEL = cv.enum(MODELS, upper=True, space="_") +COLOR_PALETTE = cv.one_of("NONE", "GRAYSCALE") + CONFIG_SCHEMA = cv.All( display.FULL_DISPLAY_SCHEMA.extend( { @@ -39,6 +45,8 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, cv.Optional(CONF_LED_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_COLOR_PALETTE, default="NONE"): COLOR_PALETTE, + cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), } ) .extend(cv.polling_component_schema("1s")) @@ -73,3 +81,13 @@ async def to_code(config): if CONF_LED_PIN in config: led_pin = await cg.gpio_pin_expression(config[CONF_LED_PIN]) cg.add(var.set_led_pin(led_pin)) + + if config[CONF_COLOR_PALETTE] == "GRAYSCALE": + cg.add(var.set_buffer_color_mode(ILI9341ColorMode.BITS_8_INDEXED)) + rhs = [] + for x in range(256): + rhs.extend([HexInt(x), HexInt(x), HexInt(x)]) + prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) + cg.add(var.set_palette(prog_arr)) + else: + pass diff --git a/esphome/components/ili9341/ili9341_display.cpp b/esphome/components/ili9341/ili9341_display.cpp index 09524ba787..0ad5446d9a 100644 --- a/esphome/components/ili9341/ili9341_display.cpp +++ b/esphome/components/ili9341/ili9341_display.cpp @@ -161,8 +161,13 @@ void HOT ILI9341Display::draw_absolute_pixel_internal(int x, int y, Color color) this->y_high_ = (y > this->y_high_) ? y : this->y_high_; uint32_t pos = (y * width_) + x; - uint8_t color332 = display::ColorUtil::color_to_332(color, display::ColorOrder::COLOR_ORDER_RGB); - buffer_[pos] = color332; + if (this->buffer_color_mode_ == BITS_8) { + uint8_t color332 = display::ColorUtil::color_to_332(color, display::ColorOrder::COLOR_ORDER_RGB); + buffer_[pos] = color332; + } else { // if (this->buffer_color_mode_ == BITS_8_INDEXED) { + uint8_t index = display::ColorUtil::color_to_index8_palette888(color, this->palette_); + buffer_[pos] = index; + } } // should return the total size: return this->get_width_internal() * this->get_height_internal() * 2 // 16bit color @@ -227,7 +232,13 @@ uint32_t ILI9341Display::buffer_to_transfer_(uint32_t pos, uint32_t sz) { } for (uint32_t i = 0; i < sz; ++i) { - uint16_t color = display::ColorUtil::color_to_565(display::ColorUtil::rgb332_to_color(*src++)); + uint16_t color; + if (this->buffer_color_mode_ == BITS_8) { + color = display::ColorUtil::color_to_565(display::ColorUtil::rgb332_to_color(*src++)); + } else { // if (this->buffer_color_mode == BITS_8_INDEXED) { + Color col = display::ColorUtil::index8_to_color_palette888(*src++, this->palette_); + color = display::ColorUtil::color_to_565(col); + } *dst++ = (uint8_t)(color >> 8); *dst++ = (uint8_t) color; } diff --git a/esphome/components/ili9341/ili9341_display.h b/esphome/components/ili9341/ili9341_display.h index eeff688f4f..6014dbf9a6 100644 --- a/esphome/components/ili9341/ili9341_display.h +++ b/esphome/components/ili9341/ili9341_display.h @@ -14,6 +14,11 @@ enum ILI9341Model { TFT_24, }; +enum ILI9341ColorMode { + BITS_8, + BITS_8_INDEXED, +}; + class ILI9341Display : public PollingComponent, public display::DisplayBuffer, public spi::SPIDevicereset_pin_ = reset; } void set_led_pin(GPIOPin *led) { this->led_pin_ = led; } void set_model(ILI9341Model model) { this->model_ = model; } + void set_palette(const uint8_t *palette) { this->palette_ = palette; } + void set_buffer_color_mode(ILI9341ColorMode color_mode) { this->buffer_color_mode_ = color_mode; } void command(uint8_t value); void data(uint8_t value); @@ -59,6 +66,9 @@ class ILI9341Display : public PollingComponent, uint16_t y_low_{0}; uint16_t x_high_{0}; uint16_t y_high_{0}; + const uint8_t *palette_; + + ILI9341ColorMode buffer_color_mode_{BITS_8}; uint32_t get_buffer_length_(); int get_width_internal() override; diff --git a/esphome/const.py b/esphome/const.py index c2aa53be70..b73d7e33bd 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -105,6 +105,7 @@ CONF_COLOR_BRIGHTNESS = "color_brightness" CONF_COLOR_CORRECT = "color_correct" CONF_COLOR_INTERLOCK = "color_interlock" CONF_COLOR_MODE = "color_mode" +CONF_COLOR_PALETTE = "color_palette" CONF_COLOR_TEMPERATURE = "color_temperature" CONF_COLORS = "colors" CONF_COMMAND = "command" From 78821056617a9acb35755b2f7a3bdf22965f5c2c Mon Sep 17 00:00:00 2001 From: user897943 Date: Wed, 18 May 2022 23:25:42 +0100 Subject: [PATCH 0485/1729] Update bedjet_const.h to remove blank spaces before speed steps, fixes Unknown Error when using climate.set_fan_mode in HA (#3476) --- esphome/components/bedjet/bedjet_const.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/bedjet/bedjet_const.h b/esphome/components/bedjet/bedjet_const.h index e6bfa45d3a..ae10ca1885 100644 --- a/esphome/components/bedjet/bedjet_const.h +++ b/esphome/components/bedjet/bedjet_const.h @@ -66,8 +66,8 @@ enum BedjetCommand : uint8_t { #define BEDJET_FAN_STEP_NAMES_ \ { \ - " 5%", " 10%", " 15%", " 20%", " 25%", " 30%", " 35%", " 40%", " 45%", " 50%", " 55%", " 60%", " 65%", " 70%", \ - " 75%", " 80%", " 85%", " 90%", " 95%", "100%" \ + "5%", "10%", "15%", "20%", "25%", "30%", "35%", "40%", "45%", "50%", "55%", "60%", "65%", "70%", "75%", "80%", \ + "85%", "90%", "95%", "100%" \ } static const char *const BEDJET_FAN_STEP_NAMES[20] = BEDJET_FAN_STEP_NAMES_; From 9c78049359711006cabf94bf02b2c1a46549777f Mon Sep 17 00:00:00 2001 From: myml Date: Thu, 19 May 2022 08:23:50 +0800 Subject: [PATCH 0486/1729] feat: esp32-camera add stream event (#3285) --- esphome/components/esp32_camera/__init__.py | 37 ++++++++++++++++++- .../components/esp32_camera/esp32_camera.cpp | 16 +++++++- .../components/esp32_camera/esp32_camera.h | 23 ++++++++++++ 3 files changed, 73 insertions(+), 3 deletions(-) diff --git a/esphome/components/esp32_camera/__init__.py b/esphome/components/esp32_camera/__init__.py index 912e705766..753b6ed9da 100644 --- a/esphome/components/esp32_camera/__init__.py +++ b/esphome/components/esp32_camera/__init__.py @@ -1,5 +1,6 @@ import esphome.codegen as cg import esphome.config_validation as cv +from esphome import automation from esphome import pins from esphome.const import ( CONF_FREQUENCY, @@ -12,6 +13,7 @@ from esphome.const import ( CONF_RESOLUTION, CONF_BRIGHTNESS, CONF_CONTRAST, + CONF_TRIGGER_ID, ) from esphome.core import CORE from esphome.components.esp32 import add_idf_sdkconfig_option @@ -23,7 +25,14 @@ AUTO_LOAD = ["psram"] esp32_camera_ns = cg.esphome_ns.namespace("esp32_camera") ESP32Camera = esp32_camera_ns.class_("ESP32Camera", cg.PollingComponent, cg.EntityBase) - +ESP32CameraStreamStartTrigger = esp32_camera_ns.class_( + "ESP32CameraStreamStartTrigger", + automation.Trigger.template(), +) +ESP32CameraStreamStopTrigger = esp32_camera_ns.class_( + "ESP32CameraStreamStopTrigger", + automation.Trigger.template(), +) ESP32CameraFrameSize = esp32_camera_ns.enum("ESP32CameraFrameSize") FRAME_SIZES = { "160X120": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_160X120, @@ -111,6 +120,10 @@ CONF_TEST_PATTERN = "test_pattern" CONF_MAX_FRAMERATE = "max_framerate" CONF_IDLE_FRAMERATE = "idle_framerate" +# stream trigger +CONF_ON_STREAM_START = "on_stream_start" +CONF_ON_STREAM_STOP = "on_stream_stop" + camera_range_param = cv.int_range(min=-2, max=2) CONFIG_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( @@ -178,6 +191,20 @@ CONFIG_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( cv.Optional(CONF_IDLE_FRAMERATE, default="0.1 fps"): cv.All( cv.framerate, cv.Range(min=0, max=1) ), + cv.Optional(CONF_ON_STREAM_START): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + ESP32CameraStreamStartTrigger + ), + } + ), + cv.Optional(CONF_ON_STREAM_STOP): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + ESP32CameraStreamStopTrigger + ), + } + ), } ).extend(cv.COMPONENT_SCHEMA) @@ -238,3 +265,11 @@ async def to_code(config): if CORE.using_esp_idf: cg.add_library("espressif/esp32-camera", "1.0.0") add_idf_sdkconfig_option("CONFIG_RTCIO_SUPPORT_RTC_GPIO_DESC", True) + + for conf in config.get(CONF_ON_STREAM_START, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + + for conf in config.get(CONF_ON_STREAM_STOP, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) diff --git a/esphome/components/esp32_camera/esp32_camera.cpp b/esphome/components/esp32_camera/esp32_camera.cpp index 851926b083..65b316dc8d 100644 --- a/esphome/components/esp32_camera/esp32_camera.cpp +++ b/esphome/components/esp32_camera/esp32_camera.cpp @@ -282,8 +282,20 @@ void ESP32Camera::set_idle_update_interval(uint32_t idle_update_interval) { void ESP32Camera::add_image_callback(std::function)> &&f) { this->new_image_callback_.add(std::move(f)); } -void ESP32Camera::start_stream(CameraRequester requester) { this->stream_requesters_ |= (1U << requester); } -void ESP32Camera::stop_stream(CameraRequester requester) { this->stream_requesters_ &= ~(1U << requester); } +void ESP32Camera::add_stream_start_callback(std::function &&callback) { + this->stream_start_callback_.add(std::move(callback)); +} +void ESP32Camera::add_stream_stop_callback(std::function &&callback) { + this->stream_stop_callback_.add(std::move(callback)); +} +void ESP32Camera::start_stream(CameraRequester requester) { + this->stream_start_callback_.call(); + this->stream_requesters_ |= (1U << requester); +} +void ESP32Camera::stop_stream(CameraRequester requester) { + this->stream_stop_callback_.call(); + this->stream_requesters_ &= ~(1U << requester); +} void ESP32Camera::request_image(CameraRequester requester) { this->single_requesters_ |= (1U << requester); } void ESP32Camera::update_camera_parameters() { sensor_t *s = esp_camera_sensor_get(); diff --git a/esphome/components/esp32_camera/esp32_camera.h b/esphome/components/esp32_camera/esp32_camera.h index 743b5bde5f..8bf73a0fa6 100644 --- a/esphome/components/esp32_camera/esp32_camera.h +++ b/esphome/components/esp32_camera/esp32_camera.h @@ -2,6 +2,7 @@ #ifdef USE_ESP32 +#include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/entity_base.h" #include "esphome/core/helpers.h" @@ -145,6 +146,9 @@ class ESP32Camera : public Component, public EntityBase { void request_image(CameraRequester requester); void update_camera_parameters(); + void add_stream_start_callback(std::function &&callback); + void add_stream_stop_callback(std::function &&callback); + protected: /* internal methods */ uint32_t hash_base() override; @@ -187,6 +191,8 @@ class ESP32Camera : public Component, public EntityBase { QueueHandle_t framebuffer_get_queue_; QueueHandle_t framebuffer_return_queue_; CallbackManager)> new_image_callback_; + CallbackManager stream_start_callback_{}; + CallbackManager stream_stop_callback_{}; uint32_t last_idle_request_{0}; uint32_t last_update_{0}; @@ -195,6 +201,23 @@ class ESP32Camera : public Component, public EntityBase { // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) extern ESP32Camera *global_esp32_camera; +class ESP32CameraStreamStartTrigger : public Trigger<> { + public: + explicit ESP32CameraStreamStartTrigger(ESP32Camera *parent) { + parent->add_stream_start_callback([this]() { this->trigger(); }); + } + + protected: +}; +class ESP32CameraStreamStopTrigger : public Trigger<> { + public: + explicit ESP32CameraStreamStopTrigger(ESP32Camera *parent) { + parent->add_stream_stop_callback([this]() { this->trigger(); }); + } + + protected: +}; + } // namespace esp32_camera } // namespace esphome From 0ed7db979be7057cd844033b37fe6f7079d16ed7 Mon Sep 17 00:00:00 2001 From: Martin <25747549+martgras@users.noreply.github.com> Date: Thu, 19 May 2022 02:47:33 +0200 Subject: [PATCH 0487/1729] Add support for SGP41 (#3382) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + .../sgp40/sensirion_voc_algorithm.cpp | 628 ------------------ .../sgp40/sensirion_voc_algorithm.h | 147 ---- esphome/components/sgp40/sensor.py | 68 +- esphome/components/sgp40/sgp40.cpp | 274 -------- esphome/components/sgp40/sgp40.h | 93 --- esphome/components/sgp4x/__init__.py | 0 esphome/components/sgp4x/sensor.py | 144 ++++ esphome/components/sgp4x/sgp4x.cpp | 343 ++++++++++ esphome/components/sgp4x/sgp4x.h | 142 ++++ platformio.ini | 2 + tests/test2.yaml | 23 +- 12 files changed, 655 insertions(+), 1210 deletions(-) delete mode 100644 esphome/components/sgp40/sensirion_voc_algorithm.cpp delete mode 100644 esphome/components/sgp40/sensirion_voc_algorithm.h delete mode 100644 esphome/components/sgp40/sgp40.cpp delete mode 100644 esphome/components/sgp40/sgp40.h create mode 100644 esphome/components/sgp4x/__init__.py create mode 100644 esphome/components/sgp4x/sensor.py create mode 100644 esphome/components/sgp4x/sgp4x.cpp create mode 100644 esphome/components/sgp4x/sgp4x.h diff --git a/CODEOWNERS b/CODEOWNERS index be6e8be3f7..3e82a372ce 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -178,6 +178,7 @@ esphome/components/sen5x/* @martgras esphome/components/sensirion_common/* @martgras esphome/components/sensor/* @esphome/core esphome/components/sgp40/* @SenexCrenshaw +esphome/components/sgp4x/* @SenexCrenshaw @martgras esphome/components/shelly_dimmer/* @edge90 @rnauber esphome/components/sht4x/* @sjtrny esphome/components/shutdown/* @esphome/core @jsuanet diff --git a/esphome/components/sgp40/sensirion_voc_algorithm.cpp b/esphome/components/sgp40/sensirion_voc_algorithm.cpp deleted file mode 100644 index d76b776641..0000000000 --- a/esphome/components/sgp40/sensirion_voc_algorithm.cpp +++ /dev/null @@ -1,628 +0,0 @@ - -#include "sensirion_voc_algorithm.h" - -namespace esphome { -namespace sgp40 { - -/* The VOC code were originally created by - * https://github.com/Sensirion/embedded-sgp - * The fixed point arithmetic parts of this code were originally created by - * https://github.com/PetteriAimonen/libfixmath - */ - -/*!< the maximum value of fix16_t */ -#define FIX16_MAXIMUM 0x7FFFFFFF -/*!< the minimum value of fix16_t */ -static const uint32_t FIX16_MINIMUM = 0x80000000; -/*!< the value used to indicate overflows when FIXMATH_NO_OVERFLOW is not - * specified */ -static const uint32_t FIX16_OVERFLOW = 0x80000000; -/*!< fix16_t value of 1 */ -const uint32_t FIX16_ONE = 0x00010000; - -inline fix16_t fix16_from_int(int32_t a) { return a * FIX16_ONE; } - -inline int32_t fix16_cast_to_int(fix16_t a) { return (a >> 16); } - -/*! Multiplies the two given fix16_t's and returns the result. */ -static fix16_t fix16_mul(fix16_t in_arg0, fix16_t in_arg1); - -/*! Divides the first given fix16_t by the second and returns the result. */ -static fix16_t fix16_div(fix16_t a, fix16_t b); - -/*! Returns the square root of the given fix16_t. */ -static fix16_t fix16_sqrt(fix16_t in_value); - -/*! Returns the exponent (e^) of the given fix16_t. */ -static fix16_t fix16_exp(fix16_t in_value); - -static fix16_t fix16_mul(fix16_t in_arg0, fix16_t in_arg1) { - // Each argument is divided to 16-bit parts. - // AB - // * CD - // ----------- - // BD 16 * 16 -> 32 bit products - // CB - // AD - // AC - // |----| 64 bit product - int32_t a = (in_arg0 >> 16), c = (in_arg1 >> 16); - uint32_t b = (in_arg0 & 0xFFFF), d = (in_arg1 & 0xFFFF); - - int32_t ac = a * c; - int32_t ad_cb = a * d + c * b; - uint32_t bd = b * d; - - int32_t product_hi = ac + (ad_cb >> 16); // NOLINT - - // Handle carry from lower 32 bits to upper part of result. - uint32_t ad_cb_temp = ad_cb << 16; // NOLINT - uint32_t product_lo = bd + ad_cb_temp; - if (product_lo < bd) - product_hi++; - -#ifndef FIXMATH_NO_OVERFLOW - // The upper 17 bits should all be the same (the sign). - if (product_hi >> 31 != product_hi >> 15) - return FIX16_OVERFLOW; -#endif - -#ifdef FIXMATH_NO_ROUNDING - return (product_hi << 16) | (product_lo >> 16); -#else - // Subtracting 0x8000 (= 0.5) and then using signed right shift - // achieves proper rounding to result-1, except in the corner - // case of negative numbers and lowest word = 0x8000. - // To handle that, we also have to subtract 1 for negative numbers. - uint32_t product_lo_tmp = product_lo; - product_lo -= 0x8000; - product_lo -= (uint32_t) product_hi >> 31; - if (product_lo > product_lo_tmp) - product_hi--; - - // Discard the lowest 16 bits. Note that this is not exactly the same - // as dividing by 0x10000. For example if product = -1, result will - // also be -1 and not 0. This is compensated by adding +1 to the result - // and compensating this in turn in the rounding above. - fix16_t result = (product_hi << 16) | (product_lo >> 16); // NOLINT - result += 1; - return result; -#endif -} - -static fix16_t fix16_div(fix16_t a, fix16_t b) { - // This uses the basic binary restoring division algorithm. - // It appears to be faster to do the whole division manually than - // trying to compose a 64-bit divide out of 32-bit divisions on - // platforms without hardware divide. - - if (b == 0) - return FIX16_MINIMUM; - - uint32_t remainder = (a >= 0) ? a : (-a); - uint32_t divider = (b >= 0) ? b : (-b); - - uint32_t quotient = 0; - uint32_t bit = 0x10000; - - /* The algorithm requires D >= R */ - while (divider < remainder) { - divider <<= 1; - bit <<= 1; - } - -#ifndef FIXMATH_NO_OVERFLOW - if (!bit) - return FIX16_OVERFLOW; -#endif - - if (divider & 0x80000000) { - // Perform one step manually to avoid overflows later. - // We know that divider's bottom bit is 0 here. - if (remainder >= divider) { - quotient |= bit; - remainder -= divider; - } - divider >>= 1; - bit >>= 1; - } - - /* Main division loop */ - while (bit && remainder) { - if (remainder >= divider) { - quotient |= bit; - remainder -= divider; - } - - remainder <<= 1; - bit >>= 1; - } - -#ifndef FIXMATH_NO_ROUNDING - if (remainder >= divider) { - quotient++; - } -#endif - - fix16_t result = quotient; - - /* Figure out the sign of result */ - if ((a ^ b) & 0x80000000) { -#ifndef FIXMATH_NO_OVERFLOW - if (result == FIX16_MINIMUM) // NOLINT(clang-diagnostic-sign-compare) - return FIX16_OVERFLOW; -#endif - - result = -result; - } - - return result; -} - -static fix16_t fix16_sqrt(fix16_t in_value) { - // It is assumed that x is not negative - - uint32_t num = in_value; - uint32_t result = 0; - uint32_t bit; - uint8_t n; - - bit = (uint32_t) 1 << 30; - while (bit > num) - bit >>= 2; - - // The main part is executed twice, in order to avoid - // using 64 bit values in computations. - for (n = 0; n < 2; n++) { - // First we get the top 24 bits of the answer. - while (bit) { - if (num >= result + bit) { - num -= result + bit; - result = (result >> 1) + bit; - } else { - result = (result >> 1); - } - bit >>= 2; - } - - if (n == 0) { - // Then process it again to get the lowest 8 bits. - if (num > 65535) { - // The remainder 'num' is too large to be shifted left - // by 16, so we have to add 1 to result manually and - // adjust 'num' accordingly. - // num = a - (result + 0.5)^2 - // = num + result^2 - (result + 0.5)^2 - // = num - result - 0.5 - num -= result; - num = (num << 16) - 0x8000; - result = (result << 16) + 0x8000; - } else { - num <<= 16; - result <<= 16; - } - - bit = 1 << 14; - } - } - -#ifndef FIXMATH_NO_ROUNDING - // Finally, if next bit would have been 1, round the result upwards. - if (num > result) { - result++; - } -#endif - - return (fix16_t) result; -} - -static fix16_t fix16_exp(fix16_t in_value) { - // Function to approximate exp(); optimized more for code size than speed - - // exp(x) for x = +/- {1, 1/8, 1/64, 1/512} - fix16_t x = in_value; - static const uint8_t NUM_EXP_VALUES = 4; - static const fix16_t EXP_POS_VALUES[4] = {F16(2.7182818), F16(1.1331485), F16(1.0157477), F16(1.0019550)}; - static const fix16_t EXP_NEG_VALUES[4] = {F16(0.3678794), F16(0.8824969), F16(0.9844964), F16(0.9980488)}; - const fix16_t *exp_values; - - fix16_t res, arg; - uint16_t i; - - if (x >= F16(10.3972)) - return FIX16_MAXIMUM; - if (x <= F16(-11.7835)) - return 0; - - if (x < 0) { - x = -x; - exp_values = EXP_NEG_VALUES; - } else { - exp_values = EXP_POS_VALUES; - } - - res = FIX16_ONE; - arg = FIX16_ONE; - for (i = 0; i < NUM_EXP_VALUES; i++) { - while (x >= arg) { - res = fix16_mul(res, exp_values[i]); - x -= arg; - } - arg >>= 3; - } - return res; -} - -static void voc_algorithm_init_instances(VocAlgorithmParams *params); -static void voc_algorithm_mean_variance_estimator_init(VocAlgorithmParams *params); -static void voc_algorithm_mean_variance_estimator_init_instances(VocAlgorithmParams *params); -static void voc_algorithm_mean_variance_estimator_set_parameters(VocAlgorithmParams *params, fix16_t std_initial, - fix16_t tau_mean_variance_hours, - fix16_t gating_max_duration_minutes); -static void voc_algorithm_mean_variance_estimator_set_states(VocAlgorithmParams *params, fix16_t mean, fix16_t std, - fix16_t uptime_gamma); -static fix16_t voc_algorithm_mean_variance_estimator_get_std(VocAlgorithmParams *params); -static fix16_t voc_algorithm_mean_variance_estimator_get_mean(VocAlgorithmParams *params); -static void voc_algorithm_mean_variance_estimator_calculate_gamma(VocAlgorithmParams *params, - fix16_t voc_index_from_prior); -static void voc_algorithm_mean_variance_estimator_process(VocAlgorithmParams *params, fix16_t sraw, - fix16_t voc_index_from_prior); -static void voc_algorithm_mean_variance_estimator_sigmoid_init(VocAlgorithmParams *params); -static void voc_algorithm_mean_variance_estimator_sigmoid_set_parameters(VocAlgorithmParams *params, fix16_t l, - fix16_t x0, fix16_t k); -static fix16_t voc_algorithm_mean_variance_estimator_sigmoid_process(VocAlgorithmParams *params, fix16_t sample); -static void voc_algorithm_mox_model_init(VocAlgorithmParams *params); -static void voc_algorithm_mox_model_set_parameters(VocAlgorithmParams *params, fix16_t sraw_std, fix16_t sraw_mean); -static fix16_t voc_algorithm_mox_model_process(VocAlgorithmParams *params, fix16_t sraw); -static void voc_algorithm_sigmoid_scaled_init(VocAlgorithmParams *params); -static void voc_algorithm_sigmoid_scaled_set_parameters(VocAlgorithmParams *params, fix16_t offset); -static fix16_t voc_algorithm_sigmoid_scaled_process(VocAlgorithmParams *params, fix16_t sample); -static void voc_algorithm_adaptive_lowpass_init(VocAlgorithmParams *params); -static void voc_algorithm_adaptive_lowpass_set_parameters(VocAlgorithmParams *params); -static fix16_t voc_algorithm_adaptive_lowpass_process(VocAlgorithmParams *params, fix16_t sample); - -void voc_algorithm_init(VocAlgorithmParams *params) { - params->mVoc_Index_Offset = F16(VOC_ALGORITHM_VOC_INDEX_OFFSET_DEFAULT); - params->mTau_Mean_Variance_Hours = F16(VOC_ALGORITHM_TAU_MEAN_VARIANCE_HOURS); - params->mGating_Max_Duration_Minutes = F16(VOC_ALGORITHM_GATING_MAX_DURATION_MINUTES); - params->mSraw_Std_Initial = F16(VOC_ALGORITHM_SRAW_STD_INITIAL); - params->mUptime = F16(0.); - params->mSraw = F16(0.); - params->mVoc_Index = 0; - voc_algorithm_init_instances(params); -} - -static void voc_algorithm_init_instances(VocAlgorithmParams *params) { - voc_algorithm_mean_variance_estimator_init(params); - voc_algorithm_mean_variance_estimator_set_parameters( - params, params->mSraw_Std_Initial, params->mTau_Mean_Variance_Hours, params->mGating_Max_Duration_Minutes); - voc_algorithm_mox_model_init(params); - voc_algorithm_mox_model_set_parameters(params, voc_algorithm_mean_variance_estimator_get_std(params), - voc_algorithm_mean_variance_estimator_get_mean(params)); - voc_algorithm_sigmoid_scaled_init(params); - voc_algorithm_sigmoid_scaled_set_parameters(params, params->mVoc_Index_Offset); - voc_algorithm_adaptive_lowpass_init(params); - voc_algorithm_adaptive_lowpass_set_parameters(params); -} - -void voc_algorithm_get_states(VocAlgorithmParams *params, int32_t *state0, int32_t *state1) { - *state0 = voc_algorithm_mean_variance_estimator_get_mean(params); - *state1 = voc_algorithm_mean_variance_estimator_get_std(params); -} - -void voc_algorithm_set_states(VocAlgorithmParams *params, int32_t state0, int32_t state1) { - voc_algorithm_mean_variance_estimator_set_states(params, state0, state1, F16(VOC_ALGORITHM_PERSISTENCE_UPTIME_GAMMA)); - params->mSraw = state0; -} - -void voc_algorithm_set_tuning_parameters(VocAlgorithmParams *params, int32_t voc_index_offset, - int32_t learning_time_hours, int32_t gating_max_duration_minutes, - int32_t std_initial) { - params->mVoc_Index_Offset = (fix16_from_int(voc_index_offset)); - params->mTau_Mean_Variance_Hours = (fix16_from_int(learning_time_hours)); - params->mGating_Max_Duration_Minutes = (fix16_from_int(gating_max_duration_minutes)); - params->mSraw_Std_Initial = (fix16_from_int(std_initial)); - voc_algorithm_init_instances(params); -} - -void voc_algorithm_process(VocAlgorithmParams *params, int32_t sraw, int32_t *voc_index) { - if ((params->mUptime <= F16(VOC_ALGORITHM_INITIAL_BLACKOUT))) { - params->mUptime = (params->mUptime + F16(VOC_ALGORITHM_SAMPLING_INTERVAL)); - } else { - if (((sraw > 0) && (sraw < 65000))) { - if ((sraw < 20001)) { - sraw = 20001; - } else if ((sraw > 52767)) { - sraw = 52767; - } - params->mSraw = (fix16_from_int((sraw - 20000))); - } - params->mVoc_Index = voc_algorithm_mox_model_process(params, params->mSraw); - params->mVoc_Index = voc_algorithm_sigmoid_scaled_process(params, params->mVoc_Index); - params->mVoc_Index = voc_algorithm_adaptive_lowpass_process(params, params->mVoc_Index); - if ((params->mVoc_Index < F16(0.5))) { - params->mVoc_Index = F16(0.5); - } - if ((params->mSraw > F16(0.))) { - voc_algorithm_mean_variance_estimator_process(params, params->mSraw, params->mVoc_Index); - voc_algorithm_mox_model_set_parameters(params, voc_algorithm_mean_variance_estimator_get_std(params), - voc_algorithm_mean_variance_estimator_get_mean(params)); - } - } - *voc_index = (fix16_cast_to_int((params->mVoc_Index + F16(0.5)))); -} - -static void voc_algorithm_mean_variance_estimator_init(VocAlgorithmParams *params) { - voc_algorithm_mean_variance_estimator_set_parameters(params, F16(0.), F16(0.), F16(0.)); - voc_algorithm_mean_variance_estimator_init_instances(params); -} - -static void voc_algorithm_mean_variance_estimator_init_instances(VocAlgorithmParams *params) { - voc_algorithm_mean_variance_estimator_sigmoid_init(params); -} - -static void voc_algorithm_mean_variance_estimator_set_parameters(VocAlgorithmParams *params, fix16_t std_initial, - fix16_t tau_mean_variance_hours, - fix16_t gating_max_duration_minutes) { - params->m_Mean_Variance_Estimator_Gating_Max_Duration_Minutes = gating_max_duration_minutes; - params->m_Mean_Variance_Estimator_Initialized = false; - params->m_Mean_Variance_Estimator_Mean = F16(0.); - params->m_Mean_Variance_Estimator_Sraw_Offset = F16(0.); - params->m_Mean_Variance_Estimator_Std = std_initial; - params->m_Mean_Variance_Estimator_Gamma = - (fix16_div(F16((VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_GAMMA_SCALING * (VOC_ALGORITHM_SAMPLING_INTERVAL / 3600.))), - (tau_mean_variance_hours + F16((VOC_ALGORITHM_SAMPLING_INTERVAL / 3600.))))); - params->m_Mean_Variance_Estimator_Gamma_Initial_Mean = - F16(((VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_GAMMA_SCALING * VOC_ALGORITHM_SAMPLING_INTERVAL) / - (VOC_ALGORITHM_TAU_INITIAL_MEAN + VOC_ALGORITHM_SAMPLING_INTERVAL))); - params->m_Mean_Variance_Estimator_Gamma_Initial_Variance = - F16(((VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_GAMMA_SCALING * VOC_ALGORITHM_SAMPLING_INTERVAL) / - (VOC_ALGORITHM_TAU_INITIAL_VARIANCE + VOC_ALGORITHM_SAMPLING_INTERVAL))); - params->m_Mean_Variance_Estimator_Gamma_Mean = F16(0.); - params->m_Mean_Variance_Estimator_Gamma_Variance = F16(0.); - params->m_Mean_Variance_Estimator_Uptime_Gamma = F16(0.); - params->m_Mean_Variance_Estimator_Uptime_Gating = F16(0.); - params->m_Mean_Variance_Estimator_Gating_Duration_Minutes = F16(0.); -} - -static void voc_algorithm_mean_variance_estimator_set_states(VocAlgorithmParams *params, fix16_t mean, fix16_t std, - fix16_t uptime_gamma) { - params->m_Mean_Variance_Estimator_Mean = mean; - params->m_Mean_Variance_Estimator_Std = std; - params->m_Mean_Variance_Estimator_Uptime_Gamma = uptime_gamma; - params->m_Mean_Variance_Estimator_Initialized = true; -} - -static fix16_t voc_algorithm_mean_variance_estimator_get_std(VocAlgorithmParams *params) { - return params->m_Mean_Variance_Estimator_Std; -} - -static fix16_t voc_algorithm_mean_variance_estimator_get_mean(VocAlgorithmParams *params) { - return (params->m_Mean_Variance_Estimator_Mean + params->m_Mean_Variance_Estimator_Sraw_Offset); -} - -static void voc_algorithm_mean_variance_estimator_calculate_gamma(VocAlgorithmParams *params, - fix16_t voc_index_from_prior) { - fix16_t uptime_limit; - fix16_t sigmoid_gamma_mean; - fix16_t gamma_mean; - fix16_t gating_threshold_mean; - fix16_t sigmoid_gating_mean; - fix16_t sigmoid_gamma_variance; - fix16_t gamma_variance; - fix16_t gating_threshold_variance; - fix16_t sigmoid_gating_variance; - - uptime_limit = F16((VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_FI_X16_MAX - VOC_ALGORITHM_SAMPLING_INTERVAL)); - if ((params->m_Mean_Variance_Estimator_Uptime_Gamma < uptime_limit)) { - params->m_Mean_Variance_Estimator_Uptime_Gamma = - (params->m_Mean_Variance_Estimator_Uptime_Gamma + F16(VOC_ALGORITHM_SAMPLING_INTERVAL)); - } - if ((params->m_Mean_Variance_Estimator_Uptime_Gating < uptime_limit)) { - params->m_Mean_Variance_Estimator_Uptime_Gating = - (params->m_Mean_Variance_Estimator_Uptime_Gating + F16(VOC_ALGORITHM_SAMPLING_INTERVAL)); - } - voc_algorithm_mean_variance_estimator_sigmoid_set_parameters(params, F16(1.), F16(VOC_ALGORITHM_INIT_DURATION_MEAN), - F16(VOC_ALGORITHM_INIT_TRANSITION_MEAN)); - sigmoid_gamma_mean = - voc_algorithm_mean_variance_estimator_sigmoid_process(params, params->m_Mean_Variance_Estimator_Uptime_Gamma); - gamma_mean = - (params->m_Mean_Variance_Estimator_Gamma + - (fix16_mul((params->m_Mean_Variance_Estimator_Gamma_Initial_Mean - params->m_Mean_Variance_Estimator_Gamma), - sigmoid_gamma_mean))); - gating_threshold_mean = (F16(VOC_ALGORITHM_GATING_THRESHOLD) + - (fix16_mul(F16((VOC_ALGORITHM_GATING_THRESHOLD_INITIAL - VOC_ALGORITHM_GATING_THRESHOLD)), - voc_algorithm_mean_variance_estimator_sigmoid_process( - params, params->m_Mean_Variance_Estimator_Uptime_Gating)))); - voc_algorithm_mean_variance_estimator_sigmoid_set_parameters(params, F16(1.), gating_threshold_mean, - F16(VOC_ALGORITHM_GATING_THRESHOLD_TRANSITION)); - sigmoid_gating_mean = voc_algorithm_mean_variance_estimator_sigmoid_process(params, voc_index_from_prior); - params->m_Mean_Variance_Estimator_Gamma_Mean = (fix16_mul(sigmoid_gating_mean, gamma_mean)); - voc_algorithm_mean_variance_estimator_sigmoid_set_parameters( - params, F16(1.), F16(VOC_ALGORITHM_INIT_DURATION_VARIANCE), F16(VOC_ALGORITHM_INIT_TRANSITION_VARIANCE)); - sigmoid_gamma_variance = - voc_algorithm_mean_variance_estimator_sigmoid_process(params, params->m_Mean_Variance_Estimator_Uptime_Gamma); - gamma_variance = - (params->m_Mean_Variance_Estimator_Gamma + - (fix16_mul((params->m_Mean_Variance_Estimator_Gamma_Initial_Variance - params->m_Mean_Variance_Estimator_Gamma), - (sigmoid_gamma_variance - sigmoid_gamma_mean)))); - gating_threshold_variance = - (F16(VOC_ALGORITHM_GATING_THRESHOLD) + - (fix16_mul(F16((VOC_ALGORITHM_GATING_THRESHOLD_INITIAL - VOC_ALGORITHM_GATING_THRESHOLD)), - voc_algorithm_mean_variance_estimator_sigmoid_process( - params, params->m_Mean_Variance_Estimator_Uptime_Gating)))); - voc_algorithm_mean_variance_estimator_sigmoid_set_parameters(params, F16(1.), gating_threshold_variance, - F16(VOC_ALGORITHM_GATING_THRESHOLD_TRANSITION)); - sigmoid_gating_variance = voc_algorithm_mean_variance_estimator_sigmoid_process(params, voc_index_from_prior); - params->m_Mean_Variance_Estimator_Gamma_Variance = (fix16_mul(sigmoid_gating_variance, gamma_variance)); - params->m_Mean_Variance_Estimator_Gating_Duration_Minutes = - (params->m_Mean_Variance_Estimator_Gating_Duration_Minutes + - (fix16_mul(F16((VOC_ALGORITHM_SAMPLING_INTERVAL / 60.)), - ((fix16_mul((F16(1.) - sigmoid_gating_mean), F16((1. + VOC_ALGORITHM_GATING_MAX_RATIO)))) - - F16(VOC_ALGORITHM_GATING_MAX_RATIO))))); - if ((params->m_Mean_Variance_Estimator_Gating_Duration_Minutes < F16(0.))) { - params->m_Mean_Variance_Estimator_Gating_Duration_Minutes = F16(0.); - } - if ((params->m_Mean_Variance_Estimator_Gating_Duration_Minutes > - params->m_Mean_Variance_Estimator_Gating_Max_Duration_Minutes)) { - params->m_Mean_Variance_Estimator_Uptime_Gating = F16(0.); - } -} - -static void voc_algorithm_mean_variance_estimator_process(VocAlgorithmParams *params, fix16_t sraw, - fix16_t voc_index_from_prior) { - fix16_t delta_sgp; - fix16_t c; - fix16_t additional_scaling; - - if ((!params->m_Mean_Variance_Estimator_Initialized)) { - params->m_Mean_Variance_Estimator_Initialized = true; - params->m_Mean_Variance_Estimator_Sraw_Offset = sraw; - params->m_Mean_Variance_Estimator_Mean = F16(0.); - } else { - if (((params->m_Mean_Variance_Estimator_Mean >= F16(100.)) || - (params->m_Mean_Variance_Estimator_Mean <= F16(-100.)))) { - params->m_Mean_Variance_Estimator_Sraw_Offset = - (params->m_Mean_Variance_Estimator_Sraw_Offset + params->m_Mean_Variance_Estimator_Mean); - params->m_Mean_Variance_Estimator_Mean = F16(0.); - } - sraw = (sraw - params->m_Mean_Variance_Estimator_Sraw_Offset); - voc_algorithm_mean_variance_estimator_calculate_gamma(params, voc_index_from_prior); - delta_sgp = (fix16_div((sraw - params->m_Mean_Variance_Estimator_Mean), - F16(VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_GAMMA_SCALING))); - if ((delta_sgp < F16(0.))) { - c = (params->m_Mean_Variance_Estimator_Std - delta_sgp); - } else { - c = (params->m_Mean_Variance_Estimator_Std + delta_sgp); - } - additional_scaling = F16(1.); - if ((c > F16(1440.))) { - additional_scaling = F16(4.); - } - params->m_Mean_Variance_Estimator_Std = (fix16_mul( - fix16_sqrt((fix16_mul(additional_scaling, (F16(VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_GAMMA_SCALING) - - params->m_Mean_Variance_Estimator_Gamma_Variance)))), - fix16_sqrt(((fix16_mul(params->m_Mean_Variance_Estimator_Std, - (fix16_div(params->m_Mean_Variance_Estimator_Std, - (fix16_mul(F16(VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_GAMMA_SCALING), - additional_scaling)))))) + - (fix16_mul((fix16_div((fix16_mul(params->m_Mean_Variance_Estimator_Gamma_Variance, delta_sgp)), - additional_scaling)), - delta_sgp)))))); - params->m_Mean_Variance_Estimator_Mean = - (params->m_Mean_Variance_Estimator_Mean + (fix16_mul(params->m_Mean_Variance_Estimator_Gamma_Mean, delta_sgp))); - } -} - -static void voc_algorithm_mean_variance_estimator_sigmoid_init(VocAlgorithmParams *params) { - voc_algorithm_mean_variance_estimator_sigmoid_set_parameters(params, F16(0.), F16(0.), F16(0.)); -} - -static void voc_algorithm_mean_variance_estimator_sigmoid_set_parameters(VocAlgorithmParams *params, fix16_t l, - fix16_t x0, fix16_t k) { - params->m_Mean_Variance_Estimator_Sigmoid_L = l; - params->m_Mean_Variance_Estimator_Sigmoid_K = k; - params->m_Mean_Variance_Estimator_Sigmoid_X0 = x0; -} - -static fix16_t voc_algorithm_mean_variance_estimator_sigmoid_process(VocAlgorithmParams *params, fix16_t sample) { - fix16_t x; - - x = (fix16_mul(params->m_Mean_Variance_Estimator_Sigmoid_K, (sample - params->m_Mean_Variance_Estimator_Sigmoid_X0))); - if ((x < F16(-50.))) { - return params->m_Mean_Variance_Estimator_Sigmoid_L; - } else if ((x > F16(50.))) { - return F16(0.); - } else { - return (fix16_div(params->m_Mean_Variance_Estimator_Sigmoid_L, (F16(1.) + fix16_exp(x)))); - } -} - -static void voc_algorithm_mox_model_init(VocAlgorithmParams *params) { - voc_algorithm_mox_model_set_parameters(params, F16(1.), F16(0.)); -} - -static void voc_algorithm_mox_model_set_parameters(VocAlgorithmParams *params, fix16_t sraw_std, fix16_t sraw_mean) { - params->m_Mox_Model_Sraw_Std = sraw_std; - params->m_Mox_Model_Sraw_Mean = sraw_mean; -} - -static fix16_t voc_algorithm_mox_model_process(VocAlgorithmParams *params, fix16_t sraw) { - return (fix16_mul((fix16_div((sraw - params->m_Mox_Model_Sraw_Mean), - (-(params->m_Mox_Model_Sraw_Std + F16(VOC_ALGORITHM_SRAW_STD_BONUS))))), - F16(VOC_ALGORITHM_VOC_INDEX_GAIN))); -} - -static void voc_algorithm_sigmoid_scaled_init(VocAlgorithmParams *params) { - voc_algorithm_sigmoid_scaled_set_parameters(params, F16(0.)); -} - -static void voc_algorithm_sigmoid_scaled_set_parameters(VocAlgorithmParams *params, fix16_t offset) { - params->m_Sigmoid_Scaled_Offset = offset; -} - -static fix16_t voc_algorithm_sigmoid_scaled_process(VocAlgorithmParams *params, fix16_t sample) { - fix16_t x; - fix16_t shift; - - x = (fix16_mul(F16(VOC_ALGORITHM_SIGMOID_K), (sample - F16(VOC_ALGORITHM_SIGMOID_X0)))); - if ((x < F16(-50.))) { - return F16(VOC_ALGORITHM_SIGMOID_L); - } else if ((x > F16(50.))) { - return F16(0.); - } else { - if ((sample >= F16(0.))) { - shift = - (fix16_div((F16(VOC_ALGORITHM_SIGMOID_L) - (fix16_mul(F16(5.), params->m_Sigmoid_Scaled_Offset))), F16(4.))); - return ((fix16_div((F16(VOC_ALGORITHM_SIGMOID_L) + shift), (F16(1.) + fix16_exp(x)))) - shift); - } else { - return (fix16_mul((fix16_div(params->m_Sigmoid_Scaled_Offset, F16(VOC_ALGORITHM_VOC_INDEX_OFFSET_DEFAULT))), - (fix16_div(F16(VOC_ALGORITHM_SIGMOID_L), (F16(1.) + fix16_exp(x)))))); - } - } -} - -static void voc_algorithm_adaptive_lowpass_init(VocAlgorithmParams *params) { - voc_algorithm_adaptive_lowpass_set_parameters(params); -} - -static void voc_algorithm_adaptive_lowpass_set_parameters(VocAlgorithmParams *params) { - params->m_Adaptive_Lowpass_A1 = - F16((VOC_ALGORITHM_SAMPLING_INTERVAL / (VOC_ALGORITHM_LP_TAU_FAST + VOC_ALGORITHM_SAMPLING_INTERVAL))); - params->m_Adaptive_Lowpass_A2 = - F16((VOC_ALGORITHM_SAMPLING_INTERVAL / (VOC_ALGORITHM_LP_TAU_SLOW + VOC_ALGORITHM_SAMPLING_INTERVAL))); - params->m_Adaptive_Lowpass_Initialized = false; -} - -static fix16_t voc_algorithm_adaptive_lowpass_process(VocAlgorithmParams *params, fix16_t sample) { - fix16_t abs_delta; - fix16_t f1; - fix16_t tau_a; - fix16_t a3; - - if ((!params->m_Adaptive_Lowpass_Initialized)) { - params->m_Adaptive_Lowpass_X1 = sample; - params->m_Adaptive_Lowpass_X2 = sample; - params->m_Adaptive_Lowpass_X3 = sample; - params->m_Adaptive_Lowpass_Initialized = true; - } - params->m_Adaptive_Lowpass_X1 = - ((fix16_mul((F16(1.) - params->m_Adaptive_Lowpass_A1), params->m_Adaptive_Lowpass_X1)) + - (fix16_mul(params->m_Adaptive_Lowpass_A1, sample))); - params->m_Adaptive_Lowpass_X2 = - ((fix16_mul((F16(1.) - params->m_Adaptive_Lowpass_A2), params->m_Adaptive_Lowpass_X2)) + - (fix16_mul(params->m_Adaptive_Lowpass_A2, sample))); - abs_delta = (params->m_Adaptive_Lowpass_X1 - params->m_Adaptive_Lowpass_X2); - if ((abs_delta < F16(0.))) { - abs_delta = (-abs_delta); - } - f1 = fix16_exp((fix16_mul(F16(VOC_ALGORITHM_LP_ALPHA), abs_delta))); - tau_a = - ((fix16_mul(F16((VOC_ALGORITHM_LP_TAU_SLOW - VOC_ALGORITHM_LP_TAU_FAST)), f1)) + F16(VOC_ALGORITHM_LP_TAU_FAST)); - a3 = (fix16_div(F16(VOC_ALGORITHM_SAMPLING_INTERVAL), (F16(VOC_ALGORITHM_SAMPLING_INTERVAL) + tau_a))); - params->m_Adaptive_Lowpass_X3 = - ((fix16_mul((F16(1.) - a3), params->m_Adaptive_Lowpass_X3)) + (fix16_mul(a3, sample))); - return params->m_Adaptive_Lowpass_X3; -} -} // namespace sgp40 -} // namespace esphome diff --git a/esphome/components/sgp40/sensirion_voc_algorithm.h b/esphome/components/sgp40/sensirion_voc_algorithm.h deleted file mode 100644 index adef6b29e8..0000000000 --- a/esphome/components/sgp40/sensirion_voc_algorithm.h +++ /dev/null @@ -1,147 +0,0 @@ -#pragma once -#include -namespace esphome { -namespace sgp40 { - -/* The VOC code were originally created by - * https://github.com/Sensirion/embedded-sgp - * The fixed point arithmetic parts of this code were originally created by - * https://github.com/PetteriAimonen/libfixmath - */ - -using fix16_t = int32_t; - -#define F16(x) ((fix16_t)(((x) >= 0) ? ((x) *65536.0 + 0.5) : ((x) *65536.0 - 0.5))) - -static const float VOC_ALGORITHM_SAMPLING_INTERVAL(1.); -static const float VOC_ALGORITHM_INITIAL_BLACKOUT(45.); -static const float VOC_ALGORITHM_VOC_INDEX_GAIN(230.); -static const float VOC_ALGORITHM_SRAW_STD_INITIAL(50.); -static const float VOC_ALGORITHM_SRAW_STD_BONUS(220.); -static const float VOC_ALGORITHM_TAU_MEAN_VARIANCE_HOURS(12.); -static const float VOC_ALGORITHM_TAU_INITIAL_MEAN(20.); -static const float VOC_ALGORITHM_INIT_DURATION_MEAN((3600. * 0.75)); -static const float VOC_ALGORITHM_INIT_TRANSITION_MEAN(0.01); -static const float VOC_ALGORITHM_TAU_INITIAL_VARIANCE(2500.); -static const float VOC_ALGORITHM_INIT_DURATION_VARIANCE((3600. * 1.45)); -static const float VOC_ALGORITHM_INIT_TRANSITION_VARIANCE(0.01); -static const float VOC_ALGORITHM_GATING_THRESHOLD(340.); -static const float VOC_ALGORITHM_GATING_THRESHOLD_INITIAL(510.); -static const float VOC_ALGORITHM_GATING_THRESHOLD_TRANSITION(0.09); -static const float VOC_ALGORITHM_GATING_MAX_DURATION_MINUTES((60. * 3.)); -static const float VOC_ALGORITHM_GATING_MAX_RATIO(0.3); -static const float VOC_ALGORITHM_SIGMOID_L(500.); -static const float VOC_ALGORITHM_SIGMOID_K(-0.0065); -static const float VOC_ALGORITHM_SIGMOID_X0(213.); -static const float VOC_ALGORITHM_VOC_INDEX_OFFSET_DEFAULT(100.); -static const float VOC_ALGORITHM_LP_TAU_FAST(20.0); -static const float VOC_ALGORITHM_LP_TAU_SLOW(500.0); -static const float VOC_ALGORITHM_LP_ALPHA(-0.2); -static const float VOC_ALGORITHM_PERSISTENCE_UPTIME_GAMMA((3. * 3600.)); -static const float VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_GAMMA_SCALING(64.); -static const float VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_FI_X16_MAX(32767.); - -/** - * Struct to hold all the states of the VOC algorithm. - */ -struct VocAlgorithmParams { - fix16_t mVoc_Index_Offset; - fix16_t mTau_Mean_Variance_Hours; - fix16_t mGating_Max_Duration_Minutes; - fix16_t mSraw_Std_Initial; - fix16_t mUptime; - fix16_t mSraw; - fix16_t mVoc_Index; - fix16_t m_Mean_Variance_Estimator_Gating_Max_Duration_Minutes; - bool m_Mean_Variance_Estimator_Initialized; - fix16_t m_Mean_Variance_Estimator_Mean; - fix16_t m_Mean_Variance_Estimator_Sraw_Offset; - fix16_t m_Mean_Variance_Estimator_Std; - fix16_t m_Mean_Variance_Estimator_Gamma; - fix16_t m_Mean_Variance_Estimator_Gamma_Initial_Mean; - fix16_t m_Mean_Variance_Estimator_Gamma_Initial_Variance; - fix16_t m_Mean_Variance_Estimator_Gamma_Mean; - fix16_t m_Mean_Variance_Estimator_Gamma_Variance; - fix16_t m_Mean_Variance_Estimator_Uptime_Gamma; - fix16_t m_Mean_Variance_Estimator_Uptime_Gating; - fix16_t m_Mean_Variance_Estimator_Gating_Duration_Minutes; - fix16_t m_Mean_Variance_Estimator_Sigmoid_L; - fix16_t m_Mean_Variance_Estimator_Sigmoid_K; - fix16_t m_Mean_Variance_Estimator_Sigmoid_X0; - fix16_t m_Mox_Model_Sraw_Std; - fix16_t m_Mox_Model_Sraw_Mean; - fix16_t m_Sigmoid_Scaled_Offset; - fix16_t m_Adaptive_Lowpass_A1; - fix16_t m_Adaptive_Lowpass_A2; - bool m_Adaptive_Lowpass_Initialized; - fix16_t m_Adaptive_Lowpass_X1; - fix16_t m_Adaptive_Lowpass_X2; - fix16_t m_Adaptive_Lowpass_X3; -}; - -/** - * Initialize the VOC algorithm parameters. Call this once at the beginning or - * whenever the sensor stopped measurements. - * @param params Pointer to the VocAlgorithmParams struct - */ -void voc_algorithm_init(VocAlgorithmParams *params); - -/** - * Get current algorithm states. Retrieved values can be used in - * voc_algorithm_set_states() to resume operation after a short interruption, - * skipping initial learning phase. This feature can only be used after at least - * 3 hours of continuous operation. - * @param params Pointer to the VocAlgorithmParams struct - * @param state0 State0 to be stored - * @param state1 State1 to be stored - */ -void voc_algorithm_get_states(VocAlgorithmParams *params, int32_t *state0, int32_t *state1); - -/** - * Set previously retrieved algorithm states to resume operation after a short - * interruption, skipping initial learning phase. This feature should not be - * used after inerruptions of more than 10 minutes. Call this once after - * voc_algorithm_init() and the optional voc_algorithm_set_tuning_parameters(), if - * desired. Otherwise, the algorithm will start with initial learning phase. - * @param params Pointer to the VocAlgorithmParams struct - * @param state0 State0 to be restored - * @param state1 State1 to be restored - */ -void voc_algorithm_set_states(VocAlgorithmParams *params, int32_t state0, int32_t state1); - -/** - * Set parameters to customize the VOC algorithm. Call this once after - * voc_algorithm_init(), if desired. Otherwise, the default values will be used. - * - * @param params Pointer to the VocAlgorithmParams struct - * @param voc_index_offset VOC index representing typical (average) - * conditions. Range 1..250, default 100 - * @param learning_time_hours Time constant of long-term estimator. - * Past events will be forgotten after about - * twice the learning time. - * Range 1..72 [hours], default 12 [hours] - * @param gating_max_duration_minutes Maximum duration of gating (freeze of - * estimator during high VOC index signal). - * 0 (no gating) or range 1..720 [minutes], - * default 180 [minutes] - * @param std_initial Initial estimate for standard deviation. - * Lower value boosts events during initial - * learning period, but may result in larger - * device-to-device variations. - * Range 10..500, default 50 - */ -void voc_algorithm_set_tuning_parameters(VocAlgorithmParams *params, int32_t voc_index_offset, - int32_t learning_time_hours, int32_t gating_max_duration_minutes, - int32_t std_initial); - -/** - * Calculate the VOC index value from the raw sensor value. - * - * @param params Pointer to the VocAlgorithmParams struct - * @param sraw Raw value from the SGP40 sensor - * @param voc_index Calculated VOC index value from the raw sensor value. Zero - * during initial blackout period and 1..500 afterwards - */ -void voc_algorithm_process(VocAlgorithmParams *params, int32_t sraw, int32_t *voc_index); -} // namespace sgp40 -} // namespace esphome diff --git a/esphome/components/sgp40/sensor.py b/esphome/components/sgp40/sensor.py index ee267d6062..cb4231c168 100644 --- a/esphome/components/sgp40/sensor.py +++ b/esphome/components/sgp40/sensor.py @@ -1,70 +1,8 @@ -import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import i2c, sensor, sensirion_common - -from esphome.const import ( - CONF_STORE_BASELINE, - CONF_TEMPERATURE_SOURCE, - ICON_RADIATOR, - DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, - STATE_CLASS_MEASUREMENT, -) - -DEPENDENCIES = ["i2c"] -AUTO_LOAD = ["sensirion_common"] CODEOWNERS = ["@SenexCrenshaw"] -sgp40_ns = cg.esphome_ns.namespace("sgp40") -SGP40Component = sgp40_ns.class_( - "SGP40Component", - sensor.Sensor, - cg.PollingComponent, - sensirion_common.SensirionI2CDevice, +CONFIG_SCHEMA = CONFIG_SCHEMA = cv.invalid( + "SGP40 is deprecated.\nPlease use the SGP4x platform instead.\nSGP4x supports both SPG40 and SGP41.\n" + " See https://esphome.io/components/sensor/sgp4x.html" ) - -CONF_COMPENSATION = "compensation" -CONF_HUMIDITY_SOURCE = "humidity_source" -CONF_VOC_BASELINE = "voc_baseline" - -CONFIG_SCHEMA = ( - sensor.sensor_schema( - SGP40Component, - icon=ICON_RADIATOR, - accuracy_decimals=0, - device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, - state_class=STATE_CLASS_MEASUREMENT, - ) - .extend( - { - cv.Optional(CONF_STORE_BASELINE, default=True): cv.boolean, - cv.Optional(CONF_VOC_BASELINE): cv.hex_uint16_t, - cv.Optional(CONF_COMPENSATION): cv.Schema( - { - cv.Required(CONF_HUMIDITY_SOURCE): cv.use_id(sensor.Sensor), - cv.Required(CONF_TEMPERATURE_SOURCE): cv.use_id(sensor.Sensor), - }, - ), - } - ) - .extend(cv.polling_component_schema("60s")) - .extend(i2c.i2c_device_schema(0x59)) -) - - -async def to_code(config): - var = await sensor.new_sensor(config) - await cg.register_component(var, config) - await i2c.register_i2c_device(var, config) - - if CONF_COMPENSATION in config: - compensation_config = config[CONF_COMPENSATION] - sens = await cg.get_variable(compensation_config[CONF_HUMIDITY_SOURCE]) - cg.add(var.set_humidity_sensor(sens)) - sens = await cg.get_variable(compensation_config[CONF_TEMPERATURE_SOURCE]) - cg.add(var.set_temperature_sensor(sens)) - - cg.add(var.set_store_baseline(config[CONF_STORE_BASELINE])) - - if CONF_VOC_BASELINE in config: - cg.add(var.set_voc_baseline(CONF_VOC_BASELINE)) diff --git a/esphome/components/sgp40/sgp40.cpp b/esphome/components/sgp40/sgp40.cpp deleted file mode 100644 index 9d78572b50..0000000000 --- a/esphome/components/sgp40/sgp40.cpp +++ /dev/null @@ -1,274 +0,0 @@ -#include "sgp40.h" -#include "esphome/core/log.h" -#include "esphome/core/hal.h" -#include - -namespace esphome { -namespace sgp40 { - -static const char *const TAG = "sgp40"; - -void SGP40Component::setup() { - ESP_LOGCONFIG(TAG, "Setting up SGP40..."); - - // Serial Number identification - if (!this->write_command(SGP40_CMD_GET_SERIAL_ID)) { - this->error_code_ = COMMUNICATION_FAILED; - this->mark_failed(); - return; - } - uint16_t raw_serial_number[3]; - - if (!this->read_data(raw_serial_number, 3)) { - this->mark_failed(); - return; - } - this->serial_number_ = (uint64_t(raw_serial_number[0]) << 24) | (uint64_t(raw_serial_number[1]) << 16) | - (uint64_t(raw_serial_number[2])); - ESP_LOGD(TAG, "Serial Number: %" PRIu64, this->serial_number_); - - // Featureset identification for future use - if (!this->write_command(SGP40_CMD_GET_FEATURESET)) { - ESP_LOGD(TAG, "raw_featureset write_command_ failed"); - this->mark_failed(); - return; - } - uint16_t raw_featureset; - if (!this->read_data(raw_featureset)) { - ESP_LOGD(TAG, "raw_featureset read_data_ failed"); - this->mark_failed(); - return; - } - - this->featureset_ = raw_featureset; - if ((this->featureset_ & 0x1FF) != SGP40_FEATURESET) { - ESP_LOGD(TAG, "Product feature set failed 0x%0X , expecting 0x%0X", uint16_t(this->featureset_ & 0x1FF), - SGP40_FEATURESET); - this->mark_failed(); - return; - } - - ESP_LOGD(TAG, "Product version: 0x%0X", uint16_t(this->featureset_ & 0x1FF)); - - voc_algorithm_init(&this->voc_algorithm_params_); - - if (this->store_baseline_) { - // Hash with compilation time - // This ensures the baseline storage is cleared after OTA - uint32_t hash = fnv1_hash(App.get_compilation_time()); - this->pref_ = global_preferences->make_preference(hash, true); - - if (this->pref_.load(&this->baselines_storage_)) { - this->state0_ = this->baselines_storage_.state0; - this->state1_ = this->baselines_storage_.state1; - ESP_LOGI(TAG, "Loaded VOC baseline state0: 0x%04X, state1: 0x%04X", this->baselines_storage_.state0, - baselines_storage_.state1); - } - - // Initialize storage timestamp - this->seconds_since_last_store_ = 0; - - if (this->baselines_storage_.state0 > 0 && this->baselines_storage_.state1 > 0) { - ESP_LOGI(TAG, "Setting VOC baseline from save state0: 0x%04X, state1: 0x%04X", this->baselines_storage_.state0, - baselines_storage_.state1); - voc_algorithm_set_states(&this->voc_algorithm_params_, this->baselines_storage_.state0, - this->baselines_storage_.state1); - } - } - - this->self_test_(); - - /* The official spec for this sensor at https://docs.rs-online.com/1956/A700000007055193.pdf - indicates this sensor should be driven at 1Hz. Comments from the developers at: - https://github.com/Sensirion/embedded-sgp/issues/136 indicate the algorithm should be a bit - resilient to slight timing variations so the software timer should be accurate enough for - this. - - This block starts sampling from the sensor at 1Hz, and is done seperately from the call - to the update method. This seperation is to support getting accurate measurements but - limit the amount of communication done over wifi for power consumption or to keep the - number of records reported from being overwhelming. - */ - ESP_LOGD(TAG, "Component requires sampling of 1Hz, setting up background sampler"); - this->set_interval(1000, [this]() { this->update_voc_index(); }); -} - -void SGP40Component::self_test_() { - ESP_LOGD(TAG, "Self-test started"); - if (!this->write_command(SGP40_CMD_SELF_TEST)) { - this->error_code_ = COMMUNICATION_FAILED; - ESP_LOGD(TAG, "Self-test communication failed"); - this->mark_failed(); - } - - this->set_timeout(250, [this]() { - uint16_t reply; - if (!this->read_data(reply)) { - ESP_LOGD(TAG, "Self-test read_data_ failed"); - this->mark_failed(); - return; - } - - if (reply == 0xD400) { - this->self_test_complete_ = true; - ESP_LOGD(TAG, "Self-test completed"); - return; - } - - ESP_LOGD(TAG, "Self-test failed"); - this->mark_failed(); - }); -} - -/** - * @brief Combined the measured gasses, temperature, and humidity - * to calculate the VOC Index - * - * @param temperature The measured temperature in degrees C - * @param humidity The measured relative humidity in % rH - * @return int32_t The VOC Index - */ -int32_t SGP40Component::measure_voc_index_() { - int32_t voc_index; - - uint16_t sraw = measure_raw_(); - - if (sraw == UINT16_MAX) - return UINT16_MAX; - - this->status_clear_warning(); - - voc_algorithm_process(&voc_algorithm_params_, sraw, &voc_index); - - // Store baselines after defined interval or if the difference between current and stored baseline becomes too - // much - if (this->store_baseline_ && this->seconds_since_last_store_ > SHORTEST_BASELINE_STORE_INTERVAL) { - voc_algorithm_get_states(&voc_algorithm_params_, &this->state0_, &this->state1_); - if ((uint32_t) abs(this->baselines_storage_.state0 - this->state0_) > MAXIMUM_STORAGE_DIFF || - (uint32_t) abs(this->baselines_storage_.state1 - this->state1_) > MAXIMUM_STORAGE_DIFF) { - this->seconds_since_last_store_ = 0; - this->baselines_storage_.state0 = this->state0_; - this->baselines_storage_.state1 = this->state1_; - - if (this->pref_.save(&this->baselines_storage_)) { - ESP_LOGI(TAG, "Stored VOC baseline state0: 0x%04X ,state1: 0x%04X", this->baselines_storage_.state0, - baselines_storage_.state1); - } else { - ESP_LOGW(TAG, "Could not store VOC baselines"); - } - } - } - - return voc_index; -} - -/** - * @brief Return the raw gas measurement - * - * @param temperature The measured temperature in degrees C - * @param humidity The measured relative humidity in % rH - * @return uint16_t The current raw gas measurement - */ -uint16_t SGP40Component::measure_raw_() { - float humidity = NAN; - - if (!this->self_test_complete_) { - ESP_LOGD(TAG, "Self-test not yet complete"); - return UINT16_MAX; - } - - if (this->humidity_sensor_ != nullptr) { - humidity = this->humidity_sensor_->state; - } - if (std::isnan(humidity) || humidity < 0.0f || humidity > 100.0f) { - humidity = 50; - } - - float temperature = NAN; - if (this->temperature_sensor_ != nullptr) { - temperature = float(this->temperature_sensor_->state); - } - if (std::isnan(temperature) || temperature < -40.0f || temperature > 85.0f) { - temperature = 25; - } - - uint16_t data[2]; - uint16_t rhticks = llround((uint16_t)((humidity * 65535) / 100)); - uint16_t tempticks = (uint16_t)(((temperature + 45) * 65535) / 175); - // first paramater is the relative humidity ticks - data[0] = rhticks; - // second paramater is the temperature ticks - data[1] = tempticks; - - if (!this->write_command(SGP40_CMD_MEASURE_RAW, data, 2)) { - this->status_set_warning(); - ESP_LOGD(TAG, "write error (%d)", this->last_error_); - return false; - } - delay(30); - - uint16_t raw_data; - if (!this->read_data(raw_data)) { - this->status_set_warning(); - ESP_LOGD(TAG, "read_data_ error"); - return UINT16_MAX; - } - return raw_data; -} - -void SGP40Component::update_voc_index() { - this->seconds_since_last_store_ += 1; - - this->voc_index_ = this->measure_voc_index_(); - if (this->samples_read_ < this->samples_to_stabalize_) { - this->samples_read_++; - ESP_LOGD(TAG, "Sensor has not collected enough samples yet. (%d/%d) VOC index is: %u", this->samples_read_, - this->samples_to_stabalize_, this->voc_index_); - return; - } -} - -void SGP40Component::update() { - if (this->samples_read_ < this->samples_to_stabalize_) { - return; - } - - if (this->voc_index_ != UINT16_MAX) { - this->status_clear_warning(); - this->publish_state(this->voc_index_); - } else { - this->status_set_warning(); - } -} - -void SGP40Component::dump_config() { - ESP_LOGCONFIG(TAG, "SGP40:"); - LOG_I2C_DEVICE(this); - ESP_LOGCONFIG(TAG, " store_baseline: %d", this->store_baseline_); - - if (this->is_failed()) { - switch (this->error_code_) { - case COMMUNICATION_FAILED: - ESP_LOGW(TAG, "Communication failed! Is the sensor connected?"); - break; - default: - ESP_LOGW(TAG, "Unknown setup error!"); - break; - } - } else { - ESP_LOGCONFIG(TAG, " Serial number: %" PRIu64, this->serial_number_); - ESP_LOGCONFIG(TAG, " Minimum Samples: %f", VOC_ALGORITHM_INITIAL_BLACKOUT); - } - LOG_UPDATE_INTERVAL(this); - - if (this->humidity_sensor_ != nullptr && this->temperature_sensor_ != nullptr) { - ESP_LOGCONFIG(TAG, " Compensation:"); - LOG_SENSOR(" ", "Temperature Source:", this->temperature_sensor_); - LOG_SENSOR(" ", "Humidity Source:", this->humidity_sensor_); - } else { - ESP_LOGCONFIG(TAG, " Compensation: No source configured"); - } -} - -} // namespace sgp40 -} // namespace esphome diff --git a/esphome/components/sgp40/sgp40.h b/esphome/components/sgp40/sgp40.h deleted file mode 100644 index c5b7d2dfa0..0000000000 --- a/esphome/components/sgp40/sgp40.h +++ /dev/null @@ -1,93 +0,0 @@ -#pragma once - -#include "esphome/core/component.h" -#include "esphome/components/sensor/sensor.h" -#include "esphome/components/sensirion_common/i2c_sensirion.h" -#include "esphome/core/application.h" -#include "esphome/core/preferences.h" -#include "sensirion_voc_algorithm.h" - -#include - -namespace esphome { -namespace sgp40 { - -struct SGP40Baselines { - int32_t state0; - int32_t state1; -} PACKED; // NOLINT - -// commands and constants -static const uint8_t SGP40_FEATURESET = 0x0020; ///< The required set for this library -static const uint8_t SGP40_CRC8_POLYNOMIAL = 0x31; ///< Seed for SGP40's CRC polynomial -static const uint8_t SGP40_CRC8_INIT = 0xFF; ///< Init value for CRC -static const uint8_t SGP40_WORD_LEN = 2; ///< 2 bytes per word - -// Commands - -static const uint16_t SGP40_CMD_GET_SERIAL_ID = 0x3682; -static const uint16_t SGP40_CMD_GET_FEATURESET = 0x202f; -static const uint16_t SGP40_CMD_SELF_TEST = 0x280e; -static const uint16_t SGP40_CMD_MEASURE_RAW = 0x260F; - -// Shortest time interval of 3H for storing baseline values. -// Prevents wear of the flash because of too many write operations -const uint32_t SHORTEST_BASELINE_STORE_INTERVAL = 10800; - -// Store anyway if the baseline difference exceeds the max storage diff value -const uint32_t MAXIMUM_STORAGE_DIFF = 50; - -class SGP40Component; - -/// This class implements support for the Sensirion sgp40 i2c GAS (VOC) sensors. -class SGP40Component : public PollingComponent, public sensor::Sensor, public sensirion_common::SensirionI2CDevice { - public: - void set_humidity_sensor(sensor::Sensor *humidity) { humidity_sensor_ = humidity; } - void set_temperature_sensor(sensor::Sensor *temperature) { temperature_sensor_ = temperature; } - - void setup() override; - void update() override; - void update_voc_index(); - void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } - void set_store_baseline(bool store_baseline) { store_baseline_ = store_baseline; } - - protected: - /// Input sensor for humidity and temperature compensation. - sensor::Sensor *humidity_sensor_{nullptr}; - sensor::Sensor *temperature_sensor_{nullptr}; - int16_t sensirion_init_sensors_(); - int16_t sgp40_probe_(); - uint64_t serial_number_; - uint16_t featureset_; - int32_t measure_voc_index_(); - uint8_t generate_crc_(const uint8_t *data, uint8_t datalen); - uint16_t measure_raw_(); - ESPPreferenceObject pref_; - uint32_t seconds_since_last_store_; - SGP40Baselines baselines_storage_; - VocAlgorithmParams voc_algorithm_params_; - bool self_test_complete_; - bool store_baseline_; - int32_t state0_; - int32_t state1_; - int32_t voc_index_ = 0; - uint8_t samples_read_ = 0; - uint8_t samples_to_stabalize_ = static_cast(VOC_ALGORITHM_INITIAL_BLACKOUT) * 2; - - /** - * @brief Request the sensor to perform a self-test, returning the result - * - * @return true: success false:failure - */ - void self_test_(); - enum ErrorCode { - COMMUNICATION_FAILED, - MEASUREMENT_INIT_FAILED, - INVALID_ID, - UNSUPPORTED_ID, - UNKNOWN - } error_code_{UNKNOWN}; -}; -} // namespace sgp40 -} // namespace esphome diff --git a/esphome/components/sgp4x/__init__.py b/esphome/components/sgp4x/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/sgp4x/sensor.py b/esphome/components/sgp4x/sensor.py new file mode 100644 index 0000000000..4855d7f066 --- /dev/null +++ b/esphome/components/sgp4x/sensor.py @@ -0,0 +1,144 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor, sensirion_common +from esphome.const import ( + CONF_ID, + CONF_STORE_BASELINE, + CONF_TEMPERATURE_SOURCE, + ICON_RADIATOR, + DEVICE_CLASS_NITROUS_OXIDE, + DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, + STATE_CLASS_MEASUREMENT, +) + +DEPENDENCIES = ["i2c"] +AUTO_LOAD = ["sensirion_common"] +CODEOWNERS = ["@SenexCrenshaw", "@martgras"] + +sgp4x_ns = cg.esphome_ns.namespace("sgp4x") +SGP4xComponent = sgp4x_ns.class_( + "SGP4xComponent", + sensor.Sensor, + cg.PollingComponent, + sensirion_common.SensirionI2CDevice, +) + +CONF_ALGORITHM_TUNING = "algorithm_tuning" +CONF_COMPENSATION = "compensation" +CONF_GAIN_FACTOR = "gain_factor" +CONF_GATING_MAX_DURATION_MINUTES = "gating_max_duration_minutes" +CONF_HUMIDITY_SOURCE = "humidity_source" +CONF_INDEX_OFFSET = "index_offset" +CONF_LEARNING_TIME_GAIN_HOURS = "learning_time_gain_hours" +CONF_LEARNING_TIME_OFFSET_HOURS = "learning_time_offset_hours" +CONF_NOX = "nox" +CONF_STD_INITIAL = "std_initial" +CONF_VOC = "voc" +CONF_VOC_BASELINE = "voc_baseline" + + +def validate_sensors(config): + if CONF_VOC not in config and CONF_NOX not in config: + raise cv.Invalid( + f"At least one sensor is required. Define {CONF_VOC} and/or {CONF_NOX}" + ) + return config + + +GAS_SENSOR = cv.Schema( + { + cv.Optional(CONF_ALGORITHM_TUNING): cv.Schema( + { + cv.Optional(CONF_INDEX_OFFSET, default=100): cv.int_, + cv.Optional(CONF_LEARNING_TIME_OFFSET_HOURS, default=12): cv.int_, + cv.Optional(CONF_LEARNING_TIME_GAIN_HOURS, default=12): cv.int_, + cv.Optional(CONF_GATING_MAX_DURATION_MINUTES, default=720): cv.int_, + cv.Optional(CONF_STD_INITIAL, default=50): cv.int_, + cv.Optional(CONF_GAIN_FACTOR, default=230): cv.int_, + } + ) + } +) + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(SGP4xComponent), + cv.Optional(CONF_VOC): sensor.sensor_schema( + icon=ICON_RADIATOR, + accuracy_decimals=0, + device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, + state_class=STATE_CLASS_MEASUREMENT, + ).extend(GAS_SENSOR), + cv.Optional(CONF_NOX): sensor.sensor_schema( + icon=ICON_RADIATOR, + accuracy_decimals=0, + device_class=DEVICE_CLASS_NITROUS_OXIDE, + state_class=STATE_CLASS_MEASUREMENT, + ).extend(GAS_SENSOR), + cv.Optional(CONF_STORE_BASELINE, default=True): cv.boolean, + cv.Optional(CONF_VOC_BASELINE): cv.hex_uint16_t, + cv.Optional(CONF_COMPENSATION): cv.Schema( + { + cv.Required(CONF_HUMIDITY_SOURCE): cv.use_id(sensor.Sensor), + cv.Required(CONF_TEMPERATURE_SOURCE): cv.use_id(sensor.Sensor), + }, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x59)), + validate_sensors, +) + + +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) + + if CONF_COMPENSATION in config: + compensation_config = config[CONF_COMPENSATION] + sens = await cg.get_variable(compensation_config[CONF_HUMIDITY_SOURCE]) + cg.add(var.set_humidity_sensor(sens)) + sens = await cg.get_variable(compensation_config[CONF_TEMPERATURE_SOURCE]) + cg.add(var.set_temperature_sensor(sens)) + + cg.add(var.set_store_baseline(config[CONF_STORE_BASELINE])) + + if CONF_VOC_BASELINE in config: + cg.add(var.set_voc_baseline(CONF_VOC_BASELINE)) + + if CONF_VOC in config: + sens = await sensor.new_sensor(config[CONF_VOC]) + cg.add(var.set_voc_sensor(sens)) + if CONF_ALGORITHM_TUNING in config[CONF_VOC]: + cfg = config[CONF_VOC][CONF_ALGORITHM_TUNING] + cg.add( + var.set_voc_algorithm_tuning( + cfg[CONF_INDEX_OFFSET], + cfg[CONF_LEARNING_TIME_OFFSET_HOURS], + cfg[CONF_LEARNING_TIME_GAIN_HOURS], + cfg[CONF_GATING_MAX_DURATION_MINUTES], + cfg[CONF_STD_INITIAL], + cfg[CONF_GAIN_FACTOR], + ) + ) + + if CONF_NOX in config: + sens = await sensor.new_sensor(config[CONF_NOX]) + cg.add(var.set_nox_sensor(sens)) + if CONF_ALGORITHM_TUNING in config[CONF_NOX]: + cfg = config[CONF_NOX][CONF_ALGORITHM_TUNING] + cg.add( + var.set_nox_algorithm_tuning( + cfg[CONF_INDEX_OFFSET], + cfg[CONF_LEARNING_TIME_OFFSET_HOURS], + cfg[CONF_LEARNING_TIME_GAIN_HOURS], + cfg[CONF_GATING_MAX_DURATION_MINUTES], + cfg[CONF_GAIN_FACTOR], + ) + ) + cg.add_library( + None, None, "https://github.com/Sensirion/arduino-gas-index-algorithm.git" + ) diff --git a/esphome/components/sgp4x/sgp4x.cpp b/esphome/components/sgp4x/sgp4x.cpp new file mode 100644 index 0000000000..a6f57e0342 --- /dev/null +++ b/esphome/components/sgp4x/sgp4x.cpp @@ -0,0 +1,343 @@ +#include "sgp4x.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" +#include + +namespace esphome { +namespace sgp4x { + +static const char *const TAG = "sgp4x"; + +void SGP4xComponent::setup() { + ESP_LOGCONFIG(TAG, "Setting up SGP4x..."); + + // Serial Number identification + uint16_t raw_serial_number[3]; + if (!this->get_register(SGP4X_CMD_GET_SERIAL_ID, raw_serial_number, 3, 1)) { + ESP_LOGE(TAG, "Failed to read serial number"); + this->error_code_ = SERIAL_NUMBER_IDENTIFICATION_FAILED; + this->mark_failed(); + return; + } + this->serial_number_ = (uint64_t(raw_serial_number[0]) << 24) | (uint64_t(raw_serial_number[1]) << 16) | + (uint64_t(raw_serial_number[2])); + ESP_LOGD(TAG, "Serial Number: %" PRIu64, this->serial_number_); + + // Featureset identification for future use + uint16_t raw_featureset; + if (!this->get_register(SGP4X_CMD_GET_FEATURESET, raw_featureset, 1)) { + ESP_LOGD(TAG, "raw_featureset write_command_ failed"); + this->mark_failed(); + return; + } + this->featureset_ = raw_featureset; + if ((this->featureset_ & 0x1FF) == SGP40_FEATURESET) { + sgp_type_ = SGP40; + self_test_time_ = SPG40_SELFTEST_TIME; + measure_time_ = SGP40_MEASURE_TIME; + if (this->nox_sensor_) { + ESP_LOGE(TAG, "Measuring NOx requires a SGP41 sensor but a SGP40 sensor is detected"); + // disable the sensor + this->nox_sensor_->set_disabled_by_default(true); + // make sure it's not visiable in HA + this->nox_sensor_->set_internal(true); + this->nox_sensor_->state = NAN; + // remove pointer to sensor + this->nox_sensor_ = nullptr; + } + } else { + if ((this->featureset_ & 0x1FF) == SGP41_FEATURESET) { + sgp_type_ = SGP41; + self_test_time_ = SPG41_SELFTEST_TIME; + measure_time_ = SGP41_MEASURE_TIME; + } else { + ESP_LOGD(TAG, "Product feature set failed 0x%0X , expecting 0x%0X", uint16_t(this->featureset_ & 0x1FF), + SGP40_FEATURESET); + this->mark_failed(); + return; + } + } + + ESP_LOGD(TAG, "Product version: 0x%0X", uint16_t(this->featureset_ & 0x1FF)); + + if (this->store_baseline_) { + // Hash with compilation time + // This ensures the baseline storage is cleared after OTA + uint32_t hash = fnv1_hash(App.get_compilation_time()); + this->pref_ = global_preferences->make_preference(hash, true); + + if (this->pref_.load(&this->voc_baselines_storage_)) { + this->voc_state0_ = this->voc_baselines_storage_.state0; + this->voc_state1_ = this->voc_baselines_storage_.state1; + ESP_LOGI(TAG, "Loaded VOC baseline state0: 0x%04X, state1: 0x%04X", this->voc_baselines_storage_.state0, + voc_baselines_storage_.state1); + } + + // Initialize storage timestamp + this->seconds_since_last_store_ = 0; + + if (this->voc_baselines_storage_.state0 > 0 && this->voc_baselines_storage_.state1 > 0) { + ESP_LOGI(TAG, "Setting VOC baseline from save state0: 0x%04X, state1: 0x%04X", + this->voc_baselines_storage_.state0, voc_baselines_storage_.state1); + voc_algorithm_.set_states(this->voc_baselines_storage_.state0, this->voc_baselines_storage_.state1); + } + } + if (this->voc_sensor_ && this->voc_tuning_params_.has_value()) { + voc_algorithm_.set_tuning_parameters( + voc_tuning_params_.value().index_offset, voc_tuning_params_.value().learning_time_offset_hours, + voc_tuning_params_.value().learning_time_gain_hours, voc_tuning_params_.value().gating_max_duration_minutes, + voc_tuning_params_.value().std_initial, voc_tuning_params_.value().gain_factor); + } + + if (this->nox_sensor_ && this->nox_tuning_params_.has_value()) { + nox_algorithm_.set_tuning_parameters( + nox_tuning_params_.value().index_offset, nox_tuning_params_.value().learning_time_offset_hours, + nox_tuning_params_.value().learning_time_gain_hours, nox_tuning_params_.value().gating_max_duration_minutes, + nox_tuning_params_.value().std_initial, nox_tuning_params_.value().gain_factor); + } + + this->self_test_(); + + /* The official spec for this sensor at + https://sensirion.com/media/documents/296373BB/6203C5DF/Sensirion_Gas_Sensors_Datasheet_SGP40.pdf indicates this + sensor should be driven at 1Hz. Comments from the developers at: + https://github.com/Sensirion/embedded-sgp/issues/136 indicate the algorithm should be a bit resilient to slight + timing variations so the software timer should be accurate enough for this. + + This block starts sampling from the sensor at 1Hz, and is done seperately from the call + to the update method. This seperation is to support getting accurate measurements but + limit the amount of communication done over wifi for power consumption or to keep the + number of records reported from being overwhelming. + */ + ESP_LOGD(TAG, "Component requires sampling of 1Hz, setting up background sampler"); + this->set_interval(1000, [this]() { this->update_gas_indices(); }); +} + +void SGP4xComponent::self_test_() { + ESP_LOGD(TAG, "Self-test started"); + if (!this->write_command(SGP4X_CMD_SELF_TEST)) { + this->error_code_ = COMMUNICATION_FAILED; + ESP_LOGD(TAG, "Self-test communication failed"); + this->mark_failed(); + } + + this->set_timeout(self_test_time_, [this]() { + uint16_t reply; + if (!this->read_data(reply)) { + this->error_code_ = SELF_TEST_FAILED; + ESP_LOGD(TAG, "Self-test read_data_ failed"); + this->mark_failed(); + return; + } + + if (reply == 0xD400) { + this->self_test_complete_ = true; + ESP_LOGD(TAG, "Self-test completed"); + return; + } else { + this->error_code_ = SELF_TEST_FAILED; + ESP_LOGD(TAG, "Self-test failed 0x%X", reply); + return; + } + + ESP_LOGD(TAG, "Self-test failed 0x%X", reply); + this->mark_failed(); + }); +} + +/** + * @brief Combined the measured gasses, temperature, and humidity + * to calculate the VOC Index + * + * @param temperature The measured temperature in degrees C + * @param humidity The measured relative humidity in % rH + * @return int32_t The VOC Index + */ +bool SGP4xComponent::measure_gas_indices_(int32_t &voc, int32_t &nox) { + uint16_t voc_sraw; + uint16_t nox_sraw; + if (!measure_raw_(voc_sraw, nox_sraw)) + return false; + + this->status_clear_warning(); + + voc = voc_algorithm_.process(voc_sraw); + if (nox_sensor_) { + nox = nox_algorithm_.process(nox_sraw); + } + ESP_LOGV(TAG, "VOC = %d, NOx = %d", voc, nox); + // Store baselines after defined interval or if the difference between current and stored baseline becomes too + // much + if (this->store_baseline_ && this->seconds_since_last_store_ > SHORTEST_BASELINE_STORE_INTERVAL) { + voc_algorithm_.get_states(this->voc_state0_, this->voc_state1_); + if ((uint32_t) abs(this->voc_baselines_storage_.state0 - this->voc_state0_) > MAXIMUM_STORAGE_DIFF || + (uint32_t) abs(this->voc_baselines_storage_.state1 - this->voc_state1_) > MAXIMUM_STORAGE_DIFF) { + this->seconds_since_last_store_ = 0; + this->voc_baselines_storage_.state0 = this->voc_state0_; + this->voc_baselines_storage_.state1 = this->voc_state1_; + + if (this->pref_.save(&this->voc_baselines_storage_)) { + ESP_LOGI(TAG, "Stored VOC baseline state0: 0x%04X ,state1: 0x%04X", this->voc_baselines_storage_.state0, + voc_baselines_storage_.state1); + } else { + ESP_LOGW(TAG, "Could not store VOC baselines"); + } + } + } + + return true; +} +/** + * @brief Return the raw gas measurement + * + * @param temperature The measured temperature in degrees C + * @param humidity The measured relative humidity in % rH + * @return uint16_t The current raw gas measurement + */ +bool SGP4xComponent::measure_raw_(uint16_t &voc_raw, uint16_t &nox_raw) { + float humidity = NAN; + static uint32_t nox_conditioning_start = millis(); + + if (!this->self_test_complete_) { + ESP_LOGD(TAG, "Self-test not yet complete"); + return false; + } + if (this->humidity_sensor_ != nullptr) { + humidity = this->humidity_sensor_->state; + } + if (std::isnan(humidity) || humidity < 0.0f || humidity > 100.0f) { + humidity = 50; + } + + float temperature = NAN; + if (this->temperature_sensor_ != nullptr) { + temperature = float(this->temperature_sensor_->state); + } + if (std::isnan(temperature) || temperature < -40.0f || temperature > 85.0f) { + temperature = 25; + } + + uint16_t command; + uint16_t data[2]; + size_t response_words; + // Use SGP40 measure command if we don't care about NOx + if (nox_sensor_ == nullptr) { + command = SGP40_CMD_MEASURE_RAW; + response_words = 1; + } else { + // SGP41 sensor must use NOx conditioning command for the first 10 seconds + if (millis() - nox_conditioning_start < 10000) { + command = SGP41_CMD_NOX_CONDITIONING; + response_words = 1; + } else { + command = SGP41_CMD_MEASURE_RAW; + response_words = 2; + } + } + uint16_t rhticks = llround((uint16_t)((humidity * 65535) / 100)); + uint16_t tempticks = (uint16_t)(((temperature + 45) * 65535) / 175); + // first paramater are the relative humidity ticks + data[0] = rhticks; + // secomd paramater are the temperature ticks + data[1] = tempticks; + + if (!this->write_command(command, data, 2)) { + this->status_set_warning(); + ESP_LOGD(TAG, "write error (%d)", this->last_error_); + return false; + } + delay(measure_time_); + uint16_t raw_data[2]; + raw_data[1] = 0; + if (!this->read_data(raw_data, response_words)) { + this->status_set_warning(); + ESP_LOGD(TAG, "read error (%d)", this->last_error_); + return false; + } + voc_raw = raw_data[0]; + nox_raw = raw_data[1]; // either 0 or the measured NOx ticks + return true; +} + +void SGP4xComponent::update_gas_indices() { + if (!this->self_test_complete_) + return; + + this->seconds_since_last_store_ += 1; + if (!this->measure_gas_indices_(this->voc_index_, this->nox_index_)) { + // Set values to UINT16_MAX to indicate failure + this->voc_index_ = this->nox_index_ = UINT16_MAX; + ESP_LOGE(TAG, "measure gas indices failed"); + return; + } + if (this->samples_read_ < this->samples_to_stabilize_) { + this->samples_read_++; + ESP_LOGD(TAG, "Sensor has not collected enough samples yet. (%d/%d) VOC index is: %u", this->samples_read_, + this->samples_to_stabilize_, this->voc_index_); + return; + } +} + +void SGP4xComponent::update() { + if (this->samples_read_ < this->samples_to_stabilize_) { + return; + } + if (this->voc_sensor_) { + if (this->voc_index_ != UINT16_MAX) { + this->status_clear_warning(); + this->voc_sensor_->publish_state(this->voc_index_); + } else { + this->status_set_warning(); + } + } + if (this->nox_sensor_) { + if (this->nox_index_ != UINT16_MAX) { + this->status_clear_warning(); + this->nox_sensor_->publish_state(this->nox_index_); + } else { + this->status_set_warning(); + } + } +} + +void SGP4xComponent::dump_config() { + ESP_LOGCONFIG(TAG, "SGP4x:"); + LOG_I2C_DEVICE(this); + ESP_LOGCONFIG(TAG, " store_baseline: %d", this->store_baseline_); + + if (this->is_failed()) { + switch (this->error_code_) { + case COMMUNICATION_FAILED: + ESP_LOGW(TAG, "Communication failed! Is the sensor connected?"); + break; + case SERIAL_NUMBER_IDENTIFICATION_FAILED: + ESP_LOGW(TAG, "Get Serial number failed."); + break; + case SELF_TEST_FAILED: + ESP_LOGW(TAG, "Self test failed."); + break; + + default: + ESP_LOGW(TAG, "Unknown setup error!"); + break; + } + } else { + ESP_LOGCONFIG(TAG, " Type: %s", sgp_type_ == SGP41 ? "SGP41" : "SPG40"); + ESP_LOGCONFIG(TAG, " Serial number: %" PRIu64, this->serial_number_); + ESP_LOGCONFIG(TAG, " Minimum Samples: %f", GasIndexAlgorithm_INITIAL_BLACKOUT); + } + LOG_UPDATE_INTERVAL(this); + + if (this->humidity_sensor_ != nullptr && this->temperature_sensor_ != nullptr) { + ESP_LOGCONFIG(TAG, " Compensation:"); + LOG_SENSOR(" ", "Temperature Source:", this->temperature_sensor_); + LOG_SENSOR(" ", "Humidity Source:", this->humidity_sensor_); + } else { + ESP_LOGCONFIG(TAG, " Compensation: No source configured"); + } + LOG_SENSOR(" ", "VOC", this->voc_sensor_); + LOG_SENSOR(" ", "NOx", this->nox_sensor_); +} + +} // namespace sgp4x +} // namespace esphome diff --git a/esphome/components/sgp4x/sgp4x.h b/esphome/components/sgp4x/sgp4x.h new file mode 100644 index 0000000000..3060972fc3 --- /dev/null +++ b/esphome/components/sgp4x/sgp4x.h @@ -0,0 +1,142 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/sensirion_common/i2c_sensirion.h" +#include "esphome/core/application.h" +#include "esphome/core/preferences.h" +#include +#include + +#include + +namespace esphome { +namespace sgp4x { + +struct SGP4xBaselines { + int32_t state0; + int32_t state1; +} PACKED; // NOLINT + +enum SgpType { SGP40, SGP41 }; + +struct GasTuning { + uint16_t index_offset; + uint16_t learning_time_offset_hours; + uint16_t learning_time_gain_hours; + uint16_t gating_max_duration_minutes; + uint16_t std_initial; + uint16_t gain_factor; +}; + +// commands and constants +static const uint8_t SGP40_FEATURESET = 0x0020; // can measure VOC +static const uint8_t SGP41_FEATURESET = 0x0040; // can measure VOC and NOX +// Commands +static const uint16_t SGP4X_CMD_GET_SERIAL_ID = 0x3682; +static const uint16_t SGP4X_CMD_GET_FEATURESET = 0x202f; +static const uint16_t SGP4X_CMD_SELF_TEST = 0x280e; +static const uint16_t SGP40_CMD_MEASURE_RAW = 0x260F; +static const uint16_t SGP41_CMD_MEASURE_RAW = 0x2619; +static const uint16_t SGP41_CMD_NOX_CONDITIONING = 0x2612; +static const uint8_t SGP41_SUBCMD_NOX_CONDITIONING = 0x12; + +// Shortest time interval of 3H for storing baseline values. +// Prevents wear of the flash because of too many write operations +const uint32_t SHORTEST_BASELINE_STORE_INTERVAL = 10800; +static const uint16_t SPG40_SELFTEST_TIME = 250; // 250 ms for self test +static const uint16_t SPG41_SELFTEST_TIME = 320; // 320 ms for self test +static const uint16_t SGP40_MEASURE_TIME = 30; +static const uint16_t SGP41_MEASURE_TIME = 55; +// Store anyway if the baseline difference exceeds the max storage diff value +const uint32_t MAXIMUM_STORAGE_DIFF = 50; + +class SGP4xComponent; + +/// This class implements support for the Sensirion sgp4x i2c GAS (VOC) sensors. +class SGP4xComponent : public PollingComponent, public sensor::Sensor, public sensirion_common::SensirionI2CDevice { + enum ErrorCode { + COMMUNICATION_FAILED, + MEASUREMENT_INIT_FAILED, + INVALID_ID, + UNSUPPORTED_ID, + SERIAL_NUMBER_IDENTIFICATION_FAILED, + SELF_TEST_FAILED, + UNKNOWN + } error_code_{UNKNOWN}; + + public: + // SGP4xComponent() {}; + void set_humidity_sensor(sensor::Sensor *humidity) { humidity_sensor_ = humidity; } + void set_temperature_sensor(sensor::Sensor *temperature) { temperature_sensor_ = temperature; } + + void setup() override; + void update() override; + void update_gas_indices(); + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void set_store_baseline(bool store_baseline) { store_baseline_ = store_baseline; } + void set_voc_sensor(sensor::Sensor *voc_sensor) { voc_sensor_ = voc_sensor; } + void set_nox_sensor(sensor::Sensor *nox_sensor) { nox_sensor_ = nox_sensor; } + void set_voc_algorithm_tuning(uint16_t index_offset, uint16_t learning_time_offset_hours, + uint16_t learning_time_gain_hours, uint16_t gating_max_duration_minutes, + uint16_t std_initial, uint16_t gain_factor) { + voc_tuning_params_.value().index_offset = index_offset; + voc_tuning_params_.value().learning_time_offset_hours = learning_time_offset_hours; + voc_tuning_params_.value().learning_time_gain_hours = learning_time_gain_hours; + voc_tuning_params_.value().gating_max_duration_minutes = gating_max_duration_minutes; + voc_tuning_params_.value().std_initial = std_initial; + voc_tuning_params_.value().gain_factor = gain_factor; + } + void set_nox_algorithm_tuning(uint16_t index_offset, uint16_t learning_time_offset_hours, + uint16_t learning_time_gain_hours, uint16_t gating_max_duration_minutes, + uint16_t gain_factor) { + nox_tuning_params_.value().index_offset = index_offset; + nox_tuning_params_.value().learning_time_offset_hours = learning_time_offset_hours; + nox_tuning_params_.value().learning_time_gain_hours = learning_time_gain_hours; + nox_tuning_params_.value().gating_max_duration_minutes = gating_max_duration_minutes; + nox_tuning_params_.value().std_initial = 50; + nox_tuning_params_.value().gain_factor = gain_factor; + } + + protected: + void self_test_(); + + /// Input sensor for humidity and temperature compensation. + sensor::Sensor *humidity_sensor_{nullptr}; + sensor::Sensor *temperature_sensor_{nullptr}; + int16_t sensirion_init_sensors_(); + + bool measure_gas_indices_(int32_t &voc, int32_t &nox); + bool measure_raw_(uint16_t &voc_raw, uint16_t &nox_raw); + + SgpType sgp_type_{SGP40}; + uint64_t serial_number_; + uint16_t featureset_; + + bool self_test_complete_; + uint16_t self_test_time_; + + sensor::Sensor *voc_sensor_{nullptr}; + VOCGasIndexAlgorithm voc_algorithm_; + optional voc_tuning_params_; + int32_t voc_state0_; + int32_t voc_state1_; + int32_t voc_index_ = 0; + + sensor::Sensor *nox_sensor_{nullptr}; + int32_t nox_index_ = 0; + NOxGasIndexAlgorithm nox_algorithm_; + optional nox_tuning_params_; + + uint16_t measure_time_; + uint8_t samples_read_ = 0; + uint8_t samples_to_stabilize_ = static_cast(GasIndexAlgorithm_INITIAL_BLACKOUT) * 2; + + bool store_baseline_; + ESPPreferenceObject pref_; + uint32_t seconds_since_last_store_; + SGP4xBaselines voc_baselines_storage_; +}; +} // namespace sgp4x +} // namespace esphome diff --git a/platformio.ini b/platformio.ini index bc2cddb9f7..82cf6eeb9a 100644 --- a/platformio.ini +++ b/platformio.ini @@ -39,6 +39,8 @@ lib_deps = bblanchon/ArduinoJson@6.18.5 ; json wjtje/qr-code-generator-library@1.7.0 ; qr_code functionpointer/arduino-MLX90393@1.0.0 ; mlx90393 + ; This is using the repository until a new release is published to PlatformIO + https://github.com/Sensirion/arduino-gas-index-algorithm.git ; Sensirion Gas Index Algorithm Arduino Library build_flags = -DESPHOME_LOG_LEVEL=ESPHOME_LOG_LEVEL_VERY_VERBOSE src_filter = diff --git a/tests/test2.yaml b/tests/test2.yaml index a7a9ef9661..f88486524f 100644 --- a/tests/test2.yaml +++ b/tests/test2.yaml @@ -281,10 +281,27 @@ sensor: window_correction_factor: 1.0 address: 0x53 update_interval: 60s - - platform: sgp40 - name: 'Workshop VOC' + - platform: sgp4x + voc: + name: "VOC Index" + id: sgp40_voc_index + algorithm_tuning: + index_offset: 100 + learning_time_offset_hours: 12 + learning_time_gain_hours: 12 + gating_max_duration_minutes: 180 + std_initial: 50 + gain_factor: 230 + nox: + name: "NOx" + algorithm_tuning: + index_offset: 100 + learning_time_offset_hours: 12 + learning_time_gain_hours: 12 + gating_max_duration_minutes: 180 + std_initial: 50 + gain_factor: 230 update_interval: 5s - store_baseline: 'true' - platform: mcp3008 update_interval: 5s mcp3008_id: 'mcp3008_hub' From 4f52d43347c6b87529238d737473bed974b5705e Mon Sep 17 00:00:00 2001 From: gazoodle <47351872+gazoodle@users.noreply.github.com> Date: Thu, 19 May 2022 01:49:12 +0100 Subject: [PATCH 0488/1729] add support user-defined modbus functions (#3461) --- esphome/components/modbus/modbus.cpp | 67 ++++++++++++++++++---------- 1 file changed, 44 insertions(+), 23 deletions(-) diff --git a/esphome/components/modbus/modbus.cpp b/esphome/components/modbus/modbus.cpp index 60ce50097c..19b5e8019e 100644 --- a/esphome/components/modbus/modbus.cpp +++ b/esphome/components/modbus/modbus.cpp @@ -68,33 +68,54 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) { uint8_t data_len = raw[2]; uint8_t data_offset = 3; - // the response for write command mirrors the requests and data startes at offset 2 instead of 3 for read commands - if (function_code == 0x5 || function_code == 0x06 || function_code == 0xF || function_code == 0x10) { - data_offset = 2; - data_len = 4; - } - // Error ( msb indicates error ) - // response format: Byte[0] = device address, Byte[1] function code | 0x80 , Byte[2] excpetion code, Byte[3-4] crc - if ((function_code & 0x80) == 0x80) { - data_offset = 2; - data_len = 1; - } + // Per https://modbus.org/docs/Modbus_Application_Protocol_V1_1b3.pdf Ch 5 User-Defined function codes + if (((function_code >= 65) && (function_code <= 72)) || ((function_code >= 100) && (function_code <= 110))) { + // Handle user-defined function, since we don't know how big this ought to be, + // ideally we should delegate the entire length detection to whatever handler is + // installed, but wait, there is the CRC, and if we get a hit there is a good + // chance that this is a complete message ... admittedly there is a small chance is + // isn't but that is quite small given the purpose of the CRC in the first place + data_len = at; + data_offset = 1; - // Byte data_offset..data_offset+data_len-1: Data - if (at < data_offset + data_len) - return true; + uint16_t computed_crc = crc16(raw, data_offset + data_len); + uint16_t remote_crc = uint16_t(raw[data_offset + data_len]) | (uint16_t(raw[data_offset + data_len + 1]) << 8); - // Byte 3+data_len: CRC_LO (over all bytes) - if (at == data_offset + data_len) - return true; + if (computed_crc != remote_crc) + return true; - // Byte data_offset+len+1: CRC_HI (over all bytes) - uint16_t computed_crc = crc16(raw, data_offset + data_len); - uint16_t remote_crc = uint16_t(raw[data_offset + data_len]) | (uint16_t(raw[data_offset + data_len + 1]) << 8); - if (computed_crc != remote_crc) { - ESP_LOGW(TAG, "Modbus CRC Check failed! %02X!=%02X", computed_crc, remote_crc); - return false; + ESP_LOGD(TAG, "Modbus user-defined function %02X found", function_code); + + } else { + // the response for write command mirrors the requests and data startes at offset 2 instead of 3 for read commands + if (function_code == 0x5 || function_code == 0x06 || function_code == 0xF || function_code == 0x10) { + data_offset = 2; + data_len = 4; + } + + // Error ( msb indicates error ) + // response format: Byte[0] = device address, Byte[1] function code | 0x80 , Byte[2] excpetion code, Byte[3-4] crc + if ((function_code & 0x80) == 0x80) { + data_offset = 2; + data_len = 1; + } + + // Byte data_offset..data_offset+data_len-1: Data + if (at < data_offset + data_len) + return true; + + // Byte 3+data_len: CRC_LO (over all bytes) + if (at == data_offset + data_len) + return true; + + // Byte data_offset+len+1: CRC_HI (over all bytes) + uint16_t computed_crc = crc16(raw, data_offset + data_len); + uint16_t remote_crc = uint16_t(raw[data_offset + data_len]) | (uint16_t(raw[data_offset + data_len + 1]) << 8); + if (computed_crc != remote_crc) { + ESP_LOGW(TAG, "Modbus CRC Check failed! %02X!=%02X", computed_crc, remote_crc); + return false; + } } std::vector data(this->rx_buffer_.begin() + data_offset, this->rx_buffer_.begin() + data_offset + data_len); bool found = false; From f0c890f160dc1f073b351d39525f0b377be2055f Mon Sep 17 00:00:00 2001 From: Keilin Bickar Date: Wed, 18 May 2022 20:50:44 -0400 Subject: [PATCH 0489/1729] Remove deprecated fan speeds (#3397) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/api/api_connection.cpp | 12 ----- esphome/components/fan/fan.cpp | 17 ------- esphome/components/fan/fan.h | 7 --- esphome/components/fan/fan_helpers.cpp | 23 ---------- esphome/components/fan/fan_helpers.h | 20 --------- .../components/hbridge/fan/hbridge_fan.cpp | 1 - esphome/components/mqtt/mqtt_fan.cpp | 45 +------------------ esphome/components/speed/fan/speed_fan.cpp | 1 - esphome/components/tuya/fan/tuya_fan.cpp | 1 - esphome/components/web_server/web_server.cpp | 24 ---------- 10 files changed, 2 insertions(+), 149 deletions(-) delete mode 100644 esphome/components/fan/fan_helpers.cpp delete mode 100644 esphome/components/fan/fan_helpers.h diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 81f2465b74..4f399d95d0 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -12,9 +12,6 @@ #ifdef USE_HOMEASSISTANT_TIME #include "esphome/components/homeassistant/time/homeassistant_time.h" #endif -#ifdef USE_FAN -#include "esphome/components/fan/fan_helpers.h" -#endif namespace esphome { namespace api { @@ -253,9 +250,6 @@ void APIConnection::cover_command(const CoverCommandRequest &msg) { #endif #ifdef USE_FAN -// Shut-up about usage of deprecated speed_level_to_enum/speed_enum_to_level functions for a bit. -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" bool APIConnection::send_fan_state(fan::Fan *fan) { if (!this->state_subscription_) return false; @@ -268,7 +262,6 @@ bool APIConnection::send_fan_state(fan::Fan *fan) { resp.oscillating = fan->oscillating; if (traits.supports_speed()) { resp.speed_level = fan->speed; - resp.speed = static_cast(fan::speed_level_to_enum(fan->speed, traits.supported_speed_count())); } if (traits.supports_direction()) resp.direction = static_cast(fan->direction); @@ -295,8 +288,6 @@ void APIConnection::fan_command(const FanCommandRequest &msg) { if (fan == nullptr) return; - auto traits = fan->get_traits(); - auto call = fan->make_call(); if (msg.has_state) call.set_state(msg.state); @@ -305,14 +296,11 @@ void APIConnection::fan_command(const FanCommandRequest &msg) { if (msg.has_speed_level) { // Prefer level call.set_speed(msg.speed_level); - } else if (msg.has_speed) { - call.set_speed(fan::speed_enum_to_level(static_cast(msg.speed), traits.supported_speed_count())); } if (msg.has_direction) call.set_direction(static_cast(msg.direction)); call.perform(); } -#pragma GCC diagnostic pop #endif #ifdef USE_LIGHT diff --git a/esphome/components/fan/fan.cpp b/esphome/components/fan/fan.cpp index 5f9660f6d6..f7c4ab2e11 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -1,5 +1,4 @@ #include "fan.h" -#include "fan_helpers.h" #include "esphome/core/log.h" namespace esphome { @@ -61,22 +60,6 @@ void FanCall::validate_() { } } -// This whole method is deprecated, don't warn about usage of deprecated methods inside of it. -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" -FanCall &FanCall::set_speed(const char *legacy_speed) { - const auto supported_speed_count = this->parent_.get_traits().supported_speed_count(); - if (strcasecmp(legacy_speed, "low") == 0) { - this->set_speed(fan::speed_enum_to_level(FAN_SPEED_LOW, supported_speed_count)); - } else if (strcasecmp(legacy_speed, "medium") == 0) { - this->set_speed(fan::speed_enum_to_level(FAN_SPEED_MEDIUM, supported_speed_count)); - } else if (strcasecmp(legacy_speed, "high") == 0) { - this->set_speed(fan::speed_enum_to_level(FAN_SPEED_HIGH, supported_speed_count)); - } - return *this; -} -#pragma GCC diagnostic pop - FanCall FanRestoreState::to_call(Fan &fan) { auto call = fan.make_call(); call.set_state(this->state); diff --git a/esphome/components/fan/fan.h b/esphome/components/fan/fan.h index cafb5843d1..ef2ecd0f3f 100644 --- a/esphome/components/fan/fan.h +++ b/esphome/components/fan/fan.h @@ -16,13 +16,6 @@ namespace fan { (obj)->dump_traits_(TAG, prefix); \ } -/// Simple enum to represent the speed of a fan. - DEPRECATED - Will be deleted soon -enum ESPDEPRECATED("FanSpeed is deprecated.", "2021.9") FanSpeed { - FAN_SPEED_LOW = 0, ///< The fan is running on low speed. - FAN_SPEED_MEDIUM = 1, ///< The fan is running on medium speed. - FAN_SPEED_HIGH = 2 ///< The fan is running on high/full speed. -}; - /// Simple enum to represent the direction of a fan. enum class FanDirection { FORWARD = 0, REVERSE = 1 }; diff --git a/esphome/components/fan/fan_helpers.cpp b/esphome/components/fan/fan_helpers.cpp deleted file mode 100644 index 34883617e6..0000000000 --- a/esphome/components/fan/fan_helpers.cpp +++ /dev/null @@ -1,23 +0,0 @@ -#include -#include "fan_helpers.h" - -namespace esphome { -namespace fan { - -// This whole file is deprecated, don't warn about usage of deprecated types in here. -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" - -FanSpeed speed_level_to_enum(int speed_level, int supported_speed_levels) { - const auto speed_ratio = static_cast(speed_level) / (supported_speed_levels + 1); - const auto legacy_level = clamp(static_cast(ceilf(speed_ratio * 3)), 1, 3); - return static_cast(legacy_level - 1); -} - -int speed_enum_to_level(FanSpeed speed, int supported_speed_levels) { - const auto enum_level = static_cast(speed) + 1; - const auto speed_level = roundf(enum_level / 3.0f * supported_speed_levels); - return static_cast(speed_level); -} - -} // namespace fan -} // namespace esphome diff --git a/esphome/components/fan/fan_helpers.h b/esphome/components/fan/fan_helpers.h deleted file mode 100644 index 8e8e3859bd..0000000000 --- a/esphome/components/fan/fan_helpers.h +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once - -#include "fan.h" - -namespace esphome { -namespace fan { - -// Shut-up about usage of deprecated FanSpeed for a bit. -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" - -ESPDEPRECATED("FanSpeed and speed_level_to_enum() are deprecated.", "2021.9") -FanSpeed speed_level_to_enum(int speed_level, int supported_speed_levels); -ESPDEPRECATED("FanSpeed and speed_enum_to_level() are deprecated.", "2021.9") -int speed_enum_to_level(FanSpeed speed, int supported_speed_levels); - -#pragma GCC diagnostic pop - -} // namespace fan -} // namespace esphome diff --git a/esphome/components/hbridge/fan/hbridge_fan.cpp b/esphome/components/hbridge/fan/hbridge_fan.cpp index 52d2b3d8b7..44cf5ae049 100644 --- a/esphome/components/hbridge/fan/hbridge_fan.cpp +++ b/esphome/components/hbridge/fan/hbridge_fan.cpp @@ -1,5 +1,4 @@ #include "hbridge_fan.h" -#include "esphome/components/fan/fan_helpers.h" #include "esphome/core/log.h" namespace esphome { diff --git a/esphome/components/mqtt/mqtt_fan.cpp b/esphome/components/mqtt/mqtt_fan.cpp index e4d867843c..6433ead6b2 100644 --- a/esphome/components/mqtt/mqtt_fan.cpp +++ b/esphome/components/mqtt/mqtt_fan.cpp @@ -5,7 +5,6 @@ #ifdef USE_MQTT #ifdef USE_FAN -#include "esphome/components/fan/fan_helpers.h" namespace esphome { namespace mqtt { @@ -88,17 +87,6 @@ void MQTTFanComponent::setup() { }); } - if (this->state_->get_traits().supports_speed()) { - this->subscribe(this->get_speed_command_topic(), [this](const std::string &topic, const std::string &payload) { -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" - this->state_->make_call() - .set_speed(payload.c_str()) // NOLINT(clang-diagnostic-deprecated-declarations) - .perform(); -#pragma GCC diagnostic pop - }); - } - auto f = std::bind(&MQTTFanComponent::publish_state, this); this->state_->add_on_state_callback([this, f]() { this->defer("send", f); }); } @@ -113,8 +101,6 @@ void MQTTFanComponent::dump_config() { if (this->state_->get_traits().supports_speed()) { ESP_LOGCONFIG(TAG, " Speed Level State Topic: '%s'", this->get_speed_level_state_topic().c_str()); ESP_LOGCONFIG(TAG, " Speed Level Command Topic: '%s'", this->get_speed_level_command_topic().c_str()); - ESP_LOGCONFIG(TAG, " Speed State Topic: '%s'", this->get_speed_state_topic().c_str()); - ESP_LOGCONFIG(TAG, " Speed Command Topic: '%s'", this->get_speed_command_topic().c_str()); } } @@ -126,10 +112,8 @@ void MQTTFanComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig root[MQTT_OSCILLATION_STATE_TOPIC] = this->get_oscillation_state_topic(); } if (this->state_->get_traits().supports_speed()) { - root["speed_level_command_topic"] = this->get_speed_level_command_topic(); - root["speed_level_state_topic"] = this->get_speed_level_state_topic(); - root[MQTT_SPEED_COMMAND_TOPIC] = this->get_speed_command_topic(); - root[MQTT_SPEED_STATE_TOPIC] = this->get_speed_state_topic(); + root[MQTT_PERCENTAGE_COMMAND_TOPIC] = this->get_speed_level_command_topic(); + root[MQTT_PERCENTAGE_STATE_TOPIC] = this->get_speed_level_state_topic(); } } bool MQTTFanComponent::publish_state() { @@ -148,31 +132,6 @@ bool MQTTFanComponent::publish_state() { bool success = this->publish(this->get_speed_level_state_topic(), payload); failed = failed || !success; } - if (traits.supports_speed()) { - const char *payload; -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" - // NOLINTNEXTLINE(clang-diagnostic-deprecated-declarations) - switch (fan::speed_level_to_enum(this->state_->speed, traits.supported_speed_count())) { - case FAN_SPEED_LOW: { // NOLINT(clang-diagnostic-deprecated-declarations) - payload = "low"; - break; - } - case FAN_SPEED_MEDIUM: { // NOLINT(clang-diagnostic-deprecated-declarations) - payload = "medium"; - break; - } - default: - case FAN_SPEED_HIGH: { // NOLINT(clang-diagnostic-deprecated-declarations) - payload = "high"; - break; - } - } -#pragma GCC diagnostic pop - bool success = this->publish(this->get_speed_state_topic(), payload); - failed = failed || !success; - } - return !failed; } diff --git a/esphome/components/speed/fan/speed_fan.cpp b/esphome/components/speed/fan/speed_fan.cpp index 9ed201982a..3a65f2c365 100644 --- a/esphome/components/speed/fan/speed_fan.cpp +++ b/esphome/components/speed/fan/speed_fan.cpp @@ -1,5 +1,4 @@ #include "speed_fan.h" -#include "esphome/components/fan/fan_helpers.h" #include "esphome/core/log.h" namespace esphome { diff --git a/esphome/components/tuya/fan/tuya_fan.cpp b/esphome/components/tuya/fan/tuya_fan.cpp index 019b504deb..813aee4aa0 100644 --- a/esphome/components/tuya/fan/tuya_fan.cpp +++ b/esphome/components/tuya/fan/tuya_fan.cpp @@ -1,5 +1,4 @@ #include "esphome/core/log.h" -#include "esphome/components/fan/fan_helpers.h" #include "tuya_fan.h" namespace esphome { diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 6822ce9953..18374d606b 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -21,10 +21,6 @@ #include "esphome/components/logger/logger.h" #endif -#ifdef USE_FAN -#include "esphome/components/fan/fan_helpers.h" -#endif - #ifdef USE_CLIMATE #include "esphome/components/climate/climate.h" #endif @@ -482,22 +478,6 @@ std::string WebServer::fan_json(fan::Fan *obj, JsonDetail start_config) { if (traits.supports_speed()) { root["speed_level"] = obj->speed; root["speed_count"] = traits.supported_speed_count(); - -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" - // NOLINTNEXTLINE(clang-diagnostic-deprecated-declarations) - switch (fan::speed_level_to_enum(obj->speed, traits.supported_speed_count())) { - case fan::FAN_SPEED_LOW: // NOLINT(clang-diagnostic-deprecated-declarations) - root["speed"] = "low"; - break; - case fan::FAN_SPEED_MEDIUM: // NOLINT(clang-diagnostic-deprecated-declarations) - root["speed"] = "medium"; - break; - case fan::FAN_SPEED_HIGH: // NOLINT(clang-diagnostic-deprecated-declarations) - root["speed"] = "high"; - break; - } -#pragma GCC diagnostic pop } if (obj->get_traits().supports_oscillation()) root["oscillation"] = obj->oscillating; @@ -518,10 +498,6 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc auto call = obj->turn_on(); if (request->hasParam("speed")) { String speed = request->getParam("speed")->value(); -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" - call.set_speed(speed.c_str()); // NOLINT(clang-diagnostic-deprecated-declarations) -#pragma GCC diagnostic pop } if (request->hasParam("speed_level")) { String speed_level = request->getParam("speed_level")->value(); From dda1ddcb26b9ca891ca5de668673a92bcf87ca22 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 19 May 2022 16:23:40 +1200 Subject: [PATCH 0490/1729] Add missing import to bedjet (#3490) --- esphome/components/bedjet/bedjet.h | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/bedjet/bedjet.h b/esphome/components/bedjet/bedjet.h index 0565be6045..750a20594f 100644 --- a/esphome/components/bedjet/bedjet.h +++ b/esphome/components/bedjet/bedjet.h @@ -4,6 +4,7 @@ #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" #include "esphome/components/climate/climate.h" #include "esphome/core/component.h" +#include "esphome/core/defines.h" #include "esphome/core/hal.h" #include "bedjet_base.h" From d9d2edeb080297c3f4b0e0d82eee161222466692 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 19 May 2022 21:21:42 +1200 Subject: [PATCH 0491/1729] Fix compile issues on windows (#3491) --- esphome/components/sonoff_d1/sonoff_d1.cpp | 3 +-- esphome/components/tuya/text_sensor/tuya_text_sensor.cpp | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/esphome/components/sonoff_d1/sonoff_d1.cpp b/esphome/components/sonoff_d1/sonoff_d1.cpp index b4bcbc6760..d07f9229b6 100644 --- a/esphome/components/sonoff_d1/sonoff_d1.cpp +++ b/esphome/components/sonoff_d1/sonoff_d1.cpp @@ -41,7 +41,6 @@ * O FF FF FF FF FF FF FF FF - Not used * M 6C - CRC over bytes 2 to F (Addition) \*********************************************************************************************/ -#include #include "sonoff_d1.h" namespace esphome { @@ -263,7 +262,7 @@ void SonoffD1Output::write_state(light::LightState *state) { state->current_values_as_brightness(&brightness); // Convert ESPHome's brightness (0-1) to the device's internal brightness (0-100) - const uint8_t calculated_brightness = std::round(brightness * 100); + const uint8_t calculated_brightness = (uint8_t) roundf(brightness * 100); if (calculated_brightness == 0) { // if(binary) ESP_LOGD(TAG, "current_values_as_binary() returns true for zero brightness"); diff --git a/esphome/components/tuya/text_sensor/tuya_text_sensor.cpp b/esphome/components/tuya/text_sensor/tuya_text_sensor.cpp index 0b51ba90c4..602595e89d 100644 --- a/esphome/components/tuya/text_sensor/tuya_text_sensor.cpp +++ b/esphome/components/tuya/text_sensor/tuya_text_sensor.cpp @@ -20,7 +20,7 @@ void TuyaTextSensor::setup() { break; } default: - ESP_LOGW(TAG, "Unsupported data type for tuya text sensor %u: %#02hhX", datapoint.id, datapoint.type); + ESP_LOGW(TAG, "Unsupported data type for tuya text sensor %u: %#02hhX", datapoint.id, (uint8_t) datapoint.type); break; } }); From 7092f7663e5d129690f5a6603c002778bb2f444d Mon Sep 17 00:00:00 2001 From: Sergey Dudanov Date: Mon, 23 May 2022 11:51:45 +0300 Subject: [PATCH 0492/1729] midea: New power_toggle action. Auto-use remote transmitter. (#3496) --- esphome/components/midea/ac_automations.h | 5 ++++ esphome/components/midea/air_conditioner.h | 1 + esphome/components/midea/climate.py | 13 +++++++++- tests/test1.yaml | 28 +++++++++++++++------- 4 files changed, 38 insertions(+), 9 deletions(-) diff --git a/esphome/components/midea/ac_automations.h b/esphome/components/midea/ac_automations.h index d4ed2e7168..5084fd1eec 100644 --- a/esphome/components/midea/ac_automations.h +++ b/esphome/components/midea/ac_automations.h @@ -56,6 +56,11 @@ template class PowerOffAction : public MideaActionBase { void play(Ts... x) override { this->parent_->do_power_off(); } }; +template class PowerToggleAction : public MideaActionBase { + public: + void play(Ts... x) override { this->parent_->do_power_toggle(); } +}; + } // namespace ac } // namespace midea } // namespace esphome diff --git a/esphome/components/midea/air_conditioner.h b/esphome/components/midea/air_conditioner.h index a6023b78bb..d809aa78f6 100644 --- a/esphome/components/midea/air_conditioner.h +++ b/esphome/components/midea/air_conditioner.h @@ -39,6 +39,7 @@ class AirConditioner : public ApplianceBase, void do_beeper_off() { this->set_beeper_feedback(false); } void do_power_on() { this->base_.setPowerState(true); } void do_power_off() { this->base_.setPowerState(false); } + void do_power_toggle() { this->base_.setPowerState(this->mode == ClimateMode::CLIMATE_MODE_OFF); } void set_supported_modes(const std::set &modes) { this->supported_modes_ = modes; } void set_supported_swing_modes(const std::set &modes) { this->supported_swing_modes_ = modes; } void set_supported_presets(const std::set &presets) { this->supported_presets_ = presets; } diff --git a/esphome/components/midea/climate.py b/esphome/components/midea/climate.py index 46c0019efa..80b1461576 100644 --- a/esphome/components/midea/climate.py +++ b/esphome/components/midea/climate.py @@ -113,7 +113,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_PERIOD, default="1s"): cv.time_period, cv.Optional(CONF_TIMEOUT, default="2s"): cv.time_period, cv.Optional(CONF_NUM_ATTEMPTS, default=3): cv.int_range(min=1, max=5), - cv.Optional(CONF_TRANSMITTER_ID): cv.use_id( + cv.OnlyWith(CONF_TRANSMITTER_ID, "remote_transmitter"): cv.use_id( remote_transmitter.RemoteTransmitterComponent ), cv.Optional(CONF_BEEPER, default=False): cv.boolean, @@ -163,6 +163,7 @@ BeeperOnAction = midea_ac_ns.class_("BeeperOnAction", automation.Action) BeeperOffAction = midea_ac_ns.class_("BeeperOffAction", automation.Action) PowerOnAction = midea_ac_ns.class_("PowerOnAction", automation.Action) PowerOffAction = midea_ac_ns.class_("PowerOffAction", automation.Action) +PowerToggleAction = midea_ac_ns.class_("PowerToggleAction", automation.Action) MIDEA_ACTION_BASE_SCHEMA = cv.Schema( { @@ -249,6 +250,16 @@ async def power_off_to_code(var, config, args): pass +# Power Toggle action +@register_action( + "power_toggle", + PowerToggleAction, + cv.Schema({}), +) +async def power_inv_to_code(var, config, args): + pass + + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) diff --git a/tests/test1.yaml b/tests/test1.yaml index 7bb1fbe954..52aa03c371 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1901,14 +1901,6 @@ script: preset: SLEEP switch: - - platform: template - name: MIDEA_AC_TOGGLE_LIGHT - turn_on_action: - midea_ac.display_toggle: - - platform: template - name: MIDEA_AC_SWING_STEP - turn_on_action: - midea_ac.swing_step: - platform: template name: MIDEA_AC_BEEPER_CONTROL optimistic: true @@ -2834,3 +2826,23 @@ button: id: scd40 - scd4x.factory_reset: id: scd40 + - platform: template + name: Midea Display Toggle + on_press: + midea_ac.display_toggle: + - platform: template + name: Midea Swing Step + on_press: + midea_ac.swing_step: + - platform: template + name: Midea Power On + on_press: + midea_ac.power_on: + - platform: template + name: Midea Power Off + on_press: + midea_ac.power_off: + - platform: template + name: Midea Power Inverse + on_press: + midea_ac.power_toggle: From a8ceeaa7b0616b132091f2fcd1d02c137238e4d8 Mon Sep 17 00:00:00 2001 From: Martin <25747549+martgras@users.noreply.github.com> Date: Mon, 23 May 2022 10:56:26 +0200 Subject: [PATCH 0493/1729] esp32: fix NVS (#3497) --- esphome/components/esp32/preferences.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/esp32/preferences.cpp b/esphome/components/esp32/preferences.cpp index a78159825e..aa03c5acc7 100644 --- a/esphome/components/esp32/preferences.cpp +++ b/esphome/components/esp32/preferences.cpp @@ -156,7 +156,7 @@ class ESP32Preferences : public ESPPreferences { ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", to_save.key.c_str(), esp_err_to_name(err)); return true; } - return to_save.data == stored_data.data; + return to_save.data != stored_data.data; } }; From 9dc804ee27426ee3550855347e30b4ebd2fcd2d9 Mon Sep 17 00:00:00 2001 From: joseph douce Date: Tue, 24 May 2022 01:52:54 +0100 Subject: [PATCH 0494/1729] Output a true RMS voltage % (#3494) --- esphome/components/ac_dimmer/ac_dimmer.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/esphome/components/ac_dimmer/ac_dimmer.cpp b/esphome/components/ac_dimmer/ac_dimmer.cpp index e9af828a9d..1d0cd8d0ab 100644 --- a/esphome/components/ac_dimmer/ac_dimmer.cpp +++ b/esphome/components/ac_dimmer/ac_dimmer.cpp @@ -121,7 +121,11 @@ void IRAM_ATTR HOT AcDimmerDataStore::gpio_intr() { // calculate time until enable in µs: (1.0-value)*cycle_time, but with integer arithmetic // also take into account min_power auto min_us = this->cycle_time_us * this->min_power / 1000; - this->enable_time_us = std::max((uint32_t) 1, ((65535 - this->value) * (this->cycle_time_us - min_us)) / 65535); + // calculate required value to provide a true RMS voltage output + this->enable_time_us = + std::max((uint32_t) 1, (uint32_t)((65535 - (acos(1 - (2 * this->value / 65535.0)) / 3.14159 * 65535)) * + (this->cycle_time_us - min_us)) / + 65535); if (this->method == DIM_METHOD_LEADING_PULSE) { // Minimum pulse time should be enough for the triac to trigger when it is close to the ZC zone // this is for brightness near 99% From cd35ead890a934d474093e6c9791c5551e08ce04 Mon Sep 17 00:00:00 2001 From: Wumpf Date: Tue, 24 May 2022 03:00:06 +0200 Subject: [PATCH 0495/1729] [scd4x] Fix not passing arguments to templatable value for perform_forced_calibration (#3495) --- esphome/components/scd4x/automation.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/scd4x/automation.h b/esphome/components/scd4x/automation.h index 21ecb2ea4c..dc43e9eb56 100644 --- a/esphome/components/scd4x/automation.h +++ b/esphome/components/scd4x/automation.h @@ -11,7 +11,7 @@ template class PerformForcedCalibrationAction : public Actionvalue_.has_value()) { - this->parent_->perform_forced_calibration(value_.value()); + this->parent_->perform_forced_calibration(this->value_.value(x...)); } } From 6617d576a7aaa4f0d4319781f83df87f7c94224b Mon Sep 17 00:00:00 2001 From: user897943 Date: Wed, 18 May 2022 23:25:42 +0100 Subject: [PATCH 0496/1729] Update bedjet_const.h to remove blank spaces before speed steps, fixes Unknown Error when using climate.set_fan_mode in HA (#3476) --- esphome/components/bedjet/bedjet_const.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/bedjet/bedjet_const.h b/esphome/components/bedjet/bedjet_const.h index e6bfa45d3a..ae10ca1885 100644 --- a/esphome/components/bedjet/bedjet_const.h +++ b/esphome/components/bedjet/bedjet_const.h @@ -66,8 +66,8 @@ enum BedjetCommand : uint8_t { #define BEDJET_FAN_STEP_NAMES_ \ { \ - " 5%", " 10%", " 15%", " 20%", " 25%", " 30%", " 35%", " 40%", " 45%", " 50%", " 55%", " 60%", " 65%", " 70%", \ - " 75%", " 80%", " 85%", " 90%", " 95%", "100%" \ + "5%", "10%", "15%", "20%", "25%", "30%", "35%", "40%", "45%", "50%", "55%", "60%", "65%", "70%", "75%", "80%", \ + "85%", "90%", "95%", "100%" \ } static const char *const BEDJET_FAN_STEP_NAMES[20] = BEDJET_FAN_STEP_NAMES_; From b66af9fb4df4e093a6bd23672621301bb1662548 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 19 May 2022 16:23:40 +1200 Subject: [PATCH 0497/1729] Add missing import to bedjet (#3490) --- esphome/components/bedjet/bedjet.h | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/bedjet/bedjet.h b/esphome/components/bedjet/bedjet.h index 0565be6045..750a20594f 100644 --- a/esphome/components/bedjet/bedjet.h +++ b/esphome/components/bedjet/bedjet.h @@ -4,6 +4,7 @@ #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" #include "esphome/components/climate/climate.h" #include "esphome/core/component.h" +#include "esphome/core/defines.h" #include "esphome/core/hal.h" #include "bedjet_base.h" From fb0fec1f25fc24d8a7130cab60e99de3567d6b43 Mon Sep 17 00:00:00 2001 From: Martin <25747549+martgras@users.noreply.github.com> Date: Mon, 23 May 2022 10:56:26 +0200 Subject: [PATCH 0498/1729] esp32: fix NVS (#3497) --- esphome/components/esp32/preferences.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/esp32/preferences.cpp b/esphome/components/esp32/preferences.cpp index a78159825e..aa03c5acc7 100644 --- a/esphome/components/esp32/preferences.cpp +++ b/esphome/components/esp32/preferences.cpp @@ -156,7 +156,7 @@ class ESP32Preferences : public ESPPreferences { ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", to_save.key.c_str(), esp_err_to_name(err)); return true; } - return to_save.data == stored_data.data; + return to_save.data != stored_data.data; } }; From f3f6e54818342e8a19328b5f069ac49a0451a312 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 24 May 2022 21:56:18 +1200 Subject: [PATCH 0499/1729] Bump version to 2022.5.1 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 913f0fd0dc..3911a5fa4c 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2022.5.0" +__version__ = "2022.5.1" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" From adb7aa69509d2389144433e36a0cf965c47776c0 Mon Sep 17 00:00:00 2001 From: Michael Davidson Date: Wed, 25 May 2022 13:44:26 +1000 Subject: [PATCH 0500/1729] Thermostat preset with modes (#3298) * Rework HOME/AWAY support to being driven via a map of ClimatePreset/ThermostatClimateTargetTempConfig This opens up to theoretically being able to support other presets (ECO, SLEEP, etc) * Add support for additional presets Configuration takes the form; ``` climate: platform: preset ... preset: [eco | away | boost | comfort | home | sleep | activity]: default_target_temperature_low: 20 default_target_temperature_high: 24 ``` These will be available in the Home Assistant UI and, like the existing Home/Away config will reset the temperature in line with these defaults when selected. The existing away_config/home_config is still respected (although preset->home/preset->away will be applied after them and override them if both styles are specified) * Add support for specifying MODE, FAN_MODE and SWING_MODE on a preset When switching presets these will implicitly flow through to the controller. However calls to climate.control which specify any of these will take precedence even when changing the mode (think of the preset version as the default for that preset) * Add `preset_change` mode trigger When defined this trigger will fire when the preset for the thermostat has been changed. The intent of this is similar to `auto_mode` - it's not intended to be used to control the preset's state (eg. communicate with the physical thermostat) but instead might be used to update a visual indicator, for instance. * Apply lint, clang-format, and clang-tidy fixes * Additional clang-format fixes * Wrap log related strings in LOG_STR_ARG * Add support for custom presets This also changes the configuration syntax to; ```yaml preset: # Standard preset - name: [eco | away | boost | comfort | home | sleep | activity] default_target_temperature_low: 20 ... # Custom preset - name: My custom preset default_target_temperature_low: 18 ``` For the end user there is no difference between a custom and built in preset. For developers custom presets are set via `climate.control` `custom_preset` property instead of the `preset` * Lint/clang-format/clang-tidy fixes * Additional lint/clang-format/clang-tidy fixes * Clang-tidy changes * Sort imports * Improve configuration validation for presets - Unify temperature validation across default, away, and preset configuration - Validate modes for presets have the required actions * Trigger a refresh after changing internals of the thermostat * Apply formatting fixes * Validate mode, fan_mode, and swing_mode on presets * Add preset temperature validation against visual min/max configuration * Apply code formatting fixes * Fix preset temperature validation --- esphome/components/thermostat/climate.py | 235 ++++++++++++++++-- .../thermostat/thermostat_climate.cpp | 191 ++++++++++---- .../thermostat/thermostat_climate.h | 42 +++- 3 files changed, 373 insertions(+), 95 deletions(-) diff --git a/esphome/components/thermostat/climate.py b/esphome/components/thermostat/climate.py index 20565e811c..5e26e6d6de 100644 --- a/esphome/components/thermostat/climate.py +++ b/esphome/components/thermostat/climate.py @@ -14,6 +14,7 @@ from esphome.const import ( CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_DRY_ACTION, CONF_DRY_MODE, + CONF_FAN_MODE, CONF_FAN_MODE_ON_ACTION, CONF_FAN_MODE_OFF_ACTION, CONF_FAN_MODE_AUTO_ACTION, @@ -37,6 +38,7 @@ from esphome.const import ( CONF_IDLE_ACTION, CONF_MAX_COOLING_RUN_TIME, CONF_MAX_HEATING_RUN_TIME, + CONF_MAX_TEMPERATURE, CONF_MIN_COOLING_OFF_TIME, CONF_MIN_COOLING_RUN_TIME, CONF_MIN_FAN_MODE_SWITCHING_TIME, @@ -45,7 +47,11 @@ from esphome.const import ( CONF_MIN_HEATING_OFF_TIME, CONF_MIN_HEATING_RUN_TIME, CONF_MIN_IDLE_TIME, + CONF_MIN_TEMPERATURE, + CONF_NAME, + CONF_MODE, CONF_OFF_MODE, + CONF_PRESET, CONF_SENSOR, CONF_SET_POINT_MINIMUM_DIFFERENTIAL, CONF_STARTUP_DELAY, @@ -55,11 +61,15 @@ from esphome.const import ( CONF_SUPPLEMENTAL_HEATING_DELTA, CONF_SWING_BOTH_ACTION, CONF_SWING_HORIZONTAL_ACTION, + CONF_SWING_MODE, CONF_SWING_OFF_ACTION, CONF_SWING_VERTICAL_ACTION, CONF_TARGET_TEMPERATURE_CHANGE_ACTION, + CONF_VISUAL, ) +CONF_PRESET_CHANGE = "preset_change" + CODEOWNERS = ["@kbx81"] climate_ns = cg.esphome_ns.namespace("climate") @@ -82,6 +92,38 @@ CLIMATE_MODES = { } validate_climate_mode = cv.enum(CLIMATE_MODES, upper=True) +ClimatePreset = climate_ns.enum("ClimatePreset") + +PRESET_CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(ThermostatClimateTargetTempConfig), + cv.Required(CONF_NAME): cv.string_strict, + cv.Optional(CONF_MODE): validate_climate_mode, + cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_HIGH): cv.temperature, + cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature, + cv.Optional(CONF_FAN_MODE): cv.templatable(climate.validate_climate_fan_mode), + cv.Optional(CONF_SWING_MODE): cv.templatable( + climate.validate_climate_swing_mode + ), + } +) + + +def validate_temperature_preset(preset, root_config, name, requirements): + # verify temperature settings for the provided preset / default / away configuration + for config_temp, req_actions in requirements.items(): + for req_action in req_actions: + # verify corresponding default target temperature exists when a given climate action exists + if config_temp not in preset and req_action in root_config: + raise cv.Invalid( + f"{config_temp} must be defined in {name} config when using {req_action}" + ) + # if a given climate action is NOT defined, it should not have a default target temperature + if config_temp in preset and req_action not in root_config: + raise cv.Invalid( + f"{config_temp} is defined in {name} config with no {req_action}" + ) + def validate_thermostat(config): # verify corresponding action(s) exist(s) for any defined climate mode or action @@ -235,33 +277,22 @@ def validate_thermostat(config): CONF_DEFAULT_TARGET_TEMPERATURE_LOW: [CONF_HEAT_ACTION], } - for config_temp, req_actions in requirements.items(): - for req_action in req_actions: - # verify corresponding default target temperature exists when a given climate action exists - if config_temp not in config and req_action in config: - raise cv.Invalid( - f"{config_temp} must be defined when using {req_action}" - ) - # if a given climate action is NOT defined, it should not have a default target temperature - if config_temp in config and req_action not in config: - raise cv.Invalid(f"{config_temp} is defined with no {req_action}") + # Validate temperature requirements for default configuraation + validate_temperature_preset(config, config, "default", requirements) + # Validate temperature requirements for away configuration if CONF_AWAY_CONFIG in config: away = config[CONF_AWAY_CONFIG] - for config_temp, req_actions in requirements.items(): - for req_action in req_actions: - # verify corresponding default target temperature exists when a given climate action exists - if config_temp not in away and req_action in config: - raise cv.Invalid( - f"{config_temp} must be defined in away configuration when using {req_action}" - ) - # if a given climate action is NOT defined, it should not have a default target temperature - if config_temp in away and req_action not in config: - raise cv.Invalid( - f"{config_temp} is defined in away configuration with no {req_action}" - ) + validate_temperature_preset(away, config, "away", requirements) - # verify default climate mode is valid given above configuration + # Validate temperature requirements for presets + if CONF_PRESET in config: + for preset_config in config[CONF_PRESET]: + validate_temperature_preset( + preset_config, config, preset_config[CONF_NAME], requirements + ) + + # Verify default climate mode is valid given above configuration default_mode = config[CONF_DEFAULT_MODE] requirements = { "HEAT_COOL": [CONF_COOL_ACTION, CONF_HEAT_ACTION], @@ -270,13 +301,108 @@ def validate_thermostat(config): "DRY": [CONF_DRY_ACTION], "FAN_ONLY": [CONF_FAN_ONLY_ACTION], "AUTO": [CONF_COOL_ACTION, CONF_HEAT_ACTION], - }.get(default_mode, []) - for req in requirements: + "OFF": [], + } + actions_for_default_mode = requirements.get(default_mode, []) + for req in actions_for_default_mode: if req not in config: raise cv.Invalid( f"{CONF_DEFAULT_MODE} is set to {default_mode} but {req} is not present in the configuration" ) + # Verify that the modes for presets are valid given the configuration + if CONF_PRESET in config: + # Preset temperature vs Visual temperature validation + + # Default visual configuration from climate_traits.h + visual_min_temperature = 10.0 + visual_max_temperature = 30.0 + if CONF_VISUAL in config: + visual_config = config[CONF_VISUAL] + + if CONF_MIN_TEMPERATURE in visual_config: + visual_min_temperature = visual_config[CONF_MIN_TEMPERATURE] + + if CONF_MAX_TEMPERATURE in visual_config: + visual_max_temperature = visual_config[CONF_MAX_TEMPERATURE] + + for preset_config in config[CONF_PRESET]: + if CONF_DEFAULT_TARGET_TEMPERATURE_LOW in preset_config: + preset_min_temperature = preset_config[ + CONF_DEFAULT_TARGET_TEMPERATURE_LOW + ] + if preset_min_temperature < visual_min_temperature: + raise cv.Invalid( + f"{CONF_DEFAULT_TARGET_TEMPERATURE_LOW} for {preset_config[CONF_NAME]} is set to {preset_min_temperature} which is less than the visual minimum temperature of {visual_min_temperature}" + ) + + if CONF_DEFAULT_TARGET_TEMPERATURE_HIGH in preset_config: + preset_max_temperature = preset_config[ + CONF_DEFAULT_TARGET_TEMPERATURE_HIGH + ] + if preset_max_temperature > visual_max_temperature: + raise cv.Invalid( + f"{CONF_DEFAULT_TARGET_TEMPERATURE_HIGH} for {preset_config[CONF_NAME]} is set to {preset_max_temperature} which is more than the visual maximum temperature of {visual_max_temperature}" + ) + + # Mode validation + for preset_config in config[CONF_PRESET]: + if CONF_MODE not in preset_config: + continue + + mode = preset_config[CONF_MODE] + + for req in requirements[mode]: + if req not in config: + raise cv.Invalid( + f"{CONF_MODE} is set to {mode} for {preset_config[CONF_NAME]} but {req} is not present in the configuration" + ) + + # Fan mode requirements + requirements = { + "ON": [CONF_FAN_MODE_ON_ACTION], + "OFF": [CONF_FAN_MODE_OFF_ACTION], + "AUTO": [CONF_FAN_MODE_AUTO_ACTION], + "LOW": [CONF_FAN_MODE_LOW_ACTION], + "MEDIUM": [CONF_FAN_MODE_MEDIUM_ACTION], + "HIGH": [CONF_FAN_MODE_HIGH_ACTION], + "MIDDLE": [CONF_FAN_MODE_MIDDLE_ACTION], + "FOCUS": [CONF_FAN_MODE_FOCUS_ACTION], + "DIFFUSE": [CONF_FAN_MODE_DIFFUSE_ACTION], + } + + for preset_config in config[CONF_PRESET]: + if CONF_FAN_MODE not in preset_config: + continue + + fan_mode = preset_config[CONF_FAN_MODE] + + for req in requirements[fan_mode]: + if req not in config: + raise cv.Invalid( + f"{CONF_FAN_MODE} is set to {fan_mode} for {preset_config[CONF_NAME]} but {req} is not present in the configuration" + ) + + # Swing mode requirements + requirements = { + "OFF": [CONF_SWING_OFF_ACTION], + "BOTH": [CONF_SWING_BOTH_ACTION], + "VERTICAL": [CONF_SWING_VERTICAL_ACTION], + "HORIZONTAL": [CONF_SWING_HORIZONTAL_ACTION], + } + + for preset_config in config[CONF_PRESET]: + if CONF_SWING_MODE not in preset_config: + continue + + swing_mode = preset_config[CONF_SWING_MODE] + + for req in requirements[swing_mode]: + if req not in config: + raise cv.Invalid( + f"{CONF_SWING_MODE} is set to {swing_mode} for {preset_config[CONF_NAME]} but {req} is not present in the configuration" + ) + if config[CONF_FAN_WITH_COOLING] is True and CONF_FAN_ONLY_ACTION not in config: raise cv.Invalid( f"{CONF_FAN_ONLY_ACTION} must be defined to use {CONF_FAN_WITH_COOLING}" @@ -415,6 +541,10 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature, } ), + cv.Optional(CONF_PRESET): cv.ensure_list(PRESET_CONFIG_SCHEMA), + cv.Optional(CONF_PRESET_CHANGE): automation.validate_automation( + single=True + ), } ).extend(cv.COMPONENT_SCHEMA), cv.has_at_least_one_key( @@ -531,7 +661,7 @@ async def to_code(config): cg.add(var.set_supports_fan_with_heating(config[CONF_FAN_WITH_HEATING])) cg.add(var.set_use_startup_delay(config[CONF_STARTUP_DELAY])) - cg.add(var.set_normal_config(normal_config)) + cg.add(var.set_preset_config(ClimatePreset.CLIMATE_PRESET_HOME, normal_config)) await automation.build_automation( var.get_idle_action_trigger(), [], config[CONF_IDLE_ACTION] @@ -694,4 +824,55 @@ async def to_code(config): away_config = ThermostatClimateTargetTempConfig( away[CONF_DEFAULT_TARGET_TEMPERATURE_LOW] ) - cg.add(var.set_away_config(away_config)) + cg.add(var.set_preset_config(ClimatePreset.CLIMATE_PRESET_AWAY, away_config)) + + if CONF_PRESET in config: + for preset_config in config[CONF_PRESET]: + + name = preset_config[CONF_NAME] + standard_preset = None + if name.upper() in climate.CLIMATE_PRESETS: + standard_preset = climate.CLIMATE_PRESETS[name.upper()] + + if two_points_available is True: + preset_target_config = ThermostatClimateTargetTempConfig( + preset_config[CONF_DEFAULT_TARGET_TEMPERATURE_LOW], + preset_config[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH], + ) + elif CONF_DEFAULT_TARGET_TEMPERATURE_HIGH in preset_config: + preset_target_config = ThermostatClimateTargetTempConfig( + preset_config[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH] + ) + elif CONF_DEFAULT_TARGET_TEMPERATURE_LOW in preset_config: + preset_target_config = ThermostatClimateTargetTempConfig( + preset_config[CONF_DEFAULT_TARGET_TEMPERATURE_LOW] + ) + + preset_target_variable = cg.new_variable( + preset_config[CONF_ID], preset_target_config + ) + + if CONF_MODE in preset_config: + cg.add(preset_target_variable.set_mode(preset_config[CONF_MODE])) + + if CONF_FAN_MODE in preset_config: + cg.add( + preset_target_variable.set_fan_mode(preset_config[CONF_FAN_MODE]) + ) + + if CONF_SWING_MODE in preset_config: + cg.add( + preset_target_variable.set_swing_mode( + preset_config[CONF_SWING_MODE] + ) + ) + + if standard_preset is not None: + cg.add(var.set_preset_config(standard_preset, preset_target_variable)) + else: + cg.add(var.set_custom_preset_config(name, preset_target_variable)) + + if CONF_PRESET_CHANGE in config: + await automation.build_automation( + var.get_preset_change_trigger(), [], config[CONF_PRESET_CHANGE] + ) diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index 760525e2cd..dc4e1e437e 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -32,7 +32,7 @@ void ThermostatClimate::setup() { } else { // restore from defaults, change_away handles temps for us this->mode = this->default_mode_; - this->change_away_(false); + this->change_preset_(climate::CLIMATE_PRESET_HOME); } // refresh the climate action based on the restored settings, we'll publish_state() later this->switch_to_action_(this->compute_action_(), false); @@ -162,11 +162,20 @@ void ThermostatClimate::control(const climate::ClimateCall &call) { if (call.get_preset().has_value()) { // setup_complete_ blocks modifying/resetting the temps immediately after boot if (this->setup_complete_) { - this->change_away_(*call.get_preset() == climate::CLIMATE_PRESET_AWAY); + this->change_preset_(*call.get_preset()); } else { this->preset = *call.get_preset(); } } + if (call.get_custom_preset().has_value()) { + // setup_complete_ blocks modifying/resetting the temps immediately after boot + if (this->setup_complete_) { + this->change_custom_preset_(*call.get_custom_preset()); + } else { + this->custom_preset = *call.get_custom_preset(); + } + } + if (call.get_mode().has_value()) this->mode = *call.get_mode(); if (call.get_fan_mode().has_value()) @@ -236,8 +245,12 @@ climate::ClimateTraits ThermostatClimate::traits() { if (supports_swing_mode_vertical_) traits.add_supported_swing_mode(climate::CLIMATE_SWING_VERTICAL); - if (supports_away_) - traits.set_supported_presets({climate::CLIMATE_PRESET_HOME, climate::CLIMATE_PRESET_AWAY}); + for (auto &it : this->preset_config_) { + traits.add_supported_preset(it.first); + } + for (auto &it : this->custom_preset_config_) { + traits.add_supported_custom_preset(it.first); + } traits.set_supports_two_point_target_temperature(this->supports_two_points_); traits.set_supports_action(true); @@ -910,30 +923,112 @@ bool ThermostatClimate::supplemental_heating_required_() { (this->supplemental_action_ == climate::CLIMATE_ACTION_HEATING)); } -void ThermostatClimate::change_away_(bool away) { - if (!away) { +void ThermostatClimate::dump_preset_config_(const std::string &preset, + const ThermostatClimateTargetTempConfig &config) { + const auto *preset_name = preset.c_str(); + + if (this->supports_heat_) { if (this->supports_two_points_) { - this->target_temperature_low = this->normal_config_.default_temperature_low; - this->target_temperature_high = this->normal_config_.default_temperature_high; - } else - this->target_temperature = this->normal_config_.default_temperature; - } else { - if (this->supports_two_points_) { - this->target_temperature_low = this->away_config_.default_temperature_low; - this->target_temperature_high = this->away_config_.default_temperature_high; - } else - this->target_temperature = this->away_config_.default_temperature; + ESP_LOGCONFIG(TAG, " %s Default Target Temperature Low: %.1f°C", preset_name, + config.default_temperature_low); + } else { + ESP_LOGCONFIG(TAG, " %s Default Target Temperature Low: %.1f°C", preset_name, config.default_temperature); + } + } + if ((this->supports_cool_) || (this->supports_fan_only_)) { + if (this->supports_two_points_) { + ESP_LOGCONFIG(TAG, " %s Default Target Temperature High: %.1f°C", preset_name, + config.default_temperature_high); + } else { + ESP_LOGCONFIG(TAG, " %s Default Target Temperature High: %.1f°C", preset_name, config.default_temperature); + } + } + + if (config.mode_.has_value()) { + ESP_LOGCONFIG(TAG, " %s Default Mode: %s", preset_name, + LOG_STR_ARG(climate::climate_mode_to_string(*config.mode_))); + } + if (config.fan_mode_.has_value()) { + ESP_LOGCONFIG(TAG, " %s Default Fan Mode: %s", preset_name, + LOG_STR_ARG(climate::climate_fan_mode_to_string(*config.fan_mode_))); + } + if (config.swing_mode_.has_value()) { + ESP_LOGCONFIG(TAG, " %s Default Swing Mode: %s", preset_name, + LOG_STR_ARG(climate::climate_swing_mode_to_string(*config.swing_mode_))); } - this->preset = away ? climate::CLIMATE_PRESET_AWAY : climate::CLIMATE_PRESET_HOME; } -void ThermostatClimate::set_normal_config(const ThermostatClimateTargetTempConfig &normal_config) { - this->normal_config_ = normal_config; +void ThermostatClimate::change_preset_(climate::ClimatePreset preset) { + auto config = this->preset_config_.find(preset); + + if (config != this->preset_config_.end()) { + ESP_LOGI(TAG, "Switching to preset %s", LOG_STR_ARG(climate::climate_preset_to_string(preset))); + this->change_preset_internal_(config->second); + + this->custom_preset.reset(); + this->preset = preset; + } else { + ESP_LOGE(TAG, "Preset %s is not configured, ignoring.", LOG_STR_ARG(climate::climate_preset_to_string(preset))); + } } -void ThermostatClimate::set_away_config(const ThermostatClimateTargetTempConfig &away_config) { - this->supports_away_ = true; - this->away_config_ = away_config; +void ThermostatClimate::change_custom_preset_(const std::string &custom_preset) { + auto config = this->custom_preset_config_.find(custom_preset); + + if (config != this->custom_preset_config_.end()) { + ESP_LOGI(TAG, "Switching to custom preset %s", custom_preset.c_str()); + this->change_preset_internal_(config->second); + + this->preset.reset(); + this->custom_preset = custom_preset; + } else { + ESP_LOGE(TAG, "Custom Preset %s is not configured, ignoring.", custom_preset.c_str()); + } +} + +void ThermostatClimate::change_preset_internal_(const ThermostatClimateTargetTempConfig &config) { + if (this->supports_two_points_) { + this->target_temperature_low = config.default_temperature_low; + this->target_temperature_high = config.default_temperature_high; + } else { + this->target_temperature = config.default_temperature; + } + + // Note: The mode, fan_mode, and swing_mode can all be defined on the preset but if the climate.control call + // also specifies them then the control's version will override these for that call + if (config.mode_.has_value()) { + this->mode = *config.mode_; + ESP_LOGV(TAG, "Setting mode to %s", LOG_STR_ARG(climate::climate_mode_to_string(*config.mode_))); + } + + if (config.fan_mode_.has_value()) { + this->fan_mode = *config.fan_mode_; + ESP_LOGV(TAG, "Setting fan mode to %s", LOG_STR_ARG(climate::climate_fan_mode_to_string(*config.fan_mode_))); + } + + if (config.swing_mode_.has_value()) { + ESP_LOGV(TAG, "Setting swing mode to %s", LOG_STR_ARG(climate::climate_swing_mode_to_string(*config.swing_mode_))); + this->swing_mode = *config.swing_mode_; + } + + // Fire any preset changed trigger if defined + if (this->preset != preset) { + Trigger<> *trig = this->preset_change_trigger_; + assert(trig != nullptr); + trig->trigger(); + } + + this->refresh(); +} + +void ThermostatClimate::set_preset_config(climate::ClimatePreset preset, + const ThermostatClimateTargetTempConfig &config) { + this->preset_config_[preset] = config; +} + +void ThermostatClimate::set_custom_preset_config(const std::string &name, + const ThermostatClimateTargetTempConfig &config) { + this->custom_preset_config_[name] = config; } ThermostatClimate::ThermostatClimate() @@ -963,7 +1058,8 @@ ThermostatClimate::ThermostatClimate() swing_mode_off_trigger_(new Trigger<>()), swing_mode_horizontal_trigger_(new Trigger<>()), swing_mode_vertical_trigger_(new Trigger<>()), - temperature_change_trigger_(new Trigger<>()) {} + temperature_change_trigger_(new Trigger<>()), + preset_change_trigger_(new Trigger<>()) {} void ThermostatClimate::set_default_mode(climate::ClimateMode default_mode) { this->default_mode_ = default_mode; } void ThermostatClimate::set_set_point_minimum_differential(float differential) { @@ -1112,23 +1208,11 @@ Trigger<> *ThermostatClimate::get_swing_mode_off_trigger() const { return this-> Trigger<> *ThermostatClimate::get_swing_mode_horizontal_trigger() const { return this->swing_mode_horizontal_trigger_; } Trigger<> *ThermostatClimate::get_swing_mode_vertical_trigger() const { return this->swing_mode_vertical_trigger_; } Trigger<> *ThermostatClimate::get_temperature_change_trigger() const { return this->temperature_change_trigger_; } +Trigger<> *ThermostatClimate::get_preset_change_trigger() const { return this->preset_change_trigger_; } void ThermostatClimate::dump_config() { LOG_CLIMATE("", "Thermostat", this); - if (this->supports_heat_) { - if (this->supports_two_points_) { - ESP_LOGCONFIG(TAG, " Default Target Temperature Low: %.1f°C", this->normal_config_.default_temperature_low); - } else { - ESP_LOGCONFIG(TAG, " Default Target Temperature Low: %.1f°C", this->normal_config_.default_temperature); - } - } - if ((this->supports_cool_) || (this->supports_fan_only_ && this->supports_fan_only_cooling_)) { - if (this->supports_two_points_) { - ESP_LOGCONFIG(TAG, " Default Target Temperature High: %.1f°C", this->normal_config_.default_temperature_high); - } else { - ESP_LOGCONFIG(TAG, " Default Target Temperature High: %.1f°C", this->normal_config_.default_temperature); - } - } + if (this->supports_two_points_) ESP_LOGCONFIG(TAG, " Minimum Set Point Differential: %.1f°C", this->set_point_minimum_differential_); ESP_LOGCONFIG(TAG, " Start-up Delay Enabled: %s", YESNO(this->use_startup_delay_)); @@ -1194,24 +1278,21 @@ void ThermostatClimate::dump_config() { ESP_LOGCONFIG(TAG, " Supports SWING MODE HORIZONTAL: %s", YESNO(this->supports_swing_mode_horizontal_)); ESP_LOGCONFIG(TAG, " Supports SWING MODE VERTICAL: %s", YESNO(this->supports_swing_mode_vertical_)); ESP_LOGCONFIG(TAG, " Supports TWO SET POINTS: %s", YESNO(this->supports_two_points_)); - ESP_LOGCONFIG(TAG, " Supports AWAY mode: %s", YESNO(this->supports_away_)); - if (this->supports_away_) { - if (this->supports_heat_) { - if (this->supports_two_points_) { - ESP_LOGCONFIG(TAG, " Away Default Target Temperature Low: %.1f°C", - this->away_config_.default_temperature_low); - } else { - ESP_LOGCONFIG(TAG, " Away Default Target Temperature Low: %.1f°C", this->away_config_.default_temperature); - } - } - if ((this->supports_cool_) || (this->supports_fan_only_)) { - if (this->supports_two_points_) { - ESP_LOGCONFIG(TAG, " Away Default Target Temperature High: %.1f°C", - this->away_config_.default_temperature_high); - } else { - ESP_LOGCONFIG(TAG, " Away Default Target Temperature High: %.1f°C", this->away_config_.default_temperature); - } - } + + ESP_LOGCONFIG(TAG, " Supported PRESETS: "); + for (auto &it : this->preset_config_) { + const auto *preset_name = LOG_STR_ARG(climate::climate_preset_to_string(it.first)); + + ESP_LOGCONFIG(TAG, " Supports %s: %s", preset_name, YESNO(true)); + this->dump_preset_config_(preset_name, it.second); + } + + ESP_LOGCONFIG(TAG, " Supported CUSTOM PRESETS: "); + for (auto &it : this->custom_preset_config_) { + const auto *preset_name = it.first.c_str(); + + ESP_LOGCONFIG(TAG, " Supports %s: %s", preset_name, YESNO(true)); + this->dump_preset_config_(preset_name, it.second); } } diff --git a/esphome/components/thermostat/thermostat_climate.h b/esphome/components/thermostat/thermostat_climate.h index 8d3e926752..c9231370ba 100644 --- a/esphome/components/thermostat/thermostat_climate.h +++ b/esphome/components/thermostat/thermostat_climate.h @@ -4,6 +4,7 @@ #include "esphome/core/automation.h" #include "esphome/components/climate/climate.h" #include "esphome/components/sensor/sensor.h" +#include namespace esphome { namespace thermostat { @@ -34,6 +35,10 @@ struct ThermostatClimateTargetTempConfig { ThermostatClimateTargetTempConfig(float default_temperature); ThermostatClimateTargetTempConfig(float default_temperature_low, float default_temperature_high); + void set_fan_mode(climate::ClimateFanMode fan_mode) { this->fan_mode_ = fan_mode; } + void set_swing_mode(climate::ClimateSwingMode swing_mode) { this->swing_mode_ = swing_mode; } + void set_mode(climate::ClimateMode mode) { this->mode_ = mode; } + float default_temperature{NAN}; float default_temperature_low{NAN}; float default_temperature_high{NAN}; @@ -41,6 +46,9 @@ struct ThermostatClimateTargetTempConfig { float cool_overrun_{NAN}; float heat_deadband_{NAN}; float heat_overrun_{NAN}; + optional fan_mode_{}; + optional swing_mode_{}; + optional mode_{}; }; class ThermostatClimate : public climate::Climate, public Component { @@ -94,8 +102,8 @@ class ThermostatClimate : public climate::Climate, public Component { void set_supports_swing_mode_vertical(bool supports_swing_mode_vertical); void set_supports_two_points(bool supports_two_points); - void set_normal_config(const ThermostatClimateTargetTempConfig &normal_config); - void set_away_config(const ThermostatClimateTargetTempConfig &away_config); + void set_preset_config(climate::ClimatePreset preset, const ThermostatClimateTargetTempConfig &config); + void set_custom_preset_config(const std::string &name, const ThermostatClimateTargetTempConfig &config); Trigger<> *get_cool_action_trigger() const; Trigger<> *get_supplemental_cool_action_trigger() const; @@ -124,6 +132,7 @@ class ThermostatClimate : public climate::Climate, public Component { Trigger<> *get_swing_mode_off_trigger() const; Trigger<> *get_swing_mode_vertical_trigger() const; Trigger<> *get_temperature_change_trigger() const; + Trigger<> *get_preset_change_trigger() const; /// Get current hysteresis values float cool_deadband(); float cool_overrun(); @@ -149,8 +158,14 @@ class ThermostatClimate : public climate::Climate, public Component { /// Override control to change settings of the climate device. void control(const climate::ClimateCall &call) override; - /// Change the away setting, will reset target temperatures to defaults. - void change_away_(bool away); + /// Change to a provided preset setting; will reset temperature, mode, fan, and swing modes accordingly + void change_preset_(climate::ClimatePreset preset); + /// Change to a provided custom preset setting; will reset temperature, mode, fan, and swing modes accordingly + void change_custom_preset_(const std::string &custom_preset); + + /// Applies the temperature, mode, fan, and swing modes of the provded config. + /// This is agnostic of custom vs built in preset + void change_preset_internal_(const ThermostatClimateTargetTempConfig &config); /// Return the traits of this controller. climate::ClimateTraits traits() override; @@ -210,6 +225,8 @@ class ThermostatClimate : public climate::Climate, public Component { bool supplemental_cooling_required_(); bool supplemental_heating_required_(); + void dump_preset_config_(const std::string &preset_name, const ThermostatClimateTargetTempConfig &config); + /// The sensor used for getting the current temperature sensor::Sensor *sensor_{nullptr}; @@ -267,11 +284,6 @@ class ThermostatClimate : public climate::Climate, public Component { /// A false value means that the controller has no such support. bool supports_two_points_{false}; - /// Whether the controller supports an "away" mode - /// - /// A false value means that the controller has no such mode. - bool supports_away_{false}; - /// Flags indicating if maximum allowable run time was exceeded bool cooling_max_runtime_exceeded_{false}; bool heating_max_runtime_exceeded_{false}; @@ -368,6 +380,9 @@ class ThermostatClimate : public climate::Climate, public Component { /// The trigger to call when the target temperature(s) change(es). Trigger<> *temperature_change_trigger_{nullptr}; + /// The triggr to call when the preset mode changes + Trigger<> *preset_change_trigger_{nullptr}; + /// A reference to the trigger that was previously active. /// /// This is so that the previous trigger can be stopped before enabling a new one @@ -409,10 +424,6 @@ class ThermostatClimate : public climate::Climate, public Component { /// Minimum allowable duration in seconds for action timers const uint8_t min_timer_duration_{1}; - /// Temperature data for normal/home and away modes - ThermostatClimateTargetTempConfig normal_config_{}; - ThermostatClimateTargetTempConfig away_config_{}; - /// Climate action timers std::vector timer_{ {"cool_run", false, 0, std::bind(&ThermostatClimate::cooling_max_run_time_timer_callback_, this)}, @@ -425,6 +436,11 @@ class ThermostatClimate : public climate::Climate, public Component { {"heat_off", false, 0, std::bind(&ThermostatClimate::heating_off_timer_callback_, this)}, {"heat_on", false, 0, std::bind(&ThermostatClimate::heating_on_timer_callback_, this)}, {"idle_on", false, 0, std::bind(&ThermostatClimate::idle_on_timer_callback_, this)}}; + + /// The set of standard preset configurations this thermostat supports (Eg. AWAY, ECO, etc) + std::map preset_config_{}; + /// The set of custom preset configurations this thermostat supports (eg. "My Custom Preset") + std::map custom_preset_config_{}; }; } // namespace thermostat From d2cefbf22495b3c62956a455c6bbc63daf40c96b Mon Sep 17 00:00:00 2001 From: Jan Grewe Date: Mon, 30 May 2022 21:29:57 +0200 Subject: [PATCH 0501/1729] Allow Prometheus component to export internal components (#3508) Co-authored-by: Jan Grewe --- esphome/components/prometheus/__init__.py | 8 +++++++- .../components/prometheus/prometheus_handler.cpp | 14 +++++++------- esphome/components/prometheus/prometheus_handler.h | 8 ++++++++ 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/esphome/components/prometheus/__init__.py b/esphome/components/prometheus/__init__.py index 45345f06e8..e7c0459251 100644 --- a/esphome/components/prometheus/__init__.py +++ b/esphome/components/prometheus/__init__.py @@ -1,6 +1,9 @@ import esphome.config_validation as cv import esphome.codegen as cg -from esphome.const import CONF_ID +from esphome.const import ( + CONF_ID, + CONF_INCLUDE_INTERNAL, +) from esphome.components.web_server_base import CONF_WEB_SERVER_BASE_ID from esphome.components import web_server_base @@ -15,6 +18,7 @@ CONFIG_SCHEMA = cv.Schema( cv.GenerateID(CONF_WEB_SERVER_BASE_ID): cv.use_id( web_server_base.WebServerBase ), + cv.Optional(CONF_INCLUDE_INTERNAL, default=False): cv.boolean, }, cv.only_with_arduino, ).extend(cv.COMPONENT_SCHEMA) @@ -27,3 +31,5 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID], paren) await cg.register_component(var, config) + + cg.add(var.set_include_internal(config[CONF_INCLUDE_INTERNAL])) diff --git a/esphome/components/prometheus/prometheus_handler.cpp b/esphome/components/prometheus/prometheus_handler.cpp index e4dd6b9043..a52347ba57 100644 --- a/esphome/components/prometheus/prometheus_handler.cpp +++ b/esphome/components/prometheus/prometheus_handler.cpp @@ -61,7 +61,7 @@ void PrometheusHandler::sensor_type_(AsyncResponseStream *stream) { stream->print(F("#TYPE esphome_sensor_failed GAUGE\n")); } void PrometheusHandler::sensor_row_(AsyncResponseStream *stream, sensor::Sensor *obj) { - if (obj->is_internal()) + if (obj->is_internal() && !this->include_internal_) return; if (!std::isnan(obj->state)) { // We have a valid value, output this value @@ -98,7 +98,7 @@ void PrometheusHandler::binary_sensor_type_(AsyncResponseStream *stream) { stream->print(F("#TYPE esphome_binary_sensor_failed GAUGE\n")); } void PrometheusHandler::binary_sensor_row_(AsyncResponseStream *stream, binary_sensor::BinarySensor *obj) { - if (obj->is_internal()) + if (obj->is_internal() && !this->include_internal_) return; if (obj->has_state()) { // We have a valid value, output this value @@ -134,7 +134,7 @@ void PrometheusHandler::fan_type_(AsyncResponseStream *stream) { stream->print(F("#TYPE esphome_fan_oscillation GAUGE\n")); } void PrometheusHandler::fan_row_(AsyncResponseStream *stream, fan::Fan *obj) { - if (obj->is_internal()) + if (obj->is_internal() && !this->include_internal_) return; stream->print(F("esphome_fan_failed{id=\"")); stream->print(obj->get_object_id().c_str()); @@ -179,7 +179,7 @@ void PrometheusHandler::light_type_(AsyncResponseStream *stream) { stream->print(F("#TYPE esphome_light_effect_active GAUGE\n")); } void PrometheusHandler::light_row_(AsyncResponseStream *stream, light::LightState *obj) { - if (obj->is_internal()) + if (obj->is_internal() && !this->include_internal_) return; // State stream->print(F("esphome_light_state{id=\"")); @@ -255,7 +255,7 @@ void PrometheusHandler::cover_type_(AsyncResponseStream *stream) { stream->print(F("#TYPE esphome_cover_failed GAUGE\n")); } void PrometheusHandler::cover_row_(AsyncResponseStream *stream, cover::Cover *obj) { - if (obj->is_internal()) + if (obj->is_internal() && !this->include_internal_) return; if (!std::isnan(obj->position)) { // We have a valid value, output this value @@ -298,7 +298,7 @@ void PrometheusHandler::switch_type_(AsyncResponseStream *stream) { stream->print(F("#TYPE esphome_switch_failed GAUGE\n")); } void PrometheusHandler::switch_row_(AsyncResponseStream *stream, switch_::Switch *obj) { - if (obj->is_internal()) + if (obj->is_internal() && !this->include_internal_) return; stream->print(F("esphome_switch_failed{id=\"")); stream->print(obj->get_object_id().c_str()); @@ -322,7 +322,7 @@ void PrometheusHandler::lock_type_(AsyncResponseStream *stream) { stream->print(F("#TYPE esphome_lock_failed GAUGE\n")); } void PrometheusHandler::lock_row_(AsyncResponseStream *stream, lock::Lock *obj) { - if (obj->is_internal()) + if (obj->is_internal() && !this->include_internal_) return; stream->print(F("esphome_lock_failed{id=\"")); stream->print(obj->get_object_id().c_str()); diff --git a/esphome/components/prometheus/prometheus_handler.h b/esphome/components/prometheus/prometheus_handler.h index 5c8d51c60f..b378e46ea3 100644 --- a/esphome/components/prometheus/prometheus_handler.h +++ b/esphome/components/prometheus/prometheus_handler.h @@ -13,6 +13,13 @@ class PrometheusHandler : public AsyncWebHandler, public Component { public: PrometheusHandler(web_server_base::WebServerBase *base) : base_(base) {} + /** Determine whether internal components should be exported as metrics. + * Defaults to false. + * + * @param include_internal Whether internal components should be exported. + */ + void set_include_internal(bool include_internal) { include_internal_ = include_internal; } + bool canHandle(AsyncWebServerRequest *request) override { if (request->method() == HTTP_GET) { if (request->url() == "/metrics") @@ -84,6 +91,7 @@ class PrometheusHandler : public AsyncWebHandler, public Component { #endif web_server_base::WebServerBase *base_; + bool include_internal_{false}; }; } // namespace prometheus From 708672ec7e18745ca76be2afffb05ca320986568 Mon Sep 17 00:00:00 2001 From: Joe Date: Mon, 30 May 2022 23:45:01 -0400 Subject: [PATCH 0502/1729] [BedJet] Add configurable heating strategy (#3519) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/bedjet/bedjet.cpp | 28 ++++++++++++++++++++++-- esphome/components/bedjet/bedjet.h | 10 +++++++-- esphome/components/bedjet/bedjet_const.h | 8 +++++++ esphome/components/bedjet/climate.py | 10 +++++++++ tests/test1.yaml | 1 + 5 files changed, 53 insertions(+), 4 deletions(-) diff --git a/esphome/components/bedjet/bedjet.cpp b/esphome/components/bedjet/bedjet.cpp index 38ed6206a8..493685448c 100644 --- a/esphome/components/bedjet/bedjet.cpp +++ b/esphome/components/bedjet/bedjet.cpp @@ -36,6 +36,14 @@ static uint8_t bedjet_fan_speed_to_step(const std::string &fan_step_percent) { return -1; } +static BedjetButton heat_button(BedjetHeatMode mode) { + BedjetButton btn = BTN_HEAT; + if (mode == HEAT_MODE_EXTENDED) { + btn = BTN_EXTHT; + } + return btn; +} + void Bedjet::upgrade_firmware() { auto *pkt = this->codec_->get_button_request(MAGIC_UPDATE); auto status = this->write_bedjet_packet_(pkt); @@ -117,7 +125,7 @@ void Bedjet::control(const ClimateCall &call) { pkt = this->codec_->get_button_request(BTN_OFF); break; case climate::CLIMATE_MODE_HEAT: - pkt = this->codec_->get_button_request(BTN_HEAT); + pkt = this->codec_->get_button_request(heat_button(this->heating_mode_)); break; case climate::CLIMATE_MODE_FAN_ONLY: pkt = this->codec_->get_button_request(BTN_COOL); @@ -186,6 +194,8 @@ void Bedjet::control(const ClimateCall &call) { pkt = this->codec_->get_button_request(BTN_M2); } else if (preset == "M3") { pkt = this->codec_->get_button_request(BTN_M3); + } else if (preset == "LTD HT") { + pkt = this->codec_->get_button_request(BTN_HEAT); } else if (preset == "EXT HT") { pkt = this->codec_->get_button_request(BTN_EXTHT); } else { @@ -557,11 +567,25 @@ bool Bedjet::update_status_() { break; case MODE_HEAT: + this->mode = climate::CLIMATE_MODE_HEAT; + this->action = climate::CLIMATE_ACTION_HEATING; + this->preset.reset(); + if (this->heating_mode_ == HEAT_MODE_EXTENDED) { + this->set_custom_preset_("LTD HT"); + } else { + this->custom_preset.reset(); + } + break; + case MODE_EXTHT: this->mode = climate::CLIMATE_MODE_HEAT; this->action = climate::CLIMATE_ACTION_HEATING; - this->custom_preset.reset(); this->preset.reset(); + if (this->heating_mode_ == HEAT_MODE_EXTENDED) { + this->custom_preset.reset(); + } else { + this->set_custom_preset_("EXT HT"); + } break; case MODE_COOL: diff --git a/esphome/components/bedjet/bedjet.h b/esphome/components/bedjet/bedjet.h index 750a20594f..5d66f6f252 100644 --- a/esphome/components/bedjet/bedjet.h +++ b/esphome/components/bedjet/bedjet.h @@ -40,6 +40,8 @@ class Bedjet : public climate::Climate, public esphome::ble_client::BLEClientNod void set_time_id(time::RealTimeClock *time_id) { this->time_id_ = time_id; } #endif void set_status_timeout(uint32_t timeout) { this->timeout_ = timeout; } + /** Sets the default strategy to use for climate::CLIMATE_MODE_HEAT. */ + void set_heating_mode(BedjetHeatMode mode) { this->heating_mode_ = mode; } /** Attempts to check for and apply firmware updates. */ void upgrade_firmware(); @@ -68,12 +70,15 @@ class Bedjet : public climate::Climate, public esphome::ble_client::BLEClientNod // We could fetch biodata from bedjet and set these names that way. // But then we have to invert the lookup in order to send the right preset. // For now, we can leave them as M1-3 to match the remote buttons. - // EXT HT added to match remote button. - "EXT HT", "M1", "M2", "M3", }); + if (this->heating_mode_ == HEAT_MODE_EXTENDED) { + traits.add_supported_custom_preset("LTD HT"); + } else { + traits.add_supported_custom_preset("EXT HT"); + } traits.set_visual_min_temperature(19.0); traits.set_visual_max_temperature(43.0); traits.set_visual_temperature_step(1.0); @@ -90,6 +95,7 @@ class Bedjet : public climate::Climate, public esphome::ble_client::BLEClientNod #endif uint32_t timeout_{DEFAULT_STATUS_TIMEOUT}; + BedjetHeatMode heating_mode_ = HEAT_MODE_HEAT; static const uint32_t MIN_NOTIFY_THROTTLE = 5000; static const uint32_t NOTIFY_WARN_THRESHOLD = 300000; diff --git a/esphome/components/bedjet/bedjet_const.h b/esphome/components/bedjet/bedjet_const.h index ae10ca1885..16f73717c6 100644 --- a/esphome/components/bedjet/bedjet_const.h +++ b/esphome/components/bedjet/bedjet_const.h @@ -24,6 +24,14 @@ enum BedjetMode : uint8_t { MODE_WAIT = 6, }; +/** Optional heating strategies to use for climate::CLIMATE_MODE_HEAT. */ +enum BedjetHeatMode { + /// HVACMode.HEAT is handled using BTN_HEAT (default) + HEAT_MODE_HEAT, + /// HVACMode.HEAT is handled using BTN_EXTHT + HEAT_MODE_EXTENDED, +}; + enum BedjetButton : uint8_t { /// Turn BedJet off BTN_OFF = 0x1, diff --git a/esphome/components/bedjet/climate.py b/esphome/components/bedjet/climate.py index 49353934f6..d718ba9969 100644 --- a/esphome/components/bedjet/climate.py +++ b/esphome/components/bedjet/climate.py @@ -2,6 +2,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import climate, ble_client, time from esphome.const import ( + CONF_HEAT_MODE, CONF_ID, CONF_RECEIVE_TIMEOUT, CONF_TIME_ID, @@ -14,11 +15,19 @@ bedjet_ns = cg.esphome_ns.namespace("bedjet") Bedjet = bedjet_ns.class_( "Bedjet", climate.Climate, ble_client.BLEClientNode, cg.PollingComponent ) +BedjetHeatMode = bedjet_ns.enum("BedjetHeatMode") +BEDJET_HEAT_MODES = { + "heat": BedjetHeatMode.HEAT_MODE_HEAT, + "extended": BedjetHeatMode.HEAT_MODE_EXTENDED, +} CONFIG_SCHEMA = ( climate.CLIMATE_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(Bedjet), + cv.Optional(CONF_HEAT_MODE, default="heat"): cv.enum( + BEDJET_HEAT_MODES, lower=True + ), cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock), cv.Optional( CONF_RECEIVE_TIMEOUT, default="0s" @@ -35,6 +44,7 @@ async def to_code(config): await cg.register_component(var, config) await climate.register_climate(var, config) await ble_client.register_ble_node(var, config) + cg.add(var.set_heating_mode(config[CONF_HEAT_MODE])) if CONF_TIME_ID in config: time_ = await cg.get_variable(config[CONF_TIME_ID]) cg.add(var.set_time_id(time_)) diff --git a/tests/test1.yaml b/tests/test1.yaml index 52aa03c371..1b8ed7e370 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1886,6 +1886,7 @@ climate: - platform: bedjet name: My Bedjet ble_client_id: my_bedjet_ble_client + heat_mode: extended script: - id: climate_custom From 5aa42e5e66a5a65528e3dd6218e17b59970dfc82 Mon Sep 17 00:00:00 2001 From: jimtng <2554958+jimtng@users.noreply.github.com> Date: Tue, 31 May 2022 14:45:18 +1000 Subject: [PATCH 0503/1729] Add variable substitutions for !include (#3510) --- esphome/components/substitutions/__init__.py | 33 +++++++------- esphome/yaml_util.py | 44 ++++++++++++++++++- .../fixtures/yaml_util/includes/included.yaml | 2 + .../fixtures/yaml_util/includes/list.yaml | 2 + .../fixtures/yaml_util/includes/scalar.yaml | 1 + .../fixtures/yaml_util/includetest.yaml | 17 +++++++ tests/unit_tests/test_yaml_util.py | 13 ++++++ 7 files changed, 95 insertions(+), 17 deletions(-) create mode 100644 tests/unit_tests/fixtures/yaml_util/includes/included.yaml create mode 100644 tests/unit_tests/fixtures/yaml_util/includes/list.yaml create mode 100644 tests/unit_tests/fixtures/yaml_util/includes/scalar.yaml create mode 100644 tests/unit_tests/fixtures/yaml_util/includetest.yaml create mode 100644 tests/unit_tests/test_yaml_util.py diff --git a/esphome/components/substitutions/__init__.py b/esphome/components/substitutions/__init__.py index 6188b14b35..5a3da1abbe 100644 --- a/esphome/components/substitutions/__init__.py +++ b/esphome/components/substitutions/__init__.py @@ -48,7 +48,7 @@ VARIABLE_PROG = re.compile( ) -def _expand_substitutions(substitutions, value, path): +def _expand_substitutions(substitutions, value, path, ignore_missing): if "$" not in value: return value @@ -66,13 +66,14 @@ def _expand_substitutions(substitutions, value, path): if name.startswith("{") and name.endswith("}"): name = name[1:-1] if name not in substitutions: - _LOGGER.warning( - "Found '%s' (see %s) which looks like a substitution, but '%s' was " - "not declared", - orig_value, - "->".join(str(x) for x in path), - name, - ) + if not ignore_missing: + _LOGGER.warning( + "Found '%s' (see %s) which looks like a substitution, but '%s' was " + "not declared", + orig_value, + "->".join(str(x) for x in path), + name, + ) i = j continue @@ -92,37 +93,37 @@ def _expand_substitutions(substitutions, value, path): return value -def _substitute_item(substitutions, item, path): +def _substitute_item(substitutions, item, path, ignore_missing): if isinstance(item, list): for i, it in enumerate(item): - sub = _substitute_item(substitutions, it, path + [i]) + sub = _substitute_item(substitutions, it, path + [i], ignore_missing) if sub is not None: item[i] = sub elif isinstance(item, dict): replace_keys = [] for k, v in item.items(): if path or k != CONF_SUBSTITUTIONS: - sub = _substitute_item(substitutions, k, path + [k]) + sub = _substitute_item(substitutions, k, path + [k], ignore_missing) if sub is not None: replace_keys.append((k, sub)) - sub = _substitute_item(substitutions, v, path + [k]) + sub = _substitute_item(substitutions, v, path + [k], ignore_missing) if sub is not None: item[k] = sub for old, new in replace_keys: item[new] = merge_config(item.get(old), item.get(new)) del item[old] elif isinstance(item, str): - sub = _expand_substitutions(substitutions, item, path) + sub = _expand_substitutions(substitutions, item, path, ignore_missing) if sub != item: return sub elif isinstance(item, core.Lambda): - sub = _expand_substitutions(substitutions, item.value, path) + sub = _expand_substitutions(substitutions, item.value, path, ignore_missing) if sub != item: item.value = sub return None -def do_substitution_pass(config, command_line_substitutions): +def do_substitution_pass(config, command_line_substitutions, ignore_missing=False): if CONF_SUBSTITUTIONS not in config and not command_line_substitutions: return @@ -151,4 +152,4 @@ def do_substitution_pass(config, command_line_substitutions): config[CONF_SUBSTITUTIONS] = substitutions # Move substitutions to the first place to replace substitutions in them correctly config.move_to_end(CONF_SUBSTITUTIONS, False) - _substitute_item(substitutions, config, []) + _substitute_item(substitutions, config, [], ignore_missing) diff --git a/esphome/yaml_util.py b/esphome/yaml_util.py index 57009be57e..75aec0edc8 100644 --- a/esphome/yaml_util.py +++ b/esphome/yaml_util.py @@ -251,7 +251,49 @@ class ESPHomeLoader(yaml.SafeLoader): # pylint: disable=too-many-ancestors @_add_data_ref def construct_include(self, node): - return _load_yaml_internal(self._rel_path(node.value)) + def extract_file_vars(node): + fields = self.construct_yaml_map(node) + file = fields.get("file") + if file is None: + raise yaml.MarkedYAMLError("Must include 'file'", node.start_mark) + vars = fields.get("vars") + if vars: + vars = {k: str(v) for k, v in vars.items()} + return file, vars + + def substitute_vars(config, vars): + from esphome.const import CONF_SUBSTITUTIONS + from esphome.components import substitutions + + org_subs = None + result = config + if not isinstance(config, dict): + # when the included yaml contains a list or a scalar + # wrap it into an OrderedDict because do_substitution_pass expects it + result = OrderedDict([("yaml", config)]) + elif CONF_SUBSTITUTIONS in result: + org_subs = result.pop(CONF_SUBSTITUTIONS) + + result[CONF_SUBSTITUTIONS] = vars + # Ignore missing vars that refer to the top level substitutions + substitutions.do_substitution_pass(result, None, ignore_missing=True) + result.pop(CONF_SUBSTITUTIONS) + + if not isinstance(config, dict): + result = result["yaml"] # unwrap the result + elif org_subs: + result[CONF_SUBSTITUTIONS] = org_subs + return result + + if isinstance(node, yaml.nodes.MappingNode): + file, vars = extract_file_vars(node) + else: + file, vars = node.value, None + + result = _load_yaml_internal(self._rel_path(file)) + if vars: + result = substitute_vars(result, vars) + return result @_add_data_ref def construct_include_dir_list(self, node): diff --git a/tests/unit_tests/fixtures/yaml_util/includes/included.yaml b/tests/unit_tests/fixtures/yaml_util/includes/included.yaml new file mode 100644 index 0000000000..e9fca324a3 --- /dev/null +++ b/tests/unit_tests/fixtures/yaml_util/includes/included.yaml @@ -0,0 +1,2 @@ +--- +ssid: ${name} diff --git a/tests/unit_tests/fixtures/yaml_util/includes/list.yaml b/tests/unit_tests/fixtures/yaml_util/includes/list.yaml new file mode 100644 index 0000000000..2fb3838631 --- /dev/null +++ b/tests/unit_tests/fixtures/yaml_util/includes/list.yaml @@ -0,0 +1,2 @@ +--- +- ${var1} diff --git a/tests/unit_tests/fixtures/yaml_util/includes/scalar.yaml b/tests/unit_tests/fixtures/yaml_util/includes/scalar.yaml new file mode 100644 index 0000000000..ddd2156b5e --- /dev/null +++ b/tests/unit_tests/fixtures/yaml_util/includes/scalar.yaml @@ -0,0 +1 @@ +${var1} diff --git a/tests/unit_tests/fixtures/yaml_util/includetest.yaml b/tests/unit_tests/fixtures/yaml_util/includetest.yaml new file mode 100644 index 0000000000..959283df60 --- /dev/null +++ b/tests/unit_tests/fixtures/yaml_util/includetest.yaml @@ -0,0 +1,17 @@ +--- +substitutions: + name: original + +wifi: !include + file: includes/included.yaml + vars: + name: my_custom_ssid + +esphome: + # should be substituted as 'original', 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 } } diff --git a/tests/unit_tests/test_yaml_util.py b/tests/unit_tests/test_yaml_util.py new file mode 100644 index 0000000000..8ee991f5b3 --- /dev/null +++ b/tests/unit_tests/test_yaml_util.py @@ -0,0 +1,13 @@ +from esphome import yaml_util +from esphome.components import substitutions + + +def test_include_with_vars(fixture_path): + yaml_file = fixture_path / "yaml_util" / "includetest.yaml" + + actual = yaml_util.load_yaml(yaml_file) + substitutions.do_substitution_pass(actual, None) + assert actual["esphome"]["name"] == "original" + assert actual["esphome"]["libraries"][0] == "Wire" + assert actual["esphome"]["board"] == "nodemcu" + assert actual["wifi"]["ssid"] == "my_custom_ssid" From a922efeafa9c46bd64fb9de7e88b05ea92de3d2c Mon Sep 17 00:00:00 2001 From: Wolfgang Tremmel Date: Tue, 31 May 2022 06:49:18 +0200 Subject: [PATCH 0504/1729] Change rain intensity sensor string (#3511) --- esphome/components/hydreon_rgxx/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/hydreon_rgxx/sensor.py b/esphome/components/hydreon_rgxx/sensor.py index 409500305a..c604f8d3c1 100644 --- a/esphome/components/hydreon_rgxx/sensor.py +++ b/esphome/components/hydreon_rgxx/sensor.py @@ -37,7 +37,7 @@ SUPPORTED_SENSORS = { PROTOCOL_NAMES = { CONF_MOISTURE: "R", CONF_ACC: "Acc", - CONF_R_INT: "Rint", + CONF_R_INT: "RInt", CONF_EVENT_ACC: "EventAcc", CONF_TOTAL_ACC: "TotalAcc", } From 6221f6d47d9522e62c1894bccb83c8cbc2bbb7f0 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 2 Jun 2022 17:00:17 +1200 Subject: [PATCH 0505/1729] Implement Media Player and I2S Media player (#3487) --- CODEOWNERS | 2 + esphome/components/api/api.proto | 62 +++- esphome/components/api/api_connection.cpp | 46 +++ esphome/components/api/api_connection.h | 5 + esphome/components/api/api_pb2.cpp | 278 ++++++++++++++++++ esphome/components/api/api_pb2.h | 67 +++++ esphome/components/api/api_pb2_service.cpp | 42 +++ esphome/components/api/api_pb2_service.h | 15 + esphome/components/api/api_server.cpp | 9 + esphome/components/api/api_server.h | 3 + esphome/components/api/list_entities.cpp | 6 + esphome/components/api/list_entities.h | 3 + esphome/components/api/subscribe_state.cpp | 5 + esphome/components/api/subscribe_state.h | 3 + esphome/components/i2s_audio/__init__.py | 0 .../i2s_audio/i2s_audio_media_player.cpp | 132 +++++++++ .../i2s_audio/i2s_audio_media_player.h | 63 ++++ esphome/components/i2s_audio/media_player.py | 94 ++++++ esphome/components/media_player/__init__.py | 64 ++++ .../components/media_player/media_player.cpp | 115 ++++++++ .../components/media_player/media_player.h | 91 ++++++ esphome/core/application.h | 19 ++ esphome/core/component_iterator.cpp | 18 ++ esphome/core/component_iterator.h | 6 + esphome/core/controller.cpp | 6 + esphome/core/controller.h | 6 + esphome/core/defines.h | 1 + platformio.ini | 2 + tests/test4.yaml | 9 + 29 files changed, 1170 insertions(+), 2 deletions(-) create mode 100644 esphome/components/i2s_audio/__init__.py create mode 100644 esphome/components/i2s_audio/i2s_audio_media_player.cpp create mode 100644 esphome/components/i2s_audio/i2s_audio_media_player.h create mode 100644 esphome/components/i2s_audio/media_player.py create mode 100644 esphome/components/media_player/__init__.py create mode 100644 esphome/components/media_player/media_player.cpp create mode 100644 esphome/components/media_player/media_player.h diff --git a/CODEOWNERS b/CODEOWNERS index 3e82a372ce..d5ce5e6920 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -88,6 +88,7 @@ esphome/components/honeywellabp/* @RubyBailey esphome/components/hrxl_maxsonar_wr/* @netmikey esphome/components/hydreon_rgxx/* @functionpointer esphome/components/i2c/* @esphome/core +esphome/components/i2s_audio/* @jesserockz esphome/components/improv_serial/* @esphome/core esphome/components/ina260/* @MrEditor97 esphome/components/inkbird_ibsth1_mini/* @fkirill @@ -119,6 +120,7 @@ esphome/components/mcp47a1/* @jesserockz esphome/components/mcp9808/* @k7hpn esphome/components/md5/* @esphome/core esphome/components/mdns/* @esphome/core +esphome/components/media_player/* @jesserockz esphome/components/midea/* @dudanov esphome/components/midea_ir/* @dudanov esphome/components/mitsubishi/* @RubyBailey diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index bd39893825..3e9a62f3d8 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -42,6 +42,7 @@ service APIConnection { rpc select_command (SelectCommandRequest) returns (void) {} rpc button_command (ButtonCommandRequest) returns (void) {} rpc lock_command (LockCommandRequest) returns (void) {} + rpc media_player_command (MediaPlayerCommandRequest) returns (void) {} } @@ -991,7 +992,7 @@ message ListEntitiesLockResponse { bool supports_open = 9; bool requires_code = 10; - # Not yet implemented: + // Not yet implemented: string code_format = 11; } message LockStateResponse { @@ -1010,7 +1011,7 @@ message LockCommandRequest { fixed32 key = 1; LockCommand command = 2; - # Not yet implemented: + // Not yet implemented: bool has_code = 3; string code = 4; } @@ -1040,3 +1041,60 @@ message ButtonCommandRequest { fixed32 key = 1; } +// ==================== MEDIA PLAYER ==================== +enum MediaPlayerState { + MEDIA_PLAYER_STATE_NONE = 0; + MEDIA_PLAYER_STATE_IDLE = 1; + MEDIA_PLAYER_STATE_PLAYING = 2; + MEDIA_PLAYER_STATE_PAUSED = 3; +} +enum MediaPlayerCommand { + MEDIA_PLAYER_COMMAND_PLAY = 0; + MEDIA_PLAYER_COMMAND_PAUSE = 1; + MEDIA_PLAYER_COMMAND_STOP = 2; + MEDIA_PLAYER_COMMAND_MUTE = 3; + MEDIA_PLAYER_COMMAND_UNMUTE = 4; +} +message ListEntitiesMediaPlayerResponse { + option (id) = 63; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_MEDIA_PLAYER"; + + string object_id = 1; + fixed32 key = 2; + string name = 3; + string unique_id = 4; + + string icon = 5; + bool disabled_by_default = 6; + EntityCategory entity_category = 7; + + bool supports_pause = 8; +} +message MediaPlayerStateResponse { + option (id) = 64; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_MEDIA_PLAYER"; + option (no_delay) = true; + fixed32 key = 1; + MediaPlayerState state = 2; + float volume = 3; + bool muted = 4; +} +message MediaPlayerCommandRequest { + option (id) = 65; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_MEDIA_PLAYER"; + option (no_delay) = true; + + fixed32 key = 1; + + bool has_command = 2; + MediaPlayerCommand command = 3; + + bool has_volume = 4; + float volume = 5; + + bool has_media_url = 6; + string media_url = 7; +} diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 4f399d95d0..9028034c90 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -733,6 +733,52 @@ void APIConnection::lock_command(const LockCommandRequest &msg) { } #endif +#ifdef USE_MEDIA_PLAYER +bool APIConnection::send_media_player_state(media_player::MediaPlayer *media_player) { + if (!this->state_subscription_) + return false; + + MediaPlayerStateResponse resp{}; + resp.key = media_player->get_object_id_hash(); + resp.state = static_cast(media_player->state); + resp.volume = media_player->volume; + resp.muted = media_player->is_muted(); + return this->send_media_player_state_response(resp); +} +bool APIConnection::send_media_player_info(media_player::MediaPlayer *media_player) { + ListEntitiesMediaPlayerResponse msg; + msg.key = media_player->get_object_id_hash(); + msg.object_id = media_player->get_object_id(); + msg.name = media_player->get_name(); + msg.unique_id = get_default_unique_id("media_player", media_player); + msg.icon = media_player->get_icon(); + msg.disabled_by_default = media_player->is_disabled_by_default(); + msg.entity_category = static_cast(media_player->get_entity_category()); + + auto traits = media_player->get_traits(); + msg.supports_pause = traits.get_supports_pause(); + + return this->send_list_entities_media_player_response(msg); +} +void APIConnection::media_player_command(const MediaPlayerCommandRequest &msg) { + media_player::MediaPlayer *media_player = App.get_media_player_by_key(msg.key); + if (media_player == nullptr) + return; + + auto call = media_player->make_call(); + if (msg.has_command) { + call.set_command(static_cast(msg.command)); + } + if (msg.has_volume) { + call.set_volume(msg.volume); + } + if (msg.has_media_url) { + call.set_media_url(msg.media_url); + } + call.perform(); +} +#endif + #ifdef USE_ESP32_CAMERA void APIConnection::send_camera_state(std::shared_ptr image) { if (!this->state_subscription_) diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 10f0becc54..0787d2f7eb 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -82,6 +82,11 @@ class APIConnection : public APIServerConnection { bool send_lock_state(lock::Lock *a_lock, lock::LockState state); bool send_lock_info(lock::Lock *a_lock); void lock_command(const LockCommandRequest &msg) override; +#endif +#ifdef USE_MEDIA_PLAYER + bool send_media_player_state(media_player::MediaPlayer *media_player); + bool send_media_player_info(media_player::MediaPlayer *media_player); + void media_player_command(const MediaPlayerCommandRequest &msg) override; #endif bool send_log_message(int level, const char *tag, const char *line); void send_homeassistant_service_call(const HomeassistantServiceResponse &call) { diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 5a78587473..70f909c07a 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -308,6 +308,36 @@ template<> const char *proto_enum_to_string(enums::LockComma return "UNKNOWN"; } } +template<> const char *proto_enum_to_string(enums::MediaPlayerState value) { + switch (value) { + case enums::MEDIA_PLAYER_STATE_NONE: + return "MEDIA_PLAYER_STATE_NONE"; + case enums::MEDIA_PLAYER_STATE_IDLE: + return "MEDIA_PLAYER_STATE_IDLE"; + case enums::MEDIA_PLAYER_STATE_PLAYING: + return "MEDIA_PLAYER_STATE_PLAYING"; + case enums::MEDIA_PLAYER_STATE_PAUSED: + return "MEDIA_PLAYER_STATE_PAUSED"; + default: + return "UNKNOWN"; + } +} +template<> const char *proto_enum_to_string(enums::MediaPlayerCommand value) { + switch (value) { + case enums::MEDIA_PLAYER_COMMAND_PLAY: + return "MEDIA_PLAYER_COMMAND_PLAY"; + case enums::MEDIA_PLAYER_COMMAND_PAUSE: + return "MEDIA_PLAYER_COMMAND_PAUSE"; + case enums::MEDIA_PLAYER_COMMAND_STOP: + return "MEDIA_PLAYER_COMMAND_STOP"; + case enums::MEDIA_PLAYER_COMMAND_MUTE: + return "MEDIA_PLAYER_COMMAND_MUTE"; + case enums::MEDIA_PLAYER_COMMAND_UNMUTE: + return "MEDIA_PLAYER_COMMAND_UNMUTE"; + default: + return "UNKNOWN"; + } +} bool HelloRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { case 1: { @@ -4574,6 +4604,254 @@ void ButtonCommandRequest::dump_to(std::string &out) const { out.append("}"); } #endif +bool ListEntitiesMediaPlayerResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 6: { + this->disabled_by_default = value.as_bool(); + return true; + } + case 7: { + this->entity_category = value.as_enum(); + return true; + } + case 8: { + this->supports_pause = value.as_bool(); + return true; + } + default: + return false; + } +} +bool ListEntitiesMediaPlayerResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 1: { + this->object_id = value.as_string(); + return true; + } + case 3: { + this->name = value.as_string(); + return true; + } + case 4: { + this->unique_id = value.as_string(); + return true; + } + case 5: { + this->icon = value.as_string(); + return true; + } + default: + return false; + } +} +bool ListEntitiesMediaPlayerResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 2: { + this->key = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_string(1, this->object_id); + buffer.encode_fixed32(2, this->key); + buffer.encode_string(3, this->name); + buffer.encode_string(4, this->unique_id); + buffer.encode_string(5, this->icon); + buffer.encode_bool(6, this->disabled_by_default); + buffer.encode_enum(7, this->entity_category); + buffer.encode_bool(8, this->supports_pause); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void ListEntitiesMediaPlayerResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("ListEntitiesMediaPlayerResponse {\n"); + out.append(" object_id: "); + out.append("'").append(this->object_id).append("'"); + out.append("\n"); + + out.append(" key: "); + sprintf(buffer, "%u", this->key); + out.append(buffer); + out.append("\n"); + + out.append(" name: "); + out.append("'").append(this->name).append("'"); + out.append("\n"); + + out.append(" unique_id: "); + out.append("'").append(this->unique_id).append("'"); + out.append("\n"); + + out.append(" icon: "); + out.append("'").append(this->icon).append("'"); + out.append("\n"); + + out.append(" disabled_by_default: "); + out.append(YESNO(this->disabled_by_default)); + out.append("\n"); + + out.append(" entity_category: "); + out.append(proto_enum_to_string(this->entity_category)); + out.append("\n"); + + out.append(" supports_pause: "); + out.append(YESNO(this->supports_pause)); + out.append("\n"); + out.append("}"); +} +#endif +bool MediaPlayerStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 2: { + this->state = value.as_enum(); + return true; + } + case 4: { + this->muted = value.as_bool(); + return true; + } + default: + return false; + } +} +bool MediaPlayerStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 1: { + this->key = value.as_fixed32(); + return true; + } + case 3: { + this->volume = value.as_float(); + return true; + } + default: + return false; + } +} +void MediaPlayerStateResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_fixed32(1, this->key); + buffer.encode_enum(2, this->state); + buffer.encode_float(3, this->volume); + buffer.encode_bool(4, this->muted); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void MediaPlayerStateResponse::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("MediaPlayerStateResponse {\n"); + out.append(" key: "); + sprintf(buffer, "%u", this->key); + out.append(buffer); + out.append("\n"); + + out.append(" state: "); + out.append(proto_enum_to_string(this->state)); + out.append("\n"); + + out.append(" volume: "); + sprintf(buffer, "%g", this->volume); + out.append(buffer); + out.append("\n"); + + out.append(" muted: "); + out.append(YESNO(this->muted)); + out.append("\n"); + out.append("}"); +} +#endif +bool MediaPlayerCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 2: { + this->has_command = value.as_bool(); + return true; + } + case 3: { + this->command = value.as_enum(); + return true; + } + case 4: { + this->has_volume = value.as_bool(); + return true; + } + case 6: { + this->has_media_url = value.as_bool(); + return true; + } + default: + return false; + } +} +bool MediaPlayerCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 7: { + this->media_url = value.as_string(); + return true; + } + default: + return false; + } +} +bool MediaPlayerCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 1: { + this->key = value.as_fixed32(); + return true; + } + case 5: { + this->volume = value.as_float(); + return true; + } + default: + return false; + } +} +void MediaPlayerCommandRequest::encode(ProtoWriteBuffer buffer) const { + buffer.encode_fixed32(1, this->key); + buffer.encode_bool(2, this->has_command); + buffer.encode_enum(3, this->command); + buffer.encode_bool(4, this->has_volume); + buffer.encode_float(5, this->volume); + buffer.encode_bool(6, this->has_media_url); + buffer.encode_string(7, this->media_url); +} +#ifdef HAS_PROTO_MESSAGE_DUMP +void MediaPlayerCommandRequest::dump_to(std::string &out) const { + __attribute__((unused)) char buffer[64]; + out.append("MediaPlayerCommandRequest {\n"); + out.append(" key: "); + sprintf(buffer, "%u", this->key); + out.append(buffer); + out.append("\n"); + + out.append(" has_command: "); + out.append(YESNO(this->has_command)); + out.append("\n"); + + out.append(" command: "); + out.append(proto_enum_to_string(this->command)); + out.append("\n"); + + out.append(" has_volume: "); + out.append(YESNO(this->has_volume)); + out.append("\n"); + + out.append(" volume: "); + sprintf(buffer, "%g", this->volume); + out.append(buffer); + out.append("\n"); + + out.append(" has_media_url: "); + out.append(YESNO(this->has_media_url)); + out.append("\n"); + + out.append(" media_url: "); + out.append("'").append(this->media_url).append("'"); + out.append("\n"); + out.append("}"); +} +#endif } // namespace api } // namespace esphome diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 28c0a7ce88..ec1cdc35ac 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -141,6 +141,19 @@ enum LockCommand : uint32_t { LOCK_LOCK = 1, LOCK_OPEN = 2, }; +enum MediaPlayerState : uint32_t { + MEDIA_PLAYER_STATE_NONE = 0, + MEDIA_PLAYER_STATE_IDLE = 1, + MEDIA_PLAYER_STATE_PLAYING = 2, + MEDIA_PLAYER_STATE_PAUSED = 3, +}; +enum MediaPlayerCommand : uint32_t { + MEDIA_PLAYER_COMMAND_PLAY = 0, + MEDIA_PLAYER_COMMAND_PAUSE = 1, + MEDIA_PLAYER_COMMAND_STOP = 2, + MEDIA_PLAYER_COMMAND_MUTE = 3, + MEDIA_PLAYER_COMMAND_UNMUTE = 4, +}; } // namespace enums @@ -1146,6 +1159,60 @@ class ButtonCommandRequest : public ProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; }; +class ListEntitiesMediaPlayerResponse : public ProtoMessage { + public: + std::string object_id{}; + uint32_t key{0}; + std::string name{}; + std::string unique_id{}; + std::string icon{}; + bool disabled_by_default{false}; + enums::EntityCategory entity_category{}; + bool supports_pause{false}; + void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class MediaPlayerStateResponse : public ProtoMessage { + public: + uint32_t key{0}; + enums::MediaPlayerState state{}; + float volume{0.0f}; + bool muted{false}; + void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class MediaPlayerCommandRequest : public ProtoMessage { + public: + uint32_t key{0}; + bool has_command{false}; + enums::MediaPlayerCommand command{}; + bool has_volume{false}; + float volume{0.0f}; + bool has_media_url{false}; + std::string media_url{}; + void encode(ProtoWriteBuffer buffer) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; } // namespace api } // namespace esphome diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index d981a3bf4e..bd146cb54d 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -310,6 +310,24 @@ bool APIServerConnectionBase::send_list_entities_button_response(const ListEntit #endif #ifdef USE_BUTTON #endif +#ifdef USE_MEDIA_PLAYER +bool APIServerConnectionBase::send_list_entities_media_player_response(const ListEntitiesMediaPlayerResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "send_list_entities_media_player_response: %s", msg.dump().c_str()); +#endif + return this->send_message_(msg, 63); +} +#endif +#ifdef USE_MEDIA_PLAYER +bool APIServerConnectionBase::send_media_player_state_response(const MediaPlayerStateResponse &msg) { +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "send_media_player_state_response: %s", msg.dump().c_str()); +#endif + return this->send_message_(msg, 64); +} +#endif +#ifdef USE_MEDIA_PLAYER +#endif bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) { switch (msg_type) { case 1: { @@ -563,6 +581,17 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, ESP_LOGVV(TAG, "on_button_command_request: %s", msg.dump().c_str()); #endif this->on_button_command_request(msg); +#endif + break; + } + case 65: { +#ifdef USE_MEDIA_PLAYER + MediaPlayerCommandRequest msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "on_media_player_command_request: %s", msg.dump().c_str()); +#endif + this->on_media_player_command_request(msg); #endif break; } @@ -813,6 +842,19 @@ void APIServerConnection::on_lock_command_request(const LockCommandRequest &msg) this->lock_command(msg); } #endif +#ifdef USE_MEDIA_PLAYER +void APIServerConnection::on_media_player_command_request(const MediaPlayerCommandRequest &msg) { + if (!this->is_connection_setup()) { + this->on_no_setup_connection(); + return; + } + if (!this->is_authenticated()) { + this->on_unauthenticated_access(); + return; + } + this->media_player_command(msg); +} +#endif } // namespace api } // namespace esphome diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index 5aaf831c91..28ad3fbd15 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -144,6 +144,15 @@ class APIServerConnectionBase : public ProtoService { #endif #ifdef USE_BUTTON virtual void on_button_command_request(const ButtonCommandRequest &value){}; +#endif +#ifdef USE_MEDIA_PLAYER + bool send_list_entities_media_player_response(const ListEntitiesMediaPlayerResponse &msg); +#endif +#ifdef USE_MEDIA_PLAYER + bool send_media_player_state_response(const MediaPlayerStateResponse &msg); +#endif +#ifdef USE_MEDIA_PLAYER + virtual void on_media_player_command_request(const MediaPlayerCommandRequest &value){}; #endif protected: bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override; @@ -192,6 +201,9 @@ class APIServerConnection : public APIServerConnectionBase { #endif #ifdef USE_LOCK virtual void lock_command(const LockCommandRequest &msg) = 0; +#endif +#ifdef USE_MEDIA_PLAYER + virtual void media_player_command(const MediaPlayerCommandRequest &msg) = 0; #endif protected: void on_hello_request(const HelloRequest &msg) override; @@ -236,6 +248,9 @@ class APIServerConnection : public APIServerConnectionBase { #ifdef USE_LOCK void on_lock_command_request(const LockCommandRequest &msg) override; #endif +#ifdef USE_MEDIA_PLAYER + void on_media_player_command_request(const MediaPlayerCommandRequest &msg) override; +#endif }; } // namespace api diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 1f2800f298..8375a82313 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -272,6 +272,15 @@ void APIServer::on_lock_update(lock::Lock *obj) { } #endif +#ifdef USE_MEDIA_PLAYER +void APIServer::on_media_player_update(media_player::MediaPlayer *obj) { + if (obj->is_internal()) + return; + for (auto &c : this->clients_) + c->send_media_player_state(obj); +} +#endif + float APIServer::get_setup_priority() const { return setup_priority::AFTER_WIFI; } void APIServer::set_port(uint16_t port) { this->port_ = port; } APIServer *global_api_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index f03a83fc7b..6997e23cac 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -68,6 +68,9 @@ class APIServer : public Component, public Controller { #endif #ifdef USE_LOCK void on_lock_update(lock::Lock *obj) override; +#endif +#ifdef USE_MEDIA_PLAYER + void on_media_player_update(media_player::MediaPlayer *obj) override; #endif void send_homeassistant_service_call(const HomeassistantServiceResponse &call); void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); } diff --git a/esphome/components/api/list_entities.cpp b/esphome/components/api/list_entities.cpp index 9f55fda617..85d4cd61ef 100644 --- a/esphome/components/api/list_entities.cpp +++ b/esphome/components/api/list_entities.cpp @@ -64,5 +64,11 @@ bool ListEntitiesIterator::on_number(number::Number *number) { return this->clie bool ListEntitiesIterator::on_select(select::Select *select) { return this->client_->send_select_info(select); } #endif +#ifdef USE_MEDIA_PLAYER +bool ListEntitiesIterator::on_media_player(media_player::MediaPlayer *media_player) { + return this->client_->send_media_player_info(media_player); +} +#endif + } // namespace api } // namespace esphome diff --git a/esphome/components/api/list_entities.h b/esphome/components/api/list_entities.h index 51c343eb03..4fbaa509a2 100644 --- a/esphome/components/api/list_entities.h +++ b/esphome/components/api/list_entities.h @@ -51,6 +51,9 @@ class ListEntitiesIterator : public ComponentIterator { #endif #ifdef USE_LOCK bool on_lock(lock::Lock *a_lock) override; +#endif +#ifdef USE_MEDIA_PLAYER + bool on_media_player(media_player::MediaPlayer *media_player) override; #endif bool on_end() override; diff --git a/esphome/components/api/subscribe_state.cpp b/esphome/components/api/subscribe_state.cpp index ba277502c8..1d1ba0245e 100644 --- a/esphome/components/api/subscribe_state.cpp +++ b/esphome/components/api/subscribe_state.cpp @@ -50,6 +50,11 @@ bool InitialStateIterator::on_select(select::Select *select) { #ifdef USE_LOCK bool InitialStateIterator::on_lock(lock::Lock *a_lock) { return this->client_->send_lock_state(a_lock, a_lock->state); } #endif +#ifdef USE_MEDIA_PLAYER +bool InitialStateIterator::on_media_player(media_player::MediaPlayer *media_player) { + return this->client_->send_media_player_state(media_player); +} +#endif InitialStateIterator::InitialStateIterator(APIConnection *client) : client_(client) {} } // namespace api diff --git a/esphome/components/api/subscribe_state.h b/esphome/components/api/subscribe_state.h index 515e1a2d07..7a7ba697c0 100644 --- a/esphome/components/api/subscribe_state.h +++ b/esphome/components/api/subscribe_state.h @@ -48,6 +48,9 @@ class InitialStateIterator : public ComponentIterator { #endif #ifdef USE_LOCK bool on_lock(lock::Lock *a_lock) override; +#endif +#ifdef USE_MEDIA_PLAYER + bool on_media_player(media_player::MediaPlayer *media_player) override; #endif protected: APIConnection *client_; diff --git a/esphome/components/i2s_audio/__init__.py b/esphome/components/i2s_audio/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/i2s_audio/i2s_audio_media_player.cpp b/esphome/components/i2s_audio/i2s_audio_media_player.cpp new file mode 100644 index 0000000000..0ab3237aeb --- /dev/null +++ b/esphome/components/i2s_audio/i2s_audio_media_player.cpp @@ -0,0 +1,132 @@ +#include "i2s_audio_media_player.h" + +#ifdef USE_ESP32_FRAMEWORK_ARDUINO + +#include "esphome/core/log.h" + +namespace esphome { +namespace i2s_audio { + +static const char *const TAG = "audio"; + +void I2SAudioMediaPlayer::control(const media_player::MediaPlayerCall &call) { + if (call.get_media_url().has_value()) { + if (this->audio_->isRunning()) + this->audio_->stopSong(); + this->high_freq_.start(); + this->audio_->connecttohost(call.get_media_url().value().c_str()); + this->state = media_player::MEDIA_PLAYER_STATE_PLAYING; + } + if (call.get_volume().has_value()) { + this->volume = call.get_volume().value(); + this->set_volume_(volume); + this->unmute_(); + } + if (call.get_command().has_value()) { + switch (call.get_command().value()) { + case media_player::MEDIA_PLAYER_COMMAND_PLAY: + if (!this->audio_->isRunning()) + this->audio_->pauseResume(); + this->state = media_player::MEDIA_PLAYER_STATE_PLAYING; + break; + case media_player::MEDIA_PLAYER_COMMAND_PAUSE: + if (this->audio_->isRunning()) + this->audio_->pauseResume(); + this->state = media_player::MEDIA_PLAYER_STATE_PAUSED; + break; + case media_player::MEDIA_PLAYER_COMMAND_STOP: + this->stop_(); + break; + case media_player::MEDIA_PLAYER_COMMAND_MUTE: + this->mute_(); + break; + case media_player::MEDIA_PLAYER_COMMAND_UNMUTE: + this->unmute_(); + break; + } + } + this->publish_state(); +} + +void I2SAudioMediaPlayer::mute_() { + if (this->mute_pin_ != nullptr) { + this->mute_pin_->digital_write(true); + } else { + this->set_volume_(0.0f, false); + } + this->muted_ = true; +} +void I2SAudioMediaPlayer::unmute_() { + if (this->mute_pin_ != nullptr) { + this->mute_pin_->digital_write(false); + } else { + this->set_volume_(this->volume, false); + } + this->muted_ = false; +} +void I2SAudioMediaPlayer::set_volume_(float volume, bool publish) { + this->audio_->setVolume(remap(volume, 0.0f, 1.0f, 0, 21)); + if (publish) + this->volume = volume; +} + +void I2SAudioMediaPlayer::stop_() { + if (this->audio_->isRunning()) + this->audio_->stopSong(); + this->high_freq_.stop(); + this->state = media_player::MEDIA_PLAYER_STATE_IDLE; +} + +void I2SAudioMediaPlayer::setup() { + ESP_LOGCONFIG(TAG, "Setting up Audio..."); + if (this->internal_dac_mode_ != I2S_DAC_CHANNEL_DISABLE) { + this->audio_ = make_unique