mirror of
https://github.com/esphome/esphome.git
synced 2026-09-16 17:48:40 +00:00
Merge remote-tracking branch 'upstream/dev' into web_server_stack_format
This commit is contained in:
+1
-1
@@ -1 +1 @@
|
||||
6857423aecf90accd0a8bf584d36ee094a4938f872447a4efc05a2efc6dc6481
|
||||
4268ab0b5150f79ab1c317e8f3834c8bb0b4c8122da4f6b1fd67c49d0f2098c9
|
||||
|
||||
@@ -91,6 +91,10 @@ venv-*/
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
|
||||
# nix
|
||||
/default.nix
|
||||
/shell.nix
|
||||
|
||||
.pioenvs
|
||||
.piolibdeps
|
||||
.pio
|
||||
|
||||
@@ -537,6 +537,7 @@ esphome/components/version/* @esphome/core
|
||||
esphome/components/voice_assistant/* @jesserockz @kahrendt
|
||||
esphome/components/wake_on_lan/* @clydebarrow @willwill2will54
|
||||
esphome/components/watchdog/* @oarcher
|
||||
esphome/components/water_heater/* @dhoeben
|
||||
esphome/components/waveshare_epaper/* @clydebarrow
|
||||
esphome/components/web_server/ota/* @esphome/core
|
||||
esphome/components/web_server_base/* @esphome/core
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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_);
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -91,6 +91,10 @@ class APIServerConnectionBase : public ProtoService {
|
||||
virtual void on_climate_command_request(const ClimateCommandRequest &value){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_WATER_HEATER
|
||||
virtual void on_water_heater_command_request(const WaterHeaterCommandRequest &value){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_NUMBER
|
||||
virtual void on_number_command_request(const NumberCommandRequest &value){};
|
||||
#endif
|
||||
|
||||
@@ -335,6 +335,10 @@ API_DISPATCH_UPDATE(valve::Valve, valve)
|
||||
API_DISPATCH_UPDATE(media_player::MediaPlayer, media_player)
|
||||
#endif
|
||||
|
||||
#ifdef USE_WATER_HEATER
|
||||
API_DISPATCH_UPDATE(water_heater::WaterHeater, water_heater)
|
||||
#endif
|
||||
|
||||
#ifdef USE_EVENT
|
||||
// Event is a special case - unlike other entities with simple state fields,
|
||||
// events store their state in a member accessed via obj->get_last_event_type()
|
||||
|
||||
@@ -133,6 +133,9 @@ class APIServer : public Component,
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
void on_media_player_update(media_player::MediaPlayer *obj) override;
|
||||
#endif
|
||||
#ifdef USE_WATER_HEATER
|
||||
void on_water_heater_update(water_heater::WaterHeater *obj) override;
|
||||
#endif
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
void send_homeassistant_action(const HomeassistantActionRequest &call);
|
||||
|
||||
|
||||
@@ -73,6 +73,9 @@ LIST_ENTITIES_HANDLER(media_player, media_player::MediaPlayer, ListEntitiesMedia
|
||||
LIST_ENTITIES_HANDLER(alarm_control_panel, alarm_control_panel::AlarmControlPanel,
|
||||
ListEntitiesAlarmControlPanelResponse)
|
||||
#endif
|
||||
#ifdef USE_WATER_HEATER
|
||||
LIST_ENTITIES_HANDLER(water_heater, water_heater::WaterHeater, ListEntitiesWaterHeaterResponse)
|
||||
#endif
|
||||
#ifdef USE_EVENT
|
||||
LIST_ENTITIES_HANDLER(event, event::Event, ListEntitiesEventResponse)
|
||||
#endif
|
||||
|
||||
@@ -82,6 +82,9 @@ class ListEntitiesIterator : public ComponentIterator {
|
||||
#ifdef USE_ALARM_CONTROL_PANEL
|
||||
bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *entity) override;
|
||||
#endif
|
||||
#ifdef USE_WATER_HEATER
|
||||
bool on_water_heater(water_heater::WaterHeater *entity) override;
|
||||
#endif
|
||||
#ifdef USE_EVENT
|
||||
bool on_event(event::Event *entity) override;
|
||||
#endif
|
||||
|
||||
@@ -60,6 +60,9 @@ INITIAL_STATE_HANDLER(media_player, media_player::MediaPlayer)
|
||||
#ifdef USE_ALARM_CONTROL_PANEL
|
||||
INITIAL_STATE_HANDLER(alarm_control_panel, alarm_control_panel::AlarmControlPanel)
|
||||
#endif
|
||||
#ifdef USE_WATER_HEATER
|
||||
INITIAL_STATE_HANDLER(water_heater, water_heater::WaterHeater)
|
||||
#endif
|
||||
#ifdef USE_UPDATE
|
||||
INITIAL_STATE_HANDLER(update, update::UpdateEntity)
|
||||
#endif
|
||||
|
||||
@@ -76,6 +76,9 @@ class InitialStateIterator : public ComponentIterator {
|
||||
#ifdef USE_ALARM_CONTROL_PANEL
|
||||
bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *entity) override;
|
||||
#endif
|
||||
#ifdef USE_WATER_HEATER
|
||||
bool on_water_heater(water_heater::WaterHeater *entity) override;
|
||||
#endif
|
||||
#ifdef USE_EVENT
|
||||
bool on_event(event::Event *event) override { return true; };
|
||||
#endif
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -160,41 +160,63 @@ HYST_LEVEL = {
|
||||
"High": HystLevel.HYST_LEVEL_HIGH,
|
||||
}
|
||||
|
||||
# Config key -> Validator mapping
|
||||
# Optional settings to generate setter calls for
|
||||
CONFIG_MAP = {
|
||||
CONF_OUTPUT_POWER: cv.float_range(min=-30.0, max=11.0),
|
||||
CONF_RX_ATTENUATION: cv.enum(RX_ATTENUATION, upper=False),
|
||||
CONF_DC_BLOCKING_FILTER: cv.boolean,
|
||||
CONF_FREQUENCY: cv.All(cv.frequency, cv.float_range(min=300.0e6, max=928.0e6)),
|
||||
CONF_IF_FREQUENCY: cv.All(cv.frequency, cv.float_range(min=25000, max=788000)),
|
||||
CONF_FILTER_BANDWIDTH: cv.All(cv.frequency, cv.float_range(min=58000, max=812000)),
|
||||
CONF_CHANNEL: cv.uint8_t,
|
||||
CONF_CHANNEL_SPACING: cv.All(cv.frequency, cv.float_range(min=25000, max=405000)),
|
||||
CONF_FSK_DEVIATION: cv.All(cv.frequency, cv.float_range(min=1500, max=381000)),
|
||||
CONF_MSK_DEVIATION: cv.int_range(min=1, max=8),
|
||||
CONF_SYMBOL_RATE: cv.float_range(min=600, max=500000),
|
||||
CONF_SYNC_MODE: cv.enum(SYNC_MODE, upper=False),
|
||||
CONF_CARRIER_SENSE_ABOVE_THRESHOLD: cv.boolean,
|
||||
CONF_MODULATION_TYPE: cv.enum(MODULATION, upper=False),
|
||||
CONF_MANCHESTER: cv.boolean,
|
||||
CONF_NUM_PREAMBLE: cv.int_range(min=0, max=7),
|
||||
CONF_SYNC1: cv.hex_uint8_t,
|
||||
CONF_SYNC0: cv.hex_uint8_t,
|
||||
CONF_MAGN_TARGET: cv.enum(MAGN_TARGET, upper=False),
|
||||
CONF_MAX_LNA_GAIN: cv.enum(MAX_LNA_GAIN, upper=False),
|
||||
CONF_MAX_DVGA_GAIN: cv.enum(MAX_DVGA_GAIN, upper=False),
|
||||
CONF_CARRIER_SENSE_ABS_THR: cv.int_range(min=-8, max=7),
|
||||
CONF_CARRIER_SENSE_REL_THR: cv.enum(CARRIER_SENSE_REL_THR, upper=False),
|
||||
CONF_LNA_PRIORITY: cv.boolean,
|
||||
CONF_FILTER_LENGTH_FSK_MSK: cv.enum(FILTER_LENGTH_FSK_MSK, upper=False),
|
||||
CONF_FILTER_LENGTH_ASK_OOK: cv.enum(FILTER_LENGTH_ASK_OOK, upper=False),
|
||||
CONF_FREEZE: cv.enum(FREEZE, upper=False),
|
||||
CONF_WAIT_TIME: cv.enum(WAIT_TIME, upper=False),
|
||||
CONF_HYST_LEVEL: cv.enum(HYST_LEVEL, upper=False),
|
||||
CONF_PACKET_MODE: cv.boolean,
|
||||
CONF_PACKET_LENGTH: cv.uint8_t,
|
||||
CONF_CRC_ENABLE: cv.boolean,
|
||||
CONF_WHITENING: cv.boolean,
|
||||
cv.Optional(CONF_OUTPUT_POWER, default=10): cv.float_range(min=-30.0, max=11.0),
|
||||
cv.Optional(CONF_RX_ATTENUATION, default="0dB"): cv.enum(
|
||||
RX_ATTENUATION, upper=False
|
||||
),
|
||||
cv.Optional(CONF_DC_BLOCKING_FILTER, default=True): cv.boolean,
|
||||
cv.Optional(CONF_FREQUENCY, default="433.92MHz"): cv.All(
|
||||
cv.frequency, cv.float_range(min=300.0e6, max=928.0e6)
|
||||
),
|
||||
cv.Optional(CONF_IF_FREQUENCY, default="153kHz"): cv.All(
|
||||
cv.frequency, cv.float_range(min=25000, max=788000)
|
||||
),
|
||||
cv.Optional(CONF_FILTER_BANDWIDTH, default="203kHz"): cv.All(
|
||||
cv.frequency, cv.float_range(min=58000, max=812000)
|
||||
),
|
||||
cv.Optional(CONF_CHANNEL, default=0): cv.uint8_t,
|
||||
cv.Optional(CONF_CHANNEL_SPACING, default="200kHz"): cv.All(
|
||||
cv.frequency, cv.float_range(min=25000, max=405000)
|
||||
),
|
||||
cv.Optional(CONF_FSK_DEVIATION): cv.All(
|
||||
cv.frequency, cv.float_range(min=1500, max=381000)
|
||||
),
|
||||
cv.Optional(CONF_MSK_DEVIATION): cv.int_range(min=1, max=8),
|
||||
cv.Optional(CONF_SYMBOL_RATE, default=5000): cv.float_range(min=600, max=500000),
|
||||
cv.Optional(CONF_SYNC_MODE, default="16/16"): cv.enum(SYNC_MODE, upper=False),
|
||||
cv.Optional(CONF_CARRIER_SENSE_ABOVE_THRESHOLD, default=False): cv.boolean,
|
||||
cv.Optional(CONF_MODULATION_TYPE, default="ASK/OOK"): cv.enum(
|
||||
MODULATION, upper=False
|
||||
),
|
||||
cv.Optional(CONF_MANCHESTER, default=False): cv.boolean,
|
||||
cv.Optional(CONF_NUM_PREAMBLE, default=2): cv.int_range(min=0, max=7),
|
||||
cv.Optional(CONF_SYNC1, default=0xD3): cv.hex_uint8_t,
|
||||
cv.Optional(CONF_SYNC0, default=0x91): cv.hex_uint8_t,
|
||||
cv.Optional(CONF_MAGN_TARGET, default="42dB"): cv.enum(MAGN_TARGET, upper=False),
|
||||
cv.Optional(CONF_MAX_LNA_GAIN, default="Default"): cv.enum(
|
||||
MAX_LNA_GAIN, upper=False
|
||||
),
|
||||
cv.Optional(CONF_MAX_DVGA_GAIN, default="-3"): cv.enum(MAX_DVGA_GAIN, upper=False),
|
||||
cv.Optional(CONF_CARRIER_SENSE_ABS_THR): cv.int_range(min=-8, max=7),
|
||||
cv.Optional(CONF_CARRIER_SENSE_REL_THR): cv.enum(
|
||||
CARRIER_SENSE_REL_THR, upper=False
|
||||
),
|
||||
cv.Optional(CONF_LNA_PRIORITY, default=False): cv.boolean,
|
||||
cv.Optional(CONF_FILTER_LENGTH_FSK_MSK): cv.enum(
|
||||
FILTER_LENGTH_FSK_MSK, upper=False
|
||||
),
|
||||
cv.Optional(CONF_FILTER_LENGTH_ASK_OOK): cv.enum(
|
||||
FILTER_LENGTH_ASK_OOK, upper=False
|
||||
),
|
||||
cv.Optional(CONF_FREEZE): cv.enum(FREEZE, upper=False),
|
||||
cv.Optional(CONF_WAIT_TIME, default="32"): cv.enum(WAIT_TIME, upper=False),
|
||||
cv.Optional(CONF_HYST_LEVEL): cv.enum(HYST_LEVEL, upper=False),
|
||||
cv.Optional(CONF_PACKET_MODE, default=False): cv.boolean,
|
||||
cv.Optional(CONF_PACKET_LENGTH): cv.uint8_t,
|
||||
cv.Optional(CONF_CRC_ENABLE, default=False): cv.boolean,
|
||||
cv.Optional(CONF_WHITENING, default=False): cv.boolean,
|
||||
}
|
||||
|
||||
|
||||
@@ -217,7 +239,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_ON_PACKET): automation.validate_automation(single=True),
|
||||
}
|
||||
)
|
||||
.extend({cv.Optional(key): validator for key, validator in CONFIG_MAP.items()})
|
||||
.extend(CONFIG_MAP)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(spi.spi_device_schema(cs_pin_required=True)),
|
||||
_validate_packet_mode,
|
||||
@@ -229,7 +251,8 @@ async def to_code(config):
|
||||
await cg.register_component(var, config)
|
||||
await spi.register_spi_device(var, config)
|
||||
|
||||
for key in CONFIG_MAP:
|
||||
for opt in CONFIG_MAP:
|
||||
key = opt.schema
|
||||
if key in config:
|
||||
cg.add(getattr(var, f"set_{key}")(config[key]))
|
||||
|
||||
|
||||
@@ -98,25 +98,8 @@ CC1101Component::CC1101Component() {
|
||||
this->state_.LENGTH_CONFIG = 2;
|
||||
this->state_.FS_AUTOCAL = 1;
|
||||
|
||||
// Default Settings
|
||||
this->set_frequency(433920000);
|
||||
this->set_if_frequency(153000);
|
||||
this->set_filter_bandwidth(203000);
|
||||
this->set_channel(0);
|
||||
this->set_channel_spacing(200000);
|
||||
this->set_symbol_rate(5000);
|
||||
this->set_sync_mode(SyncMode::SYNC_MODE_NONE);
|
||||
this->set_carrier_sense_above_threshold(true);
|
||||
this->set_modulation_type(Modulation::MODULATION_ASK_OOK);
|
||||
this->set_magn_target(MagnTarget::MAGN_TARGET_42DB);
|
||||
this->set_max_lna_gain(MaxLnaGain::MAX_LNA_GAIN_DEFAULT);
|
||||
this->set_max_dvga_gain(MaxDvgaGain::MAX_DVGA_GAIN_MINUS_3);
|
||||
this->set_lna_priority(false);
|
||||
this->set_wait_time(WaitTime::WAIT_TIME_32_SAMPLES);
|
||||
|
||||
// CRITICAL: Initialize PA Table to avoid transmitting 0 power (Silence)
|
||||
memset(this->pa_table_, 0, sizeof(this->pa_table_));
|
||||
this->set_output_power(10.0f);
|
||||
}
|
||||
|
||||
void CC1101Component::setup() {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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_;
|
||||
};
|
||||
|
||||
@@ -22,7 +22,7 @@ class DateTimeBase : public EntityBase {
|
||||
#endif
|
||||
|
||||
protected:
|
||||
CallbackManager<void()> state_callback_;
|
||||
LazyCallbackManager<void()> state_callback_;
|
||||
|
||||
#ifdef USE_TIME
|
||||
time::RealTimeClock *rtc_;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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_;
|
||||
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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_;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -111,7 +111,7 @@ class Select : public EntityBase {
|
||||
}
|
||||
}
|
||||
|
||||
CallbackManager<void(size_t)> state_callback_;
|
||||
LazyCallbackManager<void(size_t)> state_callback_;
|
||||
};
|
||||
|
||||
} // namespace esphome::select
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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_; }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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_)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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_();
|
||||
|
||||
@@ -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};
|
||||
};
|
||||
|
||||
|
||||
@@ -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_;
|
||||
};
|
||||
|
||||
@@ -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"); }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user