mirror of
https://github.com/esphome/esphome.git
synced 2026-02-18 15:35:59 -07:00
Merge branch 'dev' into wh_template
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -91,6 +91,10 @@ venv-*/
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
|
||||
# nix
|
||||
/default.nix
|
||||
/shell.nix
|
||||
|
||||
.pioenvs
|
||||
.piolibdeps
|
||||
.pio
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -824,9 +824,9 @@ message HomeAssistantStateResponse {
|
||||
option (no_delay) = true;
|
||||
option (ifdef) = "USE_API_HOMEASSISTANT_STATES";
|
||||
|
||||
string entity_id = 1;
|
||||
string state = 2;
|
||||
string attribute = 3;
|
||||
string entity_id = 1 [(pointer_to_buffer) = true];
|
||||
string state = 2 [(pointer_to_buffer) = true];
|
||||
string attribute = 3 [(pointer_to_buffer) = true];
|
||||
}
|
||||
|
||||
// ==================== IMPORT TIME ====================
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
@@ -1582,15 +1636,29 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) {
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
void APIConnection::on_home_assistant_state_response(const HomeAssistantStateResponse &msg) {
|
||||
for (auto &it : this->parent_->get_state_subs()) {
|
||||
// Compare entity_id and attribute with message fields
|
||||
bool entity_match = (strcmp(it.entity_id, msg.entity_id.c_str()) == 0);
|
||||
bool attribute_match = (it.attribute != nullptr && strcmp(it.attribute, msg.attribute.c_str()) == 0) ||
|
||||
(it.attribute == nullptr && msg.attribute.empty());
|
||||
// Skip if entity_id is empty (invalid message)
|
||||
if (msg.entity_id_len == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (entity_match && attribute_match) {
|
||||
it.callback(msg.state);
|
||||
for (auto &it : this->parent_->get_state_subs()) {
|
||||
// Compare entity_id: check length matches and content matches
|
||||
size_t entity_id_len = strlen(it.entity_id);
|
||||
if (entity_id_len != msg.entity_id_len || memcmp(it.entity_id, msg.entity_id, msg.entity_id_len) != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Compare attribute: either both have matching attribute, or both have none
|
||||
size_t sub_attr_len = it.attribute != nullptr ? strlen(it.attribute) : 0;
|
||||
if (sub_attr_len != msg.attribute_len ||
|
||||
(sub_attr_len > 0 && memcmp(it.attribute, msg.attribute, sub_attr_len) != 0)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create temporary string for callback (callback takes const std::string &)
|
||||
// Handle empty state (nullptr with len=0)
|
||||
std::string state(msg.state_len > 0 ? reinterpret_cast<const char *>(msg.state) : "", msg.state_len);
|
||||
it.callback(state);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -966,15 +966,24 @@ void SubscribeHomeAssistantStateResponse::calculate_size(ProtoSize &size) const
|
||||
}
|
||||
bool HomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||
switch (field_id) {
|
||||
case 1:
|
||||
this->entity_id = value.as_string();
|
||||
case 1: {
|
||||
// Use raw data directly to avoid allocation
|
||||
this->entity_id = value.data();
|
||||
this->entity_id_len = value.size();
|
||||
break;
|
||||
case 2:
|
||||
this->state = value.as_string();
|
||||
}
|
||||
case 2: {
|
||||
// Use raw data directly to avoid allocation
|
||||
this->state = value.data();
|
||||
this->state_len = value.size();
|
||||
break;
|
||||
case 3:
|
||||
this->attribute = value.as_string();
|
||||
}
|
||||
case 3: {
|
||||
// Use raw data directly to avoid allocation
|
||||
this->attribute = value.data();
|
||||
this->attribute_len = value.size();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -1438,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_);
|
||||
|
||||
@@ -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,
|
||||
@@ -1203,13 +1222,16 @@ class SubscribeHomeAssistantStateResponse final : public ProtoMessage {
|
||||
class HomeAssistantStateResponse final : public ProtoDecodableMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 40;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 27;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 57;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "home_assistant_state_response"; }
|
||||
#endif
|
||||
std::string entity_id{};
|
||||
std::string state{};
|
||||
std::string attribute{};
|
||||
const uint8_t *entity_id{nullptr};
|
||||
uint16_t entity_id_len{0};
|
||||
const uint8_t *state{nullptr};
|
||||
uint16_t state_len{0};
|
||||
const uint8_t *attribute{nullptr};
|
||||
uint16_t attribute_len{0};
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -1513,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:
|
||||
|
||||
@@ -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) {
|
||||
@@ -1184,9 +1225,15 @@ void SubscribeHomeAssistantStateResponse::dump_to(std::string &out) const {
|
||||
}
|
||||
void HomeAssistantStateResponse::dump_to(std::string &out) const {
|
||||
MessageDumpHelper helper(out, "HomeAssistantStateResponse");
|
||||
dump_field(out, "entity_id", this->entity_id);
|
||||
dump_field(out, "state", this->state);
|
||||
dump_field(out, "attribute", this->attribute);
|
||||
out.append(" entity_id: ");
|
||||
out.append(format_hex_pretty(this->entity_id, this->entity_id_len));
|
||||
out.append("\n");
|
||||
out.append(" state: ");
|
||||
out.append(format_hex_pretty(this->state, this->state_len));
|
||||
out.append("\n");
|
||||
out.append(" attribute: ");
|
||||
out.append(format_hex_pretty(this->attribute, this->attribute_len));
|
||||
out.append("\n");
|
||||
}
|
||||
#endif
|
||||
void GetTimeRequest::dump_to(std::string &out) const { out.append("GetTimeRequest {}"); }
|
||||
@@ -1392,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");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -160,41 +160,63 @@ HYST_LEVEL = {
|
||||
"High": HystLevel.HYST_LEVEL_HIGH,
|
||||
}
|
||||
|
||||
# Config key -> Validator mapping
|
||||
# Optional settings to generate setter calls for
|
||||
CONFIG_MAP = {
|
||||
CONF_OUTPUT_POWER: cv.float_range(min=-30.0, max=11.0),
|
||||
CONF_RX_ATTENUATION: cv.enum(RX_ATTENUATION, upper=False),
|
||||
CONF_DC_BLOCKING_FILTER: cv.boolean,
|
||||
CONF_FREQUENCY: cv.All(cv.frequency, cv.float_range(min=300.0e6, max=928.0e6)),
|
||||
CONF_IF_FREQUENCY: cv.All(cv.frequency, cv.float_range(min=25000, max=788000)),
|
||||
CONF_FILTER_BANDWIDTH: cv.All(cv.frequency, cv.float_range(min=58000, max=812000)),
|
||||
CONF_CHANNEL: cv.uint8_t,
|
||||
CONF_CHANNEL_SPACING: cv.All(cv.frequency, cv.float_range(min=25000, max=405000)),
|
||||
CONF_FSK_DEVIATION: cv.All(cv.frequency, cv.float_range(min=1500, max=381000)),
|
||||
CONF_MSK_DEVIATION: cv.int_range(min=1, max=8),
|
||||
CONF_SYMBOL_RATE: cv.float_range(min=600, max=500000),
|
||||
CONF_SYNC_MODE: cv.enum(SYNC_MODE, upper=False),
|
||||
CONF_CARRIER_SENSE_ABOVE_THRESHOLD: cv.boolean,
|
||||
CONF_MODULATION_TYPE: cv.enum(MODULATION, upper=False),
|
||||
CONF_MANCHESTER: cv.boolean,
|
||||
CONF_NUM_PREAMBLE: cv.int_range(min=0, max=7),
|
||||
CONF_SYNC1: cv.hex_uint8_t,
|
||||
CONF_SYNC0: cv.hex_uint8_t,
|
||||
CONF_MAGN_TARGET: cv.enum(MAGN_TARGET, upper=False),
|
||||
CONF_MAX_LNA_GAIN: cv.enum(MAX_LNA_GAIN, upper=False),
|
||||
CONF_MAX_DVGA_GAIN: cv.enum(MAX_DVGA_GAIN, upper=False),
|
||||
CONF_CARRIER_SENSE_ABS_THR: cv.int_range(min=-8, max=7),
|
||||
CONF_CARRIER_SENSE_REL_THR: cv.enum(CARRIER_SENSE_REL_THR, upper=False),
|
||||
CONF_LNA_PRIORITY: cv.boolean,
|
||||
CONF_FILTER_LENGTH_FSK_MSK: cv.enum(FILTER_LENGTH_FSK_MSK, upper=False),
|
||||
CONF_FILTER_LENGTH_ASK_OOK: cv.enum(FILTER_LENGTH_ASK_OOK, upper=False),
|
||||
CONF_FREEZE: cv.enum(FREEZE, upper=False),
|
||||
CONF_WAIT_TIME: cv.enum(WAIT_TIME, upper=False),
|
||||
CONF_HYST_LEVEL: cv.enum(HYST_LEVEL, upper=False),
|
||||
CONF_PACKET_MODE: cv.boolean,
|
||||
CONF_PACKET_LENGTH: cv.uint8_t,
|
||||
CONF_CRC_ENABLE: cv.boolean,
|
||||
CONF_WHITENING: cv.boolean,
|
||||
cv.Optional(CONF_OUTPUT_POWER, default=10): cv.float_range(min=-30.0, max=11.0),
|
||||
cv.Optional(CONF_RX_ATTENUATION, default="0dB"): cv.enum(
|
||||
RX_ATTENUATION, upper=False
|
||||
),
|
||||
cv.Optional(CONF_DC_BLOCKING_FILTER, default=True): cv.boolean,
|
||||
cv.Optional(CONF_FREQUENCY, default="433.92MHz"): cv.All(
|
||||
cv.frequency, cv.float_range(min=300.0e6, max=928.0e6)
|
||||
),
|
||||
cv.Optional(CONF_IF_FREQUENCY, default="153kHz"): cv.All(
|
||||
cv.frequency, cv.float_range(min=25000, max=788000)
|
||||
),
|
||||
cv.Optional(CONF_FILTER_BANDWIDTH, default="203kHz"): cv.All(
|
||||
cv.frequency, cv.float_range(min=58000, max=812000)
|
||||
),
|
||||
cv.Optional(CONF_CHANNEL, default=0): cv.uint8_t,
|
||||
cv.Optional(CONF_CHANNEL_SPACING, default="200kHz"): cv.All(
|
||||
cv.frequency, cv.float_range(min=25000, max=405000)
|
||||
),
|
||||
cv.Optional(CONF_FSK_DEVIATION): cv.All(
|
||||
cv.frequency, cv.float_range(min=1500, max=381000)
|
||||
),
|
||||
cv.Optional(CONF_MSK_DEVIATION): cv.int_range(min=1, max=8),
|
||||
cv.Optional(CONF_SYMBOL_RATE, default=5000): cv.float_range(min=600, max=500000),
|
||||
cv.Optional(CONF_SYNC_MODE, default="16/16"): cv.enum(SYNC_MODE, upper=False),
|
||||
cv.Optional(CONF_CARRIER_SENSE_ABOVE_THRESHOLD, default=False): cv.boolean,
|
||||
cv.Optional(CONF_MODULATION_TYPE, default="ASK/OOK"): cv.enum(
|
||||
MODULATION, upper=False
|
||||
),
|
||||
cv.Optional(CONF_MANCHESTER, default=False): cv.boolean,
|
||||
cv.Optional(CONF_NUM_PREAMBLE, default=2): cv.int_range(min=0, max=7),
|
||||
cv.Optional(CONF_SYNC1, default=0xD3): cv.hex_uint8_t,
|
||||
cv.Optional(CONF_SYNC0, default=0x91): cv.hex_uint8_t,
|
||||
cv.Optional(CONF_MAGN_TARGET, default="42dB"): cv.enum(MAGN_TARGET, upper=False),
|
||||
cv.Optional(CONF_MAX_LNA_GAIN, default="Default"): cv.enum(
|
||||
MAX_LNA_GAIN, upper=False
|
||||
),
|
||||
cv.Optional(CONF_MAX_DVGA_GAIN, default="-3"): cv.enum(MAX_DVGA_GAIN, upper=False),
|
||||
cv.Optional(CONF_CARRIER_SENSE_ABS_THR): cv.int_range(min=-8, max=7),
|
||||
cv.Optional(CONF_CARRIER_SENSE_REL_THR): cv.enum(
|
||||
CARRIER_SENSE_REL_THR, upper=False
|
||||
),
|
||||
cv.Optional(CONF_LNA_PRIORITY, default=False): cv.boolean,
|
||||
cv.Optional(CONF_FILTER_LENGTH_FSK_MSK): cv.enum(
|
||||
FILTER_LENGTH_FSK_MSK, upper=False
|
||||
),
|
||||
cv.Optional(CONF_FILTER_LENGTH_ASK_OOK): cv.enum(
|
||||
FILTER_LENGTH_ASK_OOK, upper=False
|
||||
),
|
||||
cv.Optional(CONF_FREEZE): cv.enum(FREEZE, upper=False),
|
||||
cv.Optional(CONF_WAIT_TIME, default="32"): cv.enum(WAIT_TIME, upper=False),
|
||||
cv.Optional(CONF_HYST_LEVEL): cv.enum(HYST_LEVEL, upper=False),
|
||||
cv.Optional(CONF_PACKET_MODE, default=False): cv.boolean,
|
||||
cv.Optional(CONF_PACKET_LENGTH): cv.uint8_t,
|
||||
cv.Optional(CONF_CRC_ENABLE, default=False): cv.boolean,
|
||||
cv.Optional(CONF_WHITENING, default=False): cv.boolean,
|
||||
}
|
||||
|
||||
|
||||
@@ -217,7 +239,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_ON_PACKET): automation.validate_automation(single=True),
|
||||
}
|
||||
)
|
||||
.extend({cv.Optional(key): validator for key, validator in CONFIG_MAP.items()})
|
||||
.extend(CONFIG_MAP)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(spi.spi_device_schema(cs_pin_required=True)),
|
||||
_validate_packet_mode,
|
||||
@@ -229,7 +251,8 @@ async def to_code(config):
|
||||
await cg.register_component(var, config)
|
||||
await spi.register_spi_device(var, config)
|
||||
|
||||
for key in CONFIG_MAP:
|
||||
for opt in CONFIG_MAP:
|
||||
key = opt.schema
|
||||
if key in config:
|
||||
cg.add(getattr(var, f"set_{key}")(config[key]))
|
||||
|
||||
|
||||
@@ -98,25 +98,8 @@ CC1101Component::CC1101Component() {
|
||||
this->state_.LENGTH_CONFIG = 2;
|
||||
this->state_.FS_AUTOCAL = 1;
|
||||
|
||||
// Default Settings
|
||||
this->set_frequency(433920000);
|
||||
this->set_if_frequency(153000);
|
||||
this->set_filter_bandwidth(203000);
|
||||
this->set_channel(0);
|
||||
this->set_channel_spacing(200000);
|
||||
this->set_symbol_rate(5000);
|
||||
this->set_sync_mode(SyncMode::SYNC_MODE_NONE);
|
||||
this->set_carrier_sense_above_threshold(true);
|
||||
this->set_modulation_type(Modulation::MODULATION_ASK_OOK);
|
||||
this->set_magn_target(MagnTarget::MAGN_TARGET_42DB);
|
||||
this->set_max_lna_gain(MaxLnaGain::MAX_LNA_GAIN_DEFAULT);
|
||||
this->set_max_dvga_gain(MaxDvgaGain::MAX_DVGA_GAIN_MINUS_3);
|
||||
this->set_lna_priority(false);
|
||||
this->set_wait_time(WaitTime::WAIT_TIME_32_SAMPLES);
|
||||
|
||||
// CRITICAL: Initialize PA Table to avoid transmitting 0 power (Silence)
|
||||
memset(this->pa_table_, 0, sizeof(this->pa_table_));
|
||||
this->set_output_power(10.0f);
|
||||
}
|
||||
|
||||
void CC1101Component::setup() {
|
||||
|
||||
@@ -11,6 +11,9 @@ namespace esphome {
|
||||
namespace esp32_camera {
|
||||
|
||||
static const char *const TAG = "esp32_camera";
|
||||
#if ESPHOME_LOG_LEVEL < ESPHOME_LOG_LEVEL_VERBOSE
|
||||
static constexpr uint32_t FRAME_LOG_INTERVAL_MS = 60000;
|
||||
#endif
|
||||
|
||||
/* ---------------- public API (derivated) ---------------- */
|
||||
void ESP32Camera::setup() {
|
||||
@@ -204,7 +207,20 @@ void ESP32Camera::loop() {
|
||||
}
|
||||
this->current_image_ = std::make_shared<ESP32CameraImage>(fb, this->single_requesters_ | this->stream_requesters_);
|
||||
|
||||
ESP_LOGD(TAG, "Got Image: len=%u", fb->len);
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
ESP_LOGV(TAG, "Got Image: len=%u", fb->len);
|
||||
#else
|
||||
// Initialize log time on first frame to ensure accurate interval measurement
|
||||
if (this->frame_count_ == 0) {
|
||||
this->last_log_time_ = now;
|
||||
}
|
||||
this->frame_count_++;
|
||||
if (now - this->last_log_time_ >= FRAME_LOG_INTERVAL_MS) {
|
||||
ESP_LOGD(TAG, "Received %u images in last %us", this->frame_count_, FRAME_LOG_INTERVAL_MS / 1000);
|
||||
this->last_log_time_ = now;
|
||||
this->frame_count_ = 0;
|
||||
}
|
||||
#endif
|
||||
for (auto *listener : this->listeners_) {
|
||||
listener->on_camera_image(this->current_image_);
|
||||
}
|
||||
|
||||
@@ -213,6 +213,10 @@ class ESP32Camera : public camera::Camera {
|
||||
|
||||
uint32_t last_idle_request_{0};
|
||||
uint32_t last_update_{0};
|
||||
#if ESPHOME_LOG_LEVEL < ESPHOME_LOG_LEVEL_VERBOSE
|
||||
uint32_t last_log_time_{0};
|
||||
uint16_t frame_count_{0};
|
||||
#endif
|
||||
#ifdef USE_I2C
|
||||
i2c::InternalI2CBus *i2c_bus_{nullptr};
|
||||
#endif // USE_I2C
|
||||
|
||||
@@ -33,15 +33,7 @@ void Syslog::log_(const int level, const char *tag, const char *message, size_t
|
||||
severity = LOG_LEVEL_TO_SYSLOG_SEVERITY[level];
|
||||
}
|
||||
int pri = this->facility_ * 8 + severity;
|
||||
auto now = this->time_->now();
|
||||
std::string timestamp;
|
||||
if (now.is_valid()) {
|
||||
timestamp = now.strftime("%b %e %H:%M:%S");
|
||||
} else {
|
||||
// RFC 5424: A syslog application MUST use the NILVALUE as TIMESTAMP if the syslog application is incapable of
|
||||
// obtaining system time.
|
||||
timestamp = "-";
|
||||
}
|
||||
|
||||
size_t len = message_len;
|
||||
// remove color formatting
|
||||
if (this->strip_ && message[0] == 0x1B && len > 11) {
|
||||
@@ -49,8 +41,40 @@ void Syslog::log_(const int level, const char *tag, const char *message, size_t
|
||||
len -= 11;
|
||||
}
|
||||
|
||||
auto data = str_sprintf("<%d>%s %s %s: %.*s", pri, timestamp.c_str(), App.get_name().c_str(), tag, len, message);
|
||||
this->parent_->send_packet((const uint8_t *) data.data(), data.size());
|
||||
// Build syslog packet on stack (508 bytes chosen as practical limit for syslog over UDP)
|
||||
char packet[508];
|
||||
size_t offset = 0;
|
||||
size_t remaining = sizeof(packet);
|
||||
|
||||
// Write PRI - abort if this fails as packet would be malformed
|
||||
int ret = snprintf(packet, remaining, "<%d>", pri);
|
||||
if (ret <= 0 || static_cast<size_t>(ret) >= remaining) {
|
||||
return;
|
||||
}
|
||||
offset = ret;
|
||||
remaining -= ret;
|
||||
|
||||
// Write timestamp directly into packet (RFC 5424: use "-" if time not valid or strftime fails)
|
||||
auto now = this->time_->now();
|
||||
size_t ts_written = now.is_valid() ? now.strftime(packet + offset, remaining, "%b %e %H:%M:%S") : 0;
|
||||
if (ts_written > 0) {
|
||||
offset += ts_written;
|
||||
remaining -= ts_written;
|
||||
} else if (remaining > 0) {
|
||||
packet[offset++] = '-';
|
||||
remaining--;
|
||||
}
|
||||
|
||||
// Write hostname, tag, and message
|
||||
ret = snprintf(packet + offset, remaining, " %s %s: %.*s", App.get_name().c_str(), tag, (int) len, message);
|
||||
if (ret > 0) {
|
||||
// snprintf returns chars that would be written; clamp to actual buffer space
|
||||
offset += std::min(static_cast<size_t>(ret), remaining > 0 ? remaining - 1 : 0);
|
||||
}
|
||||
|
||||
if (offset > 0) {
|
||||
this->parent_->send_packet(reinterpret_cast<const uint8_t *>(packet), offset);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::syslog
|
||||
|
||||
111
esphome/components/water_heater/__init__.py
Normal file
111
esphome/components/water_heater/__init__.py
Normal 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)
|
||||
281
esphome/components/water_heater/water_heater.cpp
Normal file
281
esphome/components/water_heater/water_heater.cpp
Normal 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
|
||||
259
esphome/components/water_heater/water_heater.h
Normal file
259
esphome/components/water_heater/water_heater.h
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1042,7 +1042,13 @@ std::string WebServer::date_json(datetime::DateEntity *obj, JsonDetail start_con
|
||||
json::JsonBuilder builder;
|
||||
JsonObject root = builder.root();
|
||||
|
||||
std::string value = str_sprintf("%d-%02d-%02d", obj->year, obj->month, obj->day);
|
||||
// Format: YYYY-MM-DD (max 10 chars + null)
|
||||
char value[12];
|
||||
#ifdef USE_ESP8266
|
||||
snprintf_P(value, sizeof(value), PSTR("%d-%02d-%02d"), obj->year, obj->month, obj->day);
|
||||
#else
|
||||
snprintf(value, sizeof(value), "%d-%02d-%02d", obj->year, obj->month, obj->day);
|
||||
#endif
|
||||
set_json_icon_state_value(root, obj, "date", value, value, start_config);
|
||||
if (start_config == DETAIL_ALL) {
|
||||
this->add_sorting_info_(root, obj);
|
||||
@@ -1098,7 +1104,13 @@ std::string WebServer::time_json(datetime::TimeEntity *obj, JsonDetail start_con
|
||||
json::JsonBuilder builder;
|
||||
JsonObject root = builder.root();
|
||||
|
||||
std::string value = str_sprintf("%02d:%02d:%02d", obj->hour, obj->minute, obj->second);
|
||||
// Format: HH:MM:SS (8 chars + null)
|
||||
char value[12];
|
||||
#ifdef USE_ESP8266
|
||||
snprintf_P(value, sizeof(value), PSTR("%02d:%02d:%02d"), obj->hour, obj->minute, obj->second);
|
||||
#else
|
||||
snprintf(value, sizeof(value), "%02d:%02d:%02d", obj->hour, obj->minute, obj->second);
|
||||
#endif
|
||||
set_json_icon_state_value(root, obj, "time", value, value, start_config);
|
||||
if (start_config == DETAIL_ALL) {
|
||||
this->add_sorting_info_(root, obj);
|
||||
@@ -1154,8 +1166,15 @@ std::string WebServer::datetime_json(datetime::DateTimeEntity *obj, JsonDetail s
|
||||
json::JsonBuilder builder;
|
||||
JsonObject root = builder.root();
|
||||
|
||||
std::string value =
|
||||
str_sprintf("%d-%02d-%02d %02d:%02d:%02d", obj->year, obj->month, obj->day, obj->hour, obj->minute, obj->second);
|
||||
// Format: YYYY-MM-DD HH:MM:SS (max 19 chars + null)
|
||||
char value[24];
|
||||
#ifdef USE_ESP8266
|
||||
snprintf_P(value, sizeof(value), PSTR("%d-%02d-%02d %02d:%02d:%02d"), obj->year, obj->month, obj->day, obj->hour,
|
||||
obj->minute, obj->second);
|
||||
#else
|
||||
snprintf(value, sizeof(value), "%d-%02d-%02d %02d:%02d:%02d", obj->year, obj->month, obj->day, obj->hour, obj->minute,
|
||||
obj->second);
|
||||
#endif
|
||||
set_json_icon_state_value(root, obj, "datetime", value, value, start_config);
|
||||
if (start_config == DETAIL_ALL) {
|
||||
this->add_sorting_info_(root, obj);
|
||||
|
||||
@@ -343,8 +343,9 @@ bool AsyncWebServerRequest::authenticate(const char *username, const char *passw
|
||||
|
||||
void AsyncWebServerRequest::requestAuthentication(const char *realm) const {
|
||||
httpd_resp_set_hdr(*this, "Connection", "keep-alive");
|
||||
auto auth_val = str_sprintf("Basic realm=\"%s\"", realm ? realm : "Login Required");
|
||||
httpd_resp_set_hdr(*this, "WWW-Authenticate", auth_val.c_str());
|
||||
// Note: realm is never configured in ESPHome, always nullptr -> "Login Required"
|
||||
(void) realm; // Unused - always use default
|
||||
httpd_resp_set_hdr(*this, "WWW-Authenticate", "Basic realm=\"Login Required\"");
|
||||
httpd_resp_send_err(*this, HTTPD_401_UNAUTHORIZED, nullptr);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
43
tests/integration/fixtures/syslog.yaml
Normal file
43
tests/integration/fixtures/syslog.yaml
Normal file
@@ -0,0 +1,43 @@
|
||||
esphome:
|
||||
name: syslog-test
|
||||
|
||||
host:
|
||||
|
||||
api:
|
||||
services:
|
||||
- service: log_long_message
|
||||
then:
|
||||
- lambda: |-
|
||||
// Log a message that exceeds 508 bytes to test truncation
|
||||
ESP_LOGI("trunctest", "START|%s|END",
|
||||
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
||||
"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"
|
||||
"CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"
|
||||
"DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD"
|
||||
"EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE"
|
||||
"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF");
|
||||
- service: log_short_message
|
||||
then:
|
||||
- lambda: |-
|
||||
// Log a short message that should arrive complete (not truncated)
|
||||
ESP_LOGI("shorttest", "BEGIN|SHORT_MESSAGE_CONTENT|FINISH");
|
||||
|
||||
logger:
|
||||
level: DEBUG
|
||||
|
||||
time:
|
||||
- platform: host
|
||||
id: host_time
|
||||
|
||||
udp:
|
||||
- id: syslog_udp
|
||||
addresses:
|
||||
- "127.0.0.1"
|
||||
|
||||
syslog:
|
||||
udp_id: syslog_udp
|
||||
time_id: host_time
|
||||
port: SYSLOG_PORT_PLACEHOLDER
|
||||
level: DEBUG
|
||||
strip: true
|
||||
facility: 16
|
||||
@@ -179,6 +179,12 @@ async def test_api_homeassistant(
|
||||
client.send_home_assistant_state("binary_sensor.external_motion", "", "ON")
|
||||
client.send_home_assistant_state("weather.home", "condition", "sunny")
|
||||
|
||||
# Test edge cases for zero-copy implementation safety
|
||||
# Empty entity_id should be silently ignored (no crash)
|
||||
client.send_home_assistant_state("", "", "should_be_ignored")
|
||||
# Empty state with valid entity should work (use different entity to not interfere with test)
|
||||
client.send_home_assistant_state("sensor.edge_case_empty_state", "", "")
|
||||
|
||||
# List entities and services
|
||||
_, services = await client.list_entities_services()
|
||||
|
||||
|
||||
284
tests/integration/test_syslog.py
Normal file
284
tests/integration/test_syslog.py
Normal file
@@ -0,0 +1,284 @@
|
||||
"""Integration test for syslog component."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import AsyncGenerator
|
||||
import contextlib
|
||||
from contextlib import asynccontextmanager
|
||||
from dataclasses import dataclass, field
|
||||
import re
|
||||
import socket
|
||||
from typing import TypedDict
|
||||
|
||||
import pytest
|
||||
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
|
||||
class ParsedSyslogMessage(TypedDict):
|
||||
"""Parsed syslog message components."""
|
||||
|
||||
pri: int
|
||||
facility: int
|
||||
severity: int
|
||||
timestamp: str
|
||||
hostname: str
|
||||
tag: str
|
||||
message: str
|
||||
|
||||
|
||||
# RFC 3164 syslog message pattern:
|
||||
# <PRI>TIMESTAMP HOSTNAME TAG: MESSAGE
|
||||
# Example: <134>Dec 20 14:30:45 syslog-test app: [D][app:029]: Running...
|
||||
SYSLOG_PATTERN = re.compile(
|
||||
r"<(\d+)>" # PRI (priority = facility * 8 + severity)
|
||||
r"(\S+ +\d+ \d+:\d+:\d+|-)" # TIMESTAMP (BSD-style "%b %e %H:%M:%S", e.g. "Dec 20 14:30:45", or NILVALUE "-")
|
||||
r" (\S+)" # HOSTNAME
|
||||
r" (\S+):" # TAG
|
||||
r" (.*)" # MESSAGE
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class SyslogReceiver:
|
||||
"""Collects syslog messages received over UDP."""
|
||||
|
||||
messages: list[str] = field(default_factory=list)
|
||||
message_received: asyncio.Event = field(default_factory=asyncio.Event)
|
||||
_waiters: list[tuple[re.Pattern, asyncio.Event]] = field(default_factory=list)
|
||||
|
||||
def on_message(self, msg: str) -> None:
|
||||
"""Called when a message is received."""
|
||||
self.messages.append(msg)
|
||||
self.message_received.set()
|
||||
# Check pattern waiters
|
||||
for pattern, event in self._waiters:
|
||||
if pattern.search(msg):
|
||||
event.set()
|
||||
|
||||
async def wait_for_messages(self, timeout: float = 10.0) -> None:
|
||||
"""Wait for at least one message to be received."""
|
||||
await asyncio.wait_for(self.message_received.wait(), timeout=timeout)
|
||||
|
||||
async def wait_for_pattern(self, pattern: str, timeout: float = 5.0) -> str:
|
||||
"""Wait for a message matching the pattern."""
|
||||
compiled = re.compile(pattern)
|
||||
event = asyncio.Event()
|
||||
self._waiters.append((compiled, event))
|
||||
try:
|
||||
# Check existing messages first
|
||||
for msg in self.messages:
|
||||
if compiled.search(msg):
|
||||
return msg
|
||||
# Wait for new message
|
||||
await asyncio.wait_for(event.wait(), timeout=timeout)
|
||||
# Find and return the matching message
|
||||
for msg in reversed(self.messages):
|
||||
if compiled.search(msg):
|
||||
return msg
|
||||
raise RuntimeError("Event set but no matching message found")
|
||||
finally:
|
||||
self._waiters.remove((compiled, event))
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def syslog_udp_listener() -> AsyncGenerator[tuple[int, SyslogReceiver]]:
|
||||
"""Async context manager that listens for syslog UDP messages.
|
||||
|
||||
Yields:
|
||||
Tuple of (port, SyslogReceiver) where port is the UDP port to send to
|
||||
and SyslogReceiver contains the received messages.
|
||||
"""
|
||||
# Create and bind UDP socket
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
sock.bind(("127.0.0.1", 0))
|
||||
sock.setblocking(False)
|
||||
port = sock.getsockname()[1]
|
||||
|
||||
receiver = SyslogReceiver()
|
||||
|
||||
async def receive_messages() -> None:
|
||||
"""Background task to receive syslog messages."""
|
||||
loop = asyncio.get_running_loop()
|
||||
while True:
|
||||
try:
|
||||
data = await loop.sock_recv(sock, 4096)
|
||||
if data:
|
||||
msg = data.decode("utf-8", errors="replace")
|
||||
receiver.on_message(msg)
|
||||
except BlockingIOError:
|
||||
await asyncio.sleep(0.01)
|
||||
except Exception:
|
||||
break
|
||||
|
||||
task = asyncio.create_task(receive_messages())
|
||||
try:
|
||||
yield port, receiver
|
||||
finally:
|
||||
task.cancel()
|
||||
with contextlib.suppress(asyncio.CancelledError):
|
||||
await task
|
||||
sock.close()
|
||||
|
||||
|
||||
def parse_syslog_message(msg: str) -> ParsedSyslogMessage | None:
|
||||
"""Parse a syslog message and return its components."""
|
||||
match = SYSLOG_PATTERN.match(msg)
|
||||
if not match:
|
||||
return None
|
||||
pri, timestamp, hostname, tag, message = match.groups()
|
||||
pri_val = int(pri)
|
||||
# PRI = facility * 8 + severity
|
||||
facility = pri_val // 8
|
||||
severity = pri_val % 8
|
||||
return ParsedSyslogMessage(
|
||||
pri=pri_val,
|
||||
facility=facility,
|
||||
severity=severity,
|
||||
timestamp=timestamp,
|
||||
hostname=hostname,
|
||||
tag=tag,
|
||||
message=message,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_syslog(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Test syslog component sends properly formatted messages."""
|
||||
async with syslog_udp_listener() as (udp_port, receiver):
|
||||
# Replace the placeholder port in the config
|
||||
config = yaml_config.replace("SYSLOG_PORT_PLACEHOLDER", str(udp_port))
|
||||
|
||||
async with run_compiled(config), api_client_connected() as client:
|
||||
# Verify device is running
|
||||
device_info = await client.device_info()
|
||||
assert device_info is not None
|
||||
assert device_info.name == "syslog-test"
|
||||
|
||||
# Wait for syslog messages (ESPHome logs during startup)
|
||||
try:
|
||||
await receiver.wait_for_messages(timeout=10.0)
|
||||
except TimeoutError:
|
||||
pytest.fail("No syslog messages received within timeout")
|
||||
|
||||
# Give it a moment to collect more messages
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
# Verify we received messages
|
||||
assert len(receiver.messages) > 0, "No syslog messages received"
|
||||
|
||||
# Parse and validate all messages
|
||||
parsed_messages: list[ParsedSyslogMessage] = []
|
||||
for msg in receiver.messages:
|
||||
parsed = parse_syslog_message(msg)
|
||||
if parsed:
|
||||
parsed_messages.append(parsed)
|
||||
|
||||
assert len(parsed_messages) > 0, (
|
||||
f"No valid syslog messages found. Received: {receiver.messages[:5]}"
|
||||
)
|
||||
|
||||
# Validate message format for all parsed messages
|
||||
for parsed in parsed_messages:
|
||||
# Validate PRI is in valid range (0-191)
|
||||
assert 0 <= parsed["pri"] <= 191, f"Invalid PRI: {parsed['pri']}"
|
||||
|
||||
# Validate facility matches config (16 = local0)
|
||||
assert parsed["facility"] == 16, (
|
||||
f"Expected facility 16, got {parsed['facility']}"
|
||||
)
|
||||
|
||||
# Validate severity is in valid range (0-7)
|
||||
assert 0 <= parsed["severity"] <= 7, (
|
||||
f"Invalid severity: {parsed['severity']}"
|
||||
)
|
||||
|
||||
# Validate hostname matches device name
|
||||
assert parsed["hostname"] == "syslog-test", (
|
||||
f"Unexpected hostname: {parsed['hostname']}"
|
||||
)
|
||||
|
||||
# Validate timestamp format (BSD or NILVALUE)
|
||||
if parsed["timestamp"] != "-":
|
||||
assert re.match(
|
||||
r"[A-Z][a-z]{2} +\d+ \d{2}:\d{2}:\d{2}",
|
||||
parsed["timestamp"],
|
||||
), f"Invalid timestamp format: {parsed['timestamp']}"
|
||||
|
||||
# Verify we see different severity levels in the logs
|
||||
severities_seen = {p["severity"] for p in parsed_messages}
|
||||
# ESPHome startup logs should include at least INFO (5) or DEBUG (7)
|
||||
assert len(severities_seen) >= 1, "Expected to see at least one severity"
|
||||
|
||||
# Verify messages don't contain ANSI color codes (strip=true)
|
||||
for parsed in parsed_messages:
|
||||
assert "\x1b[" not in parsed["message"], (
|
||||
f"Color codes not stripped: {parsed['message'][:50]}"
|
||||
)
|
||||
|
||||
# Verify message content is not empty for most messages
|
||||
non_empty_messages = [p for p in parsed_messages if p["message"].strip()]
|
||||
assert len(non_empty_messages) > 0, "All messages are empty"
|
||||
|
||||
# Verify tag format (should be component name like "app", "wifi", etc.)
|
||||
for parsed in parsed_messages:
|
||||
assert len(parsed["tag"]) > 0, "Empty tag"
|
||||
# Tag should not contain spaces or colons
|
||||
assert " " not in parsed["tag"], f"Tag contains space: {parsed['tag']}"
|
||||
|
||||
# Test message truncation - call service that logs a very long message
|
||||
_, services = await client.list_entities_services()
|
||||
log_service = next(
|
||||
(s for s in services if s.name == "log_long_message"), None
|
||||
)
|
||||
assert log_service is not None, "log_long_message service not found"
|
||||
|
||||
# Call the service to trigger a long log message
|
||||
await client.execute_service(log_service, {})
|
||||
|
||||
# Wait specifically for the truncation test message
|
||||
try:
|
||||
trunc_msg = await receiver.wait_for_pattern(r"trunctest.*START\|")
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
f"Truncation test message not received. Got: {receiver.messages}"
|
||||
)
|
||||
|
||||
# Verify message is truncated to max 508 bytes
|
||||
assert len(trunc_msg) <= 508, f"Message exceeds 508 bytes: {len(trunc_msg)}"
|
||||
|
||||
# Verify the message starts correctly but is truncated (no "|END")
|
||||
assert "START|" in trunc_msg, "Message should contain START marker"
|
||||
assert "|END" not in trunc_msg, (
|
||||
"Message should be truncated before END marker"
|
||||
)
|
||||
|
||||
# Test short message - should arrive complete (not truncated)
|
||||
short_service = next(
|
||||
(s for s in services if s.name == "log_short_message"), None
|
||||
)
|
||||
assert short_service is not None, "log_short_message service not found"
|
||||
|
||||
await client.execute_service(short_service, {})
|
||||
|
||||
try:
|
||||
short_msg = await receiver.wait_for_pattern(r"shorttest.*BEGIN\|")
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
f"Short test message not received. Got: {receiver.messages[-10:]}"
|
||||
)
|
||||
|
||||
# Verify short message arrived complete with both markers
|
||||
assert "BEGIN|" in short_msg, "Short message missing BEGIN marker"
|
||||
assert "|FINISH" in short_msg, (
|
||||
f"Short message truncated unexpectedly: {short_msg}"
|
||||
)
|
||||
assert "SHORT_MESSAGE_CONTENT" in short_msg, (
|
||||
f"Short message content missing: {short_msg}"
|
||||
)
|
||||
Reference in New Issue
Block a user