#include "water_heater.h" #include "esphome/core/log.h" #include #include "esphome/core/application.h" #include "esphome/core/controller_registry.h" #include "esphome/core/progmem.h" #include #include namespace esphome::water_heater { static const char *const TAG = "water_heater"; void log_water_heater(const char *tag, const char *prefix, const char *type, WaterHeater *obj) { if (obj != nullptr) { ESP_LOGCONFIG(tag, "%s%s '%s'", prefix, type, obj->get_name().c_str()); } } WaterHeaterCall::WaterHeaterCall(WaterHeater *parent) : parent_(parent) {} WaterHeaterCall &WaterHeaterCall::set_mode(WaterHeaterMode mode) { this->mode_ = mode; return *this; } WaterHeaterCall &WaterHeaterCall::set_mode(const char *mode) { return this->set_mode(mode, strlen(mode)); } WaterHeaterCall &WaterHeaterCall::set_mode(const char *mode, size_t len) { if (len == 3 && ESPHOME_strncasecmp_P(mode, ESPHOME_PSTR("OFF"), 3) == 0) { this->set_mode(WATER_HEATER_MODE_OFF); } else if (len == 3 && ESPHOME_strncasecmp_P(mode, ESPHOME_PSTR("ECO"), 3) == 0) { this->set_mode(WATER_HEATER_MODE_ECO); } else if (len == 8 && ESPHOME_strncasecmp_P(mode, ESPHOME_PSTR("ELECTRIC"), 8) == 0) { this->set_mode(WATER_HEATER_MODE_ELECTRIC); } else if (len == 11 && ESPHOME_strncasecmp_P(mode, ESPHOME_PSTR("PERFORMANCE"), 11) == 0) { this->set_mode(WATER_HEATER_MODE_PERFORMANCE); } else if (len == 11 && ESPHOME_strncasecmp_P(mode, ESPHOME_PSTR("HIGH_DEMAND"), 11) == 0) { this->set_mode(WATER_HEATER_MODE_HIGH_DEMAND); } else if (len == 9 && ESPHOME_strncasecmp_P(mode, ESPHOME_PSTR("HEAT_PUMP"), 9) == 0) { this->set_mode(WATER_HEATER_MODE_HEAT_PUMP); } else if (len == 3 && ESPHOME_strncasecmp_P(mode, ESPHOME_PSTR("GAS"), 3) == 0) { this->set_mode(WATER_HEATER_MODE_GAS); } else { ESP_LOGW(TAG, "'%s' - Unrecognized mode %.*s", this->parent_->get_name().c_str(), (int) len, mode); } return *this; } WaterHeaterCall &WaterHeaterCall::set_target_temperature(float temperature) { this->target_temperature_ = temperature; return *this; } WaterHeaterCall &WaterHeaterCall::set_target_temperature_low(float temperature) { this->target_temperature_low_ = temperature; return *this; } WaterHeaterCall &WaterHeaterCall::set_target_temperature_high(float temperature) { this->target_temperature_high_ = temperature; return *this; } WaterHeaterCall &WaterHeaterCall::set_away(bool away) { if (away) { this->state_ |= WATER_HEATER_STATE_AWAY; } else { this->state_ &= ~WATER_HEATER_STATE_AWAY; } this->state_mask_ |= WATER_HEATER_STATE_AWAY; return *this; } WaterHeaterCall &WaterHeaterCall::set_on(bool on) { if (on) { this->state_ |= WATER_HEATER_STATE_ON; } else { this->state_ &= ~WATER_HEATER_STATE_ON; } this->state_mask_ |= WATER_HEATER_STATE_ON; return *this; } void WaterHeaterCall::perform() { ESP_LOGV(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); this->validate_(); if (this->mode_.has_value()) { ESP_LOGV(TAG, " Mode: %s", LOG_STR_ARG(water_heater_mode_to_string(*this->mode_))); } if (!std::isnan(this->target_temperature_)) { ESP_LOGV(TAG, " Target Temperature: %.2f", this->target_temperature_); } if (!std::isnan(this->target_temperature_low_)) { ESP_LOGV(TAG, " Target Temperature Low: %.2f", this->target_temperature_low_); } if (!std::isnan(this->target_temperature_high_)) { ESP_LOGV(TAG, " Target Temperature High: %.2f", this->target_temperature_high_); } if (this->state_mask_ & WATER_HEATER_STATE_AWAY) { ESP_LOGV(TAG, " Away: %s", (this->state_ & WATER_HEATER_STATE_AWAY) ? "YES" : "NO"); } if (this->state_mask_ & WATER_HEATER_STATE_ON) { ESP_LOGV(TAG, " On: %s", (this->state_ & WATER_HEATER_STATE_ON) ? "YES" : "NO"); } this->parent_->control(*this); } void WaterHeaterCall::validate_() { auto traits = this->parent_->get_traits(); if (this->mode_.has_value()) { if (!traits.supports_mode(*this->mode_)) { ESP_LOGW(TAG, "'%s' - Mode %" PRIu32 " not supported", this->parent_->get_name().c_str(), static_cast(*this->mode_)); this->mode_.reset(); } } if (!std::isnan(this->target_temperature_)) { if (traits.get_supports_two_point_target_temperature()) { ESP_LOGW(TAG, "'%s' - Cannot set target temperature for device with two-point target temperature", this->parent_->get_name().c_str()); this->target_temperature_ = NAN; } else if (this->target_temperature_ < traits.get_min_temperature() || this->target_temperature_ > traits.get_max_temperature()) { ESP_LOGW(TAG, "'%s' - Target temperature %.1f is out of range [%.1f - %.1f]", this->parent_->get_name().c_str(), this->target_temperature_, traits.get_min_temperature(), traits.get_max_temperature()); this->target_temperature_ = std::max(traits.get_min_temperature(), std::min(this->target_temperature_, traits.get_max_temperature())); } } if (!std::isnan(this->target_temperature_low_) || !std::isnan(this->target_temperature_high_)) { if (!traits.get_supports_two_point_target_temperature()) { ESP_LOGW(TAG, "'%s' - Cannot set low/high target temperature", this->parent_->get_name().c_str()); this->target_temperature_low_ = NAN; this->target_temperature_high_ = NAN; } } if (!std::isnan(this->target_temperature_low_) && !std::isnan(this->target_temperature_high_)) { if (this->target_temperature_low_ > this->target_temperature_high_) { ESP_LOGW(TAG, "'%s' - Target temperature low %.2f must be less than high %.2f", this->parent_->get_name().c_str(), this->target_temperature_low_, this->target_temperature_high_); this->target_temperature_low_ = NAN; this->target_temperature_high_ = NAN; } } if (!traits.get_supports_away_mode()) { if (this->state_ & WATER_HEATER_STATE_AWAY) { ESP_LOGW(TAG, "'%s' - Away mode not supported", this->parent_->get_name().c_str()); } this->state_ &= ~WATER_HEATER_STATE_AWAY; this->state_mask_ &= ~WATER_HEATER_STATE_AWAY; } // If ON/OFF not supported, device is always on - clear the flag silently if (!traits.has_feature_flags(WATER_HEATER_SUPPORTS_ON_OFF)) { this->state_ &= ~WATER_HEATER_STATE_ON; this->state_mask_ &= ~WATER_HEATER_STATE_ON; } } void WaterHeater::publish_state() { auto traits = this->get_traits(); ESP_LOGV(TAG, "'%s' >>\n" " Mode: %s", this->name_.c_str(), LOG_STR_ARG(water_heater_mode_to_string(this->mode_))); if (!std::isnan(this->current_temperature_)) { ESP_LOGV(TAG, " Current Temperature: %.2f°C", this->current_temperature_); } if (traits.get_supports_two_point_target_temperature()) { ESP_LOGV(TAG, " Target Temperature: Low: %.2f°C High: %.2f°C", this->target_temperature_low_, this->target_temperature_high_); } else if (!std::isnan(this->target_temperature_)) { ESP_LOGV(TAG, " Target Temperature: %.2f°C", this->target_temperature_); } if (this->state_ & WATER_HEATER_STATE_AWAY) { ESP_LOGV(TAG, " Away: YES"); } if (traits.has_feature_flags(WATER_HEATER_SUPPORTS_ON_OFF)) { ESP_LOGV(TAG, " On: %s", (this->state_ & WATER_HEATER_STATE_ON) ? "YES" : "NO"); } #if defined(USE_WATER_HEATER) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_water_heater_update(this); #endif SavedWaterHeaterState saved{}; saved.mode = this->mode_; if (traits.get_supports_two_point_target_temperature()) { saved.target_temperature_low = this->target_temperature_low_; saved.target_temperature_high = this->target_temperature_high_; } else { saved.target_temperature = this->target_temperature_; } saved.state = this->state_; this->pref_.save(&saved); } optional WaterHeater::restore_state_() { this->pref_ = this->make_entity_preference(); SavedWaterHeaterState recovered{}; if (!this->pref_.load(&recovered)) return {}; auto traits = this->get_traits(); auto call = this->make_call(); call.set_mode(recovered.mode); if (traits.get_supports_two_point_target_temperature()) { call.set_target_temperature_low(recovered.target_temperature_low); call.set_target_temperature_high(recovered.target_temperature_high); } else { call.set_target_temperature(recovered.target_temperature); } call.set_away((recovered.state & WATER_HEATER_STATE_AWAY) != 0); call.set_on((recovered.state & WATER_HEATER_STATE_ON) != 0); return call; } WaterHeaterTraits WaterHeater::get_traits() { auto traits = this->traits(); #ifdef USE_WATER_HEATER_VISUAL_OVERRIDES if (!std::isnan(this->visual_min_temperature_override_)) { traits.set_min_temperature(this->visual_min_temperature_override_); } if (!std::isnan(this->visual_max_temperature_override_)) { traits.set_max_temperature(this->visual_max_temperature_override_); } if (!std::isnan(this->visual_target_temperature_step_override_)) { traits.set_target_temperature_step(this->visual_target_temperature_step_override_); } #endif return traits; } #ifdef USE_WATER_HEATER_VISUAL_OVERRIDES void WaterHeater::set_visual_min_temperature_override(float min_temperature_override) { this->visual_min_temperature_override_ = min_temperature_override; } void WaterHeater::set_visual_max_temperature_override(float max_temperature_override) { this->visual_max_temperature_override_ = max_temperature_override; } void WaterHeater::set_visual_target_temperature_step_override(float visual_target_temperature_step_override) { this->visual_target_temperature_step_override_ = visual_target_temperature_step_override; } #endif // Water heater mode strings indexed by WaterHeaterMode enum (0-6): OFF, ECO, ELECTRIC, PERFORMANCE, HIGH_DEMAND, // HEAT_PUMP, GAS PROGMEM_STRING_TABLE(WaterHeaterModeStrings, "OFF", "ECO", "ELECTRIC", "PERFORMANCE", "HIGH_DEMAND", "HEAT_PUMP", "GAS", "UNKNOWN"); const LogString *water_heater_mode_to_string(WaterHeaterMode mode) { return WaterHeaterModeStrings::get_log_str(static_cast(mode), WaterHeaterModeStrings::LAST_INDEX); } void WaterHeater::dump_traits_(const char *tag) { auto traits = this->get_traits(); ESP_LOGCONFIG(tag, " Min Temperature: %.1f°C\n" " Max Temperature: %.1f°C\n" " Temperature Step: %.1f", traits.get_min_temperature(), traits.get_max_temperature(), traits.get_target_temperature_step()); if (traits.get_supports_two_point_target_temperature()) { ESP_LOGCONFIG(tag, " Supports Two-Point Target Temperature: YES"); } if (traits.get_supports_away_mode()) { ESP_LOGCONFIG(tag, " Supports Away Mode: YES"); } if (traits.has_feature_flags(WATER_HEATER_SUPPORTS_ON_OFF)) { ESP_LOGCONFIG(tag, " Supports On/Off: YES"); } if (!traits.get_supported_modes().empty()) { ESP_LOGCONFIG(tag, " Supported Modes:"); for (WaterHeaterMode m : traits.get_supported_modes()) { ESP_LOGCONFIG(tag, " - %s", LOG_STR_ARG(water_heater_mode_to_string(m))); } } } } // namespace esphome::water_heater