1
0
mirror of https://github.com/esphome/esphome.git synced 2025-04-14 06:40:32 +01:00

Merge branch 'dev' into dev

This commit is contained in:
Evaldas Auryla 2024-12-18 09:50:47 +01:00 committed by GitHub
commit bc2ed2e105
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 844 additions and 366 deletions

View File

@ -46,7 +46,7 @@ jobs:
with: with:
python-version: "3.9" python-version: "3.9"
- name: Set up Docker Buildx - 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 - name: Set up QEMU
uses: docker/setup-qemu-action@v3.2.0 uses: docker/setup-qemu-action@v3.2.0

View File

@ -13,6 +13,7 @@ on:
- ".github/workflows/ci.yml" - ".github/workflows/ci.yml"
- "!.yamllint" - "!.yamllint"
- "!.github/dependabot.yml" - "!.github/dependabot.yml"
- "!docker/**"
merge_group: merge_group:
permissions: permissions:

View File

@ -90,7 +90,7 @@ jobs:
python-version: "3.9" python-version: "3.9"
- name: Set up Docker Buildx - 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 - name: Set up QEMU
if: matrix.platform != 'linux/amd64' if: matrix.platform != 'linux/amd64'
uses: docker/setup-qemu-action@v3.2.0 uses: docker/setup-qemu-action@v3.2.0
@ -184,7 +184,7 @@ jobs:
merge-multiple: true merge-multiple: true
- name: Set up Docker Buildx - 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 - name: Log in to docker hub
if: matrix.registry == 'dockerhub' if: matrix.registry == 'dockerhub'

View File

@ -1,11 +1,6 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import pins from esphome import pins
from esphome.const import CONF_ANALOG, CONF_INPUT, CONF_NUMBER import esphome.codegen as cg
from esphome.core import CORE
from esphome.components.esp32 import get_esp32_variant from esphome.components.esp32 import get_esp32_variant
from esphome.const import PLATFORM_ESP8266
from esphome.components.esp32.const import ( from esphome.components.esp32.const import (
VARIANT_ESP32, VARIANT_ESP32,
VARIANT_ESP32C2, VARIANT_ESP32C2,
@ -15,6 +10,9 @@ from esphome.components.esp32.const import (
VARIANT_ESP32S2, VARIANT_ESP32S2,
VARIANT_ESP32S3, 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"] CODEOWNERS = ["@esphome/core"]
@ -102,11 +100,11 @@ ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = {
6: adc1_channel_t.ADC1_CHANNEL_6, 6: adc1_channel_t.ADC1_CHANNEL_6,
}, },
VARIANT_ESP32H2: { VARIANT_ESP32H2: {
0: adc1_channel_t.ADC1_CHANNEL_0, 1: adc1_channel_t.ADC1_CHANNEL_0,
1: adc1_channel_t.ADC1_CHANNEL_1, 2: adc1_channel_t.ADC1_CHANNEL_1,
2: adc1_channel_t.ADC1_CHANNEL_2, 3: adc1_channel_t.ADC1_CHANNEL_2,
3: adc1_channel_t.ADC1_CHANNEL_3, 4: adc1_channel_t.ADC1_CHANNEL_3,
4: adc1_channel_t.ADC1_CHANNEL_4, 5: adc1_channel_t.ADC1_CHANNEL_4,
}, },
} }

View File

@ -30,109 +30,162 @@ static const char *const TAG = "debug";
std::string DebugComponent::get_reset_reason_() { std::string DebugComponent::get_reset_reason_() {
std::string reset_reason; std::string reset_reason;
switch (rtc_get_reset_reason(0)) { switch (esp_reset_reason()) {
case POWERON_RESET: case ESP_RST_POWERON:
reset_reason = "Power On Reset"; reset_reason = "Reset due to power-on event";
break; 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) #if defined(USE_ESP32_VARIANT_ESP32)
case SW_RESET: case SW_RESET:
#elif defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S2) || \ #elif defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S2) || \
defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32C6) defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32C6)
case RTC_SW_SYS_RESET: case RTC_SW_SYS_RESET:
#endif #endif
reset_reason = "Software Reset Digital Core"; reset_reason = "Software Reset Digital Core";
break; break;
#if defined(USE_ESP32_VARIANT_ESP32) #if defined(USE_ESP32_VARIANT_ESP32)
case OWDT_RESET: case OWDT_RESET:
reset_reason = "Watch Dog Reset Digital Core"; reset_reason = "Watch Dog Reset Digital Core";
break; break;
#endif #endif
case DEEPSLEEP_RESET: case DEEPSLEEP_RESET:
reset_reason = "Deep Sleep Reset Digital Core"; reset_reason = "Deep Sleep Reset Digital Core";
break; break;
#if defined(USE_ESP32_VARIANT_ESP32) #if defined(USE_ESP32_VARIANT_ESP32)
case SDIO_RESET: case SDIO_RESET:
reset_reason = "SLC Module Reset Digital Core"; reset_reason = "SLC Module Reset Digital Core";
break; break;
#endif #endif
case TG0WDT_SYS_RESET: case TG0WDT_SYS_RESET:
reset_reason = "Timer Group 0 Watch Dog Reset Digital Core"; reset_reason = "Timer Group 0 Watch Dog Reset Digital Core";
break; break;
case TG1WDT_SYS_RESET: case TG1WDT_SYS_RESET:
reset_reason = "Timer Group 1 Watch Dog Reset Digital Core"; reset_reason = "Timer Group 1 Watch Dog Reset Digital Core";
break; break;
case RTCWDT_SYS_RESET: case RTCWDT_SYS_RESET:
reset_reason = "RTC Watch Dog Reset Digital Core"; reset_reason = "RTC Watch Dog Reset Digital Core";
break; break;
#if !defined(USE_ESP32_VARIANT_ESP32C6) && !defined(USE_ESP32_VARIANT_ESP32H2) #if !defined(USE_ESP32_VARIANT_ESP32C6) && !defined(USE_ESP32_VARIANT_ESP32H2)
case INTRUSION_RESET: case INTRUSION_RESET:
reset_reason = "Intrusion Reset CPU"; reset_reason = "Intrusion Reset CPU";
break; break;
#endif #endif
#if defined(USE_ESP32_VARIANT_ESP32) #if defined(USE_ESP32_VARIANT_ESP32)
case TGWDT_CPU_RESET: case TGWDT_CPU_RESET:
reset_reason = "Timer Group Reset CPU"; reset_reason = "Timer Group Reset CPU";
break; break;
#elif defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S2) || \ #elif defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S2) || \
defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32C6) defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32C6)
case TG0WDT_CPU_RESET: case TG0WDT_CPU_RESET:
reset_reason = "Timer Group 0 Reset CPU"; reset_reason = "Timer Group 0 Reset CPU";
break; break;
#endif #endif
#if defined(USE_ESP32_VARIANT_ESP32) #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) || \ #elif defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S2) || \
defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32C6) defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32C6)
case RTC_SW_CPU_RESET: case RTC_SW_CPU_RESET:
#endif #endif
reset_reason = "Software Reset CPU"; reset_reason = "Software Reset CPU";
break; break;
case RTCWDT_CPU_RESET: case RTCWDT_CPU_RESET:
reset_reason = "RTC Watch Dog Reset CPU"; reset_reason = "RTC Watch Dog Reset CPU";
break; break;
#if defined(USE_ESP32_VARIANT_ESP32) #if defined(USE_ESP32_VARIANT_ESP32)
case EXT_CPU_RESET: case EXT_CPU_RESET:
reset_reason = "External CPU Reset"; reset_reason = "External CPU Reset";
break; break;
#endif #endif
case RTCWDT_BROWN_OUT_RESET: case RTCWDT_BROWN_OUT_RESET:
reset_reason = "Voltage Unstable Reset"; reset_reason = "Voltage Unstable Reset";
break; break;
case RTCWDT_RTC_RESET: case RTCWDT_RTC_RESET:
reset_reason = "RTC Watch Dog Reset Digital Core And RTC Module"; reset_reason = "RTC Watch Dog Reset Digital Core And RTC Module";
break; break;
#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || \ #if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || \
defined(USE_ESP32_VARIANT_ESP32C6) defined(USE_ESP32_VARIANT_ESP32C6)
case TG1WDT_CPU_RESET: case TG1WDT_CPU_RESET:
reset_reason = "Timer Group 1 Reset CPU"; reset_reason = "Timer Group 1 Reset CPU";
break; break;
case SUPER_WDT_RESET: case SUPER_WDT_RESET:
reset_reason = "Super Watchdog Reset Digital Core And RTC Module"; reset_reason = "Super Watchdog Reset Digital Core And RTC Module";
break; break;
case EFUSE_RESET: case EFUSE_RESET:
reset_reason = "eFuse Reset Digital Core"; reset_reason = "eFuse Reset Digital Core";
break; break;
#endif #endif
#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) #if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
case GLITCH_RTC_RESET: case GLITCH_RTC_RESET:
reset_reason = "Glitch Reset Digital Core And RTC Module"; reset_reason = "Glitch Reset Digital Core And RTC Module";
break; break;
#endif #endif
#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32C6) #if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32C6)
case USB_UART_CHIP_RESET: case USB_UART_CHIP_RESET:
reset_reason = "USB UART Reset Digital Core"; reset_reason = "USB UART Reset Digital Core";
break; break;
case USB_JTAG_CHIP_RESET: case USB_JTAG_CHIP_RESET:
reset_reason = "USB JTAG Reset Digital Core"; reset_reason = "USB JTAG Reset Digital Core";
break; break;
#endif #endif
#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S3) #if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S3)
case POWER_GLITCH_RESET: case POWER_GLITCH_RESET:
reset_reason = "Power Glitch Reset Digital Core And RTC Module"; reset_reason = "Power Glitch Reset Digital Core And RTC Module";
break; break;
#endif #endif
default: default:
reset_reason = "Unknown Reset Reason"; reset_reason = "Unknown Reset Reason";
}
break;
} }
ESP_LOGD(TAG, "Reset Reason: %s", reset_reason.c_str()); ESP_LOGD(TAG, "Reset Reason: %s", reset_reason.c_str());
return reset_reason; return reset_reason;
@ -294,4 +347,4 @@ void DebugComponent::update_platform_() {
} // namespace debug } // namespace debug
} // namespace esphome } // namespace esphome
#endif #endif // USE_ESP32

