Merge remote-tracking branch 'upstream/dev' into web_server_stack_format

This commit is contained in:
J. Nick Koston
2025-12-21 22:01:29 -10:00
125 changed files with 3063 additions and 767 deletions
+1 -1
View File
@@ -1 +1 @@
6857423aecf90accd0a8bf584d36ee094a4938f872447a4efc05a2efc6dc6481
4268ab0b5150f79ab1c317e8f3834c8bb0b4c8122da4f6b1fd67c49d0f2098c9
+4
View File
@@ -91,6 +91,10 @@ venv-*/
# mypy
.mypy_cache/
# nix
/default.nix
/shell.nix
.pioenvs
.piolibdeps
.pio
+1
View File
@@ -537,6 +537,7 @@ esphome/components/version/* @esphome/core
esphome/components/voice_assistant/* @jesserockz @kahrendt
esphome/components/wake_on_lan/* @clydebarrow @willwill2will54
esphome/components/watchdog/* @oarcher
esphome/components/water_heater/* @dhoeben
esphome/components/waveshare_epaper/* @clydebarrow
esphome/components/web_server/ota/* @esphome/core
esphome/components/web_server_base/* @esphome/core
@@ -35,26 +35,12 @@ void AlarmControlPanel::publish_state(AlarmControlPanelState state) {
ESP_LOGD(TAG, "Set state to: %s, previous: %s", LOG_STR_ARG(alarm_control_panel_state_to_string(state)),
LOG_STR_ARG(alarm_control_panel_state_to_string(prev_state)));
this->current_state_ = state;
// Single state callback - triggers check get_state() for specific states
this->state_callback_.call();
#if defined(USE_ALARM_CONTROL_PANEL) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_alarm_control_panel_update(this);
#endif
if (state == ACP_STATE_TRIGGERED) {
this->triggered_callback_.call();
} else if (state == ACP_STATE_ARMING) {
this->arming_callback_.call();
} else if (state == ACP_STATE_PENDING) {
this->pending_callback_.call();
} else if (state == ACP_STATE_ARMED_HOME) {
this->armed_home_callback_.call();
} else if (state == ACP_STATE_ARMED_NIGHT) {
this->armed_night_callback_.call();
} else if (state == ACP_STATE_ARMED_AWAY) {
this->armed_away_callback_.call();
} else if (state == ACP_STATE_DISARMED) {
this->disarmed_callback_.call();
}
// Cleared fires when leaving TRIGGERED state
if (prev_state == ACP_STATE_TRIGGERED) {
this->cleared_callback_.call();
}
@@ -69,34 +55,6 @@ void AlarmControlPanel::add_on_state_callback(std::function<void()> &&callback)
this->state_callback_.add(std::move(callback));
}
void AlarmControlPanel::add_on_triggered_callback(std::function<void()> &&callback) {
this->triggered_callback_.add(std::move(callback));
}
void AlarmControlPanel::add_on_arming_callback(std::function<void()> &&callback) {
this->arming_callback_.add(std::move(callback));
}
void AlarmControlPanel::add_on_armed_home_callback(std::function<void()> &&callback) {
this->armed_home_callback_.add(std::move(callback));
}
void AlarmControlPanel::add_on_armed_night_callback(std::function<void()> &&callback) {
this->armed_night_callback_.add(std::move(callback));
}
void AlarmControlPanel::add_on_armed_away_callback(std::function<void()> &&callback) {
this->armed_away_callback_.add(std::move(callback));
}
void AlarmControlPanel::add_on_pending_callback(std::function<void()> &&callback) {
this->pending_callback_.add(std::move(callback));
}
void AlarmControlPanel::add_on_disarmed_callback(std::function<void()> &&callback) {
this->disarmed_callback_.add(std::move(callback));
}
void AlarmControlPanel::add_on_cleared_callback(std::function<void()> &&callback) {
this->cleared_callback_.add(std::move(callback));
}
@@ -35,54 +35,13 @@ class AlarmControlPanel : public EntityBase {
*/
void publish_state(AlarmControlPanelState state);
/** Add a callback for when the state of the alarm_control_panel changes
/** Add a callback for when the state of the alarm_control_panel changes.
* Triggers can check get_state() to determine the new state.
*
* @param callback The callback function
*/
void add_on_state_callback(std::function<void()> &&callback);
/** Add a callback for when the state of the alarm_control_panel chanes to triggered
*
* @param callback The callback function
*/
void add_on_triggered_callback(std::function<void()> &&callback);
/** Add a callback for when the state of the alarm_control_panel chanes to arming
*
* @param callback The callback function
*/
void add_on_arming_callback(std::function<void()> &&callback);
/** Add a callback for when the state of the alarm_control_panel changes to pending
*
* @param callback The callback function
*/
void add_on_pending_callback(std::function<void()> &&callback);
/** Add a callback for when the state of the alarm_control_panel changes to armed_home
*
* @param callback The callback function
*/
void add_on_armed_home_callback(std::function<void()> &&callback);
/** Add a callback for when the state of the alarm_control_panel changes to armed_night
*
* @param callback The callback function
*/
void add_on_armed_night_callback(std::function<void()> &&callback);
/** Add a callback for when the state of the alarm_control_panel changes to armed_away
*
* @param callback The callback function
*/
void add_on_armed_away_callback(std::function<void()> &&callback);
/** Add a callback for when the state of the alarm_control_panel changes to disarmed
*
* @param callback The callback function
*/
void add_on_disarmed_callback(std::function<void()> &&callback);
/** Add a callback for when the state of the alarm_control_panel clears from triggered
*
* @param callback The callback function
@@ -172,28 +131,14 @@ class AlarmControlPanel : public EntityBase {
uint32_t last_update_;
// the call control function
virtual void control(const AlarmControlPanelCall &call) = 0;
// state callback
CallbackManager<void()> state_callback_{};
// trigger callback
CallbackManager<void()> triggered_callback_{};
// arming callback
CallbackManager<void()> arming_callback_{};
// pending callback
CallbackManager<void()> pending_callback_{};
// armed_home callback
CallbackManager<void()> armed_home_callback_{};
// armed_night callback
CallbackManager<void()> armed_night_callback_{};
// armed_away callback
CallbackManager<void()> armed_away_callback_{};
// disarmed callback
CallbackManager<void()> disarmed_callback_{};
// clear callback
CallbackManager<void()> cleared_callback_{};
// state callback - triggers check get_state() for specific state
LazyCallbackManager<void()> state_callback_{};
// clear callback - fires when leaving TRIGGERED state
LazyCallbackManager<void()> cleared_callback_{};
// chime callback
CallbackManager<void()> chime_callback_{};
LazyCallbackManager<void()> chime_callback_{};
// ready callback
CallbackManager<void()> ready_callback_{};
LazyCallbackManager<void()> ready_callback_{};
};
} // namespace alarm_control_panel
@@ -6,6 +6,7 @@
namespace esphome {
namespace alarm_control_panel {
/// Trigger on any state change
class StateTrigger : public Trigger<> {
public:
explicit StateTrigger(AlarmControlPanel *alarm_control_panel) {
@@ -13,55 +14,30 @@ class StateTrigger : public Trigger<> {
}
};
class TriggeredTrigger : public Trigger<> {
/// Template trigger that fires when entering a specific state
template<AlarmControlPanelState State> class StateEnterTrigger : public Trigger<> {
public:
explicit TriggeredTrigger(AlarmControlPanel *alarm_control_panel) {
alarm_control_panel->add_on_triggered_callback([this]() { this->trigger(); });
explicit StateEnterTrigger(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {
alarm_control_panel->add_on_state_callback([this]() {
if (this->alarm_control_panel_->get_state() == State)
this->trigger();
});
}
protected:
AlarmControlPanel *alarm_control_panel_;
};
class ArmingTrigger : public Trigger<> {
public:
explicit ArmingTrigger(AlarmControlPanel *alarm_control_panel) {
alarm_control_panel->add_on_arming_callback([this]() { this->trigger(); });
}
};
class PendingTrigger : public Trigger<> {
public:
explicit PendingTrigger(AlarmControlPanel *alarm_control_panel) {
alarm_control_panel->add_on_pending_callback([this]() { this->trigger(); });
}
};
class ArmedHomeTrigger : public Trigger<> {
public:
explicit ArmedHomeTrigger(AlarmControlPanel *alarm_control_panel) {
alarm_control_panel->add_on_armed_home_callback([this]() { this->trigger(); });
}
};
class ArmedNightTrigger : public Trigger<> {
public:
explicit ArmedNightTrigger(AlarmControlPanel *alarm_control_panel) {
alarm_control_panel->add_on_armed_night_callback([this]() { this->trigger(); });
}
};
class ArmedAwayTrigger : public Trigger<> {
public:
explicit ArmedAwayTrigger(AlarmControlPanel *alarm_control_panel) {
alarm_control_panel->add_on_armed_away_callback([this]() { this->trigger(); });
}
};
class DisarmedTrigger : public Trigger<> {
public:
explicit DisarmedTrigger(AlarmControlPanel *alarm_control_panel) {
alarm_control_panel->add_on_disarmed_callback([this]() { this->trigger(); });
}
};
// Type aliases for state-specific triggers
using TriggeredTrigger = StateEnterTrigger<ACP_STATE_TRIGGERED>;
using ArmingTrigger = StateEnterTrigger<ACP_STATE_ARMING>;
using PendingTrigger = StateEnterTrigger<ACP_STATE_PENDING>;
using ArmedHomeTrigger = StateEnterTrigger<ACP_STATE_ARMED_HOME>;
using ArmedNightTrigger = StateEnterTrigger<ACP_STATE_ARMED_NIGHT>;
using ArmedAwayTrigger = StateEnterTrigger<ACP_STATE_ARMED_AWAY>;
using DisarmedTrigger = StateEnterTrigger<ACP_STATE_DISARMED>;
/// Trigger when leaving TRIGGERED state (alarm cleared)
class ClearedTrigger : public Trigger<> {
public:
explicit ClearedTrigger(AlarmControlPanel *alarm_control_panel) {
@@ -69,6 +45,7 @@ class ClearedTrigger : public Trigger<> {
}
};
/// Trigger on chime event (zone opened while disarmed)
class ChimeTrigger : public Trigger<> {
public:
explicit ChimeTrigger(AlarmControlPanel *alarm_control_panel) {
@@ -76,6 +53,7 @@ class ChimeTrigger : public Trigger<> {
}
};
/// Trigger on ready state change
class ReadyTrigger : public Trigger<> {
public:
explicit ReadyTrigger(AlarmControlPanel *alarm_control_panel) {
+86 -7
View File
@@ -477,7 +477,7 @@ message FanCommandRequest {
bool has_speed_level = 10;
int32 speed_level = 11;
bool has_preset_mode = 12;
string preset_mode = 13;
string preset_mode = 13 [(pointer_to_buffer) = true];
uint32 device_id = 14 [(field_ifdef) = "USE_DEVICES"];
}
@@ -747,7 +747,7 @@ message NoiseEncryptionSetKeyRequest {
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_API_NOISE";
bytes key = 1;
bytes key = 1 [(pointer_to_buffer) = true];
}
message NoiseEncryptionSetKeyResponse {
@@ -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 ====================
@@ -1091,16 +1091,95 @@ message ClimateCommandRequest {
bool has_swing_mode = 14;
ClimateSwingMode swing_mode = 15;
bool has_custom_fan_mode = 16;
string custom_fan_mode = 17;
string custom_fan_mode = 17 [(pointer_to_buffer) = true];
bool has_preset = 18;
ClimatePreset preset = 19;
bool has_custom_preset = 20;
string custom_preset = 21;
string custom_preset = 21 [(pointer_to_buffer) = true];
bool has_target_humidity = 22;
float target_humidity = 23;
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;
+80 -12
View File
@@ -42,6 +42,9 @@
#ifdef USE_ZWAVE_PROXY
#include "esphome/components/zwave_proxy/zwave_proxy.h"
#endif
#ifdef USE_WATER_HEATER
#include "esphome/components/water_heater/water_heater.h"
#endif
namespace esphome::api {
@@ -447,7 +450,7 @@ void APIConnection::fan_command(const FanCommandRequest &msg) {
if (msg.has_direction)
call.set_direction(static_cast<fan::FanDirection>(msg.direction));
if (msg.has_preset_mode)
call.set_preset_mode(msg.preset_mode);
call.set_preset_mode(reinterpret_cast<const char *>(msg.preset_mode), msg.preset_mode_len);
call.perform();
}
#endif
@@ -712,11 +715,11 @@ void APIConnection::climate_command(const ClimateCommandRequest &msg) {
if (msg.has_fan_mode)
call.set_fan_mode(static_cast<climate::ClimateFanMode>(msg.fan_mode));
if (msg.has_custom_fan_mode)
call.set_fan_mode(msg.custom_fan_mode);
call.set_fan_mode(reinterpret_cast<const char *>(msg.custom_fan_mode), msg.custom_fan_mode_len);
if (msg.has_preset)
call.set_preset(static_cast<climate::ClimatePreset>(msg.preset));
if (msg.has_custom_preset)
call.set_preset(msg.custom_preset);
call.set_preset(reinterpret_cast<const char *>(msg.custom_preset), msg.custom_preset_len);
if (msg.has_swing_mode)
call.set_swing_mode(static_cast<climate::ClimateSwingMode>(msg.swing_mode));
call.perform();
@@ -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
@@ -1666,13 +1734,13 @@ bool APIConnection::send_noise_encryption_set_key_response(const NoiseEncryption
resp.success = false;
psk_t psk{};
if (msg.key.empty()) {
if (msg.key_len == 0) {
if (this->parent_->clear_noise_psk(true)) {
resp.success = true;
} else {
ESP_LOGW(TAG, "Failed to clear encryption key");
}
} else if (base64_decode(msg.key, psk.data(), psk.size()) != psk.size()) {
} else if (base64_decode(msg.key, msg.key_len, psk.data(), psk.size()) != psk.size()) {
ESP_LOGW(TAG, "Invalid encryption key length");
} else if (!this->parent_->save_noise_psk(psk, true)) {
ESP_LOGW(TAG, "Failed to save encryption key");
+11
View File
@@ -176,6 +176,11 @@ class APIConnection final : public APIServerConnection {
void alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) override;
#endif
#ifdef USE_WATER_HEATER
bool send_water_heater_state(water_heater::WaterHeater *water_heater);
void on_water_heater_command_request(const WaterHeaterCommandRequest &msg) override;
#endif
#ifdef USE_EVENT
void send_event(event::Event *event, const char *event_type);
#endif
@@ -456,6 +461,12 @@ class APIConnection final : public APIServerConnection {
static uint16_t try_send_alarm_control_panel_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single);
#endif
#ifdef USE_WATER_HEATER
static uint16_t try_send_water_heater_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single);
static uint16_t try_send_water_heater_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single);
#endif
#ifdef USE_EVENT
static uint16_t try_send_event_response(event::Event *event, const char *event_type, APIConnection *conn,
uint32_t remaining_size, bool is_single);
+143 -14
View File
@@ -447,9 +447,12 @@ bool FanCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
}
bool FanCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 13:
this->preset_mode = value.as_string();
case 13: {
// Use raw data directly to avoid allocation
this->preset_mode = value.data();
this->preset_mode_len = value.size();
break;
}
default:
return false;
}
@@ -855,9 +858,12 @@ void SubscribeLogsResponse::calculate_size(ProtoSize &size) const {
#ifdef USE_API_NOISE
bool NoiseEncryptionSetKeyRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 1:
this->key = value.as_string();
case 1: {
// Use raw data directly to avoid allocation
this->key = value.data();
this->key_len = value.size();
break;
}
default:
return false;
}
@@ -960,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;
}
@@ -1392,12 +1407,18 @@ bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value)
}
bool ClimateCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 17:
this->custom_fan_mode = value.as_string();
case 17: {
// Use raw data directly to avoid allocation
this->custom_fan_mode = value.data();
this->custom_fan_mode_len = value.size();
break;
case 21:
this->custom_preset = value.as_string();
}
case 21: {
// Use raw data directly to avoid allocation
this->custom_preset = value.data();
this->custom_preset_len = value.size();
break;
}
default:
return false;
}
@@ -1426,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_);
+101 -11
View File
@@ -129,6 +129,25 @@ enum ClimatePreset : uint32_t {
CLIMATE_PRESET_ACTIVITY = 7,
};
#endif
#ifdef USE_WATER_HEATER
enum WaterHeaterMode : uint32_t {
WATER_HEATER_MODE_OFF = 0,
WATER_HEATER_MODE_ECO = 1,
WATER_HEATER_MODE_ELECTRIC = 2,
WATER_HEATER_MODE_PERFORMANCE = 3,
WATER_HEATER_MODE_HIGH_DEMAND = 4,
WATER_HEATER_MODE_HEAT_PUMP = 5,
WATER_HEATER_MODE_GAS = 6,
};
#endif
enum WaterHeaterCommandHasField : uint32_t {
WATER_HEATER_COMMAND_HAS_NONE = 0,
WATER_HEATER_COMMAND_HAS_MODE = 1,
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE = 2,
WATER_HEATER_COMMAND_HAS_STATE = 4,
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW = 8,
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH = 16,
};
#ifdef USE_NUMBER
enum NumberMode : uint32_t {
NUMBER_MODE_AUTO = 0,
@@ -765,7 +784,7 @@ class FanStateResponse final : public StateResponseProtoMessage {
class FanCommandRequest final : public CommandProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 31;
static constexpr uint8_t ESTIMATED_SIZE = 38;
static constexpr uint8_t ESTIMATED_SIZE = 48;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "fan_command_request"; }
#endif
@@ -778,7 +797,8 @@ class FanCommandRequest final : public CommandProtoMessage {
bool has_speed_level{false};
int32_t speed_level{0};
bool has_preset_mode{false};
std::string preset_mode{};
const uint8_t *preset_mode{nullptr};
uint16_t preset_mode_len{0};
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -1053,11 +1073,12 @@ class SubscribeLogsResponse final : public ProtoMessage {
class NoiseEncryptionSetKeyRequest final : public ProtoDecodableMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 124;
static constexpr uint8_t ESTIMATED_SIZE = 9;
static constexpr uint8_t ESTIMATED_SIZE = 19;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "noise_encryption_set_key_request"; }
#endif
std::string key{};
const uint8_t *key{nullptr};
uint16_t key_len{0};
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -1201,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
@@ -1475,7 +1499,7 @@ class ClimateStateResponse final : public StateResponseProtoMessage {
class ClimateCommandRequest final : public CommandProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 48;
static constexpr uint8_t ESTIMATED_SIZE = 84;
static constexpr uint8_t ESTIMATED_SIZE = 104;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "climate_command_request"; }
#endif
@@ -1492,11 +1516,13 @@ class ClimateCommandRequest final : public CommandProtoMessage {
bool has_swing_mode{false};
enums::ClimateSwingMode swing_mode{};
bool has_custom_fan_mode{false};
std::string custom_fan_mode{};
const uint8_t *custom_fan_mode{nullptr};
uint16_t custom_fan_mode_len{0};
bool has_preset{false};
enums::ClimatePreset preset{};
bool has_custom_preset{false};
std::string custom_preset{};
const uint8_t *custom_preset{nullptr};
uint16_t custom_preset_len{0};
bool has_target_humidity{false};
float target_humidity{0.0f};
#ifdef HAS_PROTO_MESSAGE_DUMP
@@ -1509,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:
+109 -7
View File
@@ -348,6 +348,47 @@ template<> const char *proto_enum_to_string<enums::ClimatePreset>(enums::Climate
}
}
#endif
#ifdef USE_WATER_HEATER
template<> const char *proto_enum_to_string<enums::WaterHeaterMode>(enums::WaterHeaterMode value) {
switch (value) {
case enums::WATER_HEATER_MODE_OFF:
return "WATER_HEATER_MODE_OFF";
case enums::WATER_HEATER_MODE_ECO:
return "WATER_HEATER_MODE_ECO";
case enums::WATER_HEATER_MODE_ELECTRIC:
return "WATER_HEATER_MODE_ELECTRIC";
case enums::WATER_HEATER_MODE_PERFORMANCE:
return "WATER_HEATER_MODE_PERFORMANCE";
case enums::WATER_HEATER_MODE_HIGH_DEMAND:
return "WATER_HEATER_MODE_HIGH_DEMAND";
case enums::WATER_HEATER_MODE_HEAT_PUMP:
return "WATER_HEATER_MODE_HEAT_PUMP";
case enums::WATER_HEATER_MODE_GAS:
return "WATER_HEATER_MODE_GAS";
default:
return "UNKNOWN";
}
}
#endif
template<>
const char *proto_enum_to_string<enums::WaterHeaterCommandHasField>(enums::WaterHeaterCommandHasField value) {
switch (value) {
case enums::WATER_HEATER_COMMAND_HAS_NONE:
return "WATER_HEATER_COMMAND_HAS_NONE";
case enums::WATER_HEATER_COMMAND_HAS_MODE:
return "WATER_HEATER_COMMAND_HAS_MODE";
case enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE:
return "WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE";
case enums::WATER_HEATER_COMMAND_HAS_STATE:
return "WATER_HEATER_COMMAND_HAS_STATE";
case enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW:
return "WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW";
case enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH:
return "WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH";
default:
return "UNKNOWN";
}
}
#ifdef USE_NUMBER
template<> const char *proto_enum_to_string<enums::NumberMode>(enums::NumberMode value) {
switch (value) {
@@ -923,7 +964,9 @@ void FanCommandRequest::dump_to(std::string &out) const {
dump_field(out, "has_speed_level", this->has_speed_level);
dump_field(out, "speed_level", this->speed_level);
dump_field(out, "has_preset_mode", this->has_preset_mode);
dump_field(out, "preset_mode", this->preset_mode);
out.append(" preset_mode: ");
out.append(format_hex_pretty(this->preset_mode, this->preset_mode_len));
out.append("\n");
#ifdef USE_DEVICES
dump_field(out, "device_id", this->device_id);
#endif
@@ -1113,7 +1156,7 @@ void SubscribeLogsResponse::dump_to(std::string &out) const {
void NoiseEncryptionSetKeyRequest::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "NoiseEncryptionSetKeyRequest");
out.append(" key: ");
out.append(format_hex_pretty(reinterpret_cast<const uint8_t *>(this->key.data()), this->key.size()));
out.append(format_hex_pretty(this->key, this->key_len));
out.append("\n");
}
void NoiseEncryptionSetKeyResponse::dump_to(std::string &out) const { dump_field(out, "success", this->success); }
@@ -1182,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 {}"); }
@@ -1374,11 +1423,15 @@ void ClimateCommandRequest::dump_to(std::string &out) const {
dump_field(out, "has_swing_mode", this->has_swing_mode);
dump_field(out, "swing_mode", static_cast<enums::ClimateSwingMode>(this->swing_mode));
dump_field(out, "has_custom_fan_mode", this->has_custom_fan_mode);
dump_field(out, "custom_fan_mode", this->custom_fan_mode);
out.append(" custom_fan_mode: ");
out.append(format_hex_pretty(this->custom_fan_mode, this->custom_fan_mode_len));
out.append("\n");
dump_field(out, "has_preset", this->has_preset);
dump_field(out, "preset", static_cast<enums::ClimatePreset>(this->preset));
dump_field(out, "has_custom_preset", this->has_custom_preset);
dump_field(out, "custom_preset", this->custom_preset);
out.append(" custom_preset: ");
out.append(format_hex_pretty(this->custom_preset, this->custom_preset_len));
out.append("\n");
dump_field(out, "has_target_humidity", this->has_target_humidity);
dump_field(out, "target_humidity", this->target_humidity);
#ifdef USE_DEVICES
@@ -1386,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;
+4
View File
@@ -91,6 +91,10 @@ class APIServerConnectionBase : public ProtoService {
virtual void on_climate_command_request(const ClimateCommandRequest &value){};
#endif
#ifdef USE_WATER_HEATER
virtual void on_water_heater_command_request(const WaterHeaterCommandRequest &value){};
#endif
#ifdef USE_NUMBER
virtual void on_number_command_request(const NumberCommandRequest &value){};
#endif
+4
View File
@@ -335,6 +335,10 @@ API_DISPATCH_UPDATE(valve::Valve, valve)
API_DISPATCH_UPDATE(media_player::MediaPlayer, media_player)
#endif
#ifdef USE_WATER_HEATER
API_DISPATCH_UPDATE(water_heater::WaterHeater, water_heater)
#endif
#ifdef USE_EVENT
// Event is a special case - unlike other entities with simple state fields,
// events store their state in a member accessed via obj->get_last_event_type()
+3
View File
@@ -133,6 +133,9 @@ class APIServer : public Component,
#ifdef USE_MEDIA_PLAYER
void on_media_player_update(media_player::MediaPlayer *obj) override;
#endif
#ifdef USE_WATER_HEATER
void on_water_heater_update(water_heater::WaterHeater *obj) override;
#endif
#ifdef USE_API_HOMEASSISTANT_SERVICES
void send_homeassistant_action(const HomeassistantActionRequest &call);
+3
View File
@@ -73,6 +73,9 @@ LIST_ENTITIES_HANDLER(media_player, media_player::MediaPlayer, ListEntitiesMedia
LIST_ENTITIES_HANDLER(alarm_control_panel, alarm_control_panel::AlarmControlPanel,
ListEntitiesAlarmControlPanelResponse)
#endif
#ifdef USE_WATER_HEATER
LIST_ENTITIES_HANDLER(water_heater, water_heater::WaterHeater, ListEntitiesWaterHeaterResponse)
#endif
#ifdef USE_EVENT
LIST_ENTITIES_HANDLER(event, event::Event, ListEntitiesEventResponse)
#endif
+3
View File
@@ -82,6 +82,9 @@ class ListEntitiesIterator : public ComponentIterator {
#ifdef USE_ALARM_CONTROL_PANEL
bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *entity) override;
#endif
#ifdef USE_WATER_HEATER
bool on_water_heater(water_heater::WaterHeater *entity) override;
#endif
#ifdef USE_EVENT
bool on_event(event::Event *entity) override;
#endif
@@ -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
+3
View File
@@ -76,6 +76,9 @@ class InitialStateIterator : public ComponentIterator {
#ifdef USE_ALARM_CONTROL_PANEL
bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *entity) override;
#endif
#ifdef USE_WATER_HEATER
bool on_water_heater(water_heater::WaterHeater *entity) override;
#endif
#ifdef USE_EVENT
bool on_event(event::Event *event) override { return true; };
#endif
+1 -1
View File
@@ -41,7 +41,7 @@ class Button : public EntityBase, public EntityBase_DeviceClass {
*/
virtual void press_action() = 0;
CallbackManager<void()> press_callback_{};
LazyCallbackManager<void()> press_callback_{};
};
} // namespace esphome::button
+59 -36
View File
@@ -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]))
-17
View File
@@ -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() {
+33 -12
View File
@@ -2,6 +2,7 @@
#include "esphome/core/defines.h"
#include "esphome/core/controller_registry.h"
#include "esphome/core/macros.h"
#include <strings.h>
namespace esphome::climate {
@@ -190,24 +191,30 @@ ClimateCall &ClimateCall::set_fan_mode(ClimateFanMode fan_mode) {
}
ClimateCall &ClimateCall::set_fan_mode(const char *custom_fan_mode) {
return this->set_fan_mode(custom_fan_mode, strlen(custom_fan_mode));
}
ClimateCall &ClimateCall::set_fan_mode(const std::string &fan_mode) {
return this->set_fan_mode(fan_mode.data(), fan_mode.size());
}
ClimateCall &ClimateCall::set_fan_mode(const char *custom_fan_mode, size_t len) {
// Check if it's a standard enum mode first
for (const auto &mode_entry : CLIMATE_FAN_MODES_BY_STR) {
if (str_equals_case_insensitive(custom_fan_mode, mode_entry.str)) {
if (strncasecmp(custom_fan_mode, mode_entry.str, len) == 0 && mode_entry.str[len] == '\0') {
return this->set_fan_mode(static_cast<ClimateFanMode>(mode_entry.value));
}
}
// Find the matching pointer from parent climate device
if (const char *mode_ptr = this->parent_->find_custom_fan_mode_(custom_fan_mode)) {
if (const char *mode_ptr = this->parent_->find_custom_fan_mode_(custom_fan_mode, len)) {
this->custom_fan_mode_ = mode_ptr;
this->fan_mode_.reset();
return *this;
}
ESP_LOGW(TAG, "'%s' - Unrecognized fan mode %s", this->parent_->get_name().c_str(), custom_fan_mode);
ESP_LOGW(TAG, "'%s' - Unrecognized fan mode %.*s", this->parent_->get_name().c_str(), (int) len, custom_fan_mode);
return *this;
}
ClimateCall &ClimateCall::set_fan_mode(const std::string &fan_mode) { return this->set_fan_mode(fan_mode.c_str()); }
ClimateCall &ClimateCall::set_fan_mode(optional<std::string> fan_mode) {
if (fan_mode.has_value()) {
this->set_fan_mode(fan_mode.value());
@@ -222,24 +229,30 @@ ClimateCall &ClimateCall::set_preset(ClimatePreset preset) {
}
ClimateCall &ClimateCall::set_preset(const char *custom_preset) {
return this->set_preset(custom_preset, strlen(custom_preset));
}
ClimateCall &ClimateCall::set_preset(const std::string &preset) {
return this->set_preset(preset.data(), preset.size());
}
ClimateCall &ClimateCall::set_preset(const char *custom_preset, size_t len) {
// Check if it's a standard enum preset first
for (const auto &preset_entry : CLIMATE_PRESETS_BY_STR) {
if (str_equals_case_insensitive(custom_preset, preset_entry.str)) {
if (strncasecmp(custom_preset, preset_entry.str, len) == 0 && preset_entry.str[len] == '\0') {
return this->set_preset(static_cast<ClimatePreset>(preset_entry.value));
}
}
// Find the matching pointer from parent climate device
if (const char *preset_ptr = this->parent_->find_custom_preset_(custom_preset)) {
if (const char *preset_ptr = this->parent_->find_custom_preset_(custom_preset, len)) {
this->custom_preset_ = preset_ptr;
this->preset_.reset();
return *this;
}
ESP_LOGW(TAG, "'%s' - Unrecognized preset %s", this->parent_->get_name().c_str(), custom_preset);
ESP_LOGW(TAG, "'%s' - Unrecognized preset %.*s", this->parent_->get_name().c_str(), (int) len, custom_preset);
return *this;
}
ClimateCall &ClimateCall::set_preset(const std::string &preset) { return this->set_preset(preset.c_str()); }
ClimateCall &ClimateCall::set_preset(optional<std::string> preset) {
if (preset.has_value()) {
this->set_preset(preset.value());
@@ -688,11 +701,19 @@ bool Climate::set_custom_preset_(const char *preset) {
void Climate::clear_custom_preset_() { this->custom_preset_ = nullptr; }
const char *Climate::find_custom_fan_mode_(const char *custom_fan_mode) {
return this->get_traits().find_custom_fan_mode_(custom_fan_mode);
return this->find_custom_fan_mode_(custom_fan_mode, strlen(custom_fan_mode));
}
const char *Climate::find_custom_fan_mode_(const char *custom_fan_mode, size_t len) {
return this->get_traits().find_custom_fan_mode_(custom_fan_mode, len);
}
const char *Climate::find_custom_preset_(const char *custom_preset) {
return this->get_traits().find_custom_preset_(custom_preset);
return this->find_custom_preset_(custom_preset, strlen(custom_preset));
}
const char *Climate::find_custom_preset_(const char *custom_preset, size_t len) {
return this->get_traits().find_custom_preset_(custom_preset, len);
}
void Climate::dump_traits_(const char *tag) {
+8 -2
View File
@@ -78,6 +78,8 @@ class ClimateCall {
ClimateCall &set_fan_mode(optional<std::string> fan_mode);
/// Set the custom fan mode of the climate device.
ClimateCall &set_fan_mode(const char *custom_fan_mode);
/// Set the custom fan mode of the climate device (zero-copy API path).
ClimateCall &set_fan_mode(const char *custom_fan_mode, size_t len);
/// Set the swing mode of the climate device.
ClimateCall &set_swing_mode(ClimateSwingMode swing_mode);
/// Set the swing mode of the climate device.
@@ -94,6 +96,8 @@ class ClimateCall {
ClimateCall &set_preset(optional<std::string> preset);
/// Set the custom preset of the climate device.
ClimateCall &set_preset(const char *custom_preset);
/// Set the custom preset of the climate device (zero-copy API path).
ClimateCall &set_preset(const char *custom_preset, size_t len);
void perform();
@@ -290,9 +294,11 @@ class Climate : public EntityBase {
/// Find and return the matching custom fan mode pointer from traits, or nullptr if not found.
const char *find_custom_fan_mode_(const char *custom_fan_mode);
const char *find_custom_fan_mode_(const char *custom_fan_mode, size_t len);
/// Find and return the matching custom preset pointer from traits, or nullptr if not found.
const char *find_custom_preset_(const char *custom_preset);
const char *find_custom_preset_(const char *custom_preset, size_t len);
/** Get the default traits of this climate device.
*
@@ -320,8 +326,8 @@ class Climate : public EntityBase {
void dump_traits_(const char *tag);
CallbackManager<void(Climate &)> state_callback_{};
CallbackManager<void(ClimateCall &)> control_callback_{};
LazyCallbackManager<void(Climate &)> state_callback_{};
LazyCallbackManager<void(ClimateCall &)> control_callback_{};
ESPPreferenceObject rtc_;
#ifdef USE_CLIMATE_VISUAL_OVERRIDES
float visual_min_temperature_override_{NAN};
+16 -6
View File
@@ -20,18 +20,22 @@ using ClimatePresetMask = FiniteSetMask<ClimatePreset, DefaultBitPolicy<ClimateP
// Lightweight linear search for small vectors (1-20 items) of const char* pointers
// Avoids std::find template overhead
inline bool vector_contains(const std::vector<const char *> &vec, const char *value) {
inline bool vector_contains(const std::vector<const char *> &vec, const char *value, size_t len) {
for (const char *item : vec) {
if (strcmp(item, value) == 0)
if (strncmp(item, value, len) == 0 && item[len] == '\0')
return true;
}
return false;
}
inline bool vector_contains(const std::vector<const char *> &vec, const char *value) {
return vector_contains(vec, value, strlen(value));
}
// Find and return matching pointer from vector, or nullptr if not found
inline const char *vector_find(const std::vector<const char *> &vec, const char *value) {
inline const char *vector_find(const std::vector<const char *> &vec, const char *value, size_t len) {
for (const char *item : vec) {
if (strcmp(item, value) == 0)
if (strncmp(item, value, len) == 0 && item[len] == '\0')
return item;
}
return nullptr;
@@ -257,13 +261,19 @@ class ClimateTraits {
/// Find and return the matching custom fan mode pointer from supported modes, or nullptr if not found
/// This is protected as it's an implementation detail - use Climate::find_custom_fan_mode_() instead
const char *find_custom_fan_mode_(const char *custom_fan_mode) const {
return vector_find(this->supported_custom_fan_modes_, custom_fan_mode);
return this->find_custom_fan_mode_(custom_fan_mode, strlen(custom_fan_mode));
}
const char *find_custom_fan_mode_(const char *custom_fan_mode, size_t len) const {
return vector_find(this->supported_custom_fan_modes_, custom_fan_mode, len);
}
/// Find and return the matching custom preset pointer from supported presets, or nullptr if not found
/// This is protected as it's an implementation detail - use Climate::find_custom_preset_() instead
const char *find_custom_preset_(const char *custom_preset) const {
return vector_find(this->supported_custom_presets_, custom_preset);
return this->find_custom_preset_(custom_preset, strlen(custom_preset));
}
const char *find_custom_preset_(const char *custom_preset, size_t len) const {
return vector_find(this->supported_custom_presets_, custom_preset, len);
}
uint32_t feature_flags_{0};
+1 -1
View File
@@ -152,7 +152,7 @@ class Cover : public EntityBase, public EntityBase_DeviceClass {
optional<CoverRestoreState> restore_state_();
CallbackManager<void()> state_callback_{};
LazyCallbackManager<void()> state_callback_{};
ESPPreferenceObject rtc_;
};
+1 -1
View File
@@ -22,7 +22,7 @@ class DateTimeBase : public EntityBase {
#endif
protected:
CallbackManager<void()> state_callback_;
LazyCallbackManager<void()> state_callback_;
#ifdef USE_TIME
time::RealTimeClock *rtc_;
+20 -18
View File
@@ -8,17 +8,20 @@ namespace dht {
static const char *const TAG = "dht";
void DHT::setup() {
this->pin_->digital_write(true);
this->pin_->setup();
this->pin_->digital_write(true);
this->t_pin_->digital_write(true);
this->t_pin_->setup();
#ifdef USE_ESP32
this->t_pin_->pin_mode(this->t_pin_->get_flags() | gpio::FLAG_OUTPUT | gpio::FLAG_OPEN_DRAIN);
#endif
this->t_pin_->digital_write(true);
}
void DHT::dump_config() {
ESP_LOGCONFIG(TAG, "DHT:");
LOG_PIN(" Pin: ", this->pin_);
LOG_PIN(" Pin: ", this->t_pin_);
ESP_LOGCONFIG(TAG, " %sModel: %s", this->is_auto_detect_ ? "Auto-detected " : "",
this->model_ == DHT_MODEL_DHT11 ? "DHT11" : "DHT22 or equivalent");
ESP_LOGCONFIG(TAG, " Internal pull-up: %s", ONOFF(this->pin_->get_flags() & gpio::FLAG_PULLUP));
ESP_LOGCONFIG(TAG, " Internal pull-up: %s", ONOFF(this->t_pin_->get_flags() & gpio::FLAG_PULLUP));
LOG_UPDATE_INTERVAL(this);
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
@@ -72,21 +75,15 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r
int8_t i = 0;
uint8_t data[5] = {0, 0, 0, 0, 0};
this->pin_->digital_write(false);
this->pin_->pin_mode(gpio::FLAG_OUTPUT);
this->pin_->digital_write(false);
#ifndef USE_ESP32
this->pin_.pin_mode(gpio::FLAG_OUTPUT);
#endif
this->pin_.digital_write(false);
if (this->model_ == DHT_MODEL_DHT11) {
delayMicroseconds(18000);
} else if (this->model_ == DHT_MODEL_SI7021) {
#ifdef USE_ESP8266
delayMicroseconds(500);
this->pin_->digital_write(true);
delayMicroseconds(40);
#else
delayMicroseconds(400);
this->pin_->digital_write(true);
#endif
} else if (this->model_ == DHT_MODEL_DHT22_TYPE2) {
delayMicroseconds(2000);
} else if (this->model_ == DHT_MODEL_AM2120 || this->model_ == DHT_MODEL_AM2302) {
@@ -94,7 +91,12 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r
} else {
delayMicroseconds(800);
}
this->pin_->pin_mode(this->pin_->get_flags());
#ifdef USE_ESP32
this->pin_.digital_write(true);
#else
this->pin_.pin_mode(this->t_pin_->get_flags());
#endif
{
InterruptLock lock;
@@ -110,7 +112,7 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r
uint32_t start_time = micros();
// Wait for rising edge
while (!this->pin_->digital_read()) {
while (!this->pin_.digital_read()) {
if (micros() - start_time > 90) {
if (i < 0) {
error_code = 1; // line didn't clear
@@ -127,7 +129,7 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r
uint32_t end_time = start_time;
// Wait for falling edge
while (this->pin_->digital_read()) {
while (this->pin_.digital_read()) {
end_time = micros();
if (end_time - start_time > 90) {
if (i < 0) {
+6 -2
View File
@@ -38,7 +38,10 @@ class DHT : public PollingComponent {
*/
void set_dht_model(DHTModel model);
void set_pin(InternalGPIOPin *pin) { pin_ = pin; }
void set_pin(InternalGPIOPin *pin) {
this->t_pin_ = pin;
this->pin_ = pin->to_isr();
}
void set_model(DHTModel model) { model_ = model; }
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; }
@@ -54,7 +57,8 @@ class DHT : public PollingComponent {
protected:
bool read_sensor_(float *temperature, float *humidity, bool report_errors);
InternalGPIOPin *pin_;
InternalGPIOPin *t_pin_;
ISRInternalGPIOPin pin_;
DHTModel model_{DHT_MODEL_AUTO_DETECT};
bool is_auto_detect_{false};
sensor::Sensor *temperature_sensor_{nullptr};
@@ -54,6 +54,7 @@ bool MenuItemSelect::select_next() {
if (this->select_var_ != nullptr) {
this->select_var_->make_call().select_next(true).perform();
this->on_value_();
changed = true;
}
@@ -65,6 +66,7 @@ bool MenuItemSelect::select_prev() {
if (this->select_var_ != nullptr) {
this->select_var_->make_call().select_previous(true).perform();
this->on_value_();
changed = true;
}
+10 -7
View File
@@ -143,9 +143,8 @@ bool ESPBTUUID::operator==(const ESPBTUUID &uuid) const {
return this->as_128bit() == uuid.as_128bit();
}
esp_bt_uuid_t ESPBTUUID::get_uuid() const { return this->uuid_; }
std::string ESPBTUUID::to_string() const {
char buf[40]; // Enough for 128-bit UUID with dashes
char *pos = buf;
void ESPBTUUID::to_str(std::span<char, UUID_STR_LEN> output) const {
char *pos = output.data();
switch (this->uuid_.len) {
case ESP_UUID_LEN_16:
@@ -156,7 +155,7 @@ std::string ESPBTUUID::to_string() const {
*pos++ = format_hex_pretty_char((this->uuid_.uuid.uuid16 >> 4) & 0x0F);
*pos++ = format_hex_pretty_char(this->uuid_.uuid.uuid16 & 0x0F);
*pos = '\0';
return std::string(buf);
return;
case ESP_UUID_LEN_32:
*pos++ = '0';
@@ -165,7 +164,7 @@ std::string ESPBTUUID::to_string() const {
*pos++ = format_hex_pretty_char((this->uuid_.uuid.uuid32 >> shift) & 0x0F);
}
*pos = '\0';
return std::string(buf);
return;
default:
case ESP_UUID_LEN_128:
@@ -179,9 +178,13 @@ std::string ESPBTUUID::to_string() const {
}
}
*pos = '\0';
return std::string(buf);
return;
}
return "";
}
std::string ESPBTUUID::to_string() const {
char buf[UUID_STR_LEN];
this->to_str(buf);
return std::string(buf);
}
} // namespace esphome::esp32_ble
+5
View File
@@ -7,11 +7,15 @@
#ifdef USE_ESP32
#ifdef USE_ESP32_BLE_UUID
#include <span>
#include <string>
#include <esp_bt_defs.h>
namespace esphome::esp32_ble {
/// Buffer size for UUID string: "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX\0"
static constexpr size_t UUID_STR_LEN = 37;
class ESPBTUUID {
public:
ESPBTUUID();
@@ -37,6 +41,7 @@ class ESPBTUUID {
esp_bt_uuid_t get_uuid() const;
std::string to_string() const;
void to_str(std::span<char, UUID_STR_LEN> output) const;
protected:
esp_bt_uuid_t uuid_;
@@ -50,8 +50,12 @@ void BLECharacteristic::parse_descriptors() {
desc->handle = result.handle;
desc->characteristic = this;
this->descriptors.push_back(desc);
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
char uuid_buf[espbt::UUID_STR_LEN];
desc->uuid.to_str(uuid_buf);
ESP_LOGV(TAG, "[%d] [%s] descriptor %s, handle 0x%x", this->service->client->get_connection_index(),
this->service->client->address_str(), desc->uuid.to_string().c_str(), desc->handle);
this->service->client->address_str(), uuid_buf, desc->handle);
#endif
offset++;
}
}
@@ -411,12 +411,15 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
this->update_conn_params_(MEDIUM_MIN_CONN_INTERVAL, MEDIUM_MAX_CONN_INTERVAL, 0, MEDIUM_CONN_TIMEOUT, "medium");
} else if (this->connection_type_ != espbt::ConnectionType::V3_WITH_CACHE) {
#ifdef USE_ESP32_BLE_DEVICE
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
for (auto &svc : this->services_) {
ESP_LOGV(TAG, "[%d] [%s] Service UUID: %s", this->connection_index_, this->address_str_,
svc->uuid.to_string().c_str());
char uuid_buf[espbt::UUID_STR_LEN];
svc->uuid.to_str(uuid_buf);
ESP_LOGV(TAG, "[%d] [%s] Service UUID: %s", this->connection_index_, this->address_str_, uuid_buf);
ESP_LOGV(TAG, "[%d] [%s] start_handle: 0x%x end_handle: 0x%x", this->connection_index_, this->address_str_,
svc->start_handle, svc->end_handle);
}
#endif
#endif
}
ESP_LOGI(TAG, "[%d] [%s] Service discovery complete", this->connection_index_, this->address_str_);
@@ -64,9 +64,12 @@ void BLEService::parse_characteristics() {
characteristic->handle = result.char_handle;
characteristic->service = this;
this->characteristics.push_back(characteristic);
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
char uuid_buf[espbt::UUID_STR_LEN];
characteristic->uuid.to_str(uuid_buf);
ESP_LOGV(TAG, "[%d] [%s] characteristic %s, handle 0x%x, properties 0x%x", this->client->get_connection_index(),
this->client->address_str(), characteristic->uuid.to_string().c_str(), characteristic->handle,
characteristic->properties);
this->client->address_str(), uuid_buf, characteristic->handle, characteristic->properties);
#endif
offset++;
}
}
@@ -109,7 +109,11 @@ void BLECharacteristic::do_create(BLEService *service) {
esp_attr_control_t control;
control.auto_rsp = ESP_GATT_RSP_BY_APP;
ESP_LOGV(TAG, "Creating characteristic - %s", this->uuid_.to_string().c_str());
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
char uuid_buf[esp32_ble::UUID_STR_LEN];
this->uuid_.to_str(uuid_buf);
ESP_LOGV(TAG, "Creating characteristic - %s", uuid_buf);
#endif
esp_bt_uuid_t uuid = this->uuid_.get_uuid();
esp_err_t err = esp_ble_gatts_add_char(service->get_handle(), &uuid, static_cast<esp_gatt_perm_t>(this->permissions_),
@@ -34,7 +34,11 @@ void BLEDescriptor::do_create(BLECharacteristic *characteristic) {
esp_attr_control_t control;
control.auto_rsp = ESP_GATT_AUTO_RSP;
ESP_LOGV(TAG, "Creating descriptor - %s", this->uuid_.to_string().c_str());
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
char uuid_buf[esp32_ble::UUID_STR_LEN];
this->uuid_.to_str(uuid_buf);
ESP_LOGV(TAG, "Creating descriptor - %s", uuid_buf);
#endif
esp_bt_uuid_t uuid = this->uuid_.get_uuid();
esp_err_t err = esp_ble_gatts_add_char_descr(this->characteristic_->get_service()->get_handle(), &uuid,
this->permissions_, &this->value_, &control);
@@ -106,7 +106,11 @@ void BLEServer::restart_advertising_() {
}
BLEService *BLEServer::create_service(ESPBTUUID uuid, bool advertise, uint16_t num_handles) {
ESP_LOGV(TAG, "Creating BLE service - %s", uuid.to_string().c_str());
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
char uuid_buf[esp32_ble::UUID_STR_LEN];
uuid.to_str(uuid_buf);
ESP_LOGV(TAG, "Creating BLE service - %s", uuid_buf);
#endif
// Calculate the inst_id for the service
uint8_t inst_id = 0;
for (; inst_id < 0xFF; inst_id++) {
@@ -115,7 +119,9 @@ BLEService *BLEServer::create_service(ESPBTUUID uuid, bool advertise, uint16_t n
}
}
if (inst_id == 0xFF) {
ESP_LOGW(TAG, "Could not create BLE service %s, too many instances", uuid.to_string().c_str());
char warn_uuid_buf[esp32_ble::UUID_STR_LEN];
uuid.to_str(warn_uuid_buf);
ESP_LOGW(TAG, "Could not create BLE service %s, too many instances", warn_uuid_buf);
return nullptr;
}
BLEService *service = // NOLINT(cppcoreguidelines-owning-memory)
@@ -128,7 +134,11 @@ BLEService *BLEServer::create_service(ESPBTUUID uuid, bool advertise, uint16_t n
}
void BLEServer::remove_service(ESPBTUUID uuid, uint8_t inst_id) {
ESP_LOGV(TAG, "Removing BLE service - %s %d", uuid.to_string().c_str(), inst_id);
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
char uuid_buf[esp32_ble::UUID_STR_LEN];
uuid.to_str(uuid_buf);
ESP_LOGV(TAG, "Removing BLE service - %s %d", uuid_buf, inst_id);
#endif
for (auto it = this->services_.begin(); it != this->services_.end(); ++it) {
if (it->uuid == uuid && it->inst_id == inst_id) {
it->service->do_delete();
@@ -137,7 +147,9 @@ void BLEServer::remove_service(ESPBTUUID uuid, uint8_t inst_id) {
return;
}
}
ESP_LOGW(TAG, "BLE service %s %d does not exist", uuid.to_string().c_str(), inst_id);
char warn_uuid_buf[esp32_ble::UUID_STR_LEN];
uuid.to_str(warn_uuid_buf);
ESP_LOGW(TAG, "BLE service %s %d does not exist", warn_uuid_buf, inst_id);
}
BLEService *BLEServer::get_service(ESPBTUUID uuid, uint8_t inst_id) {
@@ -5,7 +5,7 @@ import logging
from esphome import automation
import esphome.codegen as cg
from esphome.components import esp32_ble
from esphome.components import esp32_ble, ota
from esphome.components.esp32 import add_idf_sdkconfig_option
from esphome.components.esp32_ble import (
IDF_MAX_CONNECTIONS,
@@ -328,7 +328,7 @@ async def to_code(config):
# Note: CONFIG_BT_ACL_CONNECTIONS and CONFIG_BTDM_CTRL_BLE_MAX_CONN are now
# configured in esp32_ble component based on max_connections setting
cg.add_define("USE_OTA_STATE_CALLBACK") # To be notified when an OTA update starts
ota.request_ota_state_listeners() # To be notified when an OTA update starts
cg.add_define("USE_ESP32_BLE_CLIENT")
CORE.add_job(_add_ble_features)
@@ -71,21 +71,24 @@ void ESP32BLETracker::setup() {
global_esp32_ble_tracker = this;
#ifdef USE_OTA
ota::get_global_ota_callback()->add_on_state_callback(
[this](ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) {
if (state == ota::OTA_STARTED) {
this->stop_scan();
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
for (auto *client : this->clients_) {
client->disconnect();
}
#endif
}
});
#ifdef USE_OTA_STATE_LISTENER
ota::get_global_ota_callback()->add_global_state_listener(this);
#endif
}
#ifdef USE_OTA_STATE_LISTENER
void ESP32BLETracker::on_ota_global_state(ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) {
if (state == ota::OTA_STARTED) {
this->stop_scan();
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
for (auto *client : this->clients_) {
client->disconnect();
}
#endif
}
}
#endif
void ESP32BLETracker::loop() {
if (!this->parent_->is_active()) {
this->ble_was_disabled_ = true;
@@ -438,24 +441,31 @@ void ESPBTDevice::parse_scan_rst(const BLEScanResult &scan_result) {
ESP_LOGVV(TAG, " Ad Flag: %u", *this->ad_flag_);
}
for (auto &uuid : this->service_uuids_) {
ESP_LOGVV(TAG, " Service UUID: %s", uuid.to_string().c_str());
char uuid_buf[esp32_ble::UUID_STR_LEN];
uuid.to_str(uuid_buf);
ESP_LOGVV(TAG, " Service UUID: %s", uuid_buf);
}
for (auto &data : this->manufacturer_datas_) {
auto ibeacon = ESPBLEiBeacon::from_manufacturer_data(data);
if (ibeacon.has_value()) {
ESP_LOGVV(TAG, " Manufacturer iBeacon:");
ESP_LOGVV(TAG, " UUID: %s", ibeacon.value().get_uuid().to_string().c_str());
char uuid_buf[esp32_ble::UUID_STR_LEN];
ibeacon.value().get_uuid().to_str(uuid_buf);
ESP_LOGVV(TAG, " UUID: %s", uuid_buf);
ESP_LOGVV(TAG, " Major: %u", ibeacon.value().get_major());
ESP_LOGVV(TAG, " Minor: %u", ibeacon.value().get_minor());
ESP_LOGVV(TAG, " TXPower: %d", ibeacon.value().get_signal_power());
} else {
ESP_LOGVV(TAG, " Manufacturer ID: %s, data: %s", data.uuid.to_string().c_str(),
format_hex_pretty(data.data).c_str());
char uuid_buf[esp32_ble::UUID_STR_LEN];
data.uuid.to_str(uuid_buf);
ESP_LOGVV(TAG, " Manufacturer ID: %s, data: %s", uuid_buf, format_hex_pretty(data.data).c_str());
}
}
for (auto &data : this->service_datas_) {
ESP_LOGVV(TAG, " Service data:");
ESP_LOGVV(TAG, " UUID: %s", data.uuid.to_string().c_str());
char uuid_buf[esp32_ble::UUID_STR_LEN];
data.uuid.to_str(uuid_buf);
ESP_LOGVV(TAG, " UUID: %s", uuid_buf);
ESP_LOGVV(TAG, " Data: %s", format_hex_pretty(data.data).c_str());
}
@@ -22,6 +22,10 @@
#include "esphome/components/esp32_ble/ble_uuid.h"
#include "esphome/components/esp32_ble/ble_scan_result.h"
#ifdef USE_OTA_STATE_LISTENER
#include "esphome/components/ota/ota_backend.h"
#endif
namespace esphome::esp32_ble_tracker {
using namespace esp32_ble;
@@ -241,6 +245,9 @@ class ESP32BLETracker : public Component,
public GAPScanEventHandler,
public GATTcEventHandler,
public BLEStatusEventHandler,
#ifdef USE_OTA_STATE_LISTENER
public ota::OTAGlobalStateListener,
#endif
public Parented<ESP32BLE> {
public:
void set_scan_duration(uint32_t scan_duration) { scan_duration_ = scan_duration; }
@@ -274,6 +281,10 @@ class ESP32BLETracker : public Component,
void gap_scan_event_handler(const BLEScanResult &scan_result) override;
void ble_before_disabled_event_handler() override;
#ifdef USE_OTA_STATE_LISTENER
void on_ota_global_state(ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) override;
#endif
/// Add a listener for scanner state changes
void add_scanner_state_listener(BLEScannerStateListener *listener) {
this->scanner_state_listeners_.push_back(listener);
@@ -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
+8 -12
View File
@@ -41,10 +41,6 @@ static constexpr size_t SHA256_HEX_SIZE = 64; // SHA256 hash as hex string (32
#endif // USE_OTA_PASSWORD
void ESPHomeOTAComponent::setup() {
#ifdef USE_OTA_STATE_CALLBACK
ota::register_ota_platform(this);
#endif
this->server_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0); // monitored for incoming connections
if (this->server_ == nullptr) {
this->log_socket_error_(LOG_STR("creation"));
@@ -297,8 +293,8 @@ void ESPHomeOTAComponent::handle_data_() {
// accidentally trigger the update process.
this->log_start_(LOG_STR("update"));
this->status_set_warning();
#ifdef USE_OTA_STATE_CALLBACK
this->state_callback_.call(ota::OTA_STARTED, 0.0f, 0);
#ifdef USE_OTA_STATE_LISTENER
this->notify_state_(ota::OTA_STARTED, 0.0f, 0);
#endif
// This will block for a few seconds as it locks flash
@@ -357,8 +353,8 @@ void ESPHomeOTAComponent::handle_data_() {
last_progress = now;
float percentage = (total * 100.0f) / ota_size;
ESP_LOGD(TAG, "Progress: %0.1f%%", percentage);
#ifdef USE_OTA_STATE_CALLBACK
this->state_callback_.call(ota::OTA_IN_PROGRESS, percentage, 0);
#ifdef USE_OTA_STATE_LISTENER
this->notify_state_(ota::OTA_IN_PROGRESS, percentage, 0);
#endif
// feed watchdog and give other tasks a chance to run
this->yield_and_feed_watchdog_();
@@ -387,8 +383,8 @@ void ESPHomeOTAComponent::handle_data_() {
delay(10);
ESP_LOGI(TAG, "Update complete");
this->status_clear_warning();
#ifdef USE_OTA_STATE_CALLBACK
this->state_callback_.call(ota::OTA_COMPLETED, 100.0f, 0);
#ifdef USE_OTA_STATE_LISTENER
this->notify_state_(ota::OTA_COMPLETED, 100.0f, 0);
#endif
delay(100); // NOLINT
App.safe_reboot();
@@ -402,8 +398,8 @@ error:
}
this->status_momentary_error("err", 5000);
#ifdef USE_OTA_STATE_CALLBACK
this->state_callback_.call(ota::OTA_ERROR, 0.0f, static_cast<uint8_t>(error_code));
#ifdef USE_OTA_STATE_LISTENER
this->notify_state_(ota::OTA_ERROR, 0.0f, static_cast<uint8_t>(error_code));
#endif
}
+1 -1
View File
@@ -50,7 +50,7 @@ class Event : public EntityBase, public EntityBase_DeviceClass {
void add_on_event_callback(std::function<void(const std::string &event_type)> &&callback);
protected:
CallbackManager<void(const std::string &event_type)> event_callback_;
LazyCallbackManager<void(const std::string &event_type)> event_callback_;
FixedVector<const char *> types_;
private:
+17 -5
View File
@@ -19,22 +19,28 @@ const LogString *fan_direction_to_string(FanDirection direction) {
}
}
FanCall &FanCall::set_preset_mode(const std::string &preset_mode) { return this->set_preset_mode(preset_mode.c_str()); }
FanCall &FanCall::set_preset_mode(const std::string &preset_mode) {
return this->set_preset_mode(preset_mode.data(), preset_mode.size());
}
FanCall &FanCall::set_preset_mode(const char *preset_mode) {
if (preset_mode == nullptr || strlen(preset_mode) == 0) {
return this->set_preset_mode(preset_mode, preset_mode ? strlen(preset_mode) : 0);
}
FanCall &FanCall::set_preset_mode(const char *preset_mode, size_t len) {
if (preset_mode == nullptr || len == 0) {
this->preset_mode_ = nullptr;
return *this;
}
// Find and validate pointer from traits immediately
auto traits = this->parent_.get_traits();
const char *validated_mode = traits.find_preset_mode(preset_mode);
const char *validated_mode = traits.find_preset_mode(preset_mode, len);
if (validated_mode != nullptr) {
this->preset_mode_ = validated_mode; // Store pointer from traits
} else {
// Preset mode not found in traits - log warning and don't set
ESP_LOGW(TAG, "%s: Preset mode '%s' not supported", this->parent_.get_name().c_str(), preset_mode);
ESP_LOGW(TAG, "%s: Preset mode '%.*s' not supported", this->parent_.get_name().c_str(), (int) len, preset_mode);
this->preset_mode_ = nullptr;
}
return *this;
@@ -140,7 +146,13 @@ FanCall Fan::turn_off() { return this->make_call().set_state(false); }
FanCall Fan::toggle() { return this->make_call().set_state(!this->state); }
FanCall Fan::make_call() { return FanCall(*this); }
const char *Fan::find_preset_mode_(const char *preset_mode) { return this->get_traits().find_preset_mode(preset_mode); }
const char *Fan::find_preset_mode_(const char *preset_mode) {
return this->find_preset_mode_(preset_mode, preset_mode ? strlen(preset_mode) : 0);
}
const char *Fan::find_preset_mode_(const char *preset_mode, size_t len) {
return this->get_traits().find_preset_mode(preset_mode, len);
}
bool Fan::set_preset_mode_(const char *preset_mode) {
if (preset_mode == nullptr) {
+3 -1
View File
@@ -72,6 +72,7 @@ class FanCall {
optional<FanDirection> get_direction() const { return this->direction_; }
FanCall &set_preset_mode(const std::string &preset_mode);
FanCall &set_preset_mode(const char *preset_mode);
FanCall &set_preset_mode(const char *preset_mode, size_t len);
const char *get_preset_mode() const { return this->preset_mode_; }
bool has_preset_mode() const { return this->preset_mode_ != nullptr; }
@@ -152,8 +153,9 @@ class Fan : public EntityBase {
void clear_preset_mode_();
/// Find and return the matching preset mode pointer from traits, or nullptr if not found.
const char *find_preset_mode_(const char *preset_mode);
const char *find_preset_mode_(const char *preset_mode, size_t len);
CallbackManager<void()> state_callback_{};
LazyCallbackManager<void()> state_callback_{};
ESPPreferenceObject rtc_;
FanRestoreMode restore_mode_;
+5 -2
View File
@@ -47,10 +47,13 @@ class FanTraits {
bool supports_preset_modes() const { return !this->preset_modes_.empty(); }
/// Find and return the matching preset mode pointer from supported modes, or nullptr if not found.
const char *find_preset_mode(const char *preset_mode) const {
if (preset_mode == nullptr)
return this->find_preset_mode(preset_mode, preset_mode ? strlen(preset_mode) : 0);
}
const char *find_preset_mode(const char *preset_mode, size_t len) const {
if (preset_mode == nullptr || len == 0)
return nullptr;
for (const char *mode : this->preset_modes_) {
if (strcmp(mode, preset_mode) == 0) {
if (strncmp(mode, preset_mode, len) == 0 && mode[len] == '\0') {
return mode; // Return pointer from traits
}
}
@@ -16,12 +16,6 @@ namespace http_request {
static const char *const TAG = "http_request.ota";
void OtaHttpRequestComponent::setup() {
#ifdef USE_OTA_STATE_CALLBACK
ota::register_ota_platform(this);
#endif
}
void OtaHttpRequestComponent::dump_config() { ESP_LOGCONFIG(TAG, "Over-The-Air updates via HTTP request"); };
void OtaHttpRequestComponent::set_md5_url(const std::string &url) {
@@ -48,24 +42,24 @@ void OtaHttpRequestComponent::flash() {
}
ESP_LOGI(TAG, "Starting update");
#ifdef USE_OTA_STATE_CALLBACK
this->state_callback_.call(ota::OTA_STARTED, 0.0f, 0);
#ifdef USE_OTA_STATE_LISTENER
this->notify_state_(ota::OTA_STARTED, 0.0f, 0);
#endif
auto ota_status = this->do_ota_();
switch (ota_status) {
case ota::OTA_RESPONSE_OK:
#ifdef USE_OTA_STATE_CALLBACK
this->state_callback_.call(ota::OTA_COMPLETED, 100.0f, ota_status);
#ifdef USE_OTA_STATE_LISTENER
this->notify_state_(ota::OTA_COMPLETED, 100.0f, ota_status);
#endif
delay(10);
App.safe_reboot();
break;
default:
#ifdef USE_OTA_STATE_CALLBACK
this->state_callback_.call(ota::OTA_ERROR, 0.0f, ota_status);
#ifdef USE_OTA_STATE_LISTENER
this->notify_state_(ota::OTA_ERROR, 0.0f, ota_status);
#endif
this->md5_computed_.clear(); // will be reset at next attempt
this->md5_expected_.clear(); // will be reset at next attempt
@@ -165,8 +159,8 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
last_progress = now;
float percentage = container->get_bytes_read() * 100.0f / container->content_length;
ESP_LOGD(TAG, "Progress: %0.1f%%", percentage);
#ifdef USE_OTA_STATE_CALLBACK
this->state_callback_.call(ota::OTA_IN_PROGRESS, percentage, 0);
#ifdef USE_OTA_STATE_LISTENER
this->notify_state_(ota::OTA_IN_PROGRESS, percentage, 0);
#endif
}
} // while
@@ -24,7 +24,6 @@ enum OtaHttpRequestError : uint8_t {
class OtaHttpRequestComponent : public ota::OTAComponent, public Parented<HttpRequestComponent> {
public:
void setup() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
@@ -1,5 +1,5 @@
import esphome.codegen as cg
from esphome.components import update
from esphome.components import ota, update
import esphome.config_validation as cv
from esphome.const import CONF_SOURCE
@@ -38,6 +38,6 @@ async def to_code(config):
cg.add(var.set_source_url(config[CONF_SOURCE]))
cg.add_define("USE_OTA_STATE_CALLBACK")
ota.request_ota_state_listeners()
await cg.register_component(var, config)
@@ -20,19 +20,19 @@ static const char *const TAG = "http_request.update";
static const size_t MAX_READ_SIZE = 256;
void HttpRequestUpdate::setup() {
this->ota_parent_->add_on_state_callback([this](ota::OTAState state, float progress, uint8_t err) {
if (state == ota::OTAState::OTA_IN_PROGRESS) {
this->state_ = update::UPDATE_STATE_INSTALLING;
this->update_info_.has_progress = true;
this->update_info_.progress = progress;
this->publish_state();
} else if (state == ota::OTAState::OTA_ABORT || state == ota::OTAState::OTA_ERROR) {
this->state_ = update::UPDATE_STATE_AVAILABLE;
this->status_set_error(LOG_STR("Failed to install firmware"));
this->publish_state();
}
});
void HttpRequestUpdate::setup() { this->ota_parent_->add_state_listener(this); }
void HttpRequestUpdate::on_ota_state(ota::OTAState state, float progress, uint8_t error) {
if (state == ota::OTAState::OTA_IN_PROGRESS) {
this->state_ = update::UPDATE_STATE_INSTALLING;
this->update_info_.has_progress = true;
this->update_info_.progress = progress;
this->publish_state();
} else if (state == ota::OTAState::OTA_ABORT || state == ota::OTAState::OTA_ERROR) {
this->state_ = update::UPDATE_STATE_AVAILABLE;
this->status_set_error(LOG_STR("Failed to install firmware"));
this->publish_state();
}
}
void HttpRequestUpdate::update() {
@@ -14,7 +14,7 @@
namespace esphome {
namespace http_request {
class HttpRequestUpdate : public update::UpdateEntity, public PollingComponent {
class HttpRequestUpdate final : public update::UpdateEntity, public PollingComponent, public ota::OTAStateListener {
public:
void setup() override;
void update() override;
@@ -29,6 +29,8 @@ class HttpRequestUpdate : public update::UpdateEntity, public PollingComponent {
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
void on_ota_state(ota::OTAState state, float progress, uint8_t error) override;
protected:
HttpRequestComponent *request_parent_;
OtaHttpRequestComponent *ota_parent_;
+23 -23
View File
@@ -95,35 +95,35 @@ CONF_DOUBLE_BUFFER = "double_buffer"
CONF_MIN_REFRESH_RATE = "min_refresh_rate"
# Map to hub75 library enums (in global namespace)
ShiftDriver = cg.global_ns.enum("ShiftDriver", is_class=True)
Hub75ShiftDriver = cg.global_ns.enum("Hub75ShiftDriver", is_class=True)
SHIFT_DRIVERS = {
"GENERIC": ShiftDriver.GENERIC,
"FM6126A": ShiftDriver.FM6126A,
"ICN2038S": ShiftDriver.ICN2038S,
"FM6124": ShiftDriver.FM6124,
"MBI5124": ShiftDriver.MBI5124,
"DP3246": ShiftDriver.DP3246,
"GENERIC": Hub75ShiftDriver.GENERIC,
"FM6126A": Hub75ShiftDriver.FM6126A,
"ICN2038S": Hub75ShiftDriver.ICN2038S,
"FM6124": Hub75ShiftDriver.FM6124,
"MBI5124": Hub75ShiftDriver.MBI5124,
"DP3246": Hub75ShiftDriver.DP3246,
}
PanelLayout = cg.global_ns.enum("PanelLayout", is_class=True)
Hub75PanelLayout = cg.global_ns.enum("Hub75PanelLayout", is_class=True)
PANEL_LAYOUTS = {
"HORIZONTAL": PanelLayout.HORIZONTAL,
"TOP_LEFT_DOWN": PanelLayout.TOP_LEFT_DOWN,
"TOP_RIGHT_DOWN": PanelLayout.TOP_RIGHT_DOWN,
"BOTTOM_LEFT_UP": PanelLayout.BOTTOM_LEFT_UP,
"BOTTOM_RIGHT_UP": PanelLayout.BOTTOM_RIGHT_UP,
"TOP_LEFT_DOWN_ZIGZAG": PanelLayout.TOP_LEFT_DOWN_ZIGZAG,
"TOP_RIGHT_DOWN_ZIGZAG": PanelLayout.TOP_RIGHT_DOWN_ZIGZAG,
"BOTTOM_LEFT_UP_ZIGZAG": PanelLayout.BOTTOM_LEFT_UP_ZIGZAG,
"BOTTOM_RIGHT_UP_ZIGZAG": PanelLayout.BOTTOM_RIGHT_UP_ZIGZAG,
"HORIZONTAL": Hub75PanelLayout.HORIZONTAL,
"TOP_LEFT_DOWN": Hub75PanelLayout.TOP_LEFT_DOWN,
"TOP_RIGHT_DOWN": Hub75PanelLayout.TOP_RIGHT_DOWN,
"BOTTOM_LEFT_UP": Hub75PanelLayout.BOTTOM_LEFT_UP,
"BOTTOM_RIGHT_UP": Hub75PanelLayout.BOTTOM_RIGHT_UP,
"TOP_LEFT_DOWN_ZIGZAG": Hub75PanelLayout.TOP_LEFT_DOWN_ZIGZAG,
"TOP_RIGHT_DOWN_ZIGZAG": Hub75PanelLayout.TOP_RIGHT_DOWN_ZIGZAG,
"BOTTOM_LEFT_UP_ZIGZAG": Hub75PanelLayout.BOTTOM_LEFT_UP_ZIGZAG,
"BOTTOM_RIGHT_UP_ZIGZAG": Hub75PanelLayout.BOTTOM_RIGHT_UP_ZIGZAG,
}
ScanPattern = cg.global_ns.enum("ScanPattern", is_class=True)
Hub75ScanWiring = cg.global_ns.enum("Hub75ScanWiring", is_class=True)
SCAN_PATTERNS = {
"STANDARD_TWO_SCAN": ScanPattern.STANDARD_TWO_SCAN,
"FOUR_SCAN_16PX_HIGH": ScanPattern.FOUR_SCAN_16PX_HIGH,
"FOUR_SCAN_32PX_HIGH": ScanPattern.FOUR_SCAN_32PX_HIGH,
"FOUR_SCAN_64PX_HIGH": ScanPattern.FOUR_SCAN_64PX_HIGH,
"STANDARD_TWO_SCAN": Hub75ScanWiring.STANDARD_TWO_SCAN,
"FOUR_SCAN_16PX_HIGH": Hub75ScanWiring.FOUR_SCAN_16PX_HIGH,
"FOUR_SCAN_32PX_HIGH": Hub75ScanWiring.FOUR_SCAN_32PX_HIGH,
"FOUR_SCAN_64PX_HIGH": Hub75ScanWiring.FOUR_SCAN_64PX_HIGH,
}
Hub75ClockSpeed = cg.global_ns.enum("Hub75ClockSpeed", is_class=True)
@@ -531,7 +531,7 @@ def _build_config_struct(
async def to_code(config: ConfigType) -> None:
add_idf_component(
name="esphome/esp-hub75",
ref="0.1.6",
ref="0.1.7",
)
# Set compile-time configuration via defines
+1 -1
View File
@@ -174,7 +174,7 @@ class Lock : public EntityBase {
*/
virtual void control(const LockCall &call) = 0;
CallbackManager<void()> state_callback_{};
LazyCallbackManager<void()> state_callback_{};
Deduplicator<LockState> publish_dedup_;
ESPPreferenceObject rtc_;
};
+2 -2
View File
@@ -118,11 +118,11 @@ static constexpr uint16_t MAX_HEADER_SIZE = 128;
static constexpr size_t MAX_POINTER_REPRESENTATION = 2 + sizeof(void *) * 2 + 1;
// Platform-specific: does write_msg_ add its own newline?
// false: Caller must add newline to buffer before calling write_msg_ (ESP32, ESP8266, LibreTiny)
// false: Caller must add newline to buffer before calling write_msg_ (ESP32, ESP8266, RP2040, LibreTiny)
// Allows single write call with newline included for efficiency
// true: write_msg_ adds newline itself via puts()/println() (other platforms)
// Newline should NOT be added to buffer
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_LIBRETINY)
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY)
static constexpr bool WRITE_MSG_ADDS_NEWLINE = false;
#else
static constexpr bool WRITE_MSG_ADDS_NEWLINE = true;
+4 -1
View File
@@ -27,7 +27,10 @@ void Logger::pre_setup() {
ESP_LOGI(TAG, "Log initialized");
}
void HOT Logger::write_msg_(const char *msg, size_t) { this->hw_serial_->println(msg); }
void HOT Logger::write_msg_(const char *msg, size_t len) {
// Single write with newline already in buffer (added by caller)
this->hw_serial_->write(msg, len);
}
const LogString *Logger::get_uart_selection_() {
switch (this->uart_) {
@@ -157,7 +157,7 @@ class MediaPlayer : public EntityBase {
virtual void control(const MediaPlayerCall &call) = 0;
CallbackManager<void()> state_callback_{};
LazyCallbackManager<void()> state_callback_{};
};
} // namespace media_player
@@ -7,7 +7,7 @@ from urllib.parse import urljoin
from esphome import automation, external_files, git
from esphome.automation import register_action, register_condition
import esphome.codegen as cg
from esphome.components import esp32, microphone, socket
from esphome.components import esp32, microphone, ota, socket
import esphome.config_validation as cv
from esphome.const import (
CONF_FILE,
@@ -452,7 +452,7 @@ async def to_code(config):
cg.add(var.set_microphone_source(mic_source))
cg.add_define("USE_MICRO_WAKE_WORD")
cg.add_define("USE_OTA_STATE_CALLBACK")
ota.request_ota_state_listeners()
esp32.add_idf_component(name="espressif/esp-tflite-micro", ref="1.3.3~1")
@@ -119,18 +119,21 @@ void MicroWakeWord::setup() {
}
});
#ifdef USE_OTA
ota::get_global_ota_callback()->add_on_state_callback(
[this](ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) {
if (state == ota::OTA_STARTED) {
this->suspend_task_();
} else if (state == ota::OTA_ERROR) {
this->resume_task_();
}
});
#ifdef USE_OTA_STATE_LISTENER
ota::get_global_ota_callback()->add_global_state_listener(this);
#endif
}
#ifdef USE_OTA_STATE_LISTENER
void MicroWakeWord::on_ota_global_state(ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) {
if (state == ota::OTA_STARTED) {
this->suspend_task_();
} else if (state == ota::OTA_ERROR) {
this->resume_task_();
}
}
#endif
void MicroWakeWord::inference_task(void *params) {
MicroWakeWord *this_mww = (MicroWakeWord *) params;
@@ -9,8 +9,13 @@
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/core/ring_buffer.h"
#ifdef USE_OTA_STATE_LISTENER
#include "esphome/components/ota/ota_backend.h"
#endif
#include <freertos/event_groups.h>
#include <frontend.h>
@@ -26,13 +31,22 @@ enum State {
STOPPED,
};
class MicroWakeWord : public Component {
class MicroWakeWord : public Component
#ifdef USE_OTA_STATE_LISTENER
,
public ota::OTAGlobalStateListener
#endif
{
public:
void setup() override;
void loop() override;
float get_setup_priority() const override;
void dump_config() override;
#ifdef USE_OTA_STATE_LISTENER
void on_ota_global_state(ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) override;
#endif
void start();
void stop();
+1 -2
View File
@@ -9,6 +9,7 @@ from esphome.const import (
CONF_GAIN_FACTOR,
CONF_ID,
CONF_MICROPHONE,
CONF_ON_DATA,
CONF_TRIGGER_ID,
)
from esphome.core import CORE
@@ -19,8 +20,6 @@ CODEOWNERS = ["@jesserockz", "@kahrendt"]
IS_PLATFORM_COMPONENT = True
CONF_ON_DATA = "on_data"
microphone_ns = cg.esphome_ns.namespace("microphone")
Microphone = microphone_ns.class_("Microphone")
+1 -1
View File
@@ -49,7 +49,7 @@ class Number : public EntityBase {
*/
virtual void control(float value) = 0;
CallbackManager<void(float)> state_callback_;
LazyCallbackManager<void(float)> state_callback_;
};
} // namespace esphome::number
+21 -1
View File
@@ -13,6 +13,8 @@ from esphome.const import (
from esphome.core import CORE, coroutine_with_priority
from esphome.coroutine import CoroPriority
OTA_STATE_LISTENER_KEY = "ota_state_listener"
CODEOWNERS = ["@esphome/core"]
AUTO_LOAD = ["md5", "safe_mode"]
@@ -86,6 +88,7 @@ BASE_OTA_SCHEMA = cv.Schema(
@coroutine_with_priority(CoroPriority.OTA_UPDATES)
async def to_code(config):
cg.add_define("USE_OTA")
CORE.add_job(final_step)
if CORE.is_rp2040 and CORE.using_arduino:
cg.add_library("Updater", None)
@@ -119,7 +122,24 @@ async def ota_to_code(var, config):
await automation.build_automation(trigger, [(cg.uint8, "x")], conf)
use_state_callback = True
if use_state_callback:
cg.add_define("USE_OTA_STATE_CALLBACK")
request_ota_state_listeners()
def request_ota_state_listeners() -> None:
"""Request that OTA state listeners be compiled in.
Components that need to be notified about OTA state changes (start, progress,
complete, error) should call this function during their code generation.
This enables the add_state_listener() API on OTAComponent.
"""
CORE.data[OTA_STATE_LISTENER_KEY] = True
@coroutine_with_priority(CoroPriority.FINAL)
async def final_step():
"""Final code generation step to configure optional OTA features."""
if CORE.data.get(OTA_STATE_LISTENER_KEY, False):
cg.add_define("USE_OTA_STATE_LISTENER")
FILTER_SOURCE_FILES = filter_source_files_from_platform(
+43 -49
View File
@@ -1,5 +1,5 @@
#pragma once
#ifdef USE_OTA_STATE_CALLBACK
#ifdef USE_OTA_STATE_LISTENER
#include "ota_backend.h"
#include "esphome/core/automation.h"
@@ -7,70 +7,64 @@
namespace esphome {
namespace ota {
class OTAStateChangeTrigger : public Trigger<OTAState> {
class OTAStateChangeTrigger final : public Trigger<OTAState>, public OTAStateListener {
public:
explicit OTAStateChangeTrigger(OTAComponent *parent) {
parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) {
if (!parent->is_failed()) {
trigger(state);
}
});
explicit OTAStateChangeTrigger(OTAComponent *parent) : parent_(parent) { parent->add_state_listener(this); }
void on_ota_state(OTAState state, float progress, uint8_t error) override {
if (!this->parent_->is_failed()) {
this->trigger(state);
}
}
protected:
OTAComponent *parent_;
};
class OTAStartTrigger : public Trigger<> {
template<OTAState State> class OTAStateTrigger final : public Trigger<>, public OTAStateListener {
public:
explicit OTAStartTrigger(OTAComponent *parent) {
parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) {
if (state == OTA_STARTED && !parent->is_failed()) {
trigger();
}
});
explicit OTAStateTrigger(OTAComponent *parent) : parent_(parent) { parent->add_state_listener(this); }
void on_ota_state(OTAState state, float progress, uint8_t error) override {
if (state == State && !this->parent_->is_failed()) {
this->trigger();
}
}
protected:
OTAComponent *parent_;
};
class OTAProgressTrigger : public Trigger<float> {
using OTAStartTrigger = OTAStateTrigger<OTA_STARTED>;
using OTAEndTrigger = OTAStateTrigger<OTA_COMPLETED>;
using OTAAbortTrigger = OTAStateTrigger<OTA_ABORT>;
class OTAProgressTrigger final : public Trigger<float>, public OTAStateListener {
public:
explicit OTAProgressTrigger(OTAComponent *parent) {
parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) {
if (state == OTA_IN_PROGRESS && !parent->is_failed()) {
trigger(progress);
}
});
explicit OTAProgressTrigger(OTAComponent *parent) : parent_(parent) { parent->add_state_listener(this); }
void on_ota_state(OTAState state, float progress, uint8_t error) override {
if (state == OTA_IN_PROGRESS && !this->parent_->is_failed()) {
this->trigger(progress);
}
}
protected:
OTAComponent *parent_;
};
class OTAEndTrigger : public Trigger<> {
class OTAErrorTrigger final : public Trigger<uint8_t>, public OTAStateListener {
public:
explicit OTAEndTrigger(OTAComponent *parent) {
parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) {
if (state == OTA_COMPLETED && !parent->is_failed()) {
trigger();
}
});
}
};
explicit OTAErrorTrigger(OTAComponent *parent) : parent_(parent) { parent->add_state_listener(this); }
class OTAAbortTrigger : public Trigger<> {
public:
explicit OTAAbortTrigger(OTAComponent *parent) {
parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) {
if (state == OTA_ABORT && !parent->is_failed()) {
trigger();
}
});
void on_ota_state(OTAState state, float progress, uint8_t error) override {
if (state == OTA_ERROR && !this->parent_->is_failed()) {
this->trigger(error);
}
}
};
class OTAErrorTrigger : public Trigger<uint8_t> {
public:
explicit OTAErrorTrigger(OTAComponent *parent) {
parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) {
if (state == OTA_ERROR && !parent->is_failed()) {
trigger(error);
}
});
}
protected:
OTAComponent *parent_;
};
} // namespace ota
+7 -2
View File
@@ -3,7 +3,7 @@
namespace esphome {
namespace ota {
#ifdef USE_OTA_STATE_CALLBACK
#ifdef USE_OTA_STATE_LISTENER
OTAGlobalCallback *global_ota_callback{nullptr}; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
OTAGlobalCallback *get_global_ota_callback() {
@@ -13,7 +13,12 @@ OTAGlobalCallback *get_global_ota_callback() {
return global_ota_callback;
}
void register_ota_platform(OTAComponent *ota_caller) { get_global_ota_callback()->register_ota(ota_caller); }
void OTAComponent::notify_state_(OTAState state, float progress, uint8_t error) {
for (auto *listener : this->state_listeners_) {
listener->on_ota_state(state, progress, error);
}
get_global_ota_callback()->notify_ota_state(state, progress, error, this);
}
#endif
} // namespace ota
+52 -39
View File
@@ -4,8 +4,8 @@
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#ifdef USE_OTA_STATE_CALLBACK
#include "esphome/core/automation.h"
#ifdef USE_OTA_STATE_LISTENER
#include <vector>
#endif
namespace esphome {
@@ -60,62 +60,75 @@ class OTABackend {
virtual bool supports_compression() = 0;
};
class OTAComponent : public Component {
#ifdef USE_OTA_STATE_CALLBACK
/** Listener interface for OTA state changes.
*
* Components can implement this interface to receive OTA state updates
* without the overhead of std::function callbacks.
*/
class OTAStateListener {
public:
void add_on_state_callback(std::function<void(ota::OTAState, float, uint8_t)> &&callback) {
this->state_callback_.add(std::move(callback));
}
virtual ~OTAStateListener() = default;
virtual void on_ota_state(OTAState state, float progress, uint8_t error) = 0;
};
class OTAComponent : public Component {
#ifdef USE_OTA_STATE_LISTENER
public:
void add_state_listener(OTAStateListener *listener) { this->state_listeners_.push_back(listener); }
protected:
/** Extended callback manager with deferred call support.
void notify_state_(OTAState state, float progress, uint8_t error);
/** Notify state with deferral to main loop (for thread safety).
*
* This adds a call_deferred() method for thread-safe execution from other tasks.
* This should be used by OTA implementations that run in separate tasks
* (like web_server OTA) to ensure listeners execute in the main loop.
*/
class StateCallbackManager : public CallbackManager<void(OTAState, float, uint8_t)> {
public:
StateCallbackManager(OTAComponent *component) : component_(component) {}
void notify_state_deferred_(OTAState state, float progress, uint8_t error) {
this->defer([this, state, progress, error]() { this->notify_state_(state, progress, error); });
}
/** Call callbacks with deferral to main loop (for thread safety).
*
* This should be used by OTA implementations that run in separate tasks
* (like web_server OTA) to ensure callbacks execute in the main loop.
*/
void call_deferred(ota::OTAState state, float progress, uint8_t error) {
component_->defer([this, state, progress, error]() { this->call(state, progress, error); });
}
private:
OTAComponent *component_;
};
StateCallbackManager state_callback_{this};
std::vector<OTAStateListener *> state_listeners_;
#endif
};
#ifdef USE_OTA_STATE_CALLBACK
#ifdef USE_OTA_STATE_LISTENER
/** Listener interface for global OTA state changes (includes OTA component pointer).
*
* Used by OTAGlobalCallback to aggregate state from multiple OTA components.
*/
class OTAGlobalStateListener {
public:
virtual ~OTAGlobalStateListener() = default;
virtual void on_ota_global_state(OTAState state, float progress, uint8_t error, OTAComponent *component) = 0;
};
/** Global callback that aggregates OTA state from all OTA components.
*
* OTA components call notify_ota_state() directly with their pointer,
* which forwards the event to all registered global listeners.
*/
class OTAGlobalCallback {
public:
void register_ota(OTAComponent *ota_caller) {
ota_caller->add_on_state_callback([this, ota_caller](OTAState state, float progress, uint8_t error) {
this->state_callback_.call(state, progress, error, ota_caller);
});
}
void add_on_state_callback(std::function<void(OTAState, float, uint8_t, OTAComponent *)> &&callback) {
this->state_callback_.add(std::move(callback));
void add_global_state_listener(OTAGlobalStateListener *listener) { this->global_listeners_.push_back(listener); }
void notify_ota_state(OTAState state, float progress, uint8_t error, OTAComponent *component) {
for (auto *listener : this->global_listeners_) {
listener->on_ota_global_state(state, progress, error, component);
}
}
protected:
CallbackManager<void(OTAState, float, uint8_t, OTAComponent *)> state_callback_{};
std::vector<OTAGlobalStateListener *> global_listeners_;
};
OTAGlobalCallback *get_global_ota_callback();
void register_ota_platform(OTAComponent *ota_caller);
// OTA implementations should use:
// - state_callback_.call() when already in main loop (e.g., esphome OTA)
// - state_callback_.call_deferred() when in separate task (e.g., web_server OTA)
// This ensures proper callback execution in all contexts.
// - notify_state_() when already in main loop (e.g., esphome OTA)
// - notify_state_deferred_() when in separate task (e.g., web_server OTA)
// This ensures proper listener execution in all contexts.
#endif
std::unique_ptr<ota::OTABackend> make_ota_backend();
+1 -1
View File
@@ -111,7 +111,7 @@ class Select : public EntityBase {
}
}
CallbackManager<void(size_t)> state_callback_;
LazyCallbackManager<void(size_t)> state_callback_;
};
} // namespace esphome::select
+11 -11
View File
@@ -4,17 +4,28 @@ import esphome.codegen as cg
from esphome.components import i2c, sensirion_common, sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_ALGORITHM_TUNING,
CONF_GAIN_FACTOR,
CONF_GATING_MAX_DURATION_MINUTES,
CONF_HUMIDITY,
CONF_ID,
CONF_INDEX_OFFSET,
CONF_LEARNING_TIME_GAIN_HOURS,
CONF_LEARNING_TIME_OFFSET_HOURS,
CONF_NORMALIZED_OFFSET_SLOPE,
CONF_NOX,
CONF_OFFSET,
CONF_PM_1_0,
CONF_PM_2_5,
CONF_PM_4_0,
CONF_PM_10_0,
CONF_STD_INITIAL,
CONF_STORE_BASELINE,
CONF_TEMPERATURE,
CONF_TEMPERATURE_COMPENSATION,
CONF_TIME_CONSTANT,
CONF_VOC,
CONF_VOC_BASELINE,
DEVICE_CLASS_AQI,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_PM1,
@@ -42,18 +53,7 @@ SEN5XComponent = sen5x_ns.class_(
RhtAccelerationMode = sen5x_ns.enum("RhtAccelerationMode")
CONF_ACCELERATION_MODE = "acceleration_mode"
CONF_ALGORITHM_TUNING = "algorithm_tuning"
CONF_AUTO_CLEANING_INTERVAL = "auto_cleaning_interval"
CONF_GATING_MAX_DURATION_MINUTES = "gating_max_duration_minutes"
CONF_INDEX_OFFSET = "index_offset"
CONF_LEARNING_TIME_GAIN_HOURS = "learning_time_gain_hours"
CONF_LEARNING_TIME_OFFSET_HOURS = "learning_time_offset_hours"
CONF_NORMALIZED_OFFSET_SLOPE = "normalized_offset_slope"
CONF_NOX = "nox"
CONF_STD_INITIAL = "std_initial"
CONF_TIME_CONSTANT = "time_constant"
CONF_VOC = "voc"
CONF_VOC_BASELINE = "voc_baseline"
# Actions
+2 -7
View File
@@ -76,9 +76,7 @@ StateClass Sensor::get_state_class() {
void Sensor::publish_state(float state) {
this->raw_state = state;
if (this->raw_callback_) {
this->raw_callback_->call(state);
}
this->raw_callback_.call(state);
ESP_LOGV(TAG, "'%s': Received new state %f", this->name_.c_str(), state);
@@ -91,10 +89,7 @@ void Sensor::publish_state(float state) {
void Sensor::add_on_state_callback(std::function<void(float)> &&callback) { this->callback_.add(std::move(callback)); }
void Sensor::add_on_raw_state_callback(std::function<void(float)> &&callback) {
if (!this->raw_callback_) {
this->raw_callback_ = make_unique<CallbackManager<void(float)>>();
}
this->raw_callback_->add(std::move(callback));
this->raw_callback_.add(std::move(callback));
}
void Sensor::add_filter(Filter *filter) {
+2 -2
View File
@@ -125,8 +125,8 @@ class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBa
void internal_send_state_to_frontend(float state);
protected:
std::unique_ptr<CallbackManager<void(float)>> raw_callback_; ///< Storage for raw state callbacks (lazy allocated).
CallbackManager<void(float)> callback_; ///< Storage for filtered state callbacks.
LazyCallbackManager<void(float)> raw_callback_; ///< Storage for raw state callbacks.
LazyCallbackManager<void(float)> callback_; ///< Storage for filtered state callbacks.
Filter *filter_list_{nullptr}; ///< Store all active filters.
+9 -9
View File
@@ -2,11 +2,20 @@ import esphome.codegen as cg
from esphome.components import i2c, sensirion_common, sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_ALGORITHM_TUNING,
CONF_COMPENSATION,
CONF_GAIN_FACTOR,
CONF_GATING_MAX_DURATION_MINUTES,
CONF_ID,
CONF_INDEX_OFFSET,
CONF_LEARNING_TIME_GAIN_HOURS,
CONF_LEARNING_TIME_OFFSET_HOURS,
CONF_NOX,
CONF_STD_INITIAL,
CONF_STORE_BASELINE,
CONF_TEMPERATURE_SOURCE,
CONF_VOC,
CONF_VOC_BASELINE,
DEVICE_CLASS_AQI,
ICON_RADIATOR,
STATE_CLASS_MEASUREMENT,
@@ -24,16 +33,7 @@ SGP4xComponent = sgp4x_ns.class_(
sensirion_common.SensirionI2CDevice,
)
CONF_ALGORITHM_TUNING = "algorithm_tuning"
CONF_GATING_MAX_DURATION_MINUTES = "gating_max_duration_minutes"
CONF_HUMIDITY_SOURCE = "humidity_source"
CONF_INDEX_OFFSET = "index_offset"
CONF_LEARNING_TIME_GAIN_HOURS = "learning_time_gain_hours"
CONF_LEARNING_TIME_OFFSET_HOURS = "learning_time_offset_hours"
CONF_NOX = "nox"
CONF_STD_INITIAL = "std_initial"
CONF_VOC = "voc"
CONF_VOC_BASELINE = "voc_baseline"
def validate_sensors(config):
+1 -2
View File
@@ -4,7 +4,7 @@ from esphome import automation
import esphome.codegen as cg
from esphome.components import uart
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_TRIGGER_ID
from esphome.const import CONF_ID, CONF_ON_DATA, CONF_TRIGGER_ID
CODEOWNERS = ["@alengwenus"]
@@ -17,7 +17,6 @@ MULTI_CONF = True
CONF_SML_ID = "sml_id"
CONF_OBIS_CODE = "obis_code"
CONF_SERVER_ID = "server_id"
CONF_ON_DATA = "on_data"
sml_ns = cg.esphome_ns.namespace("sml")
@@ -6,7 +6,7 @@ from pathlib import Path
from esphome import automation, external_files
import esphome.codegen as cg
from esphome.components import audio, esp32, media_player, network, psram, speaker
from esphome.components import audio, esp32, media_player, network, ota, psram, speaker
import esphome.config_validation as cv
from esphome.const import (
CONF_BUFFER_SIZE,
@@ -342,7 +342,7 @@ async def to_code(config):
var = await media_player.new_media_player(config)
await cg.register_component(var, config)
cg.add_define("USE_OTA_STATE_CALLBACK")
ota.request_ota_state_listeners()
cg.add(var.set_buffer_size(config[CONF_BUFFER_SIZE]))
@@ -66,25 +66,8 @@ void SpeakerMediaPlayer::setup() {
this->set_mute_state_(false);
}
#ifdef USE_OTA
ota::get_global_ota_callback()->add_on_state_callback(
[this](ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) {
if (state == ota::OTA_STARTED) {
if (this->media_pipeline_ != nullptr) {
this->media_pipeline_->suspend_tasks();
}
if (this->announcement_pipeline_ != nullptr) {
this->announcement_pipeline_->suspend_tasks();
}
} else if (state == ota::OTA_ERROR) {
if (this->media_pipeline_ != nullptr) {
this->media_pipeline_->resume_tasks();
}
if (this->announcement_pipeline_ != nullptr) {
this->announcement_pipeline_->resume_tasks();
}
}
});
#ifdef USE_OTA_STATE_LISTENER
ota::get_global_ota_callback()->add_global_state_listener(this);
#endif
this->announcement_pipeline_ =
@@ -300,6 +283,27 @@ void SpeakerMediaPlayer::watch_media_commands_() {
}
}
#ifdef USE_OTA_STATE_LISTENER
void SpeakerMediaPlayer::on_ota_global_state(ota::OTAState state, float progress, uint8_t error,
ota::OTAComponent *comp) {
if (state == ota::OTA_STARTED) {
if (this->media_pipeline_ != nullptr) {
this->media_pipeline_->suspend_tasks();
}
if (this->announcement_pipeline_ != nullptr) {
this->announcement_pipeline_->suspend_tasks();
}
} else if (state == ota::OTA_ERROR) {
if (this->media_pipeline_ != nullptr) {
this->media_pipeline_->resume_tasks();
}
if (this->announcement_pipeline_ != nullptr) {
this->announcement_pipeline_->resume_tasks();
}
}
}
#endif
void SpeakerMediaPlayer::loop() {
this->watch_media_commands_();
@@ -5,14 +5,18 @@
#include "audio_pipeline.h"
#include "esphome/components/audio/audio.h"
#include "esphome/components/media_player/media_player.h"
#include "esphome/components/speaker/speaker.h"
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/core/preferences.h"
#ifdef USE_OTA_STATE_LISTENER
#include "esphome/components/ota/ota_backend.h"
#endif
#include <deque>
#include <freertos/FreeRTOS.h>
#include <freertos/queue.h>
@@ -39,12 +43,22 @@ struct VolumeRestoreState {
bool is_muted;
};
class SpeakerMediaPlayer : public Component, public media_player::MediaPlayer {
class SpeakerMediaPlayer : public Component,
public media_player::MediaPlayer
#ifdef USE_OTA_STATE_LISTENER
,
public ota::OTAGlobalStateListener
#endif
{
public:
float get_setup_priority() const override { return esphome::setup_priority::PROCESSOR; }
void setup() override;
void loop() override;
#ifdef USE_OTA_STATE_LISTENER
void on_ota_global_state(ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) override;
#endif
// MediaPlayer implementations
media_player::MediaPlayerTraits get_traits() override;
bool is_muted() const override { return this->is_muted_; }
+2 -4
View File
@@ -4,8 +4,7 @@
#include "esphome/core/component.h"
#include "esphome/components/sprinkler/sprinkler.h"
namespace esphome {
namespace sprinkler {
namespace esphome::sprinkler {
template<typename... Ts> class SetDividerAction : public Action<Ts...> {
public:
@@ -181,5 +180,4 @@ template<typename... Ts> class ResumeOrStartAction : public Action<Ts...> {
Sprinkler *sprinkler_;
};
} // namespace sprinkler
} // namespace esphome
} // namespace esphome::sprinkler
+71 -42
View File
@@ -7,8 +7,7 @@
#include <cinttypes>
#include <utility>
namespace esphome {
namespace sprinkler {
namespace esphome::sprinkler {
static const char *const TAG = "sprinkler";
@@ -411,7 +410,8 @@ void Sprinkler::loop() {
for (auto &vo : this->valve_op_) {
vo.loop();
}
if (this->prev_req_.has_request() && this->prev_req_.valve_operator()->state() == IDLE) {
if (this->prev_req_.has_request() && this->prev_req_.has_valve_operator() &&
this->prev_req_.valve_operator()->state() == IDLE) {
this->prev_req_.reset();
}
}
@@ -808,11 +808,11 @@ bool Sprinkler::standby() {
void Sprinkler::start_from_queue() {
if (this->standby()) {
ESP_LOGD(TAG, "start_from_queue called but standby is enabled; no action taken");
this->log_standby_warning_(LOG_STR("start_from_queue"));
return;
}
if (this->multiplier() == 0) {
ESP_LOGD(TAG, "start_from_queue called but multiplier is set to zero; no action taken");
this->log_multiplier_zero_warning_(LOG_STR("start_from_queue"));
return;
}
if (this->queued_valves_.empty()) {
@@ -832,11 +832,11 @@ void Sprinkler::start_from_queue() {
void Sprinkler::start_full_cycle() {
if (this->standby()) {
ESP_LOGD(TAG, "start_full_cycle called but standby is enabled; no action taken");
this->log_standby_warning_(LOG_STR("start_full_cycle"));
return;
}
if (this->multiplier() == 0) {
ESP_LOGD(TAG, "start_full_cycle called but multiplier is set to zero; no action taken");
this->log_multiplier_zero_warning_(LOG_STR("start_full_cycle"));
return;
}
if (this->auto_advance() && this->active_valve().has_value()) {
@@ -855,11 +855,11 @@ void Sprinkler::start_full_cycle() {
void Sprinkler::start_single_valve(const optional<size_t> valve_number, optional<uint32_t> run_duration) {
if (this->standby()) {
ESP_LOGD(TAG, "start_single_valve called but standby is enabled; no action taken");
this->log_standby_warning_(LOG_STR("start_single_valve"));
return;
}
if (this->multiplier() == 0) {
ESP_LOGD(TAG, "start_single_valve called but multiplier is set to zero; no action taken");
this->log_multiplier_zero_warning_(LOG_STR("start_single_valve"));
return;
}
if (!valve_number.has_value() || (valve_number == this->active_valve())) {
@@ -891,6 +891,11 @@ void Sprinkler::clear_queued_valves() {
}
void Sprinkler::next_valve() {
if (this->standby()) {
this->log_standby_warning_(LOG_STR("next_valve"));
return;
}
if (this->state_ == IDLE) {
this->reset_cycle_states_(); // just in case auto-advance is switched on later
}
@@ -914,6 +919,11 @@ void Sprinkler::next_valve() {
}
void Sprinkler::previous_valve() {
if (this->standby()) {
this->log_standby_warning_(LOG_STR("previous_valve"));
return;
}
if (this->state_ == IDLE) {
this->reset_cycle_states_(); // just in case auto-advance is switched on later
}
@@ -964,7 +974,7 @@ void Sprinkler::pause() {
void Sprinkler::resume() {
if (this->standby()) {
ESP_LOGD(TAG, "resume called but standby is enabled; no action taken");
this->log_standby_warning_(LOG_STR("resume"));
return;
}
@@ -1009,7 +1019,7 @@ optional<SprinklerValveRunRequestOrigin> Sprinkler::active_valve_request_is_from
}
optional<size_t> Sprinkler::active_valve() {
if (!this->valve_overlap_ && this->prev_req_.has_request() &&
if (!this->valve_overlap_ && this->prev_req_.has_request() && this->prev_req_.has_valve_operator() &&
(this->prev_req_.valve_operator()->state() == STARTING || this->prev_req_.valve_operator()->state() == ACTIVE)) {
return this->prev_req_.valve_as_opt();
}
@@ -1029,9 +1039,7 @@ optional<size_t> Sprinkler::manual_valve() { return this->manual_valve_; }
size_t Sprinkler::number_of_valves() { return this->valve_.size(); }
bool Sprinkler::is_a_valid_valve(const size_t valve_number) {
return ((valve_number >= 0) && (valve_number < this->number_of_valves()));
}
bool Sprinkler::is_a_valid_valve(const size_t valve_number) { return (valve_number < this->number_of_valves()); }
bool Sprinkler::pump_in_use(SprinklerSwitch *pump_switch) {
if (pump_switch == nullptr) {
@@ -1062,8 +1070,12 @@ bool Sprinkler::pump_in_use(SprinklerSwitch *pump_switch) {
this->active_req_.has_request() && (this->state_ != STOPPING)) {
// ...the controller is configured to keep the pump on during a valve open delay, so just return
// whether or not the next valve shares the same pump
return (pump_switch->off_switch() == this->valve_pump_switch(this->active_req_.valve())->off_switch()) &&
(pump_switch->on_switch() == this->valve_pump_switch(this->active_req_.valve())->on_switch());
auto *valve_pump = this->valve_pump_switch(this->active_req_.valve());
if (valve_pump == nullptr) {
return false; // valve has no pump, so this pump isn't in use by it
}
return (pump_switch->off_switch() == valve_pump->off_switch()) &&
(pump_switch->on_switch() == valve_pump->on_switch());
}
return false;
}
@@ -1426,8 +1438,8 @@ void Sprinkler::start_valve_(SprinklerValveRunRequest *req) {
if (vo.state() == IDLE) {
auto run_duration = req->run_duration() ? req->run_duration() : this->valve_run_duration_adjusted(req->valve());
ESP_LOGD(TAG, "%s is starting valve %zu for %" PRIu32 " seconds, cycle %" PRIu32 " of %" PRIu32,
this->req_as_str_(req->request_is_from()).c_str(), req->valve(), run_duration, this->repeat_count_ + 1,
this->repeat().value_or(0) + 1);
LOG_STR_ARG(this->req_as_str_(req->request_is_from())), req->valve(), run_duration,
this->repeat_count_ + 1, this->repeat().value_or(0) + 1);
req->set_valve_operator(&vo);
vo.set_controller(this);
vo.set_valve(&this->valve_[req->valve()]);
@@ -1488,7 +1500,7 @@ void Sprinkler::fsm_kick_() {
}
void Sprinkler::fsm_transition_() {
ESP_LOGVV(TAG, "fsm_transition_ called; state is %s", this->state_as_str_(this->state_).c_str());
ESP_LOGVV(TAG, "fsm_transition_ called; state is %s", LOG_STR_ARG(this->state_as_str_(this->state_)));
switch (this->state_) {
case IDLE: // the system was off -> start it up
// advances to ACTIVE
@@ -1502,8 +1514,11 @@ void Sprinkler::fsm_transition_() {
case STARTING: {
// follows valve open delay interval
this->set_timer_duration_(sprinkler::TIMER_SM,
this->active_req_.run_duration() - this->switching_delay_.value_or(0));
uint32_t timer_duration = this->active_req_.run_duration();
if (timer_duration > this->switching_delay_.value_or(0)) {
timer_duration -= this->switching_delay_.value_or(0);
}
this->set_timer_duration_(sprinkler::TIMER_SM, timer_duration);
this->start_timer_(sprinkler::TIMER_SM);
this->start_valve_(&this->active_req_);
this->state_ = ACTIVE;
@@ -1531,7 +1546,7 @@ void Sprinkler::fsm_transition_() {
this->set_timer_duration_(sprinkler::TIMER_SM, this->manual_selection_delay_.value_or(1));
this->start_timer_(sprinkler::TIMER_SM);
}
ESP_LOGVV(TAG, "fsm_transition_ complete; new state is %s", this->state_as_str_(this->state_).c_str());
ESP_LOGVV(TAG, "fsm_transition_ complete; new state is %s", LOG_STR_ARG(this->state_as_str_(this->state_)));
}
void Sprinkler::fsm_transition_from_shutdown_() {
@@ -1543,8 +1558,11 @@ void Sprinkler::fsm_transition_from_shutdown_() {
this->active_req_.set_run_duration(this->next_req_.run_duration());
this->next_req_.reset();
this->set_timer_duration_(sprinkler::TIMER_SM,
this->active_req_.run_duration() - this->switching_delay_.value_or(0));
uint32_t timer_duration = this->active_req_.run_duration();
if (timer_duration > this->switching_delay_.value_or(0)) {
timer_duration -= this->switching_delay_.value_or(0);
}
this->set_timer_duration_(sprinkler::TIMER_SM, timer_duration);
this->start_timer_(sprinkler::TIMER_SM);
this->start_valve_(&this->active_req_);
this->state_ = ACTIVE;
@@ -1571,8 +1589,9 @@ void Sprinkler::fsm_transition_from_valve_run_() {
this->load_next_valve_run_request_(this->active_req_.valve());
if (this->next_req_.has_request()) { // there is another valve to run...
bool same_pump =
this->valve_pump_switch(this->active_req_.valve()) == this->valve_pump_switch(this->next_req_.valve());
auto *active_pump = this->valve_pump_switch(this->active_req_.valve());
auto *next_pump = this->valve_pump_switch(this->next_req_.valve());
bool same_pump = (active_pump != nullptr) && (next_pump != nullptr) && (active_pump == next_pump);
this->active_req_.set_valve(this->next_req_.valve());
this->active_req_.set_request_from(this->next_req_.request_is_from());
@@ -1581,8 +1600,11 @@ void Sprinkler::fsm_transition_from_valve_run_() {
// this->state_ = ACTIVE; // state isn't changing
if (this->valve_overlap_ || !this->switching_delay_.has_value()) {
this->set_timer_duration_(sprinkler::TIMER_SM,
this->active_req_.run_duration() - this->switching_delay_.value_or(0));
uint32_t timer_duration = this->active_req_.run_duration();
if (timer_duration > this->switching_delay_.value_or(0)) {
timer_duration -= this->switching_delay_.value_or(0);
}
this->set_timer_duration_(sprinkler::TIMER_SM, timer_duration);
this->start_timer_(sprinkler::TIMER_SM);
this->start_valve_(&this->active_req_);
} else {
@@ -1605,41 +1627,49 @@ void Sprinkler::fsm_transition_to_shutdown_() {
this->start_timer_(sprinkler::TIMER_SM);
}
std::string Sprinkler::req_as_str_(SprinklerValveRunRequestOrigin origin) {
void Sprinkler::log_standby_warning_(const LogString *method_name) {
ESP_LOGW(TAG, "%s called but standby is enabled; no action taken", LOG_STR_ARG(method_name));
}
void Sprinkler::log_multiplier_zero_warning_(const LogString *method_name) {
ESP_LOGW(TAG, "%s called but multiplier is set to zero; no action taken", LOG_STR_ARG(method_name));
}
const LogString *Sprinkler::req_as_str_(SprinklerValveRunRequestOrigin origin) {
switch (origin) {
case USER:
return "USER";
return LOG_STR("USER");
case CYCLE:
return "CYCLE";
return LOG_STR("CYCLE");
case QUEUE:
return "QUEUE";
return LOG_STR("QUEUE");
default:
return "UNKNOWN";
return LOG_STR("UNKNOWN");
}
}
std::string Sprinkler::state_as_str_(SprinklerState state) {
const LogString *Sprinkler::state_as_str_(SprinklerState state) {
switch (state) {
case IDLE:
return "IDLE";
return LOG_STR("IDLE");
case STARTING:
return "STARTING";
return LOG_STR("STARTING");
case ACTIVE:
return "ACTIVE";
return LOG_STR("ACTIVE");
case STOPPING:
return "STOPPING";
return LOG_STR("STOPPING");
case BYPASS:
return "BYPASS";
return LOG_STR("BYPASS");
default:
return "UNKNOWN";
return LOG_STR("UNKNOWN");
}
}
@@ -1737,5 +1767,4 @@ void Sprinkler::dump_config() {
}
}
} // namespace sprinkler
} // namespace esphome
} // namespace esphome::sprinkler
+10 -6
View File
@@ -8,8 +8,7 @@
#include <vector>
namespace esphome {
namespace sprinkler {
namespace esphome::sprinkler {
const std::string MIN_STR = "min";
@@ -490,11 +489,17 @@ class Sprinkler : public Component {
/// starts up the system from IDLE state
void fsm_transition_to_shutdown_();
/// log error message when a method is called but standby is enabled
void log_standby_warning_(const LogString *method_name);
/// log error message when a method is called but multiplier is zero
void log_multiplier_zero_warning_(const LogString *method_name);
/// return the specified SprinklerValveRunRequestOrigin as a string
std::string req_as_str_(SprinklerValveRunRequestOrigin origin);
const LogString *req_as_str_(SprinklerValveRunRequestOrigin origin);
/// return the specified SprinklerState state as a string
std::string state_as_str_(SprinklerState state);
const LogString *state_as_str_(SprinklerState state);
/// Start/cancel/get status of valve timers
void start_timer_(SprinklerTimerIndex timer_index);
@@ -607,5 +612,4 @@ class Sprinkler : public Component {
std::unique_ptr<Automation<>> sprinkler_standby_turn_on_automation_;
};
} // namespace sprinkler
} // namespace esphome
} // namespace esphome::sprinkler
+2 -2
View File
@@ -134,8 +134,8 @@ class Switch : public EntityBase, public EntityBase_DeviceClass {
// Pointer first (4 bytes)
ESPPreferenceObject rtc_;
// CallbackManager (12 bytes on 32-bit - contains vector)
CallbackManager<void(bool)> state_callback_{};
// LazyCallbackManager (4 bytes on 32-bit - nullptr when empty)
LazyCallbackManager<void(bool)> state_callback_{};
// Small types grouped together
Deduplicator<bool> publish_dedup_; // 2 bytes (bool has_value_ + bool last_value_)
+37 -7
View File
@@ -4,8 +4,7 @@
#include "esphome/core/application.h"
#include "esphome/core/time.h"
namespace esphome {
namespace syslog {
namespace esphome::syslog {
// Map log levels to syslog severity using an array, indexed by ESPHome log level (1-7)
constexpr int LOG_LEVEL_TO_SYSLOG_SEVERITY[] = {
@@ -34,7 +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 timestamp = this->time_->now().strftime("%b %e %H:%M:%S");
size_t len = message_len;
// remove color formatting
if (this->strip_ && message[0] == 0x1B && len > 11) {
@@ -42,9 +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 syslog
} // namespace esphome
} // namespace esphome::syslog
+2 -4
View File
@@ -7,8 +7,7 @@
#include "esphome/components/time/real_time_clock.h"
#ifdef USE_NETWORK
namespace esphome {
namespace syslog {
namespace esphome::syslog {
class Syslog : public Component, public Parented<udp::UDPComponent>, public logger::LogListener {
public:
Syslog(int level, time::RealTimeClock *time) : log_level_(level), time_(time) {}
@@ -24,6 +23,5 @@ class Syslog : public Component, public Parented<udp::UDPComponent>, public logg
bool strip_{true};
int facility_{16};
};
} // namespace syslog
} // namespace esphome
} // namespace esphome::syslog
#endif
+1 -1
View File
@@ -44,7 +44,7 @@ class Text : public EntityBase {
*/
virtual void control(const std::string &value) = 0;
CallbackManager<void(const std::string &)> state_callback_;
LazyCallbackManager<void(const std::string &)> state_callback_;
};
} // namespace text
@@ -30,9 +30,7 @@ void TextSensor::publish_state(const std::string &state) {
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
this->raw_state = state;
#pragma GCC diagnostic pop
if (this->raw_callback_) {
this->raw_callback_->call(state);
}
this->raw_callback_.call(state);
ESP_LOGV(TAG, "'%s': Received new state %s", this->name_.c_str(), state.c_str());
@@ -73,14 +71,11 @@ void TextSensor::clear_filters() {
this->filter_list_ = nullptr;
}
void TextSensor::add_on_state_callback(std::function<void(std::string)> callback) {
void TextSensor::add_on_state_callback(std::function<void(const std::string &)> callback) {
this->callback_.add(std::move(callback));
}
void TextSensor::add_on_raw_state_callback(std::function<void(std::string)> callback) {
if (!this->raw_callback_) {
this->raw_callback_ = make_unique<CallbackManager<void(std::string)>>();
}
this->raw_callback_->add(std::move(callback));
void TextSensor::add_on_raw_state_callback(std::function<void(const std::string &)> callback) {
this->raw_callback_.add(std::move(callback));
}
std::string TextSensor::get_state() const { return this->state; }
+4 -5
View File
@@ -55,9 +55,9 @@ class TextSensor : public EntityBase, public EntityBase_DeviceClass {
/// Clear the entire filter chain.
void clear_filters();
void add_on_state_callback(std::function<void(std::string)> callback);
void add_on_state_callback(std::function<void(const std::string &)> callback);
/// Add a callback that will be called every time the sensor sends a raw value.
void add_on_raw_state_callback(std::function<void(std::string)> callback);
void add_on_raw_state_callback(std::function<void(const std::string &)> callback);
// ========== INTERNAL METHODS ==========
// (In most use cases you won't need these)
@@ -65,9 +65,8 @@ class TextSensor : public EntityBase, public EntityBase_DeviceClass {
void internal_send_state_to_frontend(const std::string &state);
protected:
std::unique_ptr<CallbackManager<void(std::string)>>
raw_callback_; ///< Storage for raw state callbacks (lazy allocated).
CallbackManager<void(std::string)> callback_; ///< Storage for filtered state callbacks.
LazyCallbackManager<void(const std::string &)> raw_callback_; ///< Storage for raw state callbacks.
LazyCallbackManager<void(const std::string &)> callback_; ///< Storage for filtered state callbacks.
Filter *filter_list_{nullptr}; ///< Store all active filters.
};
@@ -1330,45 +1330,64 @@ void ThermostatClimate::set_heat_deadband(float deadband) { this->heating_deadba
void ThermostatClimate::set_heat_overrun(float overrun) { this->heating_overrun_ = overrun; }
void ThermostatClimate::set_supplemental_cool_delta(float delta) { this->supplemental_cool_delta_ = delta; }
void ThermostatClimate::set_supplemental_heat_delta(float delta) { this->supplemental_heat_delta_ = delta; }
void ThermostatClimate::set_timer_duration_in_sec_(ThermostatClimateTimerIndex timer_index, uint32_t time) {
uint32_t new_duration_ms = 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time);
if (this->timer_[timer_index].active) {
// Timer is running, calculate elapsed time and adjust if needed
uint32_t current_time = App.get_loop_component_start_time();
uint32_t elapsed = current_time - this->timer_[timer_index].started;
if (elapsed >= new_duration_ms) {
// Timer should complete immediately (including when new_duration_ms is 0)
ESP_LOGVV(TAG, "timer %d completing immediately (elapsed %d >= new %d)", timer_index, elapsed, new_duration_ms);
this->timer_[timer_index].active = false;
// Trigger the timer callback immediately
this->timer_[timer_index].func();
return;
} else {
// Adjust timer to run for remaining time - keep original start time
ESP_LOGVV(TAG, "timer %d adjusted: elapsed %d, new total %d, remaining %d", timer_index, elapsed, new_duration_ms,
new_duration_ms - elapsed);
this->timer_[timer_index].time = new_duration_ms;
return;
}
}
// Original logic for non-running timers
this->timer_[timer_index].time = new_duration_ms;
}
void ThermostatClimate::set_cooling_maximum_run_time_in_sec(uint32_t time) {
this->timer_[thermostat::THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME].time =
1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time);
this->set_timer_duration_in_sec_(thermostat::THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME, time);
}
void ThermostatClimate::set_cooling_minimum_off_time_in_sec(uint32_t time) {
this->timer_[thermostat::THERMOSTAT_TIMER_COOLING_OFF].time =
1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time);
this->set_timer_duration_in_sec_(thermostat::THERMOSTAT_TIMER_COOLING_OFF, time);
}
void ThermostatClimate::set_cooling_minimum_run_time_in_sec(uint32_t time) {
this->timer_[thermostat::THERMOSTAT_TIMER_COOLING_ON].time =
1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time);
this->set_timer_duration_in_sec_(thermostat::THERMOSTAT_TIMER_COOLING_ON, time);
}
void ThermostatClimate::set_fan_mode_minimum_switching_time_in_sec(uint32_t time) {
this->timer_[thermostat::THERMOSTAT_TIMER_FAN_MODE].time =
1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time);
this->set_timer_duration_in_sec_(thermostat::THERMOSTAT_TIMER_FAN_MODE, time);
}
void ThermostatClimate::set_fanning_minimum_off_time_in_sec(uint32_t time) {
this->timer_[thermostat::THERMOSTAT_TIMER_FANNING_OFF].time =
1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time);
this->set_timer_duration_in_sec_(thermostat::THERMOSTAT_TIMER_FANNING_OFF, time);
}
void ThermostatClimate::set_fanning_minimum_run_time_in_sec(uint32_t time) {
this->timer_[thermostat::THERMOSTAT_TIMER_FANNING_ON].time =
1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time);
this->set_timer_duration_in_sec_(thermostat::THERMOSTAT_TIMER_FANNING_ON, time);
}
void ThermostatClimate::set_heating_maximum_run_time_in_sec(uint32_t time) {
this->timer_[thermostat::THERMOSTAT_TIMER_HEATING_MAX_RUN_TIME].time =
1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time);
this->set_timer_duration_in_sec_(thermostat::THERMOSTAT_TIMER_HEATING_MAX_RUN_TIME, time);
}
void ThermostatClimate::set_heating_minimum_off_time_in_sec(uint32_t time) {
this->timer_[thermostat::THERMOSTAT_TIMER_HEATING_OFF].time =
1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time);
this->set_timer_duration_in_sec_(thermostat::THERMOSTAT_TIMER_HEATING_OFF, time);
}
void ThermostatClimate::set_heating_minimum_run_time_in_sec(uint32_t time) {
this->timer_[thermostat::THERMOSTAT_TIMER_HEATING_ON].time =
1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time);
this->set_timer_duration_in_sec_(thermostat::THERMOSTAT_TIMER_HEATING_ON, time);
}
void ThermostatClimate::set_idle_minimum_time_in_sec(uint32_t time) {
this->timer_[thermostat::THERMOSTAT_TIMER_IDLE_ON].time =
1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time);
this->set_timer_duration_in_sec_(thermostat::THERMOSTAT_TIMER_IDLE_ON, time);
}
void ThermostatClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; }
void ThermostatClimate::set_humidity_sensor(sensor::Sensor *humidity_sensor) {
@@ -267,6 +267,8 @@ class ThermostatClimate : public climate::Climate, public Component {
bool timer_active_(ThermostatClimateTimerIndex timer_index);
uint32_t timer_duration_(ThermostatClimateTimerIndex timer_index);
std::function<void()> timer_cbf_(ThermostatClimateTimerIndex timer_index);
/// Enhanced timer duration setter with running timer adjustment
void set_timer_duration_in_sec_(ThermostatClimateTimerIndex timer_index, uint32_t time);
/// set_timeout() callbacks for various actions (see above)
void cooling_max_run_time_timer_callback_();
+1 -1
View File
@@ -50,7 +50,7 @@ class UpdateEntity : public EntityBase, public EntityBase_DeviceClass {
UpdateState state_{UPDATE_STATE_UNKNOWN};
UpdateInfo update_info_;
CallbackManager<void()> state_callback_{};
LazyCallbackManager<void()> state_callback_{};
std::unique_ptr<Trigger<const UpdateInfo &>> update_available_trigger_{nullptr};
};
+1 -1
View File
@@ -144,7 +144,7 @@ class Valve : public EntityBase, public EntityBase_DeviceClass {
optional<ValveRestoreState> restore_state_();
CallbackManager<void()> state_callback_{};
LazyCallbackManager<void()> state_callback_{};
ESPPreferenceObject rtc_;
};
+111
View File
@@ -0,0 +1,111 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import (
CONF_ENTITY_CATEGORY,
CONF_ICON,
CONF_ID,
CONF_MAX_TEMPERATURE,
CONF_MIN_TEMPERATURE,
CONF_VISUAL,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
from esphome.cpp_generator import MockObjClass
from esphome.types import ConfigType
CODEOWNERS = ["@dhoeben"]
IS_PLATFORM_COMPONENT = True
water_heater_ns = cg.esphome_ns.namespace("water_heater")
WaterHeater = water_heater_ns.class_("WaterHeater", cg.EntityBase, cg.Component)
WaterHeaterCall = water_heater_ns.class_("WaterHeaterCall")
WaterHeaterTraits = water_heater_ns.class_("WaterHeaterTraits")
CONF_TARGET_TEMPERATURE_STEP = "target_temperature_step"
WaterHeaterMode = water_heater_ns.enum("WaterHeaterMode")
WATER_HEATER_MODES = {
"OFF": WaterHeaterMode.WATER_HEATER_MODE_OFF,
"ECO": WaterHeaterMode.WATER_HEATER_MODE_ECO,
"ELECTRIC": WaterHeaterMode.WATER_HEATER_MODE_ELECTRIC,
"PERFORMANCE": WaterHeaterMode.WATER_HEATER_MODE_PERFORMANCE,
"HIGH_DEMAND": WaterHeaterMode.WATER_HEATER_MODE_HIGH_DEMAND,
"HEAT_PUMP": WaterHeaterMode.WATER_HEATER_MODE_HEAT_PUMP,
"GAS": WaterHeaterMode.WATER_HEATER_MODE_GAS,
}
validate_water_heater_mode = cv.enum(WATER_HEATER_MODES, upper=True)
_WATER_HEATER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(
{
cv.Optional(CONF_VISUAL, default={}): cv.Schema(
{
cv.Optional(CONF_MIN_TEMPERATURE): cv.temperature,
cv.Optional(CONF_MAX_TEMPERATURE): cv.temperature,
cv.Optional(CONF_TARGET_TEMPERATURE_STEP): cv.float_,
}
),
}
).extend(cv.COMPONENT_SCHEMA)
_WATER_HEATER_SCHEMA.add_extra(entity_duplicate_validator("water_heater"))
def water_heater_schema(
class_: MockObjClass,
*,
icon: str = cv.UNDEFINED,
entity_category: str = cv.UNDEFINED,
) -> cv.Schema:
schema = {cv.GenerateID(): cv.declare_id(class_)}
for key, default, validator in [
(CONF_ICON, icon, cv.icon),
(CONF_ENTITY_CATEGORY, entity_category, cv.entity_category),
]:
if default is not cv.UNDEFINED:
schema[cv.Optional(key, default=default)] = validator
return _WATER_HEATER_SCHEMA.extend(schema)
async def setup_water_heater_core_(var: cg.Pvariable, config: ConfigType) -> None:
"""Set up the core water heater properties in C++."""
await setup_entity(var, config, "water_heater")
visual = config[CONF_VISUAL]
if (min_temp := visual.get(CONF_MIN_TEMPERATURE)) is not None:
cg.add_define("USE_WATER_HEATER_VISUAL_OVERRIDES")
cg.add(var.set_visual_min_temperature_override(min_temp))
if (max_temp := visual.get(CONF_MAX_TEMPERATURE)) is not None:
cg.add_define("USE_WATER_HEATER_VISUAL_OVERRIDES")
cg.add(var.set_visual_max_temperature_override(max_temp))
if (temp_step := visual.get(CONF_TARGET_TEMPERATURE_STEP)) is not None:
cg.add_define("USE_WATER_HEATER_VISUAL_OVERRIDES")
cg.add(var.set_visual_target_temperature_step_override(temp_step))
async def register_water_heater(var: cg.Pvariable, config: ConfigType) -> cg.Pvariable:
if not CORE.has_id(config[CONF_ID]):
var = cg.Pvariable(config[CONF_ID], var)
cg.add_define("USE_WATER_HEATER")
await cg.register_component(var, config)
cg.add(cg.App.register_water_heater(var))
CORE.register_platform_component("water_heater", var)
await setup_water_heater_core_(var, config)
return var
async def new_water_heater(config: ConfigType, *args) -> cg.Pvariable:
var = cg.new_Pvariable(config[CONF_ID], *args)
await register_water_heater(var, config)
return var
@coroutine_with_priority(CoroPriority.CORE)
async def to_code(config: ConfigType) -> None:
cg.add_global(water_heater_ns.using)
@@ -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
@@ -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
@@ -84,9 +84,9 @@ void OTARequestHandler::report_ota_progress_(AsyncWebServerRequest *request) {
} else {
ESP_LOGD(TAG, "OTA in progress: %" PRIu32 " bytes read", this->ota_read_length_);
}
#ifdef USE_OTA_STATE_CALLBACK
// Report progress - use call_deferred since we're in web server task
this->parent_->state_callback_.call_deferred(ota::OTA_IN_PROGRESS, percentage, 0);
#ifdef USE_OTA_STATE_LISTENER
// Report progress - use notify_state_deferred_ since we're in web server task
this->parent_->notify_state_deferred_(ota::OTA_IN_PROGRESS, percentage, 0);
#endif
this->last_ota_progress_ = now;
}
@@ -114,9 +114,9 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Platf
// Initialize OTA on first call
this->ota_init_(filename.c_str());
#ifdef USE_OTA_STATE_CALLBACK
// Notify OTA started - use call_deferred since we're in web server task
this->parent_->state_callback_.call_deferred(ota::OTA_STARTED, 0.0f, 0);
#ifdef USE_OTA_STATE_LISTENER
// Notify OTA started - use notify_state_deferred_ since we're in web server task
this->parent_->notify_state_deferred_(ota::OTA_STARTED, 0.0f, 0);
#endif
// Platform-specific pre-initialization
@@ -134,9 +134,9 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Platf
this->ota_backend_ = ota::make_ota_backend();
if (!this->ota_backend_) {
ESP_LOGE(TAG, "Failed to create OTA backend");
#ifdef USE_OTA_STATE_CALLBACK
this->parent_->state_callback_.call_deferred(ota::OTA_ERROR, 0.0f,
static_cast<uint8_t>(ota::OTA_RESPONSE_ERROR_UNKNOWN));
#ifdef USE_OTA_STATE_LISTENER
this->parent_->notify_state_deferred_(ota::OTA_ERROR, 0.0f,
static_cast<uint8_t>(ota::OTA_RESPONSE_ERROR_UNKNOWN));
#endif
return;
}
@@ -148,8 +148,8 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Platf
if (error_code != ota::OTA_RESPONSE_OK) {
ESP_LOGE(TAG, "OTA begin failed: %d", error_code);
this->ota_backend_.reset();
#ifdef USE_OTA_STATE_CALLBACK
this->parent_->state_callback_.call_deferred(ota::OTA_ERROR, 0.0f, static_cast<uint8_t>(error_code));
#ifdef USE_OTA_STATE_LISTENER
this->parent_->notify_state_deferred_(ota::OTA_ERROR, 0.0f, static_cast<uint8_t>(error_code));
#endif
return;
}
@@ -166,8 +166,8 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Platf
ESP_LOGE(TAG, "OTA write failed: %d", error_code);
this->ota_backend_->abort();
this->ota_backend_.reset();
#ifdef USE_OTA_STATE_CALLBACK
this->parent_->state_callback_.call_deferred(ota::OTA_ERROR, 0.0f, static_cast<uint8_t>(error_code));
#ifdef USE_OTA_STATE_LISTENER
this->parent_->notify_state_deferred_(ota::OTA_ERROR, 0.0f, static_cast<uint8_t>(error_code));
#endif
return;
}
@@ -186,15 +186,15 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Platf
error_code = this->ota_backend_->end();
if (error_code == ota::OTA_RESPONSE_OK) {
this->ota_success_ = true;
#ifdef USE_OTA_STATE_CALLBACK
// Report completion before reboot - use call_deferred since we're in web server task
this->parent_->state_callback_.call_deferred(ota::OTA_COMPLETED, 100.0f, 0);
#ifdef USE_OTA_STATE_LISTENER
// Report completion before reboot - use notify_state_deferred_ since we're in web server task
this->parent_->notify_state_deferred_(ota::OTA_COMPLETED, 100.0f, 0);
#endif
this->schedule_ota_reboot_();
} else {
ESP_LOGE(TAG, "OTA end failed: %d", error_code);
#ifdef USE_OTA_STATE_CALLBACK
this->parent_->state_callback_.call_deferred(ota::OTA_ERROR, 0.0f, static_cast<uint8_t>(error_code));
#ifdef USE_OTA_STATE_LISTENER
this->parent_->notify_state_deferred_(ota::OTA_ERROR, 0.0f, static_cast<uint8_t>(error_code));
#endif
}
this->ota_backend_.reset();
@@ -232,10 +232,6 @@ void WebServerOTAComponent::setup() {
// AsyncWebServer takes ownership of the handler and will delete it when the server is destroyed
base->add_handler(new OTARequestHandler(this)); // NOLINT
#ifdef USE_OTA_STATE_CALLBACK
// Register with global OTA callback system
ota::register_ota_platform(this);
#endif
}
void WebServerOTAComponent::dump_config() { ESP_LOGCONFIG(TAG, "Web Server OTA"); }
+26 -7
View File
@@ -1052,8 +1052,14 @@ 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);
set_json_icon_state_value(root, obj, "date", value.c_str(), value.c_str(), start_config);
// 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);
}
@@ -1108,8 +1114,14 @@ 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);
set_json_icon_state_value(root, obj, "time", value.c_str(), value.c_str(), start_config);
// 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);
}
@@ -1164,9 +1176,16 @@ 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);
set_json_icon_state_value(root, obj, "datetime", value.c_str(), value.c_str(), start_config);
// 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
+27 -24
View File
@@ -2,6 +2,7 @@
#ifdef USE_WIFI
#include <cassert>
#include <cinttypes>
#include <cmath>
#ifdef USE_ESP32
#if (ESP_IDF_VERSION_MAJOR >= 5 && ESP_IDF_VERSION_MINOR >= 1)
@@ -394,7 +395,7 @@ void WiFiComponent::start() {
if (this->has_sta()) {
this->wifi_sta_pre_setup_();
if (this->output_power_.has_value() && !this->wifi_apply_output_power_(*this->output_power_)) {
if (!std::isnan(this->output_power_) && !this->wifi_apply_output_power_(this->output_power_)) {
ESP_LOGV(TAG, "Setting Output Power Option failed");
}
@@ -441,7 +442,7 @@ void WiFiComponent::start() {
#ifdef USE_WIFI_AP
} else if (this->has_ap()) {
this->setup_ap_config_();
if (this->output_power_.has_value() && !this->wifi_apply_output_power_(*this->output_power_)) {
if (!std::isnan(this->output_power_) && !this->wifi_apply_output_power_(this->output_power_)) {
ESP_LOGV(TAG, "Setting Output Power Option failed");
}
#ifdef USE_CAPTIVE_PORTAL
@@ -713,8 +714,8 @@ WiFiAP WiFiComponent::build_params_for_current_phase_() {
case WiFiRetryPhase::RETRY_HIDDEN:
// Hidden network mode: clear BSSID/channel to trigger probe request
// (both explicit hidden and retry hidden use same behavior)
params.set_bssid(optional<bssid_t>{});
params.set_channel(optional<uint8_t>{});
params.clear_bssid();
params.clear_channel();
break;
case WiFiRetryPhase::SCAN_CONNECTING:
@@ -766,21 +767,20 @@ void WiFiComponent::start_connecting(const WiFiAP &ap) {
char bssid_s[18];
int8_t priority = 0;
if (ap.get_bssid().has_value()) {
format_mac_addr_upper(ap.get_bssid().value().data(), bssid_s);
priority = this->get_sta_priority(ap.get_bssid().value());
if (ap.has_bssid()) {
format_mac_addr_upper(ap.get_bssid().data(), bssid_s);
priority = this->get_sta_priority(ap.get_bssid());
}
ESP_LOGI(TAG,
"Connecting to " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") " (priority %d, attempt %u/%u in phase %s)...",
ap.get_ssid().c_str(), ap.get_bssid().has_value() ? bssid_s : LOG_STR_LITERAL("any"), priority,
this->num_retried_ + 1, get_max_retries_for_phase(this->retry_phase_),
LOG_STR_ARG(retry_phase_to_log_string(this->retry_phase_)));
ap.get_ssid().c_str(), ap.has_bssid() ? bssid_s : LOG_STR_LITERAL("any"), priority, this->num_retried_ + 1,
get_max_retries_for_phase(this->retry_phase_), LOG_STR_ARG(retry_phase_to_log_string(this->retry_phase_)));
#ifdef ESPHOME_LOG_HAS_VERBOSE
ESP_LOGV(TAG, "Connection Params:");
ESP_LOGV(TAG, " SSID: '%s'", ap.get_ssid().c_str());
if (ap.get_bssid().has_value()) {
if (ap.has_bssid()) {
ESP_LOGV(TAG, " BSSID: %s", bssid_s);
} else {
ESP_LOGV(TAG, " BSSID: Not Set");
@@ -808,8 +808,8 @@ void WiFiComponent::start_connecting(const WiFiAP &ap) {
#ifdef USE_WIFI_WPA2_EAP
}
#endif
if (ap.get_channel().has_value()) {
ESP_LOGV(TAG, " Channel: %u", *ap.get_channel());
if (ap.has_channel()) {
ESP_LOGV(TAG, " Channel: %u", ap.get_channel());
} else {
ESP_LOGV(TAG, " Channel not set");
}
@@ -919,8 +919,8 @@ void WiFiComponent::print_connect_params_() {
get_wifi_channel(), wifi_subnet_mask_().str().c_str(), wifi_gateway_ip_().str().c_str(),
wifi_dns_ip_(0).str().c_str(), wifi_dns_ip_(1).str().c_str());
#ifdef ESPHOME_LOG_HAS_VERBOSE
if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_bssid().has_value()) {
ESP_LOGV(TAG, " Priority: %d", this->get_sta_priority(*config->get_bssid()));
if (const WiFiAP *config = this->get_selected_sta_(); config && config->has_bssid()) {
ESP_LOGV(TAG, " Priority: %d", this->get_sta_priority(config->get_bssid()));
}
#endif
#ifdef USE_WIFI_11KV_SUPPORT
@@ -1514,9 +1514,9 @@ void WiFiComponent::log_and_adjust_priority_for_failed_connect_() {
if (this->retry_phase_ == WiFiRetryPhase::SCAN_CONNECTING && !this->scan_result_.empty()) {
// Scan-based phase: always use best result (index 0)
failed_bssid = this->scan_result_[0].get_bssid();
} else if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_bssid()) {
} else if (const WiFiAP *config = this->get_selected_sta_(); config && config->has_bssid()) {
// Config has specific BSSID (fast_connect or user-specified)
failed_bssid = *config->get_bssid();
failed_bssid = config->get_bssid();
}
if (!failed_bssid.has_value()) {
@@ -1784,24 +1784,27 @@ void WiFiComponent::save_fast_connect_settings_() {
#endif
void WiFiAP::set_ssid(const std::string &ssid) { this->ssid_ = ssid; }
void WiFiAP::set_bssid(bssid_t bssid) { this->bssid_ = bssid; }
void WiFiAP::set_bssid(optional<bssid_t> bssid) { this->bssid_ = bssid; }
void WiFiAP::set_bssid(const bssid_t &bssid) { this->bssid_ = bssid; }
void WiFiAP::clear_bssid() { this->bssid_ = {}; }
void WiFiAP::set_password(const std::string &password) { this->password_ = password; }
#ifdef USE_WIFI_WPA2_EAP
void WiFiAP::set_eap(optional<EAPAuth> eap_auth) { this->eap_ = std::move(eap_auth); }
#endif
void WiFiAP::set_channel(optional<uint8_t> channel) { this->channel_ = channel; }
void WiFiAP::set_channel(uint8_t channel) { this->channel_ = channel; }
void WiFiAP::clear_channel() { this->channel_ = 0; }
#ifdef USE_WIFI_MANUAL_IP
void WiFiAP::set_manual_ip(optional<ManualIP> manual_ip) { this->manual_ip_ = manual_ip; }
#endif
void WiFiAP::set_hidden(bool hidden) { this->hidden_ = hidden; }
const std::string &WiFiAP::get_ssid() const { return this->ssid_; }
const optional<bssid_t> &WiFiAP::get_bssid() const { return this->bssid_; }
const bssid_t &WiFiAP::get_bssid() const { return this->bssid_; }
bool WiFiAP::has_bssid() const { return this->bssid_ != bssid_t{}; }
const std::string &WiFiAP::get_password() const { return this->password_; }
#ifdef USE_WIFI_WPA2_EAP
const optional<EAPAuth> &WiFiAP::get_eap() const { return this->eap_; }
#endif
const optional<uint8_t> &WiFiAP::get_channel() const { return this->channel_; }
uint8_t WiFiAP::get_channel() const { return this->channel_; }
bool WiFiAP::has_channel() const { return this->channel_ != 0; }
#ifdef USE_WIFI_MANUAL_IP
const optional<ManualIP> &WiFiAP::get_manual_ip() const { return this->manual_ip_; }
#endif
@@ -1829,7 +1832,7 @@ bool WiFiScanResult::matches(const WiFiAP &config) const {
// network is configured without SSID - match other settings
}
// If BSSID configured, only match for correct BSSIDs
if (config.get_bssid().has_value() && *config.get_bssid() != this->bssid_)
if (config.has_bssid() && config.get_bssid() != this->bssid_)
return false;
#ifdef USE_WIFI_WPA2_EAP
@@ -1847,7 +1850,7 @@ bool WiFiScanResult::matches(const WiFiAP &config) const {
#endif
// If channel configured, only match networks on that channel.
if (config.get_channel().has_value() && *config.get_channel() != this->channel_) {
if (config.has_channel() && config.get_channel() != this->channel_) {
return false;
}
return true;

Some files were not shown because too many files have changed in this diff Show More