diff --git a/esphome/components/absolute_humidity/absolute_humidity.cpp b/esphome/components/absolute_humidity/absolute_humidity.cpp index 74d675b80b..b13fcd519a 100644 --- a/esphome/components/absolute_humidity/absolute_humidity.cpp +++ b/esphome/components/absolute_humidity/absolute_humidity.cpp @@ -90,13 +90,16 @@ void AbsoluteHumidityComponent::loop() { this->status_set_error(LOG_STR("Invalid saturation vapor pressure equation selection!")); return; } - ESP_LOGD(TAG, "Saturation vapor pressure %f kPa", es); // Calculate absolute humidity const float absolute_humidity = vapor_density(es, hr, temperature_k); + ESP_LOGD(TAG, + "Saturation vapor pressure %f kPa\n" + "Publishing absolute humidity %f g/m³", + es, absolute_humidity); + // Publish absolute humidity - ESP_LOGD(TAG, "Publishing absolute humidity %f g/m³", absolute_humidity); this->status_clear_warning(); this->publish_state(absolute_humidity); } diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index bf96550ab6..2f7b72a1e7 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -644,6 +644,7 @@ CONF_DISABLE_VFS_SUPPORT_SELECT = "disable_vfs_support_select" CONF_DISABLE_VFS_SUPPORT_DIR = "disable_vfs_support_dir" CONF_FREERTOS_IN_IRAM = "freertos_in_iram" CONF_RINGBUF_IN_IRAM = "ringbuf_in_iram" +CONF_HEAP_IN_IRAM = "heap_in_iram" CONF_LOOP_TASK_STACK_SIZE = "loop_task_stack_size" # VFS requirement tracking @@ -759,6 +760,7 @@ FRAMEWORK_SCHEMA = cv.Schema( cv.Optional(CONF_DISABLE_VFS_SUPPORT_DIR, default=True): cv.boolean, cv.Optional(CONF_FREERTOS_IN_IRAM, default=False): cv.boolean, cv.Optional(CONF_RINGBUF_IN_IRAM, default=False): cv.boolean, + cv.Optional(CONF_HEAP_IN_IRAM, default=False): cv.boolean, cv.Optional(CONF_EXECUTE_FROM_PSRAM, default=False): cv.boolean, cv.Optional(CONF_LOOP_TASK_STACK_SIZE, default=8192): cv.int_range( min=8192, max=32768 @@ -1108,6 +1110,12 @@ async def to_code(config): # No component needs it - place in flash to save IRAM add_idf_sdkconfig_option("CONFIG_RINGBUF_PLACE_FUNCTIONS_INTO_FLASH", True) + # Place heap functions into flash to save IRAM (~4-6KB savings) + # Safe as long as heap functions are not called from ISRs (which they shouldn't be) + # Users can set heap_in_iram: true as an escape hatch if needed + if not conf[CONF_ADVANCED][CONF_HEAP_IN_IRAM]: + add_idf_sdkconfig_option("CONFIG_HEAP_PLACE_FUNCTION_INTO_FLASH", True) + # Setup watchdog add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT", True) add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_PANIC", True) diff --git a/esphome/components/logger/logger_zephyr.cpp b/esphome/components/logger/logger_zephyr.cpp index 330dfa96ec..41f53beec0 100644 --- a/esphome/components/logger/logger_zephyr.cpp +++ b/esphome/components/logger/logger_zephyr.cpp @@ -66,6 +66,8 @@ void Logger::pre_setup() { void HOT Logger::write_msg_(const char *msg, size_t len) { // Single write with newline already in buffer (added by caller) #ifdef CONFIG_PRINTK + // Requires the debug component and an active SWD connection. + // It is used for pyocd rtt -t nrf52840 k_str_out(const_cast(msg), len); #endif if (this->uart_dev_ == nullptr) { diff --git a/esphome/components/template/water_heater/__init__.py b/esphome/components/template/water_heater/__init__.py new file mode 100644 index 0000000000..716289035a --- /dev/null +++ b/esphome/components/template/water_heater/__init__.py @@ -0,0 +1,123 @@ +from esphome import automation +import esphome.codegen as cg +from esphome.components import water_heater +import esphome.config_validation as cv +from esphome.const import ( + CONF_ID, + CONF_MODE, + CONF_OPTIMISTIC, + CONF_RESTORE_MODE, + CONF_SET_ACTION, + CONF_SUPPORTED_MODES, + CONF_TARGET_TEMPERATURE, +) +from esphome.core import ID +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType + +from .. import template_ns + +CONF_CURRENT_TEMPERATURE = "current_temperature" + +TemplateWaterHeater = template_ns.class_( + "TemplateWaterHeater", water_heater.WaterHeater +) + +TemplateWaterHeaterPublishAction = template_ns.class_( + "TemplateWaterHeaterPublishAction", + automation.Action, + cg.Parented.template(TemplateWaterHeater), +) + +TemplateWaterHeaterRestoreMode = template_ns.enum("TemplateWaterHeaterRestoreMode") +RESTORE_MODES = { + "NO_RESTORE": TemplateWaterHeaterRestoreMode.WATER_HEATER_NO_RESTORE, + "RESTORE": TemplateWaterHeaterRestoreMode.WATER_HEATER_RESTORE, + "RESTORE_AND_CALL": TemplateWaterHeaterRestoreMode.WATER_HEATER_RESTORE_AND_CALL, +} + +CONFIG_SCHEMA = water_heater.water_heater_schema(TemplateWaterHeater).extend( + { + cv.Optional(CONF_OPTIMISTIC, default=True): cv.boolean, + cv.Optional(CONF_SET_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_RESTORE_MODE, default="NO_RESTORE"): cv.enum( + RESTORE_MODES, upper=True + ), + cv.Optional(CONF_CURRENT_TEMPERATURE): cv.returning_lambda, + cv.Optional(CONF_MODE): cv.returning_lambda, + cv.Optional(CONF_SUPPORTED_MODES): cv.ensure_list( + water_heater.validate_water_heater_mode + ), + } +) + + +async def to_code(config: ConfigType) -> None: + var = cg.new_Pvariable(config[CONF_ID]) + await water_heater.register_water_heater(var, config) + + cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) + + if CONF_SET_ACTION in config: + await automation.build_automation( + var.get_set_trigger(), [], config[CONF_SET_ACTION] + ) + + cg.add(var.set_restore_mode(config[CONF_RESTORE_MODE])) + + if CONF_CURRENT_TEMPERATURE in config: + template_ = await cg.process_lambda( + config[CONF_CURRENT_TEMPERATURE], + [], + return_type=cg.optional.template(cg.float_), + ) + cg.add(var.set_current_temperature_lambda(template_)) + + if CONF_MODE in config: + template_ = await cg.process_lambda( + config[CONF_MODE], + [], + return_type=cg.optional.template(water_heater.WaterHeaterMode), + ) + cg.add(var.set_mode_lambda(template_)) + + if CONF_SUPPORTED_MODES in config: + cg.add(var.set_supported_modes(config[CONF_SUPPORTED_MODES])) + + +@automation.register_action( + "water_heater.template.publish", + TemplateWaterHeaterPublishAction, + cv.Schema( + { + cv.GenerateID(): cv.use_id(TemplateWaterHeater), + cv.Optional(CONF_CURRENT_TEMPERATURE): cv.templatable(cv.temperature), + cv.Optional(CONF_TARGET_TEMPERATURE): cv.templatable(cv.temperature), + cv.Optional(CONF_MODE): cv.templatable( + water_heater.validate_water_heater_mode + ), + } + ), +) +async def water_heater_template_publish_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + + if current_temp := config.get(CONF_CURRENT_TEMPERATURE): + template_ = await cg.templatable(current_temp, args, float) + cg.add(var.set_current_temperature(template_)) + + if target_temp := config.get(CONF_TARGET_TEMPERATURE): + template_ = await cg.templatable(target_temp, args, float) + cg.add(var.set_target_temperature(template_)) + + if mode := config.get(CONF_MODE): + template_ = await cg.templatable(mode, args, water_heater.WaterHeaterMode) + cg.add(var.set_mode(template_)) + + return var diff --git a/esphome/components/template/water_heater/automation.h b/esphome/components/template/water_heater/automation.h new file mode 100644 index 0000000000..3dad2b85ae --- /dev/null +++ b/esphome/components/template/water_heater/automation.h @@ -0,0 +1,35 @@ +#pragma once + +#include "template_water_heater.h" +#include "esphome/core/automation.h" + +namespace esphome::template_ { + +template +class TemplateWaterHeaterPublishAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(float, current_temperature) + TEMPLATABLE_VALUE(float, target_temperature) + TEMPLATABLE_VALUE(water_heater::WaterHeaterMode, mode) + + void play(const Ts &...x) override { + if (this->current_temperature_.has_value()) { + this->parent_->set_current_temperature(this->current_temperature_.value(x...)); + } + bool needs_call = this->target_temperature_.has_value() || this->mode_.has_value(); + if (needs_call) { + auto call = this->parent_->make_call(); + if (this->target_temperature_.has_value()) { + call.set_target_temperature(this->target_temperature_.value(x...)); + } + if (this->mode_.has_value()) { + call.set_mode(this->mode_.value(x...)); + } + call.perform(); + } else { + this->parent_->publish_state(); + } + } +}; + +} // namespace esphome::template_ diff --git a/esphome/components/template/water_heater/template_water_heater.cpp b/esphome/components/template/water_heater/template_water_heater.cpp new file mode 100644 index 0000000000..18ef8d3f06 --- /dev/null +++ b/esphome/components/template/water_heater/template_water_heater.cpp @@ -0,0 +1,88 @@ +#include "template_water_heater.h" +#include "esphome/core/log.h" + +namespace esphome::template_ { + +static const char *const TAG = "template.water_heater"; + +TemplateWaterHeater::TemplateWaterHeater() : set_trigger_(new Trigger<>()) {} + +void TemplateWaterHeater::setup() { + if (this->restore_mode_ == TemplateWaterHeaterRestoreMode::WATER_HEATER_RESTORE || + this->restore_mode_ == TemplateWaterHeaterRestoreMode::WATER_HEATER_RESTORE_AND_CALL) { + auto restore = this->restore_state(); + + if (restore.has_value()) { + restore->perform(); + } + } + if (!this->current_temperature_f_.has_value() && !this->mode_f_.has_value()) + this->disable_loop(); +} + +water_heater::WaterHeaterTraits TemplateWaterHeater::traits() { + auto traits = water_heater::WaterHeater::get_traits(); + + if (!this->supported_modes_.empty()) { + traits.set_supported_modes(this->supported_modes_); + } + + traits.set_supports_current_temperature(true); + return traits; +} + +void TemplateWaterHeater::loop() { + bool changed = false; + + auto curr_temp = this->current_temperature_f_.call(); + if (curr_temp.has_value()) { + if (*curr_temp != this->current_temperature_) { + this->current_temperature_ = *curr_temp; + changed = true; + } + } + + auto new_mode = this->mode_f_.call(); + if (new_mode.has_value()) { + if (*new_mode != this->mode_) { + this->mode_ = *new_mode; + changed = true; + } + } + + if (changed) { + this->publish_state(); + } +} + +void TemplateWaterHeater::dump_config() { + LOG_WATER_HEATER("", "Template Water Heater", this); + ESP_LOGCONFIG(TAG, " Optimistic: %s", YESNO(this->optimistic_)); +} + +float TemplateWaterHeater::get_setup_priority() const { return setup_priority::HARDWARE; } + +water_heater::WaterHeaterCallInternal TemplateWaterHeater::make_call() { + return water_heater::WaterHeaterCallInternal(this); +} + +void TemplateWaterHeater::control(const water_heater::WaterHeaterCall &call) { + if (call.get_mode().has_value()) { + if (this->optimistic_) { + this->mode_ = *call.get_mode(); + } + } + if (!std::isnan(call.get_target_temperature())) { + if (this->optimistic_) { + this->target_temperature_ = call.get_target_temperature(); + } + } + + this->set_trigger_->trigger(); + + if (this->optimistic_) { + this->publish_state(); + } +} + +} // namespace esphome::template_ diff --git a/esphome/components/template/water_heater/template_water_heater.h b/esphome/components/template/water_heater/template_water_heater.h new file mode 100644 index 0000000000..e5f51b72dc --- /dev/null +++ b/esphome/components/template/water_heater/template_water_heater.h @@ -0,0 +1,53 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/core/template_lambda.h" +#include "esphome/components/water_heater/water_heater.h" + +namespace esphome::template_ { + +enum TemplateWaterHeaterRestoreMode { + WATER_HEATER_NO_RESTORE, + WATER_HEATER_RESTORE, + WATER_HEATER_RESTORE_AND_CALL, +}; + +class TemplateWaterHeater : public water_heater::WaterHeater { + public: + TemplateWaterHeater(); + + template void set_current_temperature_lambda(F &&f) { + this->current_temperature_f_.set(std::forward(f)); + } + template void set_mode_lambda(F &&f) { this->mode_f_.set(std::forward(f)); } + + void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } + void set_restore_mode(TemplateWaterHeaterRestoreMode restore_mode) { this->restore_mode_ = restore_mode; } + void set_supported_modes(const std::initializer_list &modes) { + this->supported_modes_ = modes; + } + + Trigger<> *get_set_trigger() const { return this->set_trigger_; } + + void setup() override; + void loop() override; + void dump_config() override; + float get_setup_priority() const override; + + water_heater::WaterHeaterCallInternal make_call() override; + + protected: + void control(const water_heater::WaterHeaterCall &call) override; + water_heater::WaterHeaterTraits traits() override; + + // Ordered to minimize padding on 32-bit: 4-byte members first, then smaller + Trigger<> *set_trigger_; + TemplateLambda current_temperature_f_; + TemplateLambda mode_f_; + TemplateWaterHeaterRestoreMode restore_mode_{WATER_HEATER_NO_RESTORE}; + water_heater::WaterHeaterModeMask supported_modes_; + bool optimistic_{true}; +}; + +} // namespace esphome::template_ diff --git a/tests/components/template/common-base.yaml b/tests/components/template/common-base.yaml index f101eea942..e050c0b307 100644 --- a/tests/components/template/common-base.yaml +++ b/tests/components/template/common-base.yaml @@ -9,6 +9,18 @@ esphome: id: template_sens state: !lambda "return 42.0;" + - water_heater.template.publish: + id: template_water_heater + target_temperature: 50.0 + mode: ECO + + # Templated + - water_heater.template.publish: + id: template_water_heater + current_temperature: !lambda "return 45.0;" + target_temperature: !lambda "return 55.0;" + mode: !lambda "return water_heater::WATER_HEATER_MODE_GAS;" + # Test C++ API: set_template() with stateless lambda (no captures) # NOTE: set_template() is not intended to be a public API, but we test it to ensure it doesn't break. - lambda: |- @@ -299,6 +311,24 @@ alarm_control_panel: codes: - "1234" +water_heater: + - platform: template + id: template_water_heater + name: "Template Water Heater" + optimistic: true + current_temperature: !lambda "return 42.0f;" + mode: !lambda "return water_heater::WATER_HEATER_MODE_ECO;" + supported_modes: + - "OFF" + - ECO + - GAS + - ELECTRIC + - HEAT_PUMP + - HIGH_DEMAND + - PERFORMANCE + set_action: + - logger.log: "set_action" + datetime: - platform: template name: Date