View File

@ -27,6 +27,9 @@ namespace esp32_ble {
static const char *const TAG = "esp32_ble"; static const char *const TAG = "esp32_ble";
static RAMAllocator<BLEEvent> EVENT_ALLOCATOR( // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
RAMAllocator<BLEEvent>::ALLOW_FAILURE | RAMAllocator<BLEEvent>::ALLOC_INTERNAL);
void ESP32BLE::setup() { void ESP32BLE::setup() {
global_ble = this; global_ble = this;
ESP_LOGCONFIG(TAG, "Setting up BLE..."); ESP_LOGCONFIG(TAG, "Setting up BLE...");
@ -322,7 +325,8 @@ void ESP32BLE::loop() {
default: default:
break; break;
} }
delete ble_event; // NOLINT(cppcoreguidelines-owning-memory) ble_event->~BLEEvent();
EVENT_ALLOCATOR.deallocate(ble_event, 1);
ble_event = this->ble_events_.pop(); ble_event = this->ble_events_.pop();
} }
if (this->advertising_ != nullptr) { 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) { 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); 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) { 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); 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, void ESP32BLE::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,
esp_ble_gatts_cb_param_t *param) { 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); 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, 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) { 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, void ESP32BLE::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) { 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); 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, 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) { esp_ble_gattc_cb_param_t *param) {

View File

@ -51,8 +51,11 @@ CONF_IGNORE_MISSING_GLYPHS = "ignore_missing_glyphs"
# Cache loaded freetype fonts # Cache loaded freetype fonts
class FontCache(dict): class FontCache(dict):
def __missing__(self, key): def __missing__(self, key):
res = self[key] = freetype.Face(key) try:
return res 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() FONT_CACHE = FontCache()

View File

@ -19,10 +19,12 @@ template<typename... Ts> class MideaActionBase : public Action<Ts...> {
template<typename... Ts> class FollowMeAction : public MideaActionBase<Ts...> { template<typename... Ts> class FollowMeAction : public MideaActionBase<Ts...> {
TEMPLATABLE_VALUE(float, temperature) TEMPLATABLE_VALUE(float, temperature)
TEMPLATABLE_VALUE(bool, use_fahrenheit)
TEMPLATABLE_VALUE(bool, beeper) TEMPLATABLE_VALUE(bool, beeper)
void play(Ts... x) override { 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...));
} }
}; };

View File

@ -1,6 +1,7 @@
#ifdef USE_ARDUINO #ifdef USE_ARDUINO
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/helpers.h"
#include "air_conditioner.h" #include "air_conditioner.h"
#include "ac_adapter.h" #include "ac_adapter.h"
#include <cmath> #include <cmath>
@ -121,7 +122,7 @@ void AirConditioner::dump_config() {
/* ACTIONS */ /* 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 #ifdef USE_REMOTE_TRANSMITTER
// Check if temperature is finite (not NaN or infinite) // Check if temperature is finite (not NaN or infinite)
if (!std::isfinite(temperature)) { 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 // Round and convert temperature to long, then clamp and convert it to uint8_t
uint8_t temp_uint8 = uint8_t temp_uint8 =
static_cast<uint8_t>(std::max(0L, std::min(static_cast<long>(UINT8_MAX), std::lroundf(temperature)))); static_cast<uint8_t>(esphome::clamp<long>(std::lroundf(temperature), 0L, static_cast<long>(UINT8_MAX)));
ESP_LOGD(Constants::TAG, "Follow me action called with temperature: %f °C, rounded to: %u °C", temperature, char temp_symbol = use_fahrenheit ? 'F' : 'C';
temp_uint8); 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 // Create and transmit the data
IrFollowMeData data(temp_uint8, beeper); IrFollowMeData data(temp_uint8, use_fahrenheit, beeper);
this->transmitter_.transmit(data); this->transmitter_.transmit(data);
#else #else
ESP_LOGW(Constants::TAG, "Action needs remote_transmitter component"); ESP_LOGW(Constants::TAG, "Action needs remote_transmitter component");

View File

@ -32,7 +32,7 @@ class AirConditioner : public ApplianceBase<dudanov::midea::ac::AirConditioner>,
/* ### ACTIONS ### */ /* ### 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_display_toggle();
void do_swing_step(); void do_swing_step();
void do_beeper_on() { this->set_beeper_feedback(true); } void do_beeper_on() { this->set_beeper_feedback(true); }

View File

@ -18,6 +18,7 @@ from esphome.const import (
CONF_SUPPORTED_SWING_MODES, CONF_SUPPORTED_SWING_MODES,
CONF_TIMEOUT, CONF_TIMEOUT,
CONF_TEMPERATURE, CONF_TEMPERATURE,
CONF_USE_FAHRENHEIT,
DEVICE_CLASS_POWER, DEVICE_CLASS_POWER,
DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_HUMIDITY,
@ -172,11 +173,10 @@ MIDEA_ACTION_BASE_SCHEMA = cv.Schema(
) )
# FollowMe action # FollowMe action
MIDEA_FOLLOW_ME_MIN = 0
MIDEA_FOLLOW_ME_MAX = 37
MIDEA_FOLLOW_ME_SCHEMA = cv.Schema( MIDEA_FOLLOW_ME_SCHEMA = cv.Schema(
{ {
cv.Required(CONF_TEMPERATURE): cv.templatable(cv.temperature), 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), 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): async def follow_me_to_code(var, config, args):
template_ = await cg.templatable(config[CONF_BEEPER], args, cg.bool_) template_ = await cg.templatable(config[CONF_BEEPER], args, cg.bool_)
cg.add(var.set_beeper(template_)) 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_) template_ = await cg.templatable(config[CONF_TEMPERATURE], args, cg.float_)
cg.add(var.set_temperature(template_)) cg.add(var.set_temperature(template_))

View File

@ -16,22 +16,53 @@ class IrFollowMeData : public IrData {
IrFollowMeData() : IrData({MIDEA_TYPE_FOLLOW_ME, 0x82, 0x48, 0x7F, 0x1F}) {} IrFollowMeData() : IrData({MIDEA_TYPE_FOLLOW_ME, 0x82, 0x48, 0x7F, 0x1F}) {}
// Copy from Base // Copy from Base
IrFollowMeData(const IrData &data) : IrData(data) {} 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() { 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); this->set_beeper(beeper);
} }
/* TEMPERATURE */ /* TEMPERATURE */
uint8_t temp() const { return this->get_value_(4) - 1; } uint8_t temp() const {
void set_temp(uint8_t val) { this->set_value_(4, std::min(MAX_TEMP, val) + 1); } 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<uint8_t>(val, MIN_TEMP_F, MAX_TEMP_F) - 31;
} else {
val = esphome::clamp<uint8_t>(val, MIN_TEMP_C, MAX_TEMP_C) + 1;
}
this->set_value_(4, val);
}
/* BEEPER */ /* BEEPER */
bool beeper() const { return this->get_value_(3, 128); } bool beeper() const { return this->get_value_(3, 128); }
void set_beeper(bool val) { this->set_mask_(3, val, 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: 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 { class IrSpecialData : public IrData {

View File

@ -119,17 +119,17 @@ async def to_code(config):
cg.add_library("ESP8266HTTPClient", None) cg.add_library("ESP8266HTTPClient", None)
if CONF_TOUCH_SLEEP_TIMEOUT in config: 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: 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: 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])) cg.add(var.set_skip_connection_handshake(config[CONF_SKIP_CONNECTION_HANDSHAKE]))

View File

@ -40,7 +40,7 @@ bool Nextion::send_command_(const std::string &command) {
} }
bool Nextion::check_connect_() { bool Nextion::check_connect_() {
if (this->get_is_connected_()) if (this->is_connected_)
return true; return true;
// Check if the handshake should be skipped for the Nextion connection // 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->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) { if (this->wake_up_page_ != -1) {
this->set_wake_up_page(this->wake_up_page_); this->set_wake_up_page(this->wake_up_page_);
} }

