diff --git a/.github/workflows/ci-docker.yml b/.github/workflows/ci-docker.yml index 435a58498e..cf7b08f3d4 100644 --- a/.github/workflows/ci-docker.yml +++ b/.github/workflows/ci-docker.yml @@ -46,7 +46,7 @@ jobs: with: python-version: "3.9" - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3.7.1 + uses: docker/setup-buildx-action@v3.8.0 - name: Set up QEMU uses: docker/setup-qemu-action@v3.2.0 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6ce4159da0..a344b177ae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,6 +13,7 @@ on: - ".github/workflows/ci.yml" - "!.yamllint" - "!.github/dependabot.yml" + - "!docker/**" merge_group: permissions: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a4e4305207..6cd86854b2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -90,7 +90,7 @@ jobs: python-version: "3.9" - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3.7.1 + uses: docker/setup-buildx-action@v3.8.0 - name: Set up QEMU if: matrix.platform != 'linux/amd64' uses: docker/setup-qemu-action@v3.2.0 @@ -184,7 +184,7 @@ jobs: merge-multiple: true - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3.7.1 + uses: docker/setup-buildx-action@v3.8.0 - name: Log in to docker hub if: matrix.registry == 'dockerhub' diff --git a/esphome/components/adc/__init__.py b/esphome/components/adc/__init__.py index 11b0ba2389..d8d21523b9 100644 --- a/esphome/components/adc/__init__.py +++ b/esphome/components/adc/__init__.py @@ -1,11 +1,6 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import pins -from esphome.const import CONF_ANALOG, CONF_INPUT, CONF_NUMBER - -from esphome.core import CORE +import esphome.codegen as cg from esphome.components.esp32 import get_esp32_variant -from esphome.const import PLATFORM_ESP8266 from esphome.components.esp32.const import ( VARIANT_ESP32, VARIANT_ESP32C2, @@ -15,6 +10,9 @@ from esphome.components.esp32.const import ( VARIANT_ESP32S2, VARIANT_ESP32S3, ) +import esphome.config_validation as cv +from esphome.const import CONF_ANALOG, CONF_INPUT, CONF_NUMBER, PLATFORM_ESP8266 +from esphome.core import CORE CODEOWNERS = ["@esphome/core"] @@ -102,11 +100,11 @@ ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = { 6: adc1_channel_t.ADC1_CHANNEL_6, }, VARIANT_ESP32H2: { - 0: adc1_channel_t.ADC1_CHANNEL_0, - 1: adc1_channel_t.ADC1_CHANNEL_1, - 2: adc1_channel_t.ADC1_CHANNEL_2, - 3: adc1_channel_t.ADC1_CHANNEL_3, - 4: adc1_channel_t.ADC1_CHANNEL_4, + 1: adc1_channel_t.ADC1_CHANNEL_0, + 2: adc1_channel_t.ADC1_CHANNEL_1, + 3: adc1_channel_t.ADC1_CHANNEL_2, + 4: adc1_channel_t.ADC1_CHANNEL_3, + 5: adc1_channel_t.ADC1_CHANNEL_4, }, } diff --git a/esphome/components/debug/debug_esp32.cpp b/esphome/components/debug/debug_esp32.cpp index cb4330f422..5f7b9cdbb0 100644 --- a/esphome/components/debug/debug_esp32.cpp +++ b/esphome/components/debug/debug_esp32.cpp @@ -30,109 +30,162 @@ static const char *const TAG = "debug"; std::string DebugComponent::get_reset_reason_() { std::string reset_reason; - switch (rtc_get_reset_reason(0)) { - case POWERON_RESET: - reset_reason = "Power On Reset"; + switch (esp_reset_reason()) { + case ESP_RST_POWERON: + reset_reason = "Reset due to power-on event"; break; + case ESP_RST_EXT: + reset_reason = "Reset by external pin"; + break; + case ESP_RST_SW: + reset_reason = "Software reset via esp_restart"; + break; + case ESP_RST_PANIC: + reset_reason = "Software reset due to exception/panic"; + break; + case ESP_RST_INT_WDT: + reset_reason = "Reset (software or hardware) due to interrupt watchdog"; + break; + case ESP_RST_TASK_WDT: + reset_reason = "Reset due to task watchdog"; + break; + case ESP_RST_WDT: + reset_reason = "Reset due to other watchdogs"; + break; + case ESP_RST_DEEPSLEEP: + reset_reason = "Reset after exiting deep sleep mode"; + break; + case ESP_RST_BROWNOUT: + reset_reason = "Brownout reset (software or hardware)"; + break; + case ESP_RST_SDIO: + reset_reason = "Reset over SDIO"; + break; +#ifdef USE_ESP32_VARIANT_ESP32 +#if (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 4)) + case ESP_RST_USB: + reset_reason = "Reset by USB peripheral"; + break; + case ESP_RST_JTAG: + reset_reason = "Reset by JTAG"; + break; + case ESP_RST_EFUSE: + reset_reason = "Reset due to efuse error"; + break; + case ESP_RST_PWR_GLITCH: + reset_reason = "Reset due to power glitch detected"; + break; + case ESP_RST_CPU_LOCKUP: + reset_reason = "Reset due to CPU lock up (double exception)"; + break; +#endif // ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 4) +#endif // USE_ESP32_VARIANT_ESP32 + default: // Includes ESP_RST_UNKNOWN + switch (rtc_get_reset_reason(0)) { + case POWERON_RESET: + reset_reason = "Power On Reset"; + break; #if defined(USE_ESP32_VARIANT_ESP32) - case SW_RESET: + case SW_RESET: #elif defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S2) || \ defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32C6) - case RTC_SW_SYS_RESET: + case RTC_SW_SYS_RESET: #endif - reset_reason = "Software Reset Digital Core"; - break; + reset_reason = "Software Reset Digital Core"; + break; #if defined(USE_ESP32_VARIANT_ESP32) - case OWDT_RESET: - reset_reason = "Watch Dog Reset Digital Core"; - break; + case OWDT_RESET: + reset_reason = "Watch Dog Reset Digital Core"; + break; #endif - case DEEPSLEEP_RESET: - reset_reason = "Deep Sleep Reset Digital Core"; - break; + case DEEPSLEEP_RESET: + reset_reason = "Deep Sleep Reset Digital Core"; + break; #if defined(USE_ESP32_VARIANT_ESP32) - case SDIO_RESET: - reset_reason = "SLC Module Reset Digital Core"; - break; + case SDIO_RESET: + reset_reason = "SLC Module Reset Digital Core"; + break; #endif - case TG0WDT_SYS_RESET: - reset_reason = "Timer Group 0 Watch Dog Reset Digital Core"; - break; - case TG1WDT_SYS_RESET: - reset_reason = "Timer Group 1 Watch Dog Reset Digital Core"; - break; - case RTCWDT_SYS_RESET: - reset_reason = "RTC Watch Dog Reset Digital Core"; - break; + case TG0WDT_SYS_RESET: + reset_reason = "Timer Group 0 Watch Dog Reset Digital Core"; + break; + case TG1WDT_SYS_RESET: + reset_reason = "Timer Group 1 Watch Dog Reset Digital Core"; + break; + case RTCWDT_SYS_RESET: + reset_reason = "RTC Watch Dog Reset Digital Core"; + break; #if !defined(USE_ESP32_VARIANT_ESP32C6) && !defined(USE_ESP32_VARIANT_ESP32H2) - case INTRUSION_RESET: - reset_reason = "Intrusion Reset CPU"; - break; + case INTRUSION_RESET: + reset_reason = "Intrusion Reset CPU"; + break; #endif #if defined(USE_ESP32_VARIANT_ESP32) - case TGWDT_CPU_RESET: - reset_reason = "Timer Group Reset CPU"; - break; + case TGWDT_CPU_RESET: + reset_reason = "Timer Group Reset CPU"; + break; #elif defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S2) || \ defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32C6) - case TG0WDT_CPU_RESET: - reset_reason = "Timer Group 0 Reset CPU"; - break; + case TG0WDT_CPU_RESET: + reset_reason = "Timer Group 0 Reset CPU"; + break; #endif #if defined(USE_ESP32_VARIANT_ESP32) - case SW_CPU_RESET: + case SW_CPU_RESET: #elif defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S2) || \ defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32C6) - case RTC_SW_CPU_RESET: + case RTC_SW_CPU_RESET: #endif - reset_reason = "Software Reset CPU"; - break; - case RTCWDT_CPU_RESET: - reset_reason = "RTC Watch Dog Reset CPU"; - break; + reset_reason = "Software Reset CPU"; + break; + case RTCWDT_CPU_RESET: + reset_reason = "RTC Watch Dog Reset CPU"; + break; #if defined(USE_ESP32_VARIANT_ESP32) - case EXT_CPU_RESET: - reset_reason = "External CPU Reset"; - break; + case EXT_CPU_RESET: + reset_reason = "External CPU Reset"; + break; #endif - case RTCWDT_BROWN_OUT_RESET: - reset_reason = "Voltage Unstable Reset"; - break; - case RTCWDT_RTC_RESET: - reset_reason = "RTC Watch Dog Reset Digital Core And RTC Module"; - break; + case RTCWDT_BROWN_OUT_RESET: + reset_reason = "Voltage Unstable Reset"; + break; + case RTCWDT_RTC_RESET: + reset_reason = "RTC Watch Dog Reset Digital Core And RTC Module"; + break; #if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || \ defined(USE_ESP32_VARIANT_ESP32C6) - case TG1WDT_CPU_RESET: - reset_reason = "Timer Group 1 Reset CPU"; - break; - case SUPER_WDT_RESET: - reset_reason = "Super Watchdog Reset Digital Core And RTC Module"; - break; - case EFUSE_RESET: - reset_reason = "eFuse Reset Digital Core"; - break; + case TG1WDT_CPU_RESET: + reset_reason = "Timer Group 1 Reset CPU"; + break; + case SUPER_WDT_RESET: + reset_reason = "Super Watchdog Reset Digital Core And RTC Module"; + break; + case EFUSE_RESET: + reset_reason = "eFuse Reset Digital Core"; + break; #endif #if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) - case GLITCH_RTC_RESET: - reset_reason = "Glitch Reset Digital Core And RTC Module"; - break; + case GLITCH_RTC_RESET: + reset_reason = "Glitch Reset Digital Core And RTC Module"; + break; #endif #if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32C6) - case USB_UART_CHIP_RESET: - reset_reason = "USB UART Reset Digital Core"; - break; - case USB_JTAG_CHIP_RESET: - reset_reason = "USB JTAG Reset Digital Core"; - break; + case USB_UART_CHIP_RESET: + reset_reason = "USB UART Reset Digital Core"; + break; + case USB_JTAG_CHIP_RESET: + reset_reason = "USB JTAG Reset Digital Core"; + break; #endif #if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S3) - case POWER_GLITCH_RESET: - reset_reason = "Power Glitch Reset Digital Core And RTC Module"; - break; + case POWER_GLITCH_RESET: + reset_reason = "Power Glitch Reset Digital Core And RTC Module"; + break; #endif - default: - reset_reason = "Unknown Reset Reason"; + default: + reset_reason = "Unknown Reset Reason"; + } + break; } ESP_LOGD(TAG, "Reset Reason: %s", reset_reason.c_str()); return reset_reason; @@ -294,4 +347,4 @@ void DebugComponent::update_platform_() { } // namespace debug } // namespace esphome -#endif +#endif // USE_ESP32 diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index d7dcf93f86..b10e454c21 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -27,6 +27,9 @@ namespace esp32_ble { static const char *const TAG = "esp32_ble"; +static RAMAllocator EVENT_ALLOCATOR( // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + RAMAllocator::ALLOW_FAILURE | RAMAllocator::ALLOC_INTERNAL); + void ESP32BLE::setup() { global_ble = this; ESP_LOGCONFIG(TAG, "Setting up BLE..."); @@ -322,7 +325,8 @@ void ESP32BLE::loop() { default: break; } - delete ble_event; // NOLINT(cppcoreguidelines-owning-memory) + ble_event->~BLEEvent(); + EVENT_ALLOCATOR.deallocate(ble_event, 1); ble_event = this->ble_events_.pop(); } if (this->advertising_ != nullptr) { @@ -331,9 +335,14 @@ void ESP32BLE::loop() { } void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { - BLEEvent *new_event = new BLEEvent(event, param); // NOLINT(cppcoreguidelines-owning-memory) + BLEEvent *new_event = EVENT_ALLOCATOR.allocate(1); + if (new_event == nullptr) { + // Memory too fragmented to allocate new event. Can only drop it until memory comes back + return; + } + new (new_event) BLEEvent(event, param); global_ble->ble_events_.push(new_event); -} // NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks) +} // NOLINT(clang-analyzer-unix.Malloc) void ESP32BLE::real_gap_event_handler_(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { ESP_LOGV(TAG, "(BLE) gap_event_handler - %d", event); @@ -344,9 +353,14 @@ void ESP32BLE::real_gap_event_handler_(esp_gap_ble_cb_event_t event, esp_ble_gap void ESP32BLE::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param) { - BLEEvent *new_event = new BLEEvent(event, gatts_if, param); // NOLINT(cppcoreguidelines-owning-memory) + BLEEvent *new_event = EVENT_ALLOCATOR.allocate(1); + if (new_event == nullptr) { + // Memory too fragmented to allocate new event. Can only drop it until memory comes back + return; + } + new (new_event) BLEEvent(event, gatts_if, param); global_ble->ble_events_.push(new_event); -} // NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks) +} // NOLINT(clang-analyzer-unix.Malloc) void ESP32BLE::real_gatts_event_handler_(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param) { @@ -358,9 +372,14 @@ void ESP32BLE::real_gatts_event_handler_(esp_gatts_cb_event_t event, esp_gatt_if void ESP32BLE::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) { - BLEEvent *new_event = new BLEEvent(event, gattc_if, param); // NOLINT(cppcoreguidelines-owning-memory) + BLEEvent *new_event = EVENT_ALLOCATOR.allocate(1); + if (new_event == nullptr) { + // Memory too fragmented to allocate new event. Can only drop it until memory comes back + return; + } + new (new_event) BLEEvent(event, gattc_if, param); global_ble->ble_events_.push(new_event); -} // NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks) +} // NOLINT(clang-analyzer-unix.Malloc) void ESP32BLE::real_gattc_event_handler_(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) { diff --git a/esphome/components/font/__init__.py b/esphome/components/font/__init__.py index c397ba8306..e2051298fe 100644 --- a/esphome/components/font/__init__.py +++ b/esphome/components/font/__init__.py @@ -51,8 +51,11 @@ CONF_IGNORE_MISSING_GLYPHS = "ignore_missing_glyphs" # Cache loaded freetype fonts class FontCache(dict): def __missing__(self, key): - res = self[key] = freetype.Face(key) - return res + try: + res = self[key] = freetype.Face(key) + return res + except freetype.FT_Exception as e: + raise cv.Invalid(f"Could not load Font file {key}: {e}") from e FONT_CACHE = FontCache() diff --git a/esphome/components/midea/ac_automations.h b/esphome/components/midea/ac_automations.h index 5084fd1eec..e6fffa2511 100644 --- a/esphome/components/midea/ac_automations.h +++ b/esphome/components/midea/ac_automations.h @@ -19,10 +19,12 @@ template class MideaActionBase : public Action { template class FollowMeAction : public MideaActionBase { TEMPLATABLE_VALUE(float, temperature) + TEMPLATABLE_VALUE(bool, use_fahrenheit) TEMPLATABLE_VALUE(bool, beeper) void play(Ts... x) override { - this->parent_->do_follow_me(this->temperature_.value(x...), this->beeper_.value(x...)); + this->parent_->do_follow_me(this->temperature_.value(x...), this->use_fahrenheit_.value(x...), + this->beeper_.value(x...)); } }; diff --git a/esphome/components/midea/air_conditioner.cpp b/esphome/components/midea/air_conditioner.cpp index a823680d03..247aea0488 100644 --- a/esphome/components/midea/air_conditioner.cpp +++ b/esphome/components/midea/air_conditioner.cpp @@ -1,6 +1,7 @@ #ifdef USE_ARDUINO #include "esphome/core/log.h" +#include "esphome/core/helpers.h" #include "air_conditioner.h" #include "ac_adapter.h" #include @@ -121,7 +122,7 @@ void AirConditioner::dump_config() { /* ACTIONS */ -void AirConditioner::do_follow_me(float temperature, bool beeper) { +void AirConditioner::do_follow_me(float temperature, bool use_fahrenheit, bool beeper) { #ifdef USE_REMOTE_TRANSMITTER // Check if temperature is finite (not NaN or infinite) if (!std::isfinite(temperature)) { @@ -131,13 +132,14 @@ void AirConditioner::do_follow_me(float temperature, bool beeper) { // Round and convert temperature to long, then clamp and convert it to uint8_t uint8_t temp_uint8 = - static_cast(std::max(0L, std::min(static_cast(UINT8_MAX), std::lroundf(temperature)))); + static_cast(esphome::clamp(std::lroundf(temperature), 0L, static_cast(UINT8_MAX))); - ESP_LOGD(Constants::TAG, "Follow me action called with temperature: %f °C, rounded to: %u °C", temperature, - temp_uint8); + char temp_symbol = use_fahrenheit ? 'F' : 'C'; + ESP_LOGD(Constants::TAG, "Follow me action called with temperature: %.5f °%c, rounded to: %u °%c", temperature, + temp_symbol, temp_uint8, temp_symbol); // Create and transmit the data - IrFollowMeData data(temp_uint8, beeper); + IrFollowMeData data(temp_uint8, use_fahrenheit, beeper); this->transmitter_.transmit(data); #else ESP_LOGW(Constants::TAG, "Action needs remote_transmitter component"); diff --git a/esphome/components/midea/air_conditioner.h b/esphome/components/midea/air_conditioner.h index d809aa78f6..e70bd34e71 100644 --- a/esphome/components/midea/air_conditioner.h +++ b/esphome/components/midea/air_conditioner.h @@ -32,7 +32,7 @@ class AirConditioner : public ApplianceBase, /* ### ACTIONS ### */ /* ############### */ - void do_follow_me(float temperature, bool beeper = false); + void do_follow_me(float temperature, bool use_fahrenheit, bool beeper = false); void do_display_toggle(); void do_swing_step(); void do_beeper_on() { this->set_beeper_feedback(true); } diff --git a/esphome/components/midea/climate.py b/esphome/components/midea/climate.py index e5612796a3..b7fef5e1ab 100644 --- a/esphome/components/midea/climate.py +++ b/esphome/components/midea/climate.py @@ -18,6 +18,7 @@ from esphome.const import ( CONF_SUPPORTED_SWING_MODES, CONF_TIMEOUT, CONF_TEMPERATURE, + CONF_USE_FAHRENHEIT, DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY, @@ -172,11 +173,10 @@ MIDEA_ACTION_BASE_SCHEMA = cv.Schema( ) # FollowMe action -MIDEA_FOLLOW_ME_MIN = 0 -MIDEA_FOLLOW_ME_MAX = 37 MIDEA_FOLLOW_ME_SCHEMA = cv.Schema( { cv.Required(CONF_TEMPERATURE): cv.templatable(cv.temperature), + cv.Optional(CONF_USE_FAHRENHEIT, default=False): cv.templatable(cv.boolean), cv.Optional(CONF_BEEPER, default=False): cv.templatable(cv.boolean), } ) @@ -186,6 +186,8 @@ MIDEA_FOLLOW_ME_SCHEMA = cv.Schema( async def follow_me_to_code(var, config, args): template_ = await cg.templatable(config[CONF_BEEPER], args, cg.bool_) cg.add(var.set_beeper(template_)) + template_ = await cg.templatable(config[CONF_USE_FAHRENHEIT], args, cg.bool_) + cg.add(var.set_use_fahrenheit(template_)) template_ = await cg.templatable(config[CONF_TEMPERATURE], args, cg.float_) cg.add(var.set_temperature(template_)) diff --git a/esphome/components/midea/ir_transmitter.h b/esphome/components/midea/ir_transmitter.h index a8b89f9b7b..eba8fc87f7 100644 --- a/esphome/components/midea/ir_transmitter.h +++ b/esphome/components/midea/ir_transmitter.h @@ -16,22 +16,53 @@ class IrFollowMeData : public IrData { IrFollowMeData() : IrData({MIDEA_TYPE_FOLLOW_ME, 0x82, 0x48, 0x7F, 0x1F}) {} // Copy from Base IrFollowMeData(const IrData &data) : IrData(data) {} - // Direct from temperature and beeper values + // Direct from temperature in celsius and beeper values IrFollowMeData(uint8_t temp, bool beeper = false) : IrFollowMeData() { - this->set_temp(temp); + this->set_temp(temp, false); + this->set_beeper(beeper); + } + // Direct from temperature, fahrenheit and beeper values + IrFollowMeData(uint8_t temp, bool fahrenheit, bool beeper) : IrFollowMeData() { + this->set_temp(temp, fahrenheit); this->set_beeper(beeper); } /* TEMPERATURE */ - uint8_t temp() const { return this->get_value_(4) - 1; } - void set_temp(uint8_t val) { this->set_value_(4, std::min(MAX_TEMP, val) + 1); } + uint8_t temp() const { + if (this->fahrenheit()) { + return this->get_value_(4) + 31; + } + return this->get_value_(4) - 1; + } + void set_temp(uint8_t val, bool fahrenheit = false) { + this->set_fahrenheit(fahrenheit); + if (this->fahrenheit()) { + // see https://github.com/esphome/feature-requests/issues/1627#issuecomment-1365639966 + val = esphome::clamp(val, MIN_TEMP_F, MAX_TEMP_F) - 31; + } else { + val = esphome::clamp(val, MIN_TEMP_C, MAX_TEMP_C) + 1; + } + this->set_value_(4, val); + } /* BEEPER */ bool beeper() const { return this->get_value_(3, 128); } void set_beeper(bool val) { this->set_mask_(3, val, 128); } + /* FAHRENHEIT */ + bool fahrenheit() const { return this->get_value_(2, 32); } + void set_fahrenheit(bool val) { this->set_mask_(2, val, 32); } + protected: - static const uint8_t MAX_TEMP = 37; + static const uint8_t MIN_TEMP_C = 0; + static const uint8_t MAX_TEMP_C = 37; + + // see + // https://github.com/crankyoldgit/IRremoteESP8266/blob/9bdf8abcb465268c5409db99dc83a26df64c7445/src/ir_Midea.h#L116 + static const uint8_t MIN_TEMP_F = 32; + // see + // https://github.com/crankyoldgit/IRremoteESP8266/blob/9bdf8abcb465268c5409db99dc83a26df64c7445/src/ir_Midea.h#L117 + static const uint8_t MAX_TEMP_F = 99; }; class IrSpecialData : public IrData { diff --git a/esphome/components/nextion/display.py b/esphome/components/nextion/display.py index 6f284376af..f6bd863d42 100644 --- a/esphome/components/nextion/display.py +++ b/esphome/components/nextion/display.py @@ -119,17 +119,17 @@ async def to_code(config): cg.add_library("ESP8266HTTPClient", None) if CONF_TOUCH_SLEEP_TIMEOUT in config: - cg.add(var.set_touch_sleep_timeout_internal(config[CONF_TOUCH_SLEEP_TIMEOUT])) + cg.add(var.set_touch_sleep_timeout(config[CONF_TOUCH_SLEEP_TIMEOUT])) if CONF_WAKE_UP_PAGE in config: - cg.add(var.set_wake_up_page_internal(config[CONF_WAKE_UP_PAGE])) + cg.add(var.set_wake_up_page(config[CONF_WAKE_UP_PAGE])) if CONF_START_UP_PAGE in config: - cg.add(var.set_start_up_page_internal(config[CONF_START_UP_PAGE])) + cg.add(var.set_start_up_page(config[CONF_START_UP_PAGE])) - cg.add(var.set_auto_wake_on_touch_internal(config[CONF_AUTO_WAKE_ON_TOUCH])) + cg.add(var.set_auto_wake_on_touch(config[CONF_AUTO_WAKE_ON_TOUCH])) - cg.add(var.set_exit_reparse_on_start_internal(config[CONF_EXIT_REPARSE_ON_START])) + cg.add(var.set_exit_reparse_on_start(config[CONF_EXIT_REPARSE_ON_START])) cg.add(var.set_skip_connection_handshake(config[CONF_SKIP_CONNECTION_HANDSHAKE])) diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index 50a5834347..e5df13c64e 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -40,7 +40,7 @@ bool Nextion::send_command_(const std::string &command) { } bool Nextion::check_connect_() { - if (this->get_is_connected_()) + if (this->is_connected_) return true; // Check if the handshake should be skipped for the Nextion connection @@ -280,14 +280,6 @@ void Nextion::loop() { this->goto_page(this->start_up_page_); } - // This could probably be removed from the loop area, as those are redundant. - this->set_auto_wake_on_touch(this->auto_wake_on_touch_); - this->set_exit_reparse_on_start(this->exit_reparse_on_start_); - - if (this->touch_sleep_timeout_ != 0) { - this->set_touch_sleep_timeout(this->touch_sleep_timeout_); - } - if (this->wake_up_page_ != -1) { this->set_wake_up_page(this->wake_up_page_); } diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index f539c79718..c293f80aee 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -856,76 +856,6 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe */ void set_backlight_brightness(float brightness); - /** - * Set the touch sleep timeout of the display. - * @param timeout Timeout in seconds. - * - * Example: - * ```cpp - * it.set_touch_sleep_timeout(30); - * ``` - * - * After 30 seconds the display will go to sleep. Note: the display will only wakeup by a restart or by setting up - * `thup`. - */ - void set_touch_sleep_timeout(uint16_t timeout); - - /** - * Sets which page Nextion loads when exiting sleep mode. Note this can be set even when Nextion is in sleep mode. - * @param page_id The page id, from 0 to the lage page in Nextion. Set 255 (not set to any existing page) to - * wakes up to current page. - * - * Example: - * ```cpp - * it.set_wake_up_page(2); - * ``` - * - * The display will wake up to page 2. - */ - void set_wake_up_page(uint8_t page_id = 255); - - /** - * Sets which page Nextion loads when connecting to ESPHome. - * @param page_id The page id, from 0 to the lage page in Nextion. Set 255 (not set to any existing page) to - * wakes up to current page. - * - * Example: - * ```cpp - * it.set_start_up_page(2); - * ``` - * - * The display will go to page 2 when it establishes a connection to ESPHome. - */ - void set_start_up_page(uint8_t page_id = 255); - - /** - * Sets if Nextion should auto-wake from sleep when touch press occurs. - * @param auto_wake True or false. When auto_wake is true and Nextion is in sleep mode, - * the first touch will only trigger the auto wake mode and not trigger a Touch Event. - * - * Example: - * ```cpp - * it.set_auto_wake_on_touch(true); - * ``` - * - * The display will wake up by touch. - */ - void set_auto_wake_on_touch(bool auto_wake); - - /** - * Sets if Nextion should exit the active reparse mode before the "connect" command is sent - * @param exit_reparse True or false. When exit_reparse is true, the exit reparse command - * will be sent before requesting the connection from Nextion. - * - * Example: - * ```cpp - * it.set_exit_reparse_on_start(true); - * ``` - * - * The display will be requested to leave active reparse mode before setup. - */ - void set_exit_reparse_on_start(bool exit_reparse); - /** * Sets whether the Nextion display should skip the connection handshake process. * @param skip_handshake True or false. When skip_connection_handshake is true, @@ -1172,15 +1102,75 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe void update_components_by_prefix(const std::string &prefix); - void set_touch_sleep_timeout_internal(uint32_t touch_sleep_timeout) { - this->touch_sleep_timeout_ = touch_sleep_timeout; - } - void set_wake_up_page_internal(uint8_t wake_up_page) { this->wake_up_page_ = wake_up_page; } - void set_start_up_page_internal(uint8_t start_up_page) { this->start_up_page_ = start_up_page; } - void set_auto_wake_on_touch_internal(bool auto_wake_on_touch) { this->auto_wake_on_touch_ = auto_wake_on_touch; } - void set_exit_reparse_on_start_internal(bool exit_reparse_on_start) { - this->exit_reparse_on_start_ = exit_reparse_on_start; - } + /** + * Set the touch sleep timeout of the display. + * @param timeout Timeout in seconds. + * + * Example: + * ```cpp + * it.set_touch_sleep_timeout(30); + * ``` + * + * After 30 seconds the display will go to sleep. Note: the display will only wakeup by a restart or by setting up + * `thup`. + */ + void set_touch_sleep_timeout(uint32_t touch_sleep_timeout); + + /** + * Sets which page Nextion loads when exiting sleep mode. Note this can be set even when Nextion is in sleep mode. + * @param wake_up_page The page id, from 0 to the lage page in Nextion. Set 255 (not set to any existing page) to + * wakes up to current page. + * + * Example: + * ```cpp + * it.set_wake_up_page(2); + * ``` + * + * The display will wake up to page 2. + */ + void set_wake_up_page(uint8_t wake_up_page = 255); + + /** + * Sets which page Nextion loads when connecting to ESPHome. + * @param start_up_page The page id, from 0 to the lage page in Nextion. Set 255 (not set to any existing page) to + * wakes up to current page. + * + * Example: + * ```cpp + * it.set_start_up_page(2); + * ``` + * + * The display will go to page 2 when it establishes a connection to ESPHome. + */ + void set_start_up_page(uint8_t start_up_page = 255) { this->start_up_page_ = start_up_page; } + + /** + * Sets if Nextion should auto-wake from sleep when touch press occurs. + * @param auto_wake_on_touch True or false. When auto_wake is true and Nextion is in sleep mode, + * the first touch will only trigger the auto wake mode and not trigger a Touch Event. + * + * Example: + * ```cpp + * it.set_auto_wake_on_touch(true); + * ``` + * + * The display will wake up by touch. + */ + void set_auto_wake_on_touch(bool auto_wake_on_touch); + + /** + * Sets if Nextion should exit the active reparse mode before the "connect" command is sent + * @param exit_reparse_on_start True or false. When exit_reparse_on_start is true, the exit reparse command + * will be sent before requesting the connection from Nextion. + * + * Example: + * ```cpp + * it.set_exit_reparse_on_start(true); + * ``` + * + * The display will be requested to leave active reparse mode before setup. + */ + void set_exit_reparse_on_start(bool exit_reparse_on_start) { this->exit_reparse_on_start_ = exit_reparse_on_start; } /** * @brief Retrieves the number of commands pending in the Nextion command queue. @@ -1217,6 +1207,25 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe */ bool is_updating() override; + /** + * @brief Check if the Nextion display is successfully connected. + * + * This method returns whether a successful connection has been established with + * the Nextion display. A connection is considered established when: + * + * - The initial handshake with the display is completed successfully, or + * - The handshake is skipped via skip_connection_handshake_ flag + * + * The connection status is particularly useful when: + * - Troubleshooting communication issues + * - Ensuring the display is ready before sending commands + * - Implementing connection-dependent behaviors + * + * @return true if the Nextion display is connected and ready to receive commands + * @return false if the display is not yet connected or connection was lost + */ + bool is_connected() { return this->is_connected_; } + protected: std::deque nextion_queue_; std::deque waveform_queue_; @@ -1315,8 +1324,6 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe #endif // USE_NEXTION_TFT_UPLOAD - bool get_is_connected_() { return this->is_connected_; } - bool check_connect_(); std::vector touch_; diff --git a/esphome/components/nextion/nextion_commands.cpp b/esphome/components/nextion/nextion_commands.cpp index 398e9dd502..e3172c8c1b 100644 --- a/esphome/components/nextion/nextion_commands.cpp +++ b/esphome/components/nextion/nextion_commands.cpp @@ -10,19 +10,19 @@ static const char *const TAG = "nextion"; // Sleep safe commands void Nextion::soft_reset() { this->send_command_("rest"); } -void Nextion::set_wake_up_page(uint8_t page_id) { - this->add_no_result_to_queue_with_set_internal_("wake_up_page", "wup", page_id, true); +void Nextion::set_wake_up_page(uint8_t wake_up_page) { + this->wake_up_page_ = wake_up_page; + this->add_no_result_to_queue_with_set_internal_("wake_up_page", "wup", wake_up_page, true); } -void Nextion::set_start_up_page(uint8_t page_id) { this->start_up_page_ = page_id; } - -void Nextion::set_touch_sleep_timeout(uint16_t timeout) { - if (timeout < 3) { +void Nextion::set_touch_sleep_timeout(uint32_t touch_sleep_timeout) { + if (touch_sleep_timeout < 3) { ESP_LOGD(TAG, "Sleep timeout out of bounds, range 3-65535"); return; } - this->add_no_result_to_queue_with_set_internal_("touch_sleep_timeout", "thsp", timeout, true); + this->touch_sleep_timeout_ = touch_sleep_timeout; + this->add_no_result_to_queue_with_set_internal_("touch_sleep_timeout", "thsp", touch_sleep_timeout, true); } void Nextion::sleep(bool sleep) { @@ -54,7 +54,6 @@ bool Nextion::set_protocol_reparse_mode(bool active_mode) { this->ignore_is_setup_ = false; return all_commands_sent; } -void Nextion::set_exit_reparse_on_start(bool exit_reparse) { this->exit_reparse_on_start_ = exit_reparse; } // Set Colors - Background void Nextion::set_component_background_color(const char *component, uint16_t color) { @@ -191,8 +190,9 @@ void Nextion::set_backlight_brightness(float brightness) { this->add_no_result_to_queue_with_printf_("backlight_brightness", "dim=%d", static_cast(brightness * 100)); } -void Nextion::set_auto_wake_on_touch(bool auto_wake) { - this->add_no_result_to_queue_with_set("auto_wake_on_touch", "thup", auto_wake ? 1 : 0); +void Nextion::set_auto_wake_on_touch(bool auto_wake_on_touch) { + this->auto_wake_on_touch_ = auto_wake_on_touch; + this->add_no_result_to_queue_with_set("auto_wake_on_touch", "thup", auto_wake_on_touch ? 1 : 0); } // General Component diff --git a/esphome/components/opentherm/__init__.py b/esphome/components/opentherm/__init__.py index 81cd78af08..42b476eb87 100644 --- a/esphome/components/opentherm/__init__.py +++ b/esphome/components/opentherm/__init__.py @@ -1,10 +1,12 @@ from typing import Any +import logging +from esphome import automation import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins from esphome.components import sensor -from esphome.const import CONF_ID, PLATFORM_ESP32, PLATFORM_ESP8266 +from esphome.const import CONF_ID, PLATFORM_ESP32, PLATFORM_ESP8266, CONF_TRIGGER_ID from . import const, schema, validate, generate CODEOWNERS = ["@olegtarasov"] @@ -20,7 +22,21 @@ CONF_CH2_ACTIVE = "ch2_active" CONF_SUMMER_MODE_ACTIVE = "summer_mode_active" CONF_DHW_BLOCK = "dhw_block" CONF_SYNC_MODE = "sync_mode" -CONF_OPENTHERM_VERSION = "opentherm_version" +CONF_OPENTHERM_VERSION = "opentherm_version" # Deprecated, will be removed +CONF_BEFORE_SEND = "before_send" +CONF_BEFORE_PROCESS_RESPONSE = "before_process_response" + +# Triggers +BeforeSendTrigger = generate.opentherm_ns.class_( + "BeforeSendTrigger", + automation.Trigger.template(generate.OpenthermData.operator("ref")), +) +BeforeProcessResponseTrigger = generate.opentherm_ns.class_( + "BeforeProcessResponseTrigger", + automation.Trigger.template(generate.OpenthermData.operator("ref")), +) + +_LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = cv.All( cv.Schema( @@ -36,7 +52,19 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_SUMMER_MODE_ACTIVE, False): cv.boolean, cv.Optional(CONF_DHW_BLOCK, False): cv.boolean, cv.Optional(CONF_SYNC_MODE, False): cv.boolean, - cv.Optional(CONF_OPENTHERM_VERSION): cv.positive_float, + cv.Optional(CONF_OPENTHERM_VERSION): cv.positive_float, # Deprecated + cv.Optional(CONF_BEFORE_SEND): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(BeforeSendTrigger), + } + ), + cv.Optional(CONF_BEFORE_PROCESS_RESPONSE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + BeforeProcessResponseTrigger + ), + } + ), } ) .extend( @@ -44,6 +72,11 @@ CONFIG_SCHEMA = cv.All( schema.INPUTS, (lambda _: cv.use_id(sensor.Sensor)) ) ) + .extend( + validate.create_entities_schema( + schema.SETTINGS, (lambda s: s.validation_schema) + ) + ) .extend(cv.COMPONENT_SCHEMA), cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266]), ) @@ -60,18 +93,33 @@ async def to_code(config: dict[str, Any]) -> None: out_pin = await cg.gpio_pin_expression(config[CONF_OUT_PIN]) cg.add(var.set_out_pin(out_pin)) - non_sensors = {CONF_ID, CONF_IN_PIN, CONF_OUT_PIN} + non_sensors = { + CONF_ID, + CONF_IN_PIN, + CONF_OUT_PIN, + CONF_BEFORE_SEND, + CONF_BEFORE_PROCESS_RESPONSE, + } input_sensors = [] + settings = [] for key, value in config.items(): if key in non_sensors: continue if key in schema.INPUTS: input_sensor = await cg.get_variable(value) - cg.add( - getattr(var, f"set_{key}_{const.INPUT_SENSOR.lower()}")(input_sensor) - ) + cg.add(getattr(var, f"set_{key}_{const.INPUT_SENSOR}")(input_sensor)) input_sensors.append(key) + elif key in schema.SETTINGS: + if value == schema.SETTINGS[key].default_value: + continue + cg.add(getattr(var, f"set_{key}_{const.SETTING}")(value)) + settings.append(key) else: + if key == CONF_OPENTHERM_VERSION: + _LOGGER.warning( + "opentherm_version is deprecated and will be removed in esphome 2025.2.0\n" + "Please change to 'opentherm_version_controller'." + ) cg.add(getattr(var, f"set_{key}")(value)) if len(input_sensors) > 0: @@ -81,3 +129,21 @@ async def to_code(config: dict[str, Any]) -> None: ) generate.define_readers(const.INPUT_SENSOR, input_sensors) generate.add_messages(var, input_sensors, schema.INPUTS) + + if len(settings) > 0: + generate.define_has_settings(settings, schema.SETTINGS) + generate.define_message_handler(const.SETTING, settings, schema.SETTINGS) + generate.define_setting_readers(const.SETTING, settings) + generate.add_messages(var, settings, schema.SETTINGS) + + for conf in config.get(CONF_BEFORE_SEND, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation( + trigger, [(generate.OpenthermData.operator("ref"), "x")], conf + ) + + for conf in config.get(CONF_BEFORE_PROCESS_RESPONSE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation( + trigger, [(generate.OpenthermData.operator("ref"), "x")], conf + ) diff --git a/esphome/components/opentherm/automation.h b/esphome/components/opentherm/automation.h new file mode 100644 index 0000000000..acbe33ac8f --- /dev/null +++ b/esphome/components/opentherm/automation.h @@ -0,0 +1,25 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "hub.h" +#include "opentherm.h" + +namespace esphome { +namespace opentherm { + +class BeforeSendTrigger : public Trigger { + public: + BeforeSendTrigger(OpenthermHub *hub) { + hub->add_on_before_send_callback([this](OpenthermData &x) { this->trigger(x); }); + } +}; + +class BeforeProcessResponseTrigger : public Trigger { + public: + BeforeProcessResponseTrigger(OpenthermHub *hub) { + hub->add_on_before_process_response_callback([this](OpenthermData &x) { this->trigger(x); }); + } +}; + +} // namespace opentherm +} // namespace esphome diff --git a/esphome/components/opentherm/const.py b/esphome/components/opentherm/const.py index a113331585..51ad84ce46 100644 --- a/esphome/components/opentherm/const.py +++ b/esphome/components/opentherm/const.py @@ -9,3 +9,4 @@ SWITCH = "switch" NUMBER = "number" OUTPUT = "output" INPUT_SENSOR = "input_sensor" +SETTING = "setting" diff --git a/esphome/components/opentherm/generate.py b/esphome/components/opentherm/generate.py index 9716cab093..6b6a0255a8 100644 --- a/esphome/components/opentherm/generate.py +++ b/esphome/components/opentherm/generate.py @@ -1,13 +1,14 @@ from collections.abc import Awaitable -from typing import Any, Callable +from typing import Any, Callable, Optional import esphome.codegen as cg from esphome.const import CONF_ID from . import const -from .schema import TSchema +from .schema import TSchema, SettingSchema opentherm_ns = cg.esphome_ns.namespace("opentherm") OpenthermHub = opentherm_ns.class_("OpenthermHub", cg.Component) +OpenthermData = opentherm_ns.class_("OpenthermData") def define_has_component(component_type: str, keys: list[str]) -> None: @@ -21,6 +22,24 @@ def define_has_component(component_type: str, keys: list[str]) -> None: cg.add_define(f"OPENTHERM_HAS_{component_type.upper()}_{key}") +# We need a separate set of macros for settings because there are different backing field types we need to take +# into account +def define_has_settings(keys: list[str], schemas: dict[str, SettingSchema]) -> None: + cg.add_define( + "OPENTHERM_SETTING_LIST(F, sep)", + cg.RawExpression( + " sep ".join( + map( + lambda key: f"F({schemas[key].backing_type}, {key}_setting, {schemas[key].default_value})", + keys, + ) + ) + ), + ) + for key in keys: + cg.add_define(f"OPENTHERM_HAS_SETTING_{key}") + + def define_message_handler( component_type: str, keys: list[str], schemas: dict[str, TSchema] ) -> None: @@ -74,16 +93,30 @@ def define_readers(component_type: str, keys: list[str]) -> None: ) -def add_messages(hub: cg.MockObj, keys: list[str], schemas: dict[str, TSchema]): - messages: set[tuple[str, bool]] = set() +def define_setting_readers(component_type: str, keys: list[str]) -> None: for key in keys: - messages.add((schemas[key].message, schemas[key].keep_updated)) - for msg, keep_updated in messages: + cg.add_define( + f"OPENTHERM_READ_{key}", + cg.RawExpression(f"this->{key}_{component_type.lower()}"), + ) + + +def add_messages(hub: cg.MockObj, keys: list[str], schemas: dict[str, TSchema]): + messages: dict[str, tuple[bool, Optional[int]]] = {} + for key in keys: + messages[schemas[key].message] = ( + schemas[key].keep_updated, + schemas[key].order if hasattr(schemas[key], "order") else None, + ) + for msg, (keep_updated, order) in messages.items(): msg_expr = cg.RawExpression(f"esphome::opentherm::MessageId::{msg}") if keep_updated: cg.add(hub.add_repeating_message(msg_expr)) else: - cg.add(hub.add_initial_message(msg_expr)) + if order is not None: + cg.add(hub.add_initial_message(msg_expr, order)) + else: + cg.add(hub.add_initial_message(msg_expr)) def add_property_set(var: cg.MockObj, config_key: str, config: dict[str, Any]) -> None: diff --git a/esphome/components/opentherm/hub.cpp b/esphome/components/opentherm/hub.cpp index aac2966ed1..97adf71752 100644 --- a/esphome/components/opentherm/hub.cpp +++ b/esphome/components/opentherm/hub.cpp @@ -63,7 +63,7 @@ void write_f88(const float value, OpenthermData &data) { data.f88(value); } OpenthermData OpenthermHub::build_request_(MessageId request_id) const { OpenthermData data; data.type = 0; - data.id = 0; + data.id = request_id; data.valueHB = 0; data.valueLB = 0; @@ -82,28 +82,13 @@ OpenthermData OpenthermHub::build_request_(MessageId request_id) const { // NOLINTEND data.type = MessageType::READ_DATA; - data.id = MessageId::STATUS; data.valueHB = ch_enabled | (dhw_enabled << 1) | (cooling_enabled << 2) | (otc_enabled << 3) | (ch2_enabled << 4) | (summer_mode_is_active << 5) | (dhw_blocked << 6); return data; } - // Another special case is OpenTherm version number which is configured at hub level as a constant - if (request_id == MessageId::OT_VERSION_CONTROLLER) { - data.type = MessageType::WRITE_DATA; - data.id = MessageId::OT_VERSION_CONTROLLER; - data.f88(this->opentherm_version_); - - return data; - } - -// Disable incomplete switch statement warnings, because the cases in each -// switch are generated based on the configured sensors and inputs. -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wswitch" - - // Next, we start with the write requests from switches and other inputs, + // Next, we start with write requests from switches and other inputs, // because we would want to write that data if it is available, rather than // request a read for that type (in the case that both read and write are // supported). @@ -116,14 +101,23 @@ OpenthermData OpenthermHub::build_request_(MessageId request_id) const { OPENTHERM_MESSAGE_WRITE_POSTSCRIPT, ) OPENTHERM_INPUT_SENSOR_MESSAGE_HANDLERS(OPENTHERM_MESSAGE_WRITE_MESSAGE, OPENTHERM_MESSAGE_WRITE_ENTITY, , OPENTHERM_MESSAGE_WRITE_POSTSCRIPT, ) + OPENTHERM_SETTING_MESSAGE_HANDLERS(OPENTHERM_MESSAGE_WRITE_MESSAGE, OPENTHERM_MESSAGE_WRITE_SETTING, , + OPENTHERM_MESSAGE_WRITE_POSTSCRIPT, ) + default: + break; } // Finally, handle the simple read requests, which only change with the message id. - switch (request_id) { OPENTHERM_SENSOR_MESSAGE_HANDLERS(OPENTHERM_MESSAGE_READ_MESSAGE, OPENTHERM_IGNORE, , , ) } + switch (request_id) { + OPENTHERM_SENSOR_MESSAGE_HANDLERS(OPENTHERM_MESSAGE_READ_MESSAGE, OPENTHERM_IGNORE, , , ) + default: + break; + } switch (request_id) { OPENTHERM_BINARY_SENSOR_MESSAGE_HANDLERS(OPENTHERM_MESSAGE_READ_MESSAGE, OPENTHERM_IGNORE, , , ) + default: + break; } -#pragma GCC diagnostic pop // And if we get here, a message was requested which somehow wasn't handled. // This shouldn't happen due to the way the defines are configured, so we @@ -163,19 +157,37 @@ void OpenthermHub::setup() { // communicate at least once every second. Sending the status request is // good practice anyway. this->add_repeating_message(MessageId::STATUS); - - // Also ensure that we start communication with the STATUS message - this->initial_messages_.insert(this->initial_messages_.begin(), MessageId::STATUS); - - if (this->opentherm_version_ > 0.0f) { - this->initial_messages_.insert(this->initial_messages_.begin(), MessageId::OT_VERSION_CONTROLLER); - } - - this->current_message_iterator_ = this->initial_messages_.begin(); + this->write_initial_messages_(this->messages_); + this->message_iterator_ = this->messages_.begin(); } void OpenthermHub::on_shutdown() { this->opentherm_->stop(); } +// Disabling clang-tidy for this particular line since it keeps removing the trailing underscore (bug?) +void OpenthermHub::write_initial_messages_(std::vector &target) { // NOLINT + std::vector> sorted; + std::copy_if(this->configured_messages_.begin(), this->configured_messages_.end(), std::back_inserter(sorted), + [](const std::pair &pair) { return pair.second < REPEATING_MESSAGE_ORDER; }); + std::sort(sorted.begin(), sorted.end(), + [](const std::pair &a, const std::pair &b) { + return a.second < b.second; + }); + + target.clear(); + std::transform(sorted.begin(), sorted.end(), std::back_inserter(target), + [](const std::pair &pair) { return pair.first; }); +} + +// Disabling clang-tidy for this particular line since it keeps removing the trailing underscore (bug?) +void OpenthermHub::write_repeating_messages_(std::vector &target) { // NOLINT + target.clear(); + for (auto const &pair : this->configured_messages_) { + if (pair.second == REPEATING_MESSAGE_ORDER) { + target.push_back(pair.first); + } + } +} + void OpenthermHub::loop() { if (this->sync_mode_) { this->sync_loop_(); @@ -184,29 +196,18 @@ void OpenthermHub::loop() { auto cur_time = millis(); auto const cur_mode = this->opentherm_->get_mode(); + + if (this->handle_error_(cur_mode)) { + return; + } + switch (cur_mode) { case OperationMode::WRITE: case OperationMode::READ: case OperationMode::LISTEN: - if (!this->check_timings_(cur_time)) { - break; - } - this->last_mode_ = cur_mode; - break; - case OperationMode::ERROR_PROTOCOL: - if (this->last_mode_ == OperationMode::WRITE) { - this->handle_protocol_write_error_(); - } else if (this->last_mode_ == OperationMode::READ) { - this->handle_protocol_read_error_(); - } - - this->stop_opentherm_(); - break; - case OperationMode::ERROR_TIMEOUT: - this->handle_timeout_error_(); - this->stop_opentherm_(); break; case OperationMode::IDLE: + this->check_timings_(cur_time); if (this->should_skip_loop_(cur_time)) { break; } @@ -219,6 +220,28 @@ void OpenthermHub::loop() { case OperationMode::RECEIVED: this->read_response_(); break; + default: + break; + } + this->last_mode_ = cur_mode; +} + +bool OpenthermHub::handle_error_(OperationMode mode) { + switch (mode) { + case OperationMode::ERROR_PROTOCOL: + // Protocol error can happen only while reading boiler response. + this->handle_protocol_error_(); + return true; + case OperationMode::ERROR_TIMEOUT: + // Timeout error might happen while we wait for device to respond. + this->handle_timeout_error_(); + return true; + case OperationMode::ERROR_TIMER: + // Timer error can happen only on ESP32. + this->handle_timer_error_(); + return true; + default: + return false; } } @@ -237,16 +260,20 @@ void OpenthermHub::sync_loop_() { } this->start_conversation_(); + // There may be a timer error at this point + if (this->handle_error_(this->opentherm_->get_mode())) { + return; + } + // Spin while message is being sent to device if (!this->spin_wait_(1150, [&] { return this->opentherm_->is_active(); })) { ESP_LOGE(TAG, "Hub timeout triggered during send"); this->stop_opentherm_(); return; } - if (this->opentherm_->is_error()) { - this->handle_protocol_write_error_(); - this->stop_opentherm_(); + // Check for errors and ensure we are in the right state (message sent successfully) + if (this->handle_error_(this->opentherm_->get_mode())) { return; } else if (!this->opentherm_->is_sent()) { ESP_LOGW(TAG, "Unexpected state after sending request: %s", @@ -257,19 +284,20 @@ void OpenthermHub::sync_loop_() { // Listen for the response this->opentherm_->listen(); + // There may be a timer error at this point + if (this->handle_error_(this->opentherm_->get_mode())) { + return; + } + + // Spin while response is being received if (!this->spin_wait_(1150, [&] { return this->opentherm_->is_active(); })) { ESP_LOGE(TAG, "Hub timeout triggered during receive"); this->stop_opentherm_(); return; } - if (this->opentherm_->is_timeout()) { - this->handle_timeout_error_(); - this->stop_opentherm_(); - return; - } else if (this->opentherm_->is_protocol_error()) { - this->handle_protocol_read_error_(); - this->stop_opentherm_(); + // Check for errors and ensure we are in the right state (message received successfully) + if (this->handle_error_(this->opentherm_->get_mode())) { return; } else if (!this->opentherm_->has_message()) { ESP_LOGW(TAG, "Unexpected state after receiving response: %s", @@ -281,17 +309,13 @@ void OpenthermHub::sync_loop_() { this->read_response_(); } -bool OpenthermHub::check_timings_(uint32_t cur_time) { +void OpenthermHub::check_timings_(uint32_t cur_time) { if (this->last_conversation_start_ > 0 && (cur_time - this->last_conversation_start_) > 1150) { ESP_LOGW(TAG, "%d ms elapsed since the start of the last convo, but 1150 ms are allowed at maximum. Look at other " "components that might slow the loop down.", (int) (cur_time - this->last_conversation_start_)); - this->stop_opentherm_(); - return false; } - - return true; } bool OpenthermHub::should_skip_loop_(uint32_t cur_time) const { @@ -304,14 +328,17 @@ bool OpenthermHub::should_skip_loop_(uint32_t cur_time) const { } void OpenthermHub::start_conversation_() { - if (this->sending_initial_ && this->current_message_iterator_ == this->initial_messages_.end()) { - this->sending_initial_ = false; - this->current_message_iterator_ = this->repeating_messages_.begin(); - } else if (this->current_message_iterator_ == this->repeating_messages_.end()) { - this->current_message_iterator_ = this->repeating_messages_.begin(); + if (this->message_iterator_ == this->messages_.end()) { + if (this->sending_initial_) { + this->sending_initial_ = false; + this->write_repeating_messages_(this->messages_); + } + this->message_iterator_ = this->messages_.begin(); } - auto request = this->build_request_(*this->current_message_iterator_); + auto request = this->build_request_(*this->message_iterator_); + + this->before_send_callback_.call(request); ESP_LOGD(TAG, "Sending request with id %d (%s)", request.id, this->opentherm_->message_id_to_str((MessageId) request.id)); @@ -331,37 +358,48 @@ void OpenthermHub::read_response_() { this->stop_opentherm_(); + this->before_process_response_callback_.call(response); this->process_response(response); - this->current_message_iterator_++; + this->message_iterator_++; } void OpenthermHub::stop_opentherm_() { this->opentherm_->stop(); this->last_conversation_end_ = millis(); } -void OpenthermHub::handle_protocol_write_error_() { - ESP_LOGW(TAG, "Error while sending request: %s", - this->opentherm_->operation_mode_to_str(this->opentherm_->get_mode())); - this->opentherm_->debug_data(this->last_request_); -} -void OpenthermHub::handle_protocol_read_error_() { + +void OpenthermHub::handle_protocol_error_() { OpenThermError error; this->opentherm_->get_protocol_error(error); ESP_LOGW(TAG, "Protocol error occured while receiving response: %s", - this->opentherm_->protocol_error_to_to_str(error.error_type)); + this->opentherm_->protocol_error_to_str(error.error_type)); this->opentherm_->debug_error(error); -} -void OpenthermHub::handle_timeout_error_() { - ESP_LOGW(TAG, "Receive response timed out at a protocol level"); this->stop_opentherm_(); } +void OpenthermHub::handle_timeout_error_() { + ESP_LOGW(TAG, "Timeout while waiting for response from device"); + this->stop_opentherm_(); +} + +void OpenthermHub::handle_timer_error_() { + this->opentherm_->report_and_reset_timer_error(); + this->stop_opentherm_(); + // Timer error is critical, there is no point in retrying. + this->mark_failed(); +} + void OpenthermHub::dump_config() { + std::vector initial_messages; + std::vector repeating_messages; + this->write_initial_messages_(initial_messages); + this->write_repeating_messages_(repeating_messages); + ESP_LOGCONFIG(TAG, "OpenTherm:"); LOG_PIN(" In: ", this->in_pin_); LOG_PIN(" Out: ", this->out_pin_); - ESP_LOGCONFIG(TAG, " Sync mode: %d", this->sync_mode_); + ESP_LOGCONFIG(TAG, " Sync mode: %s", YESNO(this->sync_mode_)); ESP_LOGCONFIG(TAG, " Sensors: %s", SHOW(OPENTHERM_SENSOR_LIST(ID, ))); ESP_LOGCONFIG(TAG, " Binary sensors: %s", SHOW(OPENTHERM_BINARY_SENSOR_LIST(ID, ))); ESP_LOGCONFIG(TAG, " Switches: %s", SHOW(OPENTHERM_SWITCH_LIST(ID, ))); @@ -369,12 +407,12 @@ void OpenthermHub::dump_config() { ESP_LOGCONFIG(TAG, " Outputs: %s", SHOW(OPENTHERM_OUTPUT_LIST(ID, ))); ESP_LOGCONFIG(TAG, " Numbers: %s", SHOW(OPENTHERM_NUMBER_LIST(ID, ))); ESP_LOGCONFIG(TAG, " Initial requests:"); - for (auto type : this->initial_messages_) { - ESP_LOGCONFIG(TAG, " - %d (%s)", type, this->opentherm_->message_id_to_str((type))); + for (auto type : initial_messages) { + ESP_LOGCONFIG(TAG, " - %d (%s)", type, this->opentherm_->message_id_to_str(type)); } ESP_LOGCONFIG(TAG, " Repeating requests:"); - for (auto type : this->repeating_messages_) { - ESP_LOGCONFIG(TAG, " - %d (%s)", type, this->opentherm_->message_id_to_str((type))); + for (auto type : repeating_messages) { + ESP_LOGCONFIG(TAG, " - %d (%s)", type, this->opentherm_->message_id_to_str(type)); } } diff --git a/esphome/components/opentherm/hub.h b/esphome/components/opentherm/hub.h index 1f536653e8..80fd268820 100644 --- a/esphome/components/opentherm/hub.h +++ b/esphome/components/opentherm/hub.h @@ -38,6 +38,9 @@ namespace esphome { namespace opentherm { +static const uint8_t REPEATING_MESSAGE_ORDER = 255; +static const uint8_t INITIAL_UNORDERED_MESSAGE_ORDER = 254; + // OpenTherm component for ESPHome class OpenthermHub : public Component { protected: @@ -58,15 +61,12 @@ class OpenthermHub : public Component { OPENTHERM_INPUT_SENSOR_LIST(OPENTHERM_DECLARE_INPUT_SENSOR, ) - // The set of initial messages to send on starting communication with the boiler - std::vector initial_messages_; - // and the repeating messages which are sent repeatedly to update various sensors - // and boiler parameters (like the setpoint). - std::vector repeating_messages_; - // Indicates if we are still working on the initial requests or not + OPENTHERM_SETTING_LIST(OPENTHERM_DECLARE_SETTING, ) + bool sending_initial_ = true; - // Index for the current request in one of the _requests sets. - std::vector::const_iterator current_message_iterator_; + std::unordered_map configured_messages_; + std::vector messages_; + std::vector::const_iterator message_iterator_; uint32_t last_conversation_start_ = 0; uint32_t last_conversation_end_ = 0; @@ -78,20 +78,25 @@ class OpenthermHub : public Component { // Very likely to happen while using Dallas temperature sensors. bool sync_mode_ = false; - float opentherm_version_ = 0.0f; + CallbackManager before_send_callback_; + CallbackManager before_process_response_callback_; // Create OpenTherm messages based on the message id OpenthermData build_request_(MessageId request_id) const; - void handle_protocol_write_error_(); - void handle_protocol_read_error_(); + bool handle_error_(OperationMode mode); + void handle_protocol_error_(); void handle_timeout_error_(); + void handle_timer_error_(); void stop_opentherm_(); void start_conversation_(); void read_response_(); - bool check_timings_(uint32_t cur_time); + void check_timings_(uint32_t cur_time); bool should_skip_loop_(uint32_t cur_time) const; void sync_loop_(); + void write_initial_messages_(std::vector &target); + void write_repeating_messages_(std::vector &target); + template bool spin_wait_(uint32_t timeout, F func) { auto start_time = millis(); while (func()) { @@ -127,13 +132,18 @@ class OpenthermHub : public Component { OPENTHERM_INPUT_SENSOR_LIST(OPENTHERM_SET_INPUT_SENSOR, ) + OPENTHERM_SETTING_LIST(OPENTHERM_SET_SETTING, ) + // Add a request to the vector of initial requests - void add_initial_message(MessageId message_id) { this->initial_messages_.push_back(message_id); } + void add_initial_message(MessageId message_id) { + this->configured_messages_[message_id] = INITIAL_UNORDERED_MESSAGE_ORDER; + } + void add_initial_message(MessageId message_id, uint8_t order) { this->configured_messages_[message_id] = order; } // Add a request to the set of repeating requests. Note that a large number of repeating // requests will slow down communication with the boiler. Each request may take up to 1 second, // so with all sensors enabled, it may take about half a minute before a change in setpoint // will be processed. - void add_repeating_message(MessageId message_id) { this->repeating_messages_.push_back(message_id); } + void add_repeating_message(MessageId message_id) { this->configured_messages_[message_id] = REPEATING_MESSAGE_ORDER; } // There are seven status variables, which can either be set as a simple variable, // or using a switch. ch_enable and dhw_enable default to true, the others to false. @@ -149,7 +159,13 @@ class OpenthermHub : public Component { void set_summer_mode_active(bool value) { this->summer_mode_active = value; } void set_dhw_block(bool value) { this->dhw_block = value; } void set_sync_mode(bool sync_mode) { this->sync_mode_ = sync_mode; } - void set_opentherm_version(float value) { this->opentherm_version_ = value; } + + void add_on_before_send_callback(std::function &&callback) { + this->before_send_callback_.add(std::move(callback)); + } + void add_on_before_process_response_callback(std::function &&callback) { + this->before_process_response_callback_.add(std::move(callback)); + } float get_setup_priority() const override { return setup_priority::HARDWARE; } diff --git a/esphome/components/opentherm/opentherm.cpp b/esphome/components/opentherm/opentherm.cpp index 62ab1d3860..49482316ee 100644 --- a/esphome/components/opentherm/opentherm.cpp +++ b/esphome/components/opentherm/opentherm.cpp @@ -52,7 +52,9 @@ bool OpenTherm::initialize() { OpenTherm::instance = this; #endif this->in_pin_->pin_mode(gpio::FLAG_INPUT); + this->in_pin_->setup(); this->out_pin_->pin_mode(gpio::FLAG_OUTPUT); + this->out_pin_->setup(); this->out_pin_->digital_write(true); #if defined(ESP32) || defined(USE_ESP_IDF) @@ -182,7 +184,7 @@ bool IRAM_ATTR OpenTherm::timer_isr(OpenTherm *arg) { } arg->capture_ = 1; // reset counter } else if (arg->capture_ > 0xFF) { - // no change for too long, invalid mancheter encoding + // no change for too long, invalid manchester encoding arg->mode_ = OperationMode::ERROR_PROTOCOL; arg->error_type_ = ProtocolErrorType::NO_CHANGE_TOO_LONG; arg->stop_timer_(); @@ -312,21 +314,31 @@ bool OpenTherm::init_esp32_timer_() { } void IRAM_ATTR OpenTherm::start_esp32_timer_(uint64_t alarm_value) { - esp_err_t result; + // We will report timer errors outside of interrupt handler + this->timer_error_ = ESP_OK; + this->timer_error_type_ = TimerErrorType::NO_TIMER_ERROR; - result = timer_set_alarm_value(this->timer_group_, this->timer_idx_, alarm_value); - if (result != ESP_OK) { - const auto *error = esp_err_to_name(result); - ESP_LOGE(TAG, "Failed to set alarm value. Error: %s", error); + this->timer_error_ = timer_set_alarm_value(this->timer_group_, this->timer_idx_, alarm_value); + if (this->timer_error_ != ESP_OK) { + this->timer_error_type_ = TimerErrorType::SET_ALARM_VALUE_ERROR; + return; + } + this->timer_error_ = timer_start(this->timer_group_, this->timer_idx_); + if (this->timer_error_ != ESP_OK) { + this->timer_error_type_ = TimerErrorType::TIMER_START_ERROR; + } +} + +void OpenTherm::report_and_reset_timer_error() { + if (this->timer_error_ == ESP_OK) { return; } - result = timer_start(this->timer_group_, this->timer_idx_); - if (result != ESP_OK) { - const auto *error = esp_err_to_name(result); - ESP_LOGE(TAG, "Failed to start the timer. Error: %s", error); - return; - } + ESP_LOGE(TAG, "Error occured while manipulating timer (%s): %s", this->timer_error_to_str(this->timer_error_type_), + esp_err_to_name(this->timer_error_)); + + this->timer_error_ = ESP_OK; + this->timer_error_type_ = NO_TIMER_ERROR; } // 5 kHz timer_ @@ -343,21 +355,18 @@ void IRAM_ATTR OpenTherm::start_write_timer_() { void IRAM_ATTR OpenTherm::stop_timer_() { InterruptLock const lock; + // We will report timer errors outside of interrupt handler + this->timer_error_ = ESP_OK; + this->timer_error_type_ = TimerErrorType::NO_TIMER_ERROR; - esp_err_t result; - - result = timer_pause(this->timer_group_, this->timer_idx_); - if (result != ESP_OK) { - const auto *error = esp_err_to_name(result); - ESP_LOGE(TAG, "Failed to pause the timer. Error: %s", error); + this->timer_error_ = timer_pause(this->timer_group_, this->timer_idx_); + if (this->timer_error_ != ESP_OK) { + this->timer_error_type_ = TimerErrorType::TIMER_PAUSE_ERROR; return; } - - result = timer_set_counter_value(this->timer_group_, this->timer_idx_, 0); - if (result != ESP_OK) { - const auto *error = esp_err_to_name(result); - ESP_LOGE(TAG, "Failed to set timer counter to 0 after pausing. Error: %s", error); - return; + this->timer_error_ = timer_set_counter_value(this->timer_group_, this->timer_idx_, 0); + if (this->timer_error_ != ESP_OK) { + this->timer_error_type_ = TimerErrorType::SET_COUNTER_VALUE_ERROR; } } @@ -386,6 +395,9 @@ void IRAM_ATTR OpenTherm::stop_timer_() { timer1_detachInterrupt(); } +// There is nothing to report on ESP8266 +void OpenTherm::report_and_reset_timer_error() {} + #endif // END ESP8266 // https://stackoverflow.com/questions/21617970/how-to-check-if-value-has-even-parity-of-bits-or-odd @@ -412,11 +424,12 @@ const char *OpenTherm::operation_mode_to_str(OperationMode mode) { TO_STRING_MEMBER(SENT) TO_STRING_MEMBER(ERROR_PROTOCOL) TO_STRING_MEMBER(ERROR_TIMEOUT) + TO_STRING_MEMBER(ERROR_TIMER) default: return ""; } } -const char *OpenTherm::protocol_error_to_to_str(ProtocolErrorType error_type) { +const char *OpenTherm::protocol_error_to_str(ProtocolErrorType error_type) { switch (error_type) { TO_STRING_MEMBER(NO_ERROR) TO_STRING_MEMBER(NO_TRANSITION) @@ -427,6 +440,17 @@ const char *OpenTherm::protocol_error_to_to_str(ProtocolErrorType error_type) { return ""; } } +const char *OpenTherm::timer_error_to_str(TimerErrorType error_type) { + switch (error_type) { + TO_STRING_MEMBER(NO_TIMER_ERROR) + TO_STRING_MEMBER(SET_ALARM_VALUE_ERROR) + TO_STRING_MEMBER(TIMER_START_ERROR) + TO_STRING_MEMBER(TIMER_PAUSE_ERROR) + TO_STRING_MEMBER(SET_COUNTER_VALUE_ERROR) + default: + return ""; + } +} const char *OpenTherm::message_type_to_str(MessageType message_type) { switch (message_type) { TO_STRING_MEMBER(READ_DATA) diff --git a/esphome/components/opentherm/opentherm.h b/esphome/components/opentherm/opentherm.h index 3be0191c63..4280832d09 100644 --- a/esphome/components/opentherm/opentherm.h +++ b/esphome/components/opentherm/opentherm.h @@ -36,11 +36,12 @@ enum OperationMode { READ = 2, // reading 32-bit data frame RECEIVED = 3, // data frame received with valid start and stop bit - WRITE = 4, // writing data with timer_ + WRITE = 4, // writing data to output SENT = 5, // all data written to output - ERROR_PROTOCOL = 8, // manchester protocol data transfer error - ERROR_TIMEOUT = 9 // read timeout + ERROR_PROTOCOL = 8, // protocol error, can happed only during READ + ERROR_TIMEOUT = 9, // timeout while waiting for response from device, only during LISTEN + ERROR_TIMER = 10 // error operating the ESP32 timer }; enum ProtocolErrorType { @@ -51,6 +52,14 @@ enum ProtocolErrorType { NO_CHANGE_TOO_LONG = 4, // No level change for too much timer ticks }; +enum TimerErrorType { + NO_TIMER_ERROR = 0, // No error + SET_ALARM_VALUE_ERROR = 1, // No transition in the middle of the bit + TIMER_START_ERROR = 2, // Stop bit wasn't present when expected + TIMER_PAUSE_ERROR = 3, // Parity check didn't pass + SET_COUNTER_VALUE_ERROR = 4, // No level change for too much timer ticks +}; + enum MessageType { READ_DATA = 0, READ_ACK = 4, @@ -299,7 +308,9 @@ class OpenTherm { * * @return true if last listen() or send() operation ends up with an error. */ - bool is_error() { return mode_ == OperationMode::ERROR_TIMEOUT || mode_ == OperationMode::ERROR_PROTOCOL; } + bool is_error() { + return mode_ == OperationMode::ERROR_TIMEOUT || mode_ == OperationMode::ERROR_PROTOCOL || mode_ == ERROR_TIMER; + } /** * Indicates whether last listen() or send() operation ends up with a *timeout* error @@ -313,14 +324,22 @@ class OpenTherm { */ bool is_protocol_error() { return mode_ == OperationMode::ERROR_PROTOCOL; } + /** + * Indicates whether start_esp32_timer_() or stop_timer_() had an error. Only relevant when used on ESP32. + * @return true if there was an error. + */ + bool is_timer_error() { return mode_ == OperationMode::ERROR_TIMER; } + bool is_active() { return mode_ == LISTEN || mode_ == READ || mode_ == WRITE; } OperationMode get_mode() { return mode_; } void debug_data(OpenthermData &data); void debug_error(OpenThermError &error) const; + void report_and_reset_timer_error(); - const char *protocol_error_to_to_str(ProtocolErrorType error_type); + const char *protocol_error_to_str(ProtocolErrorType error_type); + const char *timer_error_to_str(TimerErrorType error_type); const char *message_type_to_str(MessageType message_type); const char *operation_mode_to_str(OperationMode mode); const char *message_id_to_str(MessageId id); @@ -349,10 +368,12 @@ class OpenTherm { uint32_t data_; uint8_t bit_pos_; int32_t timeout_counter_; // <0 no timeout - int32_t device_timeout_; #if defined(ESP32) || defined(USE_ESP_IDF) + esp_err_t timer_error_ = ESP_OK; + TimerErrorType timer_error_type_ = TimerErrorType::NO_TIMER_ERROR; + bool init_esp32_timer_(); void start_esp32_timer_(uint64_t alarm_value); #endif diff --git a/esphome/components/opentherm/opentherm_macros.h b/esphome/components/opentherm/opentherm_macros.h index 8aaec0b48a..398c64aa8f 100644 --- a/esphome/components/opentherm/opentherm_macros.h +++ b/esphome/components/opentherm/opentherm_macros.h @@ -28,6 +28,9 @@ namespace opentherm { #ifndef OPENTHERM_INPUT_SENSOR_LIST #define OPENTHERM_INPUT_SENSOR_LIST(F, sep) #endif +#ifndef OPENTHERM_SETTING_LIST +#define OPENTHERM_SETTING_LIST(F, sep) +#endif // Use macros to create fields for every entity specified in the ESPHome configuration #define OPENTHERM_DECLARE_SENSOR(entity) sensor::Sensor *entity; @@ -36,6 +39,7 @@ namespace opentherm { #define OPENTHERM_DECLARE_NUMBER(entity) OpenthermNumber *entity; #define OPENTHERM_DECLARE_OUTPUT(entity) OpenthermOutput *entity; #define OPENTHERM_DECLARE_INPUT_SENSOR(entity) sensor::Sensor *entity; +#define OPENTHERM_DECLARE_SETTING(type, entity, def) type entity = def; // Setter macros #define OPENTHERM_SET_SENSOR(entity) \ @@ -56,6 +60,9 @@ namespace opentherm { #define OPENTHERM_SET_INPUT_SENSOR(entity) \ void set_##entity(sensor::Sensor *sensor) { this->entity = sensor; } +#define OPENTHERM_SET_SETTING(type, entity, def) \ + void set_##entity(type value) { this->entity = value; } + // ===== hub.cpp macros ===== // *_MESSAGE_HANDLERS are generated in defines.h and look like this: @@ -85,6 +92,9 @@ namespace opentherm { #ifndef OPENTHERM_INPUT_SENSOR_MESSAGE_HANDLERS #define OPENTHERM_INPUT_SENSOR_MESSAGE_HANDLERS(MESSAGE, ENTITY, entity_sep, postscript, msg_sep) #endif +#ifndef OPENTHERM_SETTING_MESSAGE_HANDLERS +#define OPENTHERM_SETTING_MESSAGE_HANDLERS(MESSAGE, ENTITY, entity_sep, postscript, msg_sep) +#endif // Write data request builders #define OPENTHERM_MESSAGE_WRITE_MESSAGE(msg) \ @@ -92,6 +102,7 @@ namespace opentherm { data.type = MessageType::WRITE_DATA; \ data.id = request_id; #define OPENTHERM_MESSAGE_WRITE_ENTITY(key, msg_data) message_data::write_##msg_data(this->key->state, data); +#define OPENTHERM_MESSAGE_WRITE_SETTING(key, msg_data) message_data::write_##msg_data(this->key, data); #define OPENTHERM_MESSAGE_WRITE_POSTSCRIPT \ return data; \ } diff --git a/esphome/components/opentherm/schema.py b/esphome/components/opentherm/schema.py index fe0f2a77a3..a58de8e2da 100644 --- a/esphome/components/opentherm/schema.py +++ b/esphome/components/opentherm/schema.py @@ -2,8 +2,9 @@ # inputs of the OpenTherm component. from dataclasses import dataclass -from typing import Optional, TypeVar +from typing import Optional, TypeVar, Any +import esphome.config_validation as cv from esphome.const import ( UNIT_CELSIUS, UNIT_EMPTY, @@ -64,6 +65,7 @@ class SensorSchema(EntitySchema): icon: Optional[str] = None device_class: Optional[str] = None disabled_by_default: bool = False + order: Optional[int] = None SENSORS: dict[str, SensorSchema] = { @@ -399,6 +401,7 @@ SENSORS: dict[str, SensorSchema] = { message="OT_VERSION_DEVICE", keep_updated=False, message_data="f88", + order=2, ), "device_type": SensorSchema( description="Device product type", @@ -409,6 +412,7 @@ SENSORS: dict[str, SensorSchema] = { message="VERSION_DEVICE", keep_updated=False, message_data="u8_hb", + order=0, ), "device_version": SensorSchema( description="Device product version", @@ -419,6 +423,7 @@ SENSORS: dict[str, SensorSchema] = { message="VERSION_DEVICE", keep_updated=False, message_data="u8_lb", + order=0, ), "device_id": SensorSchema( description="Device ID code", @@ -429,6 +434,7 @@ SENSORS: dict[str, SensorSchema] = { message="DEVICE_CONFIG", keep_updated=False, message_data="u8_lb", + order=4, ), "otc_hc_ratio_ub": SensorSchema( description="OTC heat curve ratio upper bound", @@ -457,6 +463,7 @@ SENSORS: dict[str, SensorSchema] = { class BinarySensorSchema(EntitySchema): icon: Optional[str] = None device_class: Optional[str] = None + order: Optional[int] = None BINARY_SENSORS: dict[str, BinarySensorSchema] = { @@ -525,48 +532,56 @@ BINARY_SENSORS: dict[str, BinarySensorSchema] = { message="DEVICE_CONFIG", keep_updated=False, message_data="flag8_hb_0", + order=4, ), "control_type_on_off": BinarySensorSchema( description="Configuration: Control type is on/off", message="DEVICE_CONFIG", keep_updated=False, message_data="flag8_hb_1", + order=4, ), "cooling_supported": BinarySensorSchema( description="Configuration: Cooling supported", message="DEVICE_CONFIG", keep_updated=False, message_data="flag8_hb_2", + order=4, ), "dhw_storage_tank": BinarySensorSchema( description="Configuration: DHW storage tank", message="DEVICE_CONFIG", keep_updated=False, message_data="flag8_hb_3", + order=4, ), "controller_pump_control_allowed": BinarySensorSchema( description="Configuration: Controller pump control allowed", message="DEVICE_CONFIG", keep_updated=False, message_data="flag8_hb_4", + order=4, ), "ch2_present": BinarySensorSchema( description="Configuration: CH2 present", message="DEVICE_CONFIG", keep_updated=False, message_data="flag8_hb_5", + order=4, ), "water_filling": BinarySensorSchema( description="Configuration: Remote water filling", message="DEVICE_CONFIG", keep_updated=False, message_data="flag8_hb_6", + order=4, ), "heat_mode": BinarySensorSchema( description="Configuration: Heating or cooling", message="DEVICE_CONFIG", keep_updated=False, message_data="flag8_hb_7", + order=4, ), "dhw_setpoint_transfer_enabled": BinarySensorSchema( description="Remote boiler parameters: DHW setpoint transfer enabled", @@ -812,3 +827,65 @@ INPUTS: dict[str, InputSchema] = { auto_max_value=AutoConfigure(message="OTC_CURVE_BOUNDS", message_data="u8_hb"), ), } + + +@dataclass +class SettingSchema(EntitySchema): + backing_type: str + validation_schema: cv.Schema + default_value: Any + order: Optional[int] = None + + +SETTINGS: dict[str, SettingSchema] = { + "controller_product_type": SettingSchema( + description="Controller product type", + message="VERSION_CONTROLLER", + keep_updated=False, + message_data="u8_hb", + backing_type="uint8_t", + validation_schema=cv.int_range(min=0, max=255), + default_value=0, + order=1, + ), + "controller_product_version": SettingSchema( + description="Controller product version", + message="VERSION_CONTROLLER", + keep_updated=False, + message_data="u8_lb", + backing_type="uint8_t", + validation_schema=cv.int_range(min=0, max=255), + default_value=0, + order=1, + ), + "opentherm_version_controller": SettingSchema( + description="Version of OpenTherm implemented by controller", + message="OT_VERSION_CONTROLLER", + keep_updated=False, + message_data="f88", + backing_type="float", + validation_schema=cv.positive_float, + default_value=0, + order=3, + ), + "controller_configuration": SettingSchema( + description="Controller configuration", + message="CONTROLLER_CONFIG", + keep_updated=False, + message_data="u8_hb", + backing_type="uint8_t", + validation_schema=cv.int_range(min=0, max=255), + default_value=0, + order=5, + ), + "controller_id": SettingSchema( + description="Controller ID code", + message="CONTROLLER_CONFIG", + keep_updated=False, + message_data="u8_lb", + backing_type="uint8_t", + validation_schema=cv.int_range(min=0, max=255), + default_value=0, + order=5, + ), +} diff --git a/esphome/components/opentherm/validate.py b/esphome/components/opentherm/validate.py index d4507672a5..055cbfa827 100644 --- a/esphome/components/opentherm/validate.py +++ b/esphome/components/opentherm/validate.py @@ -9,12 +9,17 @@ from .schema import TSchema def create_entities_schema( - entities: dict[str, schema.EntitySchema], + entities: dict[str, TSchema], get_entity_validation_schema: Callable[[TSchema], cv.Schema], ) -> Schema: entity_schema = {} for key, entity in entities.items(): - entity_schema[cv.Optional(key)] = get_entity_validation_schema(entity) + schema_key = ( + cv.Optional(key, entity.default_value) + if hasattr(entity, "default_value") + else cv.Optional(key) + ) + entity_schema[schema_key] = get_entity_validation_schema(entity) return cv.Schema(entity_schema) diff --git a/esphome/components/uart/uart_component_esp32_arduino.cpp b/esphome/components/uart/uart_component_esp32_arduino.cpp index 793c1d52f4..b241c03de4 100644 --- a/esphome/components/uart/uart_component_esp32_arduino.cpp +++ b/esphome/components/uart/uart_component_esp32_arduino.cpp @@ -1,9 +1,9 @@ #ifdef USE_ESP32_FRAMEWORK_ARDUINO +#include "uart_component_esp32_arduino.h" #include "esphome/core/application.h" #include "esphome/core/defines.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#include "uart_component_esp32_arduino.h" #ifdef USE_LOGGER #include "esphome/components/logger/logger.h" @@ -118,7 +118,7 @@ void ESP32ArduinoUARTComponent::setup() { } #endif // USE_LOGGER - if (next_uart_num >= UART_NUM_MAX) { + if (next_uart_num >= SOC_UART_NUM) { ESP_LOGW(TAG, "Maximum number of UART components created already."); this->mark_failed(); return; diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index 6999dfb619..122d4105c8 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -1,11 +1,11 @@ #ifdef USE_ESP_IDF #include "uart_component_esp_idf.h" +#include #include "esphome/core/application.h" #include "esphome/core/defines.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#include #ifdef USE_LOGGER #include "esphome/components/logger/logger.h" @@ -84,7 +84,7 @@ void IDFUARTComponent::setup() { } #endif // USE_LOGGER - if (next_uart_num >= UART_NUM_MAX) { + if (next_uart_num >= SOC_UART_NUM) { ESP_LOGW(TAG, "Maximum number of UART components created already."); this->mark_failed(); return; diff --git a/esphome/config_validation.py b/esphome/config_validation.py index ebfb2631c3..38fd677a2a 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -1660,6 +1660,12 @@ class SplitDefault(Optional): esp32_c3=vol.UNDEFINED, esp32_c3_arduino=vol.UNDEFINED, esp32_c3_idf=vol.UNDEFINED, + esp32_c6=vol.UNDEFINED, + esp32_c6_arduino=vol.UNDEFINED, + esp32_c6_idf=vol.UNDEFINED, + esp32_h2=vol.UNDEFINED, + esp32_h2_arduino=vol.UNDEFINED, + esp32_h2_idf=vol.UNDEFINED, rp2040=vol.UNDEFINED, bk72xx=vol.UNDEFINED, rtl87xx=vol.UNDEFINED, @@ -1691,6 +1697,18 @@ class SplitDefault(Optional): self._esp32_c3_idf_default = vol.default_factory( _get_priority_default(esp32_c3_idf, esp32_c3, esp32_idf, esp32) ) + self._esp32_c6_arduino_default = vol.default_factory( + _get_priority_default(esp32_c6_arduino, esp32_c6, esp32_arduino, esp32) + ) + self._esp32_c6_idf_default = vol.default_factory( + _get_priority_default(esp32_c6_idf, esp32_c6, esp32_idf, esp32) + ) + self._esp32_h2_arduino_default = vol.default_factory( + _get_priority_default(esp32_h2_arduino, esp32_h2, esp32_arduino, esp32) + ) + self._esp32_h2_idf_default = vol.default_factory( + _get_priority_default(esp32_h2_idf, esp32_h2, esp32_idf, esp32) + ) self._rp2040_default = vol.default_factory(rp2040) self._bk72xx_default = vol.default_factory(bk72xx) self._rtl87xx_default = vol.default_factory(rtl87xx) @@ -1704,6 +1722,8 @@ class SplitDefault(Optional): from esphome.components.esp32 import get_esp32_variant from esphome.components.esp32.const import ( VARIANT_ESP32C3, + VARIANT_ESP32C6, + VARIANT_ESP32H2, VARIANT_ESP32S2, VARIANT_ESP32S3, ) @@ -1724,6 +1744,16 @@ class SplitDefault(Optional): return self._esp32_c3_arduino_default if CORE.using_esp_idf: return self._esp32_c3_idf_default + elif variant == VARIANT_ESP32C6: + if CORE.using_arduino: + return self._esp32_c6_arduino_default + if CORE.using_esp_idf: + return self._esp32_c6_idf_default + elif variant == VARIANT_ESP32H2: + if CORE.using_arduino: + return self._esp32_h2_arduino_default + if CORE.using_esp_idf: + return self._esp32_h2_idf_default else: if CORE.using_arduino: return self._esp32_arduino_default diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py index 0fed8e9c53..67712da9b6 100644 --- a/esphome/dashboard/web_server.py +++ b/esphome/dashboard/web_server.py @@ -108,6 +108,12 @@ def is_authenticated(handler: BaseHandler) -> bool: return True if settings.using_auth: + if auth_header := handler.request.headers.get("Authorization"): + assert isinstance(auth_header, str) + if auth_header.startswith("Basic "): + auth_decoded = base64.b64decode(auth_header[6:]).decode() + username, password = auth_decoded.split(":", 1) + return settings.check_password(username, password) return handler.get_secure_cookie(AUTH_COOKIE_NAME) == COOKIE_AUTHENTICATED_YES return True diff --git a/requirements.txt b/requirements.txt index 7bc1c895df..d96004c8ef 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ pyserial==3.5 platformio==6.1.16 # When updating platformio, also update Dockerfile esptool==4.7.0 click==8.1.7 -esphome-dashboard==20241120.0 +esphome-dashboard==20241217.1 aioesphomeapi==24.6.2 zeroconf==0.132.2 puremagic==1.27 diff --git a/tests/components/nextion/common.yaml b/tests/components/nextion/common.yaml index e84cd08422..73fc8484c0 100644 --- a/tests/components/nextion/common.yaml +++ b/tests/components/nextion/common.yaml @@ -1,5 +1,7 @@ esphome: on_boot: + - lambda: 'ESP_LOGD("display","is_connected(): %s", YESNO(id(main_lcd).is_connected()));' + # Binary sensor publish action tests - binary_sensor.nextion.publish: id: r0_sensor diff --git a/tests/components/opentherm/common.yaml b/tests/components/opentherm/common.yaml index 744580f18b..5edacc6f17 100644 --- a/tests/components/opentherm/common.yaml +++ b/tests/components/opentherm/common.yaml @@ -16,6 +16,19 @@ opentherm: summer_mode_active: true dhw_block: true sync_mode: true + controller_product_type: 63 + controller_product_version: 1 + opentherm_version_controller: 2.2 + controller_id: 1 + controller_configuration: 1 + before_send: + then: + - lambda: |- + ESP_LOGW("OT", ">> Sending message %d", x.id); + before_process_response: + then: + - lambda: |- + ESP_LOGW("OT", "<< Processing response %d", x.id); output: - platform: opentherm