[water_heater] (1/4) Implement API/Core/component for new water_heater component (#12498)

Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick+github@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Douwe
2025-12-21 22:36:34 +01:00
committed by GitHub
parent 637e032528
commit 39926909af
29 changed files with 1177 additions and 0 deletions

View File

@@ -537,6 +537,7 @@ esphome/components/version/* @esphome/core
esphome/components/voice_assistant/* @jesserockz @kahrendt
esphome/components/wake_on_lan/* @clydebarrow @willwill2will54
esphome/components/watchdog/* @oarcher
esphome/components/water_heater/* @dhoeben
esphome/components/waveshare_epaper/* @clydebarrow
esphome/components/web_server/ota/* @esphome/core
esphome/components/web_server_base/* @esphome/core

View File

@@ -1101,6 +1101,85 @@ message ClimateCommandRequest {
uint32 device_id = 24 [(field_ifdef) = "USE_DEVICES"];
}
// ==================== WATER_HEATER ====================
enum WaterHeaterMode {
WATER_HEATER_MODE_OFF = 0;
WATER_HEATER_MODE_ECO = 1;
WATER_HEATER_MODE_ELECTRIC = 2;
WATER_HEATER_MODE_PERFORMANCE = 3;
WATER_HEATER_MODE_HIGH_DEMAND = 4;
WATER_HEATER_MODE_HEAT_PUMP = 5;
WATER_HEATER_MODE_GAS = 6;
}
message ListEntitiesWaterHeaterResponse {
option (id) = 132;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_WATER_HEATER";
string object_id = 1;
fixed32 key = 2;
string name = 3;
string icon = 4 [(field_ifdef) = "USE_ENTITY_ICON"];
bool disabled_by_default = 5;
EntityCategory entity_category = 6;
uint32 device_id = 7 [(field_ifdef) = "USE_DEVICES"];
float min_temperature = 8;
float max_temperature = 9;
float target_temperature_step = 10;
repeated WaterHeaterMode supported_modes = 11 [(container_pointer_no_template) = "water_heater::WaterHeaterModeMask"];
// Bitmask of WaterHeaterFeature flags
uint32 supported_features = 12;
}
message WaterHeaterStateResponse {
option (id) = 133;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_WATER_HEATER";
option (no_delay) = true;
fixed32 key = 1;
float current_temperature = 2;
float target_temperature = 3;
WaterHeaterMode mode = 4;
uint32 device_id = 5 [(field_ifdef) = "USE_DEVICES"];
// Bitmask of current state flags (bit 0 = away, bit 1 = on)
uint32 state = 6;
float target_temperature_low = 7;
float target_temperature_high = 8;
}
// Bitmask for WaterHeaterCommandRequest.has_fields
enum WaterHeaterCommandHasField {
WATER_HEATER_COMMAND_HAS_NONE = 0;
WATER_HEATER_COMMAND_HAS_MODE = 1;
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE = 2;
WATER_HEATER_COMMAND_HAS_STATE = 4;
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW = 8;
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH = 16;
}
message WaterHeaterCommandRequest {
option (id) = 134;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_WATER_HEATER";
option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1;
// Bitmask of which fields are set (see WaterHeaterCommandHasField)
uint32 has_fields = 2;
WaterHeaterMode mode = 3;
float target_temperature = 4;
uint32 device_id = 5 [(field_ifdef) = "USE_DEVICES"];
// State flags bitmask (bit 0 = away, bit 1 = on)
uint32 state = 6;
float target_temperature_low = 7;
float target_temperature_high = 8;
}
// ==================== NUMBER ====================
enum NumberMode {
NUMBER_MODE_AUTO = 0;

View File

@@ -42,6 +42,9 @@
#ifdef USE_ZWAVE_PROXY
#include "esphome/components/zwave_proxy/zwave_proxy.h"
#endif
#ifdef USE_WATER_HEATER
#include "esphome/components/water_heater/water_heater.h"
#endif
namespace esphome::api {
@@ -1306,6 +1309,57 @@ void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRe
}
#endif
#ifdef USE_WATER_HEATER
bool APIConnection::send_water_heater_state(water_heater::WaterHeater *water_heater) {
return this->send_message_smart_(water_heater, &APIConnection::try_send_water_heater_state,
WaterHeaterStateResponse::MESSAGE_TYPE, WaterHeaterStateResponse::ESTIMATED_SIZE);
}
uint16_t APIConnection::try_send_water_heater_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single) {
auto *wh = static_cast<water_heater::WaterHeater *>(entity);
WaterHeaterStateResponse resp;
resp.mode = static_cast<enums::WaterHeaterMode>(wh->get_mode());
resp.current_temperature = wh->get_current_temperature();
resp.target_temperature = wh->get_target_temperature();
resp.target_temperature_low = wh->get_target_temperature_low();
resp.target_temperature_high = wh->get_target_temperature_high();
resp.state = wh->get_state();
resp.key = wh->get_object_id_hash();
return encode_message_to_buffer(resp, WaterHeaterStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
}
uint16_t APIConnection::try_send_water_heater_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single) {
auto *wh = static_cast<water_heater::WaterHeater *>(entity);
ListEntitiesWaterHeaterResponse msg;
auto traits = wh->get_traits();
msg.min_temperature = traits.get_min_temperature();
msg.max_temperature = traits.get_max_temperature();
msg.target_temperature_step = traits.get_target_temperature_step();
msg.supported_modes = &traits.get_supported_modes();
msg.supported_features = traits.get_feature_flags();
return fill_and_encode_entity_info(wh, msg, ListEntitiesWaterHeaterResponse::MESSAGE_TYPE, conn, remaining_size,
is_single);
}
void APIConnection::on_water_heater_command_request(const WaterHeaterCommandRequest &msg) {
ENTITY_COMMAND_MAKE_CALL(water_heater::WaterHeater, water_heater, water_heater)
if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_MODE)
call.set_mode(static_cast<water_heater::WaterHeaterMode>(msg.mode));
if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE)
call.set_target_temperature(msg.target_temperature);
if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW)
call.set_target_temperature_low(msg.target_temperature_low);
if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH)
call.set_target_temperature_high(msg.target_temperature_high);
if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_STATE) {
call.set_away((msg.state & water_heater::WATER_HEATER_STATE_AWAY) != 0);
call.set_on((msg.state & water_heater::WATER_HEATER_STATE_ON) != 0);
}
call.perform();
}
#endif
#ifdef USE_EVENT
void APIConnection::send_event(event::Event *event, const char *event_type) {
this->send_message_smart_(event, MessageCreator(event_type), EventResponse::MESSAGE_TYPE,

View File

@@ -176,6 +176,11 @@ class APIConnection final : public APIServerConnection {
void alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) override;
#endif
#ifdef USE_WATER_HEATER
bool send_water_heater_state(water_heater::WaterHeater *water_heater);
void on_water_heater_command_request(const WaterHeaterCommandRequest &msg) override;
#endif
#ifdef USE_EVENT
void send_event(event::Event *event, const char *event_type);
#endif
@@ -456,6 +461,12 @@ class APIConnection final : public APIServerConnection {
static uint16_t try_send_alarm_control_panel_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single);
#endif
#ifdef USE_WATER_HEATER
static uint16_t try_send_water_heater_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single);
static uint16_t try_send_water_heater_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single);
#endif
#ifdef USE_EVENT
static uint16_t try_send_event_response(event::Event *event, const char *event_type, APIConnection *conn,
uint32_t remaining_size, bool is_single);

View File

@@ -1447,6 +1447,114 @@ bool ClimateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
return true;
}
#endif
#ifdef USE_WATER_HEATER
void ListEntitiesWaterHeaterResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_string(1, this->object_id_ref_);
buffer.encode_fixed32(2, this->key);
buffer.encode_string(3, this->name_ref_);
#ifdef USE_ENTITY_ICON
buffer.encode_string(4, this->icon_ref_);
#endif
buffer.encode_bool(5, this->disabled_by_default);
buffer.encode_uint32(6, static_cast<uint32_t>(this->entity_category));
#ifdef USE_DEVICES
buffer.encode_uint32(7, this->device_id);
#endif
buffer.encode_float(8, this->min_temperature);
buffer.encode_float(9, this->max_temperature);
buffer.encode_float(10, this->target_temperature_step);
for (const auto &it : *this->supported_modes) {
buffer.encode_uint32(11, static_cast<uint32_t>(it), true);
}
buffer.encode_uint32(12, this->supported_features);
}
void ListEntitiesWaterHeaterResponse::calculate_size(ProtoSize &size) const {
size.add_length(1, this->object_id_ref_.size());
size.add_fixed32(1, this->key);
size.add_length(1, this->name_ref_.size());
#ifdef USE_ENTITY_ICON
size.add_length(1, this->icon_ref_.size());
#endif
size.add_bool(1, this->disabled_by_default);
size.add_uint32(1, static_cast<uint32_t>(this->entity_category));
#ifdef USE_DEVICES
size.add_uint32(1, this->device_id);
#endif
size.add_float(1, this->min_temperature);
size.add_float(1, this->max_temperature);
size.add_float(1, this->target_temperature_step);
if (!this->supported_modes->empty()) {
for (const auto &it : *this->supported_modes) {
size.add_uint32_force(1, static_cast<uint32_t>(it));
}
}
size.add_uint32(1, this->supported_features);
}
void WaterHeaterStateResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_fixed32(1, this->key);
buffer.encode_float(2, this->current_temperature);
buffer.encode_float(3, this->target_temperature);
buffer.encode_uint32(4, static_cast<uint32_t>(this->mode));
#ifdef USE_DEVICES
buffer.encode_uint32(5, this->device_id);
#endif
buffer.encode_uint32(6, this->state);
buffer.encode_float(7, this->target_temperature_low);
buffer.encode_float(8, this->target_temperature_high);
}
void WaterHeaterStateResponse::calculate_size(ProtoSize &size) const {
size.add_fixed32(1, this->key);
size.add_float(1, this->current_temperature);
size.add_float(1, this->target_temperature);
size.add_uint32(1, static_cast<uint32_t>(this->mode));
#ifdef USE_DEVICES
size.add_uint32(1, this->device_id);
#endif
size.add_uint32(1, this->state);
size.add_float(1, this->target_temperature_low);
size.add_float(1, this->target_temperature_high);
}
bool WaterHeaterCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
case 2:
this->has_fields = value.as_uint32();
break;
case 3:
this->mode = static_cast<enums::WaterHeaterMode>(value.as_uint32());
break;
#ifdef USE_DEVICES
case 5:
this->device_id = value.as_uint32();
break;
#endif
case 6:
this->state = value.as_uint32();
break;
default:
return false;
}
return true;
}
bool WaterHeaterCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
switch (field_id) {
case 1:
this->key = value.as_fixed32();
break;
case 4:
this->target_temperature = value.as_float();
break;
case 7:
this->target_temperature_low = value.as_float();
break;
case 8:
this->target_temperature_high = value.as_float();
break;
default:
return false;
}
return true;
}
#endif
#ifdef USE_NUMBER
void ListEntitiesNumberResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_string(1, this->object_id_ref_);

View File

@@ -129,6 +129,25 @@ enum ClimatePreset : uint32_t {
CLIMATE_PRESET_ACTIVITY = 7,
};
#endif
#ifdef USE_WATER_HEATER
enum WaterHeaterMode : uint32_t {
WATER_HEATER_MODE_OFF = 0,
WATER_HEATER_MODE_ECO = 1,
WATER_HEATER_MODE_ELECTRIC = 2,
WATER_HEATER_MODE_PERFORMANCE = 3,
WATER_HEATER_MODE_HIGH_DEMAND = 4,
WATER_HEATER_MODE_HEAT_PUMP = 5,
WATER_HEATER_MODE_GAS = 6,
};
#endif
enum WaterHeaterCommandHasField : uint32_t {
WATER_HEATER_COMMAND_HAS_NONE = 0,
WATER_HEATER_COMMAND_HAS_MODE = 1,
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE = 2,
WATER_HEATER_COMMAND_HAS_STATE = 4,
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW = 8,
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH = 16,
};
#ifdef USE_NUMBER
enum NumberMode : uint32_t {
NUMBER_MODE_AUTO = 0,
@@ -1516,6 +1535,70 @@ class ClimateCommandRequest final : public CommandProtoMessage {
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
#endif
#ifdef USE_WATER_HEATER
class ListEntitiesWaterHeaterResponse final : public InfoResponseProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 132;
static constexpr uint8_t ESTIMATED_SIZE = 63;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_water_heater_response"; }
#endif
float min_temperature{0.0f};
float max_temperature{0.0f};
float target_temperature_step{0.0f};
const water_heater::WaterHeaterModeMask *supported_modes{};
uint32_t supported_features{0};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
};
class WaterHeaterStateResponse final : public StateResponseProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 133;
static constexpr uint8_t ESTIMATED_SIZE = 35;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "water_heater_state_response"; }
#endif
float current_temperature{0.0f};
float target_temperature{0.0f};
enums::WaterHeaterMode mode{};
uint32_t state{0};
float target_temperature_low{0.0f};
float target_temperature_high{0.0f};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
};
class WaterHeaterCommandRequest final : public CommandProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 134;
static constexpr uint8_t ESTIMATED_SIZE = 34;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "water_heater_command_request"; }
#endif
uint32_t has_fields{0};
enums::WaterHeaterMode mode{};
float target_temperature{0.0f};
uint32_t state{0};
float target_temperature_low{0.0f};
float target_temperature_high{0.0f};
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
#endif
#ifdef USE_NUMBER
class ListEntitiesNumberResponse final : public InfoResponseProtoMessage {
public:

View File

@@ -348,6 +348,47 @@ template<> const char *proto_enum_to_string<enums::ClimatePreset>(enums::Climate
}
}
#endif
#ifdef USE_WATER_HEATER
template<> const char *proto_enum_to_string<enums::WaterHeaterMode>(enums::WaterHeaterMode value) {
switch (value) {
case enums::WATER_HEATER_MODE_OFF:
return "WATER_HEATER_MODE_OFF";
case enums::WATER_HEATER_MODE_ECO:
return "WATER_HEATER_MODE_ECO";
case enums::WATER_HEATER_MODE_ELECTRIC:
return "WATER_HEATER_MODE_ELECTRIC";
case enums::WATER_HEATER_MODE_PERFORMANCE:
return "WATER_HEATER_MODE_PERFORMANCE";
case enums::WATER_HEATER_MODE_HIGH_DEMAND:
return "WATER_HEATER_MODE_HIGH_DEMAND";
case enums::WATER_HEATER_MODE_HEAT_PUMP:
return "WATER_HEATER_MODE_HEAT_PUMP";
case enums::WATER_HEATER_MODE_GAS:
return "WATER_HEATER_MODE_GAS";
default:
return "UNKNOWN";
}
}
#endif
template<>
const char *proto_enum_to_string<enums::WaterHeaterCommandHasField>(enums::WaterHeaterCommandHasField value) {
switch (value) {
case enums::WATER_HEATER_COMMAND_HAS_NONE:
return "WATER_HEATER_COMMAND_HAS_NONE";
case enums::WATER_HEATER_COMMAND_HAS_MODE:
return "WATER_HEATER_COMMAND_HAS_MODE";
case enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE:
return "WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE";
case enums::WATER_HEATER_COMMAND_HAS_STATE:
return "WATER_HEATER_COMMAND_HAS_STATE";
case enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW:
return "WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW";
case enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH:
return "WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH";
default:
return "UNKNOWN";
}
}
#ifdef USE_NUMBER
template<> const char *proto_enum_to_string<enums::NumberMode>(enums::NumberMode value) {
switch (value) {
@@ -1398,6 +1439,55 @@ void ClimateCommandRequest::dump_to(std::string &out) const {
#endif
}
#endif
#ifdef USE_WATER_HEATER
void ListEntitiesWaterHeaterResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "ListEntitiesWaterHeaterResponse");
dump_field(out, "object_id", this->object_id_ref_);
dump_field(out, "key", this->key);
dump_field(out, "name", this->name_ref_);
#ifdef USE_ENTITY_ICON
dump_field(out, "icon", this->icon_ref_);
#endif
dump_field(out, "disabled_by_default", this->disabled_by_default);
dump_field(out, "entity_category", static_cast<enums::EntityCategory>(this->entity_category));
#ifdef USE_DEVICES
dump_field(out, "device_id", this->device_id);
#endif
dump_field(out, "min_temperature", this->min_temperature);
dump_field(out, "max_temperature", this->max_temperature);
dump_field(out, "target_temperature_step", this->target_temperature_step);
for (const auto &it : *this->supported_modes) {
dump_field(out, "supported_modes", static_cast<enums::WaterHeaterMode>(it), 4);
}
dump_field(out, "supported_features", this->supported_features);
}
void WaterHeaterStateResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "WaterHeaterStateResponse");
dump_field(out, "key", this->key);
dump_field(out, "current_temperature", this->current_temperature);
dump_field(out, "target_temperature", this->target_temperature);
dump_field(out, "mode", static_cast<enums::WaterHeaterMode>(this->mode));
#ifdef USE_DEVICES
dump_field(out, "device_id", this->device_id);
#endif
dump_field(out, "state", this->state);
dump_field(out, "target_temperature_low", this->target_temperature_low);
dump_field(out, "target_temperature_high", this->target_temperature_high);
}
void WaterHeaterCommandRequest::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "WaterHeaterCommandRequest");
dump_field(out, "key", this->key);
dump_field(out, "has_fields", this->has_fields);
dump_field(out, "mode", static_cast<enums::WaterHeaterMode>(this->mode));
dump_field(out, "target_temperature", this->target_temperature);
#ifdef USE_DEVICES
dump_field(out, "device_id", this->device_id);
#endif
dump_field(out, "state", this->state);
dump_field(out, "target_temperature_low", this->target_temperature_low);
dump_field(out, "target_temperature_high", this->target_temperature_high);
}
#endif
#ifdef USE_NUMBER
void ListEntitiesNumberResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "ListEntitiesNumberResponse");

View File

@@ -10,6 +10,10 @@
#include "esphome/components/climate/climate_traits.h"
#endif
#ifdef USE_WATER_HEATER
#include "esphome/components/water_heater/water_heater.h"
#endif
#ifdef USE_LIGHT
#include "esphome/components/light/light_traits.h"
#endif

View File

@@ -621,6 +621,17 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
this->on_homeassistant_action_response(msg);
break;
}
#endif
#ifdef USE_WATER_HEATER
case WaterHeaterCommandRequest::MESSAGE_TYPE: {
WaterHeaterCommandRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "on_water_heater_command_request: %s", msg.dump().c_str());
#endif
this->on_water_heater_command_request(msg);
break;
}
#endif
default:
break;

View File

@@ -91,6 +91,10 @@ class APIServerConnectionBase : public ProtoService {
virtual void on_climate_command_request(const ClimateCommandRequest &value){};
#endif
#ifdef USE_WATER_HEATER
virtual void on_water_heater_command_request(const WaterHeaterCommandRequest &value){};
#endif
#ifdef USE_NUMBER
virtual void on_number_command_request(const NumberCommandRequest &value){};
#endif

View File

@@ -335,6 +335,10 @@ API_DISPATCH_UPDATE(valve::Valve, valve)
API_DISPATCH_UPDATE(media_player::MediaPlayer, media_player)
#endif
#ifdef USE_WATER_HEATER
API_DISPATCH_UPDATE(water_heater::WaterHeater, water_heater)
#endif
#ifdef USE_EVENT
// Event is a special case - unlike other entities with simple state fields,
// events store their state in a member accessed via obj->get_last_event_type()

View File

@@ -133,6 +133,9 @@ class APIServer : public Component,
#ifdef USE_MEDIA_PLAYER
void on_media_player_update(media_player::MediaPlayer *obj) override;
#endif
#ifdef USE_WATER_HEATER
void on_water_heater_update(water_heater::WaterHeater *obj) override;
#endif
#ifdef USE_API_HOMEASSISTANT_SERVICES
void send_homeassistant_action(const HomeassistantActionRequest &call);

View File

@@ -73,6 +73,9 @@ LIST_ENTITIES_HANDLER(media_player, media_player::MediaPlayer, ListEntitiesMedia
LIST_ENTITIES_HANDLER(alarm_control_panel, alarm_control_panel::AlarmControlPanel,
ListEntitiesAlarmControlPanelResponse)
#endif
#ifdef USE_WATER_HEATER
LIST_ENTITIES_HANDLER(water_heater, water_heater::WaterHeater, ListEntitiesWaterHeaterResponse)
#endif
#ifdef USE_EVENT
LIST_ENTITIES_HANDLER(event, event::Event, ListEntitiesEventResponse)
#endif

View File

@@ -82,6 +82,9 @@ class ListEntitiesIterator : public ComponentIterator {
#ifdef USE_ALARM_CONTROL_PANEL
bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *entity) override;
#endif
#ifdef USE_WATER_HEATER
bool on_water_heater(water_heater::WaterHeater *entity) override;
#endif
#ifdef USE_EVENT
bool on_event(event::Event *entity) override;
#endif

View File

@@ -60,6 +60,9 @@ INITIAL_STATE_HANDLER(media_player, media_player::MediaPlayer)
#ifdef USE_ALARM_CONTROL_PANEL
INITIAL_STATE_HANDLER(alarm_control_panel, alarm_control_panel::AlarmControlPanel)
#endif
#ifdef USE_WATER_HEATER
INITIAL_STATE_HANDLER(water_heater, water_heater::WaterHeater)
#endif
#ifdef USE_UPDATE
INITIAL_STATE_HANDLER(update, update::UpdateEntity)
#endif

View File

@@ -76,6 +76,9 @@ class InitialStateIterator : public ComponentIterator {
#ifdef USE_ALARM_CONTROL_PANEL
bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *entity) override;
#endif
#ifdef USE_WATER_HEATER
bool on_water_heater(water_heater::WaterHeater *entity) override;
#endif
#ifdef USE_EVENT
bool on_event(event::Event *event) override { return true; };
#endif

View File

@@ -0,0 +1,111 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import (
CONF_ENTITY_CATEGORY,
CONF_ICON,
CONF_ID,
CONF_MAX_TEMPERATURE,
CONF_MIN_TEMPERATURE,
CONF_VISUAL,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
from esphome.cpp_generator import MockObjClass
from esphome.types import ConfigType
CODEOWNERS = ["@dhoeben"]
IS_PLATFORM_COMPONENT = True
water_heater_ns = cg.esphome_ns.namespace("water_heater")
WaterHeater = water_heater_ns.class_("WaterHeater", cg.EntityBase, cg.Component)
WaterHeaterCall = water_heater_ns.class_("WaterHeaterCall")
WaterHeaterTraits = water_heater_ns.class_("WaterHeaterTraits")
CONF_TARGET_TEMPERATURE_STEP = "target_temperature_step"
WaterHeaterMode = water_heater_ns.enum("WaterHeaterMode")
WATER_HEATER_MODES = {
"OFF": WaterHeaterMode.WATER_HEATER_MODE_OFF,
"ECO": WaterHeaterMode.WATER_HEATER_MODE_ECO,
"ELECTRIC": WaterHeaterMode.WATER_HEATER_MODE_ELECTRIC,
"PERFORMANCE": WaterHeaterMode.WATER_HEATER_MODE_PERFORMANCE,
"HIGH_DEMAND": WaterHeaterMode.WATER_HEATER_MODE_HIGH_DEMAND,
"HEAT_PUMP": WaterHeaterMode.WATER_HEATER_MODE_HEAT_PUMP,
"GAS": WaterHeaterMode.WATER_HEATER_MODE_GAS,
}
validate_water_heater_mode = cv.enum(WATER_HEATER_MODES, upper=True)
_WATER_HEATER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(
{
cv.Optional(CONF_VISUAL, default={}): cv.Schema(
{
cv.Optional(CONF_MIN_TEMPERATURE): cv.temperature,
cv.Optional(CONF_MAX_TEMPERATURE): cv.temperature,
cv.Optional(CONF_TARGET_TEMPERATURE_STEP): cv.float_,
}
),
}
).extend(cv.COMPONENT_SCHEMA)
_WATER_HEATER_SCHEMA.add_extra(entity_duplicate_validator("water_heater"))
def water_heater_schema(
class_: MockObjClass,
*,
icon: str = cv.UNDEFINED,
entity_category: str = cv.UNDEFINED,
) -> cv.Schema:
schema = {cv.GenerateID(): cv.declare_id(class_)}
for key, default, validator in [
(CONF_ICON, icon, cv.icon),
(CONF_ENTITY_CATEGORY, entity_category, cv.entity_category),
]:
if default is not cv.UNDEFINED:
schema[cv.Optional(key, default=default)] = validator
return _WATER_HEATER_SCHEMA.extend(schema)
async def setup_water_heater_core_(var: cg.Pvariable, config: ConfigType) -> None:
"""Set up the core water heater properties in C++."""
await setup_entity(var, config, "water_heater")
visual = config[CONF_VISUAL]
if (min_temp := visual.get(CONF_MIN_TEMPERATURE)) is not None:
cg.add_define("USE_WATER_HEATER_VISUAL_OVERRIDES")
cg.add(var.set_visual_min_temperature_override(min_temp))
if (max_temp := visual.get(CONF_MAX_TEMPERATURE)) is not None:
cg.add_define("USE_WATER_HEATER_VISUAL_OVERRIDES")
cg.add(var.set_visual_max_temperature_override(max_temp))
if (temp_step := visual.get(CONF_TARGET_TEMPERATURE_STEP)) is not None:
cg.add_define("USE_WATER_HEATER_VISUAL_OVERRIDES")
cg.add(var.set_visual_target_temperature_step_override(temp_step))
async def register_water_heater(var: cg.Pvariable, config: ConfigType) -> cg.Pvariable:
if not CORE.has_id(config[CONF_ID]):
var = cg.Pvariable(config[CONF_ID], var)
cg.add_define("USE_WATER_HEATER")
await cg.register_component(var, config)
cg.add(cg.App.register_water_heater(var))
CORE.register_platform_component("water_heater", var)
await setup_water_heater_core_(var, config)
return var
async def new_water_heater(config: ConfigType, *args) -> cg.Pvariable:
var = cg.new_Pvariable(config[CONF_ID], *args)
await register_water_heater(var, config)
return var
@coroutine_with_priority(CoroPriority.CORE)
async def to_code(config: ConfigType) -> None:
cg.add_global(water_heater_ns.using)

View File

@@ -0,0 +1,281 @@
#include "water_heater.h"
#include "esphome/core/log.h"
#include "esphome/core/application.h"
#include "esphome/core/controller_registry.h"
#include <cmath>
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 std::string &mode) {
if (str_equals_case_insensitive(mode, "OFF")) {
this->set_mode(WATER_HEATER_MODE_OFF);
} else if (str_equals_case_insensitive(mode, "ECO")) {
this->set_mode(WATER_HEATER_MODE_ECO);
} else if (str_equals_case_insensitive(mode, "ELECTRIC")) {
this->set_mode(WATER_HEATER_MODE_ELECTRIC);
} else if (str_equals_case_insensitive(mode, "PERFORMANCE")) {
this->set_mode(WATER_HEATER_MODE_PERFORMANCE);
} else if (str_equals_case_insensitive(mode, "HIGH_DEMAND")) {
this->set_mode(WATER_HEATER_MODE_HIGH_DEMAND);
} else if (str_equals_case_insensitive(mode, "HEAT_PUMP")) {
this->set_mode(WATER_HEATER_MODE_HEAT_PUMP);
} else if (str_equals_case_insensitive(mode, "GAS")) {
this->set_mode(WATER_HEATER_MODE_GAS);
} else {
ESP_LOGW(TAG, "'%s' - Unrecognized mode %s", this->parent_->get_name().c_str(), mode.c_str());
}
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;
}
return *this;
}
WaterHeaterCall &WaterHeaterCall::set_on(bool on) {
if (on) {
this->state_ |= WATER_HEATER_STATE_ON;
} else {
this->state_ &= ~WATER_HEATER_STATE_ON;
}
return *this;
}
void WaterHeaterCall::perform() {
ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str());
this->validate_();
if (this->mode_.has_value()) {
ESP_LOGD(TAG, " Mode: %s", LOG_STR_ARG(water_heater_mode_to_string(*this->mode_)));
}
if (!std::isnan(this->target_temperature_)) {
ESP_LOGD(TAG, " Target Temperature: %.2f", this->target_temperature_);
}
if (!std::isnan(this->target_temperature_low_)) {
ESP_LOGD(TAG, " Target Temperature Low: %.2f", this->target_temperature_low_);
}
if (!std::isnan(this->target_temperature_high_)) {
ESP_LOGD(TAG, " Target Temperature High: %.2f", this->target_temperature_high_);
}
if (this->state_ & WATER_HEATER_STATE_AWAY) {
ESP_LOGD(TAG, " Away: YES");
}
if (this->state_ & WATER_HEATER_STATE_ON) {
ESP_LOGD(TAG, " On: YES");
}
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 %d not supported", this->parent_->get_name().c_str(), *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 ((this->state_ & WATER_HEATER_STATE_AWAY) && !traits.get_supports_away_mode()) {
ESP_LOGW(TAG, "'%s' - Away mode not supported", this->parent_->get_name().c_str());
this->state_ &= ~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;
}
}
void WaterHeater::setup() {
this->pref_ = global_preferences->make_preference<SavedWaterHeaterState>(this->get_preference_hash());
}
void WaterHeater::publish_state() {
auto traits = this->get_traits();
ESP_LOGD(TAG, "'%s' - Sending state:", this->name_.c_str());
ESP_LOGD(TAG, " Mode: %s", LOG_STR_ARG(water_heater_mode_to_string(this->mode_)));
if (!std::isnan(this->current_temperature_)) {
ESP_LOGD(TAG, " Current Temperature: %.2f°C", this->current_temperature_);
}
if (traits.get_supports_two_point_target_temperature()) {
ESP_LOGD(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_LOGD(TAG, " Target Temperature: %.2f°C", this->target_temperature_);
}
if (this->state_ & WATER_HEATER_STATE_AWAY) {
ESP_LOGD(TAG, " Away: YES");
}
if (traits.has_feature_flags(WATER_HEATER_SUPPORTS_ON_OFF)) {
ESP_LOGD(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<WaterHeaterCall> WaterHeater::restore_state() {
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
const LogString *water_heater_mode_to_string(WaterHeaterMode mode) {
switch (mode) {
case WATER_HEATER_MODE_OFF:
return LOG_STR("OFF");
case WATER_HEATER_MODE_ECO:
return LOG_STR("ECO");
case WATER_HEATER_MODE_ELECTRIC:
return LOG_STR("ELECTRIC");
case WATER_HEATER_MODE_PERFORMANCE:
return LOG_STR("PERFORMANCE");
case WATER_HEATER_MODE_HIGH_DEMAND:
return LOG_STR("HIGH_DEMAND");
case WATER_HEATER_MODE_HEAT_PUMP:
return LOG_STR("HEAT_PUMP");
case WATER_HEATER_MODE_GAS:
return LOG_STR("GAS");
default:
return LOG_STR("UNKNOWN");
}
}
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

View File

@@ -0,0 +1,259 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/entity_base.h"
#include "esphome/core/finite_set_mask.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/preferences.h"
namespace esphome::water_heater {
class WaterHeater;
struct WaterHeaterCallInternal;
void log_water_heater(const char *tag, const char *prefix, const char *type, WaterHeater *obj);
#define LOG_WATER_HEATER(prefix, type, obj) log_water_heater(TAG, prefix, LOG_STR_LITERAL(type), obj)
enum WaterHeaterMode : uint32_t {
WATER_HEATER_MODE_OFF = 0,
WATER_HEATER_MODE_ECO = 1,
WATER_HEATER_MODE_ELECTRIC = 2,
WATER_HEATER_MODE_PERFORMANCE = 3,
WATER_HEATER_MODE_HIGH_DEMAND = 4,
WATER_HEATER_MODE_HEAT_PUMP = 5,
WATER_HEATER_MODE_GAS = 6,
};
// Type alias for water heater mode bitmask
// Replaces std::set<WaterHeaterMode> to eliminate red-black tree overhead
using WaterHeaterModeMask =
FiniteSetMask<WaterHeaterMode, DefaultBitPolicy<WaterHeaterMode, WATER_HEATER_MODE_GAS + 1>>;
/// Feature flags for water heater capabilities (matches Home Assistant WaterHeaterEntityFeature)
enum WaterHeaterFeature : uint32_t {
/// The water heater supports reporting the current temperature.
WATER_HEATER_SUPPORTS_CURRENT_TEMPERATURE = 1 << 0,
/// The water heater supports a target temperature.
WATER_HEATER_SUPPORTS_TARGET_TEMPERATURE = 1 << 1,
/// The water heater supports operation mode selection.
WATER_HEATER_SUPPORTS_OPERATION_MODE = 1 << 2,
/// The water heater supports an away/vacation mode.
WATER_HEATER_SUPPORTS_AWAY_MODE = 1 << 3,
/// The water heater can be turned on/off.
WATER_HEATER_SUPPORTS_ON_OFF = 1 << 4,
/// The water heater supports two-point target temperature (low/high range).
WATER_HEATER_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE = 1 << 5,
};
/// State flags for water heater current state (bitmask)
enum WaterHeaterStateFlag : uint32_t {
/// Away/vacation mode is currently active
WATER_HEATER_STATE_AWAY = 1 << 0,
/// Water heater is on (not in standby)
WATER_HEATER_STATE_ON = 1 << 1,
};
struct SavedWaterHeaterState {
WaterHeaterMode mode;
union {
float target_temperature;
struct {
float target_temperature_low;
float target_temperature_high;
};
} __attribute__((packed));
uint32_t state;
} __attribute__((packed));
class WaterHeaterCall {
friend struct WaterHeaterCallInternal;
public:
WaterHeaterCall() : parent_(nullptr) {}
WaterHeaterCall(WaterHeater *parent);
WaterHeaterCall &set_mode(WaterHeaterMode mode);
WaterHeaterCall &set_mode(const std::string &mode);
WaterHeaterCall &set_target_temperature(float temperature);
WaterHeaterCall &set_target_temperature_low(float temperature);
WaterHeaterCall &set_target_temperature_high(float temperature);
WaterHeaterCall &set_away(bool away);
WaterHeaterCall &set_on(bool on);
void perform();
const optional<WaterHeaterMode> &get_mode() const { return this->mode_; }
float get_target_temperature() const { return this->target_temperature_; }
float get_target_temperature_low() const { return this->target_temperature_low_; }
float get_target_temperature_high() const { return this->target_temperature_high_; }
/// Get state flags value
uint32_t get_state() const { return this->state_; }
protected:
void validate_();
WaterHeater *parent_;
optional<WaterHeaterMode> mode_;
float target_temperature_{NAN};
float target_temperature_low_{NAN};
float target_temperature_high_{NAN};
uint32_t state_{0};
};
struct WaterHeaterCallInternal : public WaterHeaterCall {
WaterHeaterCallInternal(WaterHeater *parent) : WaterHeaterCall(parent) {}
WaterHeaterCallInternal &set_from_restore(const WaterHeaterCall &restore) {
this->mode_ = restore.mode_;
this->target_temperature_ = restore.target_temperature_;
this->target_temperature_low_ = restore.target_temperature_low_;
this->target_temperature_high_ = restore.target_temperature_high_;
this->state_ = restore.state_;
return *this;
}
};
class WaterHeaterTraits {
public:
/// Get/set feature flags (see WaterHeaterFeature enum)
void add_feature_flags(uint32_t flags) { this->feature_flags_ |= flags; }
void clear_feature_flags(uint32_t flags) { this->feature_flags_ &= ~flags; }
bool has_feature_flags(uint32_t flags) const { return (this->feature_flags_ & flags) == flags; }
uint32_t get_feature_flags() const { return this->feature_flags_; }
bool get_supports_current_temperature() const {
return this->has_feature_flags(WATER_HEATER_SUPPORTS_CURRENT_TEMPERATURE);
}
void set_supports_current_temperature(bool supports) {
if (supports) {
this->add_feature_flags(WATER_HEATER_SUPPORTS_CURRENT_TEMPERATURE);
} else {
this->clear_feature_flags(WATER_HEATER_SUPPORTS_CURRENT_TEMPERATURE);
}
}
bool get_supports_away_mode() const { return this->has_feature_flags(WATER_HEATER_SUPPORTS_AWAY_MODE); }
void set_supports_away_mode(bool supports) {
if (supports) {
this->add_feature_flags(WATER_HEATER_SUPPORTS_AWAY_MODE);
} else {
this->clear_feature_flags(WATER_HEATER_SUPPORTS_AWAY_MODE);
}
}
bool get_supports_two_point_target_temperature() const {
return this->has_feature_flags(WATER_HEATER_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE);
}
void set_supports_two_point_target_temperature(bool supports) {
if (supports) {
this->add_feature_flags(WATER_HEATER_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE);
} else {
this->clear_feature_flags(WATER_HEATER_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE);
}
}
void set_min_temperature(float min_temperature) { this->min_temperature_ = min_temperature; }
float get_min_temperature() const { return this->min_temperature_; }
void set_max_temperature(float max_temperature) { this->max_temperature_ = max_temperature; }
float get_max_temperature() const { return this->max_temperature_; }
void set_target_temperature_step(float target_temperature_step) {
this->target_temperature_step_ = target_temperature_step;
}
float get_target_temperature_step() const { return this->target_temperature_step_; }
void set_supported_modes(WaterHeaterModeMask modes) { this->supported_modes_ = modes; }
const WaterHeaterModeMask &get_supported_modes() const { return this->supported_modes_; }
bool supports_mode(WaterHeaterMode mode) const { return this->supported_modes_.count(mode); }
protected:
// Ordered to minimize padding: 4-byte members first
uint32_t feature_flags_{0};
float min_temperature_{0.0f};
float max_temperature_{0.0f};
float target_temperature_step_{0.0f};
WaterHeaterModeMask supported_modes_;
};
class WaterHeater : public EntityBase, public Component {
public:
WaterHeaterMode get_mode() const { return this->mode_; }
float get_current_temperature() const { return this->current_temperature_; }
float get_target_temperature() const { return this->target_temperature_; }
float get_target_temperature_low() const { return this->target_temperature_low_; }
float get_target_temperature_high() const { return this->target_temperature_high_; }
/// Get the current state flags bitmask
uint32_t get_state() const { return this->state_; }
/// Check if away mode is currently active
bool is_away() const { return (this->state_ & WATER_HEATER_STATE_AWAY) != 0; }
/// Check if the water heater is on
bool is_on() const { return (this->state_ & WATER_HEATER_STATE_ON) != 0; }
void set_current_temperature(float current_temperature) { this->current_temperature_ = current_temperature; }
virtual void publish_state();
virtual WaterHeaterTraits get_traits();
virtual WaterHeaterCallInternal make_call() = 0;
#ifdef USE_WATER_HEATER_VISUAL_OVERRIDES
void set_visual_min_temperature_override(float min_temperature_override);
void set_visual_max_temperature_override(float max_temperature_override);
void set_visual_target_temperature_step_override(float visual_target_temperature_step_override);
#endif
virtual void control(const WaterHeaterCall &call) = 0;
void setup() override;
optional<WaterHeaterCall> restore_state();
protected:
virtual WaterHeaterTraits traits() = 0;
/// Log the traits of this water heater for dump_config().
void dump_traits_(const char *tag);
/// Set the mode of the water heater. Should only be called from control().
void set_mode_(WaterHeaterMode mode) { this->mode_ = mode; }
/// Set the target temperature of the water heater. Should only be called from control().
void set_target_temperature_(float target_temperature) { this->target_temperature_ = target_temperature; }
/// Set the low target temperature (for two-point control). Should only be called from control().
void set_target_temperature_low_(float target_temperature_low) {
this->target_temperature_low_ = target_temperature_low;
}
/// Set the high target temperature (for two-point control). Should only be called from control().
void set_target_temperature_high_(float target_temperature_high) {
this->target_temperature_high_ = target_temperature_high;
}
/// Set the state flags. Should only be called from control().
void set_state_(uint32_t state) { this->state_ = state; }
/// Set or clear a state flag. Should only be called from control().
void set_state_flag_(uint32_t flag, bool value) {
if (value) {
this->state_ |= flag;
} else {
this->state_ &= ~flag;
}
}
WaterHeaterMode mode_{WATER_HEATER_MODE_OFF};
float current_temperature_{NAN};
float target_temperature_{NAN};
float target_temperature_low_{NAN};
float target_temperature_high_{NAN};
uint32_t state_{0}; // Bitmask of WaterHeaterStateFlag
#ifdef USE_WATER_HEATER_VISUAL_OVERRIDES
float visual_min_temperature_override_{NAN};
float visual_max_temperature_override_{NAN};
float visual_target_temperature_step_override_{NAN};
#endif
ESPPreferenceObject pref_;
};
/// Convert the given WaterHeaterMode to a human-readable string for logging.
const LogString *water_heater_mode_to_string(WaterHeaterMode mode);
} // namespace esphome::water_heater

View File

@@ -135,6 +135,13 @@ bool ListEntitiesIterator::on_alarm_control_panel(alarm_control_panel::AlarmCont
}
#endif
#ifdef USE_WATER_HEATER
bool ListEntitiesIterator::on_water_heater(water_heater::WaterHeater *obj) {
// Water heater web_server support not yet implemented - this stub acknowledges the entity
return true;
}
#endif
#ifdef USE_EVENT
bool ListEntitiesIterator::on_event(event::Event *obj) {
// Null event type, since we are just iterating over entities

View File

@@ -79,6 +79,9 @@ class ListEntitiesIterator : public ComponentIterator {
#ifdef USE_ALARM_CONTROL_PANEL
bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *obj) override;
#endif
#ifdef USE_WATER_HEATER
bool on_water_heater(water_heater::WaterHeater *obj) override;
#endif
#ifdef USE_EVENT
bool on_event(event::Event *obj) override;
#endif

View File

@@ -1086,6 +1086,7 @@ CONF_WARM_WHITE = "warm_white"
CONF_WARM_WHITE_COLOR_TEMPERATURE = "warm_white_color_temperature"
CONF_WATCHDOG_THRESHOLD = "watchdog_threshold"
CONF_WATCHDOG_TIMEOUT = "watchdog_timeout"
CONF_WATER_HEATER = "water_heater"
CONF_WEB_SERVER = "web_server"
CONF_WEB_SERVER_ID = "web_server_id"
CONF_WEIGHT = "weight"
@@ -1179,6 +1180,7 @@ ICON_TIMELAPSE = "mdi:timelapse"
ICON_TIMER = "mdi:timer-outline"
ICON_VIBRATE = "mdi:vibrate"
ICON_WATER = "mdi:water"
ICON_WATER_HEATER = "mdi:water-boiler"
ICON_WATER_PERCENT = "mdi:water-percent"
ICON_WEATHER_SUNSET = "mdi:weather-sunset"
ICON_WEATHER_SUNSET_DOWN = "mdi:weather-sunset-down"

View File

@@ -87,6 +87,9 @@
#ifdef USE_ALARM_CONTROL_PANEL
#include "esphome/components/alarm_control_panel/alarm_control_panel.h"
#endif
#ifdef USE_WATER_HEATER
#include "esphome/components/water_heater/water_heater.h"
#endif
#ifdef USE_EVENT
#include "esphome/components/event/event.h"
#endif
@@ -217,6 +220,10 @@ class Application {
}
#endif
#ifdef USE_WATER_HEATER
void register_water_heater(water_heater::WaterHeater *water_heater) { this->water_heaters_.push_back(water_heater); }
#endif
#ifdef USE_EVENT
void register_event(event::Event *event) { this->events_.push_back(event); }
#endif
@@ -437,6 +444,11 @@ class Application {
GET_ENTITY_METHOD(alarm_control_panel::AlarmControlPanel, alarm_control_panel, alarm_control_panels)
#endif
#ifdef USE_WATER_HEATER
auto &get_water_heaters() const { return this->water_heaters_; }
GET_ENTITY_METHOD(water_heater::WaterHeater, water_heater, water_heaters)
#endif
#ifdef USE_EVENT
auto &get_events() const { return this->events_; }
GET_ENTITY_METHOD(event::Event, event, events)
@@ -634,6 +646,9 @@ class Application {
StaticVector<alarm_control_panel::AlarmControlPanel *, ESPHOME_ENTITY_ALARM_CONTROL_PANEL_COUNT>
alarm_control_panels_{};
#endif
#ifdef USE_WATER_HEATER
StaticVector<water_heater::WaterHeater *, ESPHOME_ENTITY_WATER_HEATER_COUNT> water_heaters_{};
#endif
#ifdef USE_UPDATE
StaticVector<update::UpdateEntity *, ESPHOME_ENTITY_UPDATE_COUNT> updates_{};
#endif

View File

@@ -163,6 +163,12 @@ void ComponentIterator::advance() {
break;
#endif
#ifdef USE_WATER_HEATER
case IteratorState::WATER_HEATER:
this->process_platform_item_(App.get_water_heaters(), &ComponentIterator::on_water_heater);
break;
#endif
#ifdef USE_EVENT
case IteratorState::EVENT:
this->process_platform_item_(App.get_events(), &ComponentIterator::on_event);

View File

@@ -84,6 +84,9 @@ class ComponentIterator {
#ifdef USE_ALARM_CONTROL_PANEL
virtual bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) = 0;
#endif
#ifdef USE_WATER_HEATER
virtual bool on_water_heater(water_heater::WaterHeater *water_heater) = 0;
#endif
#ifdef USE_EVENT
virtual bool on_event(event::Event *event) = 0;
#endif
@@ -161,6 +164,9 @@ class ComponentIterator {
#ifdef USE_ALARM_CONTROL_PANEL
ALARM_CONTROL_PANEL,
#endif
#ifdef USE_WATER_HEATER
WATER_HEATER,
#endif
#ifdef USE_EVENT
EVENT,
#endif

View File

@@ -58,6 +58,9 @@
#ifdef USE_ALARM_CONTROL_PANEL
#include "esphome/components/alarm_control_panel/alarm_control_panel.h"
#endif
#ifdef USE_WATER_HEATER
#include "esphome/components/water_heater/water_heater.h"
#endif
#ifdef USE_EVENT
#include "esphome/components/event/event.h"
#endif
@@ -123,6 +126,9 @@ class Controller {
#ifdef USE_ALARM_CONTROL_PANEL
virtual void on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj){};
#endif
#ifdef USE_WATER_HEATER
virtual void on_water_heater_update(water_heater::WaterHeater *obj){};
#endif
#ifdef USE_EVENT
virtual void on_event(event::Event *obj){};
#endif

View File

@@ -98,6 +98,10 @@ CONTROLLER_REGISTRY_NOTIFY(media_player::MediaPlayer, media_player)
CONTROLLER_REGISTRY_NOTIFY(alarm_control_panel::AlarmControlPanel, alarm_control_panel)
#endif
#ifdef USE_WATER_HEATER
CONTROLLER_REGISTRY_NOTIFY(water_heater::WaterHeater, water_heater)
#endif
#ifdef USE_EVENT
CONTROLLER_REGISTRY_NOTIFY_NO_UPDATE_SUFFIX(event::Event, event)
#endif

View File

@@ -119,6 +119,12 @@ class AlarmControlPanel;
}
#endif
#ifdef USE_WATER_HEATER
namespace water_heater {
class WaterHeater;
}
#endif
#ifdef USE_EVENT
namespace event {
class Event;
@@ -228,6 +234,10 @@ class ControllerRegistry {
static void notify_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj);
#endif
#ifdef USE_WATER_HEATER
static void notify_water_heater_update(water_heater::WaterHeater *obj);
#endif
#ifdef USE_EVENT
static void notify_event(event::Event *obj);
#endif

View File

@@ -113,6 +113,8 @@
#define USE_UART_WAKE_LOOP_ON_RX
#define USE_UPDATE
#define USE_VALVE
#define USE_WATER_HEATER
#define USE_WATER_HEATER_VISUAL_OVERRIDES
#define USE_ZWAVE_PROXY
// Feature flags which do not work for zephyr
@@ -337,3 +339,4 @@
#define ESPHOME_ENTITY_TIME_COUNT 1
#define ESPHOME_ENTITY_UPDATE_COUNT 1
#define ESPHOME_ENTITY_VALVE_COUNT 1
#define ESPHOME_ENTITY_WATER_HEATER_COUNT 1