View File

@ -856,76 +856,6 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
*/ */
void set_backlight_brightness(float brightness); 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. * Sets whether the Nextion display should skip the connection handshake process.
* @param skip_handshake True or false. When skip_connection_handshake is true, * @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 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; * Set the touch sleep timeout of the display.
} * @param timeout Timeout in seconds.
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; } * Example:
void set_auto_wake_on_touch_internal(bool auto_wake_on_touch) { this->auto_wake_on_touch_ = auto_wake_on_touch; } * ```cpp
void set_exit_reparse_on_start_internal(bool exit_reparse_on_start) { * it.set_touch_sleep_timeout(30);
this->exit_reparse_on_start_ = exit_reparse_on_start; * ```
} *
* 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. * @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; 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: protected:
std::deque<NextionQueue *> nextion_queue_; std::deque<NextionQueue *> nextion_queue_;
std::deque<NextionQueue *> waveform_queue_; std::deque<NextionQueue *> waveform_queue_;
@ -1315,8 +1324,6 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
#endif // USE_NEXTION_TFT_UPLOAD #endif // USE_NEXTION_TFT_UPLOAD
bool get_is_connected_() { return this->is_connected_; }
bool check_connect_(); bool check_connect_();
std::vector<NextionComponentBase *> touch_; std::vector<NextionComponentBase *> touch_;

View File

@ -10,19 +10,19 @@ static const char *const TAG = "nextion";
// Sleep safe commands // Sleep safe commands
void Nextion::soft_reset() { this->send_command_("rest"); } void Nextion::soft_reset() { this->send_command_("rest"); }
void Nextion::set_wake_up_page(uint8_t page_id) { void Nextion::set_wake_up_page(uint8_t wake_up_page) {
this->add_no_result_to_queue_with_set_internal_("wake_up_page", "wup", page_id, true); 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(uint32_t touch_sleep_timeout) {
if (touch_sleep_timeout < 3) {
void Nextion::set_touch_sleep_timeout(uint16_t timeout) {
if (timeout < 3) {
ESP_LOGD(TAG, "Sleep timeout out of bounds, range 3-65535"); ESP_LOGD(TAG, "Sleep timeout out of bounds, range 3-65535");
return; 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) { void Nextion::sleep(bool sleep) {
@ -54,7 +54,6 @@ bool Nextion::set_protocol_reparse_mode(bool active_mode) {
this->ignore_is_setup_ = false; this->ignore_is_setup_ = false;
return all_commands_sent; return all_commands_sent;
} }
void Nextion::set_exit_reparse_on_start(bool exit_reparse) { this->exit_reparse_on_start_ = exit_reparse; }
// Set Colors - Background // Set Colors - Background
void Nextion::set_component_background_color(const char *component, uint16_t color) { 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<int>(brightness * 100)); this->add_no_result_to_queue_with_printf_("backlight_brightness", "dim=%d", static_cast<int>(brightness * 100));
} }
void Nextion::set_auto_wake_on_touch(bool auto_wake) { void Nextion::set_auto_wake_on_touch(bool auto_wake_on_touch) {
this->add_no_result_to_queue_with_set("auto_wake_on_touch", "thup", auto_wake ? 1 : 0); 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 // General Component

View File

@ -1,10 +1,12 @@
from typing import Any from typing import Any
import logging
from esphome import automation
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome import pins from esphome import pins
from esphome.components import sensor 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 from . import const, schema, validate, generate
CODEOWNERS = ["@olegtarasov"] CODEOWNERS = ["@olegtarasov"]
@ -20,7 +22,21 @@ CONF_CH2_ACTIVE = "ch2_active"
CONF_SUMMER_MODE_ACTIVE = "summer_mode_active" CONF_SUMMER_MODE_ACTIVE = "summer_mode_active"
CONF_DHW_BLOCK = "dhw_block" CONF_DHW_BLOCK = "dhw_block"
CONF_SYNC_MODE = "sync_mode" 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( CONFIG_SCHEMA = cv.All(
cv.Schema( cv.Schema(
@ -36,7 +52,19 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_SUMMER_MODE_ACTIVE, False): cv.boolean, cv.Optional(CONF_SUMMER_MODE_ACTIVE, False): cv.boolean,
cv.Optional(CONF_DHW_BLOCK, False): cv.boolean, cv.Optional(CONF_DHW_BLOCK, False): cv.boolean,
cv.Optional(CONF_SYNC_MODE, 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( .extend(
@ -44,6 +72,11 @@ CONFIG_SCHEMA = cv.All(
schema.INPUTS, (lambda _: cv.use_id(sensor.Sensor)) schema.INPUTS, (lambda _: cv.use_id(sensor.Sensor))
) )
) )
.extend(
validate.create_entities_schema(
schema.SETTINGS, (lambda s: s.validation_schema)
)
)
.extend(cv.COMPONENT_SCHEMA), .extend(cv.COMPONENT_SCHEMA),
cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266]), 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]) out_pin = await cg.gpio_pin_expression(config[CONF_OUT_PIN])
cg.add(var.set_out_pin(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 = [] input_sensors = []
settings = []
for key, value in config.items(): for key, value in config.items():
if key in non_sensors: if key in non_sensors:
continue continue
if key in schema.INPUTS: if key in schema.INPUTS:
input_sensor = await cg.get_variable(value) input_sensor = await cg.get_variable(value)
cg.add( cg.add(getattr(var, f"set_{key}_{const.INPUT_SENSOR}")(input_sensor))
getattr(var, f"set_{key}_{const.INPUT_SENSOR.lower()}")(input_sensor)
)
input_sensors.append(key) 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: 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)) cg.add(getattr(var, f"set_{key}")(value))
if len(input_sensors) > 0: 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.define_readers(const.INPUT_SENSOR, input_sensors)
generate.add_messages(var, input_sensors, schema.INPUTS) 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
)

View File

@ -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<OpenthermData &> {
public:
BeforeSendTrigger(OpenthermHub *hub) {
hub->add_on_before_send_callback([this](OpenthermData &x) { this->trigger(x); });
}
};
class BeforeProcessResponseTrigger : public Trigger<OpenthermData &> {
public:
BeforeProcessResponseTrigger(OpenthermHub *hub) {
hub->add_on_before_process_response_callback([this](OpenthermData &x) { this->trigger(x); });
}
};
} // namespace opentherm
} // namespace esphome

View File

@ -9,3 +9,4 @@ SWITCH = "switch"
NUMBER = "number" NUMBER = "number"
OUTPUT = "output" OUTPUT = "output"
INPUT_SENSOR = "input_sensor" INPUT_SENSOR = "input_sensor"
SETTING = "setting"

View File

@ -1,13 +1,14 @@
from collections.abc import Awaitable from collections.abc import Awaitable
from typing import Any, Callable from typing import Any, Callable, Optional
import esphome.codegen as cg import esphome.codegen as cg
from esphome.const import CONF_ID from esphome.const import CONF_ID
from . import const from . import const
from .schema import TSchema from .schema import TSchema, SettingSchema
opentherm_ns = cg.esphome_ns.namespace("opentherm") opentherm_ns = cg.esphome_ns.namespace("opentherm")
OpenthermHub = opentherm_ns.class_("OpenthermHub", cg.Component) OpenthermHub = opentherm_ns.class_("OpenthermHub", cg.Component)
OpenthermData = opentherm_ns.class_("OpenthermData")
def define_has_component(component_type: str, keys: list[str]) -> None: 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}") 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( def define_message_handler(
component_type: str, keys: list[str], schemas: dict[str, TSchema] component_type: str, keys: list[str], schemas: dict[str, TSchema]
) -> None: ) -> 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]): def define_setting_readers(component_type: str, keys: list[str]) -> None:
messages: set[tuple[str, bool]] = set()
for key in keys: for key in keys:
messages.add((schemas[key].message, schemas[key].keep_updated)) cg.add_define(
for msg, keep_updated in messages: 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}") msg_expr = cg.RawExpression(f"esphome::opentherm::MessageId::{msg}")
if keep_updated: if keep_updated:
cg.add(hub.add_repeating_message(msg_expr)) cg.add(hub.add_repeating_message(msg_expr))
else: 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: def add_property_set(var: cg.MockObj, config_key: str, config: dict[str, Any]) -> None:

View File

@ -63,7 +63,7 @@ void write_f88(const float value, OpenthermData &data) { data.f88(value); }
OpenthermData OpenthermHub::build_request_(MessageId request_id) const { OpenthermData OpenthermHub::build_request_(MessageId request_id) const {
OpenthermData data; OpenthermData data;
data.type = 0; data.type = 0;
data.id = 0; data.id = request_id;
data.valueHB = 0; data.valueHB = 0;
data.valueLB = 0; data.valueLB = 0;
@ -82,28 +82,13 @@ OpenthermData OpenthermHub::build_request_(MessageId request_id) const {
// NOLINTEND // NOLINTEND
data.type = MessageType::READ_DATA; 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) | data.valueHB = ch_enabled | (dhw_enabled << 1) | (cooling_enabled << 2) | (otc_enabled << 3) | (ch2_enabled << 4) |
(summer_mode_is_active << 5) | (dhw_blocked << 6); (summer_mode_is_active << 5) | (dhw_blocked << 6);
return data; return data;
} }
// Another special case is OpenTherm version number which is configured at hub level as a constant // Next, we start with write requests from switches and other inputs,
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,
// because we would want to write that data if it is available, rather than // 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 // request a read for that type (in the case that both read and write are
// supported). // supported).
@ -116,14 +101,23 @@ OpenthermData OpenthermHub::build_request_(MessageId request_id) const {
OPENTHERM_MESSAGE_WRITE_POSTSCRIPT, ) OPENTHERM_MESSAGE_WRITE_POSTSCRIPT, )
OPENTHERM_INPUT_SENSOR_MESSAGE_HANDLERS(OPENTHERM_MESSAGE_WRITE_MESSAGE, OPENTHERM_MESSAGE_WRITE_ENTITY, , OPENTHERM_INPUT_SENSOR_MESSAGE_HANDLERS(OPENTHERM_MESSAGE_WRITE_MESSAGE, OPENTHERM_MESSAGE_WRITE_ENTITY, ,
OPENTHERM_MESSAGE_WRITE_POSTSCRIPT, ) 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. // 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) { switch (request_id) {
OPENTHERM_BINARY_SENSOR_MESSAGE_HANDLERS(OPENTHERM_MESSAGE_READ_MESSAGE, OPENTHERM_IGNORE, , , ) 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. // 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 // 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 // communicate at least once every second. Sending the status request is
// good practice anyway. // good practice anyway.
this->add_repeating_message(MessageId::STATUS); this->add_repeating_message(MessageId::STATUS);
this->write_initial_messages_(this->messages_);
// Also ensure that we start communication with the STATUS message this->message_iterator_ = this->messages_.begin();
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();
} }
void OpenthermHub::on_shutdown() { this->opentherm_->stop(); } 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<MessageId> &target) { // NOLINT
std::vector<std::pair<MessageId, uint8_t>> sorted;
std::copy_if(this->configured_messages_.begin(), this->configured_messages_.end(), std::back_inserter(sorted),
[](const std::pair<MessageId, uint8_t> &pair) { return pair.second < REPEATING_MESSAGE_ORDER; });
std::sort(sorted.begin(), sorted.end(),
[](const std::pair<MessageId, uint8_t> &a, const std::pair<MessageId, uint8_t> &b) {
return a.second < b.second;
});
target.clear();
std::transform(sorted.begin(), sorted.end(), std::back_inserter(target),
[](const std::pair<MessageId, uint8_t> &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<MessageId> &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() { void OpenthermHub::loop() {
if (this->sync_mode_) { if (this->sync_mode_) {
this->sync_loop_(); this->sync_loop_();
@ -184,29 +196,18 @@ void OpenthermHub::loop() {
auto cur_time = millis(); auto cur_time = millis();
auto const cur_mode = this->opentherm_->get_mode(); auto const cur_mode = this->opentherm_->get_mode();
if (this->handle_error_(cur_mode)) {
return;
}
switch (cur_mode) { switch (cur_mode) {
case OperationMode::WRITE: case OperationMode::WRITE:
case OperationMode::READ: case OperationMode::READ:
case OperationMode::LISTEN: 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; break;
case OperationMode::IDLE: case OperationMode::IDLE:
this->check_timings_(cur_time);
if (this->should_skip_loop_(cur_time)) { if (this->should_skip_loop_(cur_time)) {
break; break;
} }
@ -219,6 +220,28 @@ void OpenthermHub::loop() {
case OperationMode::RECEIVED: case OperationMode::RECEIVED:
this->read_response_(); this->read_response_();
break; 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_(); 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(); })) { if (!this->spin_wait_(1150, [&] { return this->opentherm_->is_active(); })) {
ESP_LOGE(TAG, "Hub timeout triggered during send"); ESP_LOGE(TAG, "Hub timeout triggered during send");
this->stop_opentherm_(); this->stop_opentherm_();
return; return;
} }
if (this->opentherm_->is_error()) { // Check for errors and ensure we are in the right state (message sent successfully)
this->handle_protocol_write_error_(); if (this->handle_error_(this->opentherm_->get_mode())) {
this->stop_opentherm_();
return; return;
} else if (!this->opentherm_->is_sent()) { } else if (!this->opentherm_->is_sent()) {
ESP_LOGW(TAG, "Unexpected state after sending request: %s", ESP_LOGW(TAG, "Unexpected state after sending request: %s",
@ -257,19 +284,20 @@ void OpenthermHub::sync_loop_() {
// Listen for the response // Listen for the response
this->opentherm_->listen(); 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(); })) { if (!this->spin_wait_(1150, [&] { return this->opentherm_->is_active(); })) {
ESP_LOGE(TAG, "Hub timeout triggered during receive"); ESP_LOGE(TAG, "Hub timeout triggered during receive");
this->stop_opentherm_(); this->stop_opentherm_();
return; return;
} }
if (this->opentherm_->is_timeout()) { // Check for errors and ensure we are in the right state (message received successfully)
this->handle_timeout_error_(); if (this->handle_error_(this->opentherm_->get_mode())) {
this->stop_opentherm_();
return;
} else if (this->opentherm_->is_protocol_error()) {
this->handle_protocol_read_error_();
this->stop_opentherm_();
return; return;
} else if (!this->opentherm_->has_message()) { } else if (!this->opentherm_->has_message()) {
ESP_LOGW(TAG, "Unexpected state after receiving response: %s", ESP_LOGW(TAG, "Unexpected state after receiving response: %s",
@ -281,17 +309,13 @@ void OpenthermHub::sync_loop_() {
this->read_response_(); 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) { if (this->last_conversation_start_ > 0 && (cur_time - this->last_conversation_start_) > 1150) {
ESP_LOGW(TAG, ESP_LOGW(TAG,
"%d ms elapsed since the start of the last convo, but 1150 ms are allowed at maximum. Look at other " "%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.", "components that might slow the loop down.",
(int) (cur_time - this->last_conversation_start_)); (int) (cur_time - this->last_conversation_start_));
this->stop_opentherm_();
return false;
} }
return true;
} }
bool OpenthermHub::should_skip_loop_(uint32_t cur_time) const { 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_() { void OpenthermHub::start_conversation_() {
if (this->sending_initial_ && this->current_message_iterator_ == this->initial_messages_.end()) { if (this->message_iterator_ == this->messages_.end()) {
this->sending_initial_ = false; if (this->sending_initial_) {
this->current_message_iterator_ = this->repeating_messages_.begin(); this->sending_initial_ = false;
} else if (this->current_message_iterator_ == this->repeating_messages_.end()) { this->write_repeating_messages_(this->messages_);
this->current_message_iterator_ = this->repeating_messages_.begin(); }
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, ESP_LOGD(TAG, "Sending request with id %d (%s)", request.id,
this->opentherm_->message_id_to_str((MessageId) request.id)); this->opentherm_->message_id_to_str((MessageId) request.id));
@ -331,37 +358,48 @@ void OpenthermHub::read_response_() {
this->stop_opentherm_(); this->stop_opentherm_();
this->before_process_response_callback_.call(response);
this->process_response(response); this->process_response(response);
this->current_message_iterator_++; this->message_iterator_++;
} }
void OpenthermHub::stop_opentherm_() { void OpenthermHub::stop_opentherm_() {
this->opentherm_->stop(); this->opentherm_->stop();
this->last_conversation_end_ = millis(); this->last_conversation_end_ = millis();
} }
void OpenthermHub::handle_protocol_write_error_() {
ESP_LOGW(TAG, "Error while sending request: %s", void OpenthermHub::handle_protocol_error_() {
this->opentherm_->operation_mode_to_str(this->opentherm_->get_mode()));
this->opentherm_->debug_data(this->last_request_);
}
void OpenthermHub::handle_protocol_read_error_() {
OpenThermError error; OpenThermError error;
this->opentherm_->get_protocol_error(error); this->opentherm_->get_protocol_error(error);
ESP_LOGW(TAG, "Protocol error occured while receiving response: %s", 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); this->opentherm_->debug_error(error);
}
void OpenthermHub::handle_timeout_error_() {
ESP_LOGW(TAG, "Receive response timed out at a protocol level");
this->stop_opentherm_(); 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() { void OpenthermHub::dump_config() {
std::vector<MessageId> initial_messages;
std::vector<MessageId> repeating_messages;
this->write_initial_messages_(initial_messages);
this->write_repeating_messages_(repeating_messages);
ESP_LOGCONFIG(TAG, "OpenTherm:"); ESP_LOGCONFIG(TAG, "OpenTherm:");
LOG_PIN(" In: ", this->in_pin_); LOG_PIN(" In: ", this->in_pin_);
LOG_PIN(" Out: ", this->out_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, " Sensors: %s", SHOW(OPENTHERM_SENSOR_LIST(ID, )));
ESP_LOGCONFIG(TAG, " Binary sensors: %s", SHOW(OPENTHERM_BINARY_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, ))); 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, " Outputs: %s", SHOW(OPENTHERM_OUTPUT_LIST(ID, )));
ESP_LOGCONFIG(TAG, " Numbers: %s", SHOW(OPENTHERM_NUMBER_LIST(ID, ))); ESP_LOGCONFIG(TAG, " Numbers: %s", SHOW(OPENTHERM_NUMBER_LIST(ID, )));
ESP_LOGCONFIG(TAG, " Initial requests:"); ESP_LOGCONFIG(TAG, " Initial requests:");
for (auto type : this->initial_messages_) { for (auto type : initial_messages) {
ESP_LOGCONFIG(TAG, " - %d (%s)", type, this->opentherm_->message_id_to_str((type))); ESP_LOGCONFIG(TAG, " - %d (%s)", type, this->opentherm_->message_id_to_str(type));
} }
ESP_LOGCONFIG(TAG, " Repeating requests:"); ESP_LOGCONFIG(TAG, " Repeating requests:");
for (auto type : this->repeating_messages_) { for (auto type : repeating_messages) {
ESP_LOGCONFIG(TAG, " - %d (%s)", type, this->opentherm_->message_id_to_str((type))); ESP_LOGCONFIG(TAG, " - %d (%s)", type, this->opentherm_->message_id_to_str(type));
} }
} }

View File

@ -38,6 +38,9 @@
namespace esphome { namespace esphome {
namespace opentherm { namespace opentherm {
static const uint8_t REPEATING_MESSAGE_ORDER = 255;
static const uint8_t INITIAL_UNORDERED_MESSAGE_ORDER = 254;
// OpenTherm component for ESPHome // OpenTherm component for ESPHome
class OpenthermHub : public Component { class OpenthermHub : public Component {
protected: protected:
@ -58,15 +61,12 @@ class OpenthermHub : public Component {
OPENTHERM_INPUT_SENSOR_LIST(OPENTHERM_DECLARE_INPUT_SENSOR, ) OPENTHERM_INPUT_SENSOR_LIST(OPENTHERM_DECLARE_INPUT_SENSOR, )
// The set of initial messages to send on starting communication with the boiler OPENTHERM_SETTING_LIST(OPENTHERM_DECLARE_SETTING, )
std::vector<MessageId> initial_messages_;
// and the repeating messages which are sent repeatedly to update various sensors
// and boiler parameters (like the setpoint).
std::vector<MessageId> repeating_messages_;
// Indicates if we are still working on the initial requests or not
bool sending_initial_ = true; bool sending_initial_ = true;
// Index for the current request in one of the _requests sets. std::unordered_map<MessageId, uint8_t> configured_messages_;
std::vector<MessageId>::const_iterator current_message_iterator_; std::vector<MessageId> messages_;
std::vector<MessageId>::const_iterator message_iterator_;
uint32_t last_conversation_start_ = 0; uint32_t last_conversation_start_ = 0;
uint32_t last_conversation_end_ = 0; uint32_t last_conversation_end_ = 0;
@ -78,20 +78,25 @@ class OpenthermHub : public Component {
// Very likely to happen while using Dallas temperature sensors. // Very likely to happen while using Dallas temperature sensors.
bool sync_mode_ = false; bool sync_mode_ = false;
float opentherm_version_ = 0.0f; CallbackManager<void(OpenthermData &)> before_send_callback_;
CallbackManager<void(OpenthermData &)> before_process_response_callback_;
// Create OpenTherm messages based on the message id // Create OpenTherm messages based on the message id
OpenthermData build_request_(MessageId request_id) const; OpenthermData build_request_(MessageId request_id) const;
void handle_protocol_write_error_(); bool handle_error_(OperationMode mode);
void handle_protocol_read_error_(); void handle_protocol_error_();
void handle_timeout_error_(); void handle_timeout_error_();
void handle_timer_error_();
void stop_opentherm_(); void stop_opentherm_();
void start_conversation_(); void start_conversation_();
void read_response_(); 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; bool should_skip_loop_(uint32_t cur_time) const;
void sync_loop_(); void sync_loop_();
void write_initial_messages_(std::vector<MessageId> &target);
void write_repeating_messages_(std::vector<MessageId> &target);
template<typename F> bool spin_wait_(uint32_t timeout, F func) { template<typename F> bool spin_wait_(uint32_t timeout, F func) {
auto start_time = millis(); auto start_time = millis();
while (func()) { while (func()) {
@ -127,13 +132,18 @@ class OpenthermHub : public Component {
OPENTHERM_INPUT_SENSOR_LIST(OPENTHERM_SET_INPUT_SENSOR, ) OPENTHERM_INPUT_SENSOR_LIST(OPENTHERM_SET_INPUT_SENSOR, )
OPENTHERM_SETTING_LIST(OPENTHERM_SET_SETTING, )
// Add a request to the vector of initial requests // 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 // 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, // 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 // so with all sensors enabled, it may take about half a minute before a change in setpoint
// will be processed. // 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, // 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. // 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_summer_mode_active(bool value) { this->summer_mode_active = value; }
void set_dhw_block(bool value) { this->dhw_block = 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_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<void(OpenthermData &)> &&callback) {
this->before_send_callback_.add(std::move(callback));
}
void add_on_before_process_response_callback(std::function<void(OpenthermData &)> &&callback) {
this->before_process_response_callback_.add(std::move(callback));
}
float get_setup_priority() const override { return setup_priority::HARDWARE; } float get_setup_priority() const override { return setup_priority::HARDWARE; }

View File

@ -52,7 +52,9 @@ bool OpenTherm::initialize() {
OpenTherm::instance = this; OpenTherm::instance = this;
#endif #endif
this->in_pin_->pin_mode(gpio::FLAG_INPUT); this->in_pin_->pin_mode(gpio::FLAG_INPUT);
this->in_pin_->setup();
this->out_pin_->pin_mode(gpio::FLAG_OUTPUT); this->out_pin_->pin_mode(gpio::FLAG_OUTPUT);
this->out_pin_->setup();
this->out_pin_->digital_write(true); this->out_pin_->digital_write(true);
#if defined(ESP32) || defined(USE_ESP_IDF) #if defined(ESP32) || defined(USE_ESP_IDF)
@ -182,7 +184,7 @@ bool IRAM_ATTR OpenTherm::timer_isr(OpenTherm *arg) {
} }
arg->capture_ = 1; // reset counter arg->capture_ = 1; // reset counter
} else if (arg->capture_ > 0xFF) { } 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->mode_ = OperationMode::ERROR_PROTOCOL;
arg->error_type_ = ProtocolErrorType::NO_CHANGE_TOO_LONG; arg->error_type_ = ProtocolErrorType::NO_CHANGE_TOO_LONG;
arg->stop_timer_(); arg->stop_timer_();
@ -312,21 +314,31 @@ bool OpenTherm::init_esp32_timer_() {
} }
void IRAM_ATTR OpenTherm::start_esp32_timer_(uint64_t alarm_value) { 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); this->timer_error_ = timer_set_alarm_value(this->timer_group_, this->timer_idx_, alarm_value);
if (result != ESP_OK) { if (this->timer_error_ != ESP_OK) {
const auto *error = esp_err_to_name(result); this->timer_error_type_ = TimerErrorType::SET_ALARM_VALUE_ERROR;
ESP_LOGE(TAG, "Failed to set alarm value. Error: %s", 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; return;
} }
result = timer_start(this->timer_group_, this->timer_idx_); ESP_LOGE(TAG, "Error occured while manipulating timer (%s): %s", this->timer_error_to_str(this->timer_error_type_),
if (result != ESP_OK) { esp_err_to_name(this->timer_error_));
const auto *error = esp_err_to_name(result);
ESP_LOGE(TAG, "Failed to start the timer. Error: %s", error); this->timer_error_ = ESP_OK;
return; this->timer_error_type_ = NO_TIMER_ERROR;
}
} }
// 5 kHz timer_ // 5 kHz timer_
@ -343,21 +355,18 @@ void IRAM_ATTR OpenTherm::start_write_timer_() {
void IRAM_ATTR OpenTherm::stop_timer_() { void IRAM_ATTR OpenTherm::stop_timer_() {
InterruptLock const lock; 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; this->timer_error_ = timer_pause(this->timer_group_, this->timer_idx_);
if (this->timer_error_ != ESP_OK) {
result = timer_pause(this->timer_group_, this->timer_idx_); this->timer_error_type_ = TimerErrorType::TIMER_PAUSE_ERROR;
if (result != ESP_OK) {
const auto *error = esp_err_to_name(result);
ESP_LOGE(TAG, "Failed to pause the timer. Error: %s", error);
return; return;
} }
this->timer_error_ = timer_set_counter_value(this->timer_group_, this->timer_idx_, 0);
result = timer_set_counter_value(this->timer_group_, this->timer_idx_, 0); if (this->timer_error_ != ESP_OK) {
if (result != ESP_OK) { this->timer_error_type_ = TimerErrorType::SET_COUNTER_VALUE_ERROR;
const auto *error = esp_err_to_name(result);
ESP_LOGE(TAG, "Failed to set timer counter to 0 after pausing. Error: %s", error);
return;
} }
} }
@ -386,6 +395,9 @@ void IRAM_ATTR OpenTherm::stop_timer_() {
timer1_detachInterrupt(); timer1_detachInterrupt();
} }
// There is nothing to report on ESP8266
void OpenTherm::report_and_reset_timer_error() {}
#endif // END ESP8266 #endif // END ESP8266
// https://stackoverflow.com/questions/21617970/how-to-check-if-value-has-even-parity-of-bits-or-odd // 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(SENT)
TO_STRING_MEMBER(ERROR_PROTOCOL) TO_STRING_MEMBER(ERROR_PROTOCOL)
TO_STRING_MEMBER(ERROR_TIMEOUT) TO_STRING_MEMBER(ERROR_TIMEOUT)
TO_STRING_MEMBER(ERROR_TIMER)
default: default:
return "<INVALID>"; return "<INVALID>";
} }
} }
const char *OpenTherm::protocol_error_to_to_str(ProtocolErrorType error_type) { const char *OpenTherm::protocol_error_to_str(ProtocolErrorType error_type) {
switch (error_type) { switch (error_type) {
TO_STRING_MEMBER(NO_ERROR) TO_STRING_MEMBER(NO_ERROR)
TO_STRING_MEMBER(NO_TRANSITION) TO_STRING_MEMBER(NO_TRANSITION)
@ -427,6 +440,17 @@ const char *OpenTherm::protocol_error_to_to_str(ProtocolErrorType error_type) {
return "<INVALID>"; return "<INVALID>";
} }
} }
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 "<INVALID>";
}
}
const char *OpenTherm::message_type_to_str(MessageType message_type) { const char *OpenTherm::message_type_to_str(MessageType message_type) {
switch (message_type) { switch (message_type) {
TO_STRING_MEMBER(READ_DATA) TO_STRING_MEMBER(READ_DATA)

View File

@ -36,11 +36,12 @@ enum OperationMode {
READ = 2, // reading 32-bit data frame READ = 2, // reading 32-bit data frame
RECEIVED = 3, // data frame received with valid start and stop bit 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 SENT = 5, // all data written to output
ERROR_PROTOCOL = 8, // manchester protocol data transfer error ERROR_PROTOCOL = 8, // protocol error, can happed only during READ
ERROR_TIMEOUT = 9 // read timeout ERROR_TIMEOUT = 9, // timeout while waiting for response from device, only during LISTEN
ERROR_TIMER = 10 // error operating the ESP32 timer
}; };
enum ProtocolErrorType { enum ProtocolErrorType {
@ -51,6 +52,14 @@ enum ProtocolErrorType {
NO_CHANGE_TOO_LONG = 4, // No level change for too much timer ticks 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 { enum MessageType {
READ_DATA = 0, READ_DATA = 0,
READ_ACK = 4, READ_ACK = 4,
@ -299,7 +308,9 @@ class OpenTherm {
* *
* @return true if last listen() or send() operation ends up with an error. * @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 * 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; } 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; } bool is_active() { return mode_ == LISTEN || mode_ == READ || mode_ == WRITE; }
OperationMode get_mode() { return mode_; } OperationMode get_mode() { return mode_; }
void debug_data(OpenthermData &data); void debug_data(OpenthermData &data);
void debug_error(OpenThermError &error) const; 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 *message_type_to_str(MessageType message_type);
const char *operation_mode_to_str(OperationMode mode); const char *operation_mode_to_str(OperationMode mode);
const char *message_id_to_str(MessageId id); const char *message_id_to_str(MessageId id);
@ -349,10 +368,12 @@ class OpenTherm {
uint32_t data_; uint32_t data_;
uint8_t bit_pos_; uint8_t bit_pos_;
int32_t timeout_counter_; // <0 no timeout int32_t timeout_counter_; // <0 no timeout
int32_t device_timeout_; int32_t device_timeout_;
#if defined(ESP32) || defined(USE_ESP_IDF) #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_(); bool init_esp32_timer_();
void start_esp32_timer_(uint64_t alarm_value); void start_esp32_timer_(uint64_t alarm_value);
#endif #endif

View File

@ -28,6 +28,9 @@ namespace opentherm {
#ifndef OPENTHERM_INPUT_SENSOR_LIST #ifndef OPENTHERM_INPUT_SENSOR_LIST
#define OPENTHERM_INPUT_SENSOR_LIST(F, sep) #define OPENTHERM_INPUT_SENSOR_LIST(F, sep)
#endif #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 // Use macros to create fields for every entity specified in the ESPHome configuration
#define OPENTHERM_DECLARE_SENSOR(entity) sensor::Sensor *entity; #define OPENTHERM_DECLARE_SENSOR(entity) sensor::Sensor *entity;
@ -36,6 +39,7 @@ namespace opentherm {
#define OPENTHERM_DECLARE_NUMBER(entity) OpenthermNumber *entity; #define OPENTHERM_DECLARE_NUMBER(entity) OpenthermNumber *entity;
#define OPENTHERM_DECLARE_OUTPUT(entity) OpenthermOutput *entity; #define OPENTHERM_DECLARE_OUTPUT(entity) OpenthermOutput *entity;
#define OPENTHERM_DECLARE_INPUT_SENSOR(entity) sensor::Sensor *entity; #define OPENTHERM_DECLARE_INPUT_SENSOR(entity) sensor::Sensor *entity;
#define OPENTHERM_DECLARE_SETTING(type, entity, def) type entity = def;
// Setter macros // Setter macros
#define OPENTHERM_SET_SENSOR(entity) \ #define OPENTHERM_SET_SENSOR(entity) \
@ -56,6 +60,9 @@ namespace opentherm {
#define OPENTHERM_SET_INPUT_SENSOR(entity) \ #define OPENTHERM_SET_INPUT_SENSOR(entity) \
void set_##entity(sensor::Sensor *sensor) { this->entity = sensor; } 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 ===== // ===== hub.cpp macros =====
// *_MESSAGE_HANDLERS are generated in defines.h and look like this: // *_MESSAGE_HANDLERS are generated in defines.h and look like this:
@ -85,6 +92,9 @@ namespace opentherm {
#ifndef OPENTHERM_INPUT_SENSOR_MESSAGE_HANDLERS #ifndef OPENTHERM_INPUT_SENSOR_MESSAGE_HANDLERS
#define OPENTHERM_INPUT_SENSOR_MESSAGE_HANDLERS(MESSAGE, ENTITY, entity_sep, postscript, msg_sep) #define OPENTHERM_INPUT_SENSOR_MESSAGE_HANDLERS(MESSAGE, ENTITY, entity_sep, postscript, msg_sep)
#endif #endif
#ifndef OPENTHERM_SETTING_MESSAGE_HANDLERS
#define OPENTHERM_SETTING_MESSAGE_HANDLERS(MESSAGE, ENTITY, entity_sep, postscript, msg_sep)
#endif
// Write data request builders // Write data request builders
#define OPENTHERM_MESSAGE_WRITE_MESSAGE(msg) \ #define OPENTHERM_MESSAGE_WRITE_MESSAGE(msg) \
@ -92,6 +102,7 @@ namespace opentherm {
data.type = MessageType::WRITE_DATA; \ data.type = MessageType::WRITE_DATA; \
data.id = request_id; 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_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 \ #define OPENTHERM_MESSAGE_WRITE_POSTSCRIPT \
return data; \ return data; \
} }

View File

@ -2,8 +2,9 @@
# inputs of the OpenTherm component. # inputs of the OpenTherm component.
from dataclasses import dataclass 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 ( from esphome.const import (
UNIT_CELSIUS, UNIT_CELSIUS,
UNIT_EMPTY, UNIT_EMPTY,
@ -64,6 +65,7 @@ class SensorSchema(EntitySchema):
icon: Optional[str] = None icon: Optional[str] = None
device_class: Optional[str] = None device_class: Optional[str] = None
disabled_by_default: bool = False disabled_by_default: bool = False
order: Optional[int] = None
SENSORS: dict[str, SensorSchema] = { SENSORS: dict[str, SensorSchema] = {
@ -399,6 +401,7 @@ SENSORS: dict[str, SensorSchema] = {
message="OT_VERSION_DEVICE", message="OT_VERSION_DEVICE",
keep_updated=False, keep_updated=False,
message_data="f88", message_data="f88",
order=2,
), ),
"device_type": SensorSchema( "device_type": SensorSchema(
description="Device product type", description="Device product type",
@ -409,6 +412,7 @@ SENSORS: dict[str, SensorSchema] = {
message="VERSION_DEVICE", message="VERSION_DEVICE",
keep_updated=False, keep_updated=False,
message_data="u8_hb", message_data="u8_hb",
order=0,
), ),
"device_version": SensorSchema( "device_version": SensorSchema(
description="Device product version", description="Device product version",
@ -419,6 +423,7 @@ SENSORS: dict[str, SensorSchema] = {
message="VERSION_DEVICE", message="VERSION_DEVICE",
keep_updated=False, keep_updated=False,
message_data="u8_lb", message_data="u8_lb",
order=0,
), ),
"device_id": SensorSchema( "device_id": SensorSchema(
description="Device ID code", description="Device ID code",
@ -429,6 +434,7 @@ SENSORS: dict[str, SensorSchema] = {
message="DEVICE_CONFIG", message="DEVICE_CONFIG",
keep_updated=False, keep_updated=False,
message_data="u8_lb", message_data="u8_lb",
order=4,
), ),
"otc_hc_ratio_ub": SensorSchema( "otc_hc_ratio_ub": SensorSchema(
description="OTC heat curve ratio upper bound", description="OTC heat curve ratio upper bound",
@ -457,6 +463,7 @@ SENSORS: dict[str, SensorSchema] = {
class BinarySensorSchema(EntitySchema): class BinarySensorSchema(EntitySchema):
icon: Optional[str] = None icon: Optional[str] = None
device_class: Optional[str] = None device_class: Optional[str] = None
order: Optional[int] = None
BINARY_SENSORS: dict[str, BinarySensorSchema] = { BINARY_SENSORS: dict[str, BinarySensorSchema] = {
@ -525,48 +532,56 @@ BINARY_SENSORS: dict[str, BinarySensorSchema] = {
message="DEVICE_CONFIG", message="DEVICE_CONFIG",
keep_updated=False, keep_updated=False,
message_data="flag8_hb_0", message_data="flag8_hb_0",
order=4,
), ),
"control_type_on_off": BinarySensorSchema( "control_type_on_off": BinarySensorSchema(
description="Configuration: Control type is on/off", description="Configuration: Control type is on/off",
message="DEVICE_CONFIG", message="DEVICE_CONFIG",
keep_updated=False, keep_updated=False,
message_data="flag8_hb_1", message_data="flag8_hb_1",
order=4,
), ),
"cooling_supported": BinarySensorSchema( "cooling_supported": BinarySensorSchema(
description="Configuration: Cooling supported", description="Configuration: Cooling supported",
message="DEVICE_CONFIG", message="DEVICE_CONFIG",
keep_updated=False, keep_updated=False,
message_data="flag8_hb_2", message_data="flag8_hb_2",
order=4,
), ),
"dhw_storage_tank": BinarySensorSchema( "dhw_storage_tank": BinarySensorSchema(
description="Configuration: DHW storage tank", description="Configuration: DHW storage tank",
message="DEVICE_CONFIG", message="DEVICE_CONFIG",
keep_updated=False, keep_updated=False,
message_data="flag8_hb_3", message_data="flag8_hb_3",
order=4,
), ),
"controller_pump_control_allowed": BinarySensorSchema( "controller_pump_control_allowed": BinarySensorSchema(
description="Configuration: Controller pump control allowed", description="Configuration: Controller pump control allowed",
message="DEVICE_CONFIG", message="DEVICE_CONFIG",
keep_updated=False, keep_updated=False,
message_data="flag8_hb_4", message_data="flag8_hb_4",
order=4,
), ),
"ch2_present": BinarySensorSchema( "ch2_present": BinarySensorSchema(
description="Configuration: CH2 present", description="Configuration: CH2 present",
message="DEVICE_CONFIG", message="DEVICE_CONFIG",
keep_updated=False, keep_updated=False,
message_data="flag8_hb_5", message_data="flag8_hb_5",
order=4,
), ),
"water_filling": BinarySensorSchema( "water_filling": BinarySensorSchema(
description="Configuration: Remote water filling", description="Configuration: Remote water filling",
message="DEVICE_CONFIG", message="DEVICE_CONFIG",
keep_updated=False, keep_updated=False,
message_data="flag8_hb_6", message_data="flag8_hb_6",
order=4,
), ),
"heat_mode": BinarySensorSchema( "heat_mode": BinarySensorSchema(
description="Configuration: Heating or cooling", description="Configuration: Heating or cooling",
message="DEVICE_CONFIG", message="DEVICE_CONFIG",
keep_updated=False, keep_updated=False,
message_data="flag8_hb_7", message_data="flag8_hb_7",
order=4,
), ),
"dhw_setpoint_transfer_enabled": BinarySensorSchema( "dhw_setpoint_transfer_enabled": BinarySensorSchema(
description="Remote boiler parameters: DHW setpoint transfer enabled", 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"), 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,
),
}

View File

@ -9,12 +9,17 @@ from .schema import TSchema
def create_entities_schema( def create_entities_schema(
entities: dict[str, schema.EntitySchema], entities: dict[str, TSchema],
get_entity_validation_schema: Callable[[TSchema], cv.Schema], get_entity_validation_schema: Callable[[TSchema], cv.Schema],
) -> Schema: ) -> Schema:
entity_schema = {} entity_schema = {}
for key, entity in entities.items(): 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) return cv.Schema(entity_schema)

View File

@ -1,9 +1,9 @@
#ifdef USE_ESP32_FRAMEWORK_ARDUINO #ifdef USE_ESP32_FRAMEWORK_ARDUINO
#include "uart_component_esp32_arduino.h"
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/defines.h" #include "esphome/core/defines.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "uart_component_esp32_arduino.h"
#ifdef USE_LOGGER #ifdef USE_LOGGER
#include "esphome/components/logger/logger.h" #include "esphome/components/logger/logger.h"
@ -118,7 +118,7 @@ void ESP32ArduinoUARTComponent::setup() {
} }
#endif // USE_LOGGER #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."); ESP_LOGW(TAG, "Maximum number of UART components created already.");
this->mark_failed(); this->mark_failed();
return; return;

View File

@ -1,11 +1,11 @@
#ifdef USE_ESP_IDF #ifdef USE_ESP_IDF
#include "uart_component_esp_idf.h" #include "uart_component_esp_idf.h"
#include <cinttypes>
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/defines.h" #include "esphome/core/defines.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include <cinttypes>
#ifdef USE_LOGGER #ifdef USE_LOGGER
#include "esphome/components/logger/logger.h" #include "esphome/components/logger/logger.h"
@ -84,7 +84,7 @@ void IDFUARTComponent::setup() {
} }
#endif // USE_LOGGER #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."); ESP_LOGW(TAG, "Maximum number of UART components created already.");
this->mark_failed(); this->mark_failed();
return; return;

View File

@ -1660,6 +1660,12 @@ class SplitDefault(Optional):
esp32_c3=vol.UNDEFINED, esp32_c3=vol.UNDEFINED,
esp32_c3_arduino=vol.UNDEFINED, esp32_c3_arduino=vol.UNDEFINED,
esp32_c3_idf=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, rp2040=vol.UNDEFINED,
bk72xx=vol.UNDEFINED, bk72xx=vol.UNDEFINED,
rtl87xx=vol.UNDEFINED, rtl87xx=vol.UNDEFINED,
@ -1691,6 +1697,18 @@ class SplitDefault(Optional):
self._esp32_c3_idf_default = vol.default_factory( self._esp32_c3_idf_default = vol.default_factory(
_get_priority_default(esp32_c3_idf, esp32_c3, esp32_idf, esp32) _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._rp2040_default = vol.default_factory(rp2040)
self._bk72xx_default = vol.default_factory(bk72xx) self._bk72xx_default = vol.default_factory(bk72xx)
self._rtl87xx_default = vol.default_factory(rtl87xx) 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 import get_esp32_variant
from esphome.components.esp32.const import ( from esphome.components.esp32.const import (
VARIANT_ESP32C3, VARIANT_ESP32C3,
VARIANT_ESP32C6,
VARIANT_ESP32H2,
VARIANT_ESP32S2, VARIANT_ESP32S2,
VARIANT_ESP32S3, VARIANT_ESP32S3,
) )
@ -1724,6 +1744,16 @@ class SplitDefault(Optional):
return self._esp32_c3_arduino_default return self._esp32_c3_arduino_default
if CORE.using_esp_idf: if CORE.using_esp_idf:
return self._esp32_c3_idf_default 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: else:
if CORE.using_arduino: if CORE.using_arduino:
return self._esp32_arduino_default return self._esp32_arduino_default

View File

@ -108,6 +108,12 @@ def is_authenticated(handler: BaseHandler) -> bool:
return True return True
if settings.using_auth: 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 handler.get_secure_cookie(AUTH_COOKIE_NAME) == COOKIE_AUTHENTICATED_YES
return True return True

View File

@ -12,7 +12,7 @@ pyserial==3.5
platformio==6.1.16 # When updating platformio, also update Dockerfile platformio==6.1.16 # When updating platformio, also update Dockerfile
esptool==4.7.0 esptool==4.7.0
click==8.1.7 click==8.1.7
esphome-dashboard==20241120.0 esphome-dashboard==20241217.1
aioesphomeapi==24.6.2 aioesphomeapi==24.6.2
zeroconf==0.132.2 zeroconf==0.132.2
puremagic==1.27 puremagic==1.27

View File

@ -1,5 +1,7 @@
esphome: esphome:
on_boot: on_boot:
- lambda: 'ESP_LOGD("display","is_connected(): %s", YESNO(id(main_lcd).is_connected()));'
# Binary sensor publish action tests # Binary sensor publish action tests
- binary_sensor.nextion.publish: - binary_sensor.nextion.publish:
id: r0_sensor id: r0_sensor

View File

@ -16,6 +16,19 @@ opentherm:
summer_mode_active: true summer_mode_active: true
dhw_block: true dhw_block: true
sync_mode: 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: output:
- platform: opentherm - platform: opentherm