Merge branch 'dev' into no_alloc_object_id

This commit is contained in:
J. Nick Koston
2025-12-20 06:47:49 -10:00
committed by GitHub
35 changed files with 186 additions and 80 deletions

View File

@@ -132,13 +132,13 @@ class AlarmControlPanel : public EntityBase {
// the call control function
virtual void control(const AlarmControlPanelCall &call) = 0;
// state callback - triggers check get_state() for specific state
CallbackManager<void()> state_callback_{};
LazyCallbackManager<void()> state_callback_{};
// clear callback - fires when leaving TRIGGERED state
CallbackManager<void()> cleared_callback_{};
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

View File

@@ -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 {

View File

@@ -1666,13 +1666,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");

View File

@@ -858,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;
}

View File

@@ -1054,11 +1054,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

View File

@@ -1115,7 +1115,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); }

View File

@@ -41,7 +41,7 @@ class Button : public EntityBase, public EntityBase_DeviceClass {
*/
virtual void press_action() = 0;
CallbackManager<void()> press_callback_{};
LazyCallbackManager<void()> press_callback_{};
};
} // namespace esphome::button

View File

@@ -326,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};

View File

@@ -152,7 +152,7 @@ class Cover : public EntityBase, public EntityBase_DeviceClass {
optional<CoverRestoreState> restore_state_();
CallbackManager<void()> state_callback_{};
LazyCallbackManager<void()> state_callback_{};
ESPPreferenceObject rtc_;
};

View File

@@ -22,7 +22,7 @@ class DateTimeBase : public EntityBase {
#endif
protected:
CallbackManager<void()> state_callback_;
LazyCallbackManager<void()> state_callback_;
#ifdef USE_TIME
time::RealTimeClock *rtc_;

View File

@@ -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;
}

View File

@@ -50,7 +50,7 @@ class Event : public EntityBase, public EntityBase_DeviceClass {
void add_on_event_callback(std::function<void(const std::string &event_type)> &&callback);
protected:
CallbackManager<void(const std::string &event_type)> event_callback_;
LazyCallbackManager<void(const std::string &event_type)> event_callback_;
FixedVector<const char *> types_;
private:

View File

@@ -155,7 +155,7 @@ class Fan : public EntityBase {
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_;

View File

@@ -174,7 +174,7 @@ class Lock : public EntityBase {
*/
virtual void control(const LockCall &call) = 0;
CallbackManager<void()> state_callback_{};
LazyCallbackManager<void()> state_callback_{};
Deduplicator<LockState> publish_dedup_;
ESPPreferenceObject rtc_;
};

View File

@@ -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

View File

@@ -49,7 +49,7 @@ class Number : public EntityBase {
*/
virtual void control(float value) = 0;
CallbackManager<void(float)> state_callback_;
LazyCallbackManager<void(float)> state_callback_;
};
} // namespace esphome::number

View File

@@ -111,7 +111,7 @@ class Select : public EntityBase {
}
}
CallbackManager<void(size_t)> state_callback_;
LazyCallbackManager<void(size_t)> state_callback_;
};
} // namespace esphome::select

View File

@@ -4,17 +4,28 @@ import esphome.codegen as cg
from esphome.components import i2c, sensirion_common, sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_ALGORITHM_TUNING,
CONF_GAIN_FACTOR,
CONF_GATING_MAX_DURATION_MINUTES,
CONF_HUMIDITY,
CONF_ID,
CONF_INDEX_OFFSET,
CONF_LEARNING_TIME_GAIN_HOURS,
CONF_LEARNING_TIME_OFFSET_HOURS,
CONF_NORMALIZED_OFFSET_SLOPE,
CONF_NOX,
CONF_OFFSET,
CONF_PM_1_0,
CONF_PM_2_5,
CONF_PM_4_0,
CONF_PM_10_0,
CONF_STD_INITIAL,
CONF_STORE_BASELINE,
CONF_TEMPERATURE,
CONF_TEMPERATURE_COMPENSATION,
CONF_TIME_CONSTANT,
CONF_VOC,
CONF_VOC_BASELINE,
DEVICE_CLASS_AQI,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_PM1,
@@ -42,18 +53,7 @@ SEN5XComponent = sen5x_ns.class_(
RhtAccelerationMode = sen5x_ns.enum("RhtAccelerationMode")
CONF_ACCELERATION_MODE = "acceleration_mode"
CONF_ALGORITHM_TUNING = "algorithm_tuning"
CONF_AUTO_CLEANING_INTERVAL = "auto_cleaning_interval"
CONF_GATING_MAX_DURATION_MINUTES = "gating_max_duration_minutes"
CONF_INDEX_OFFSET = "index_offset"
CONF_LEARNING_TIME_GAIN_HOURS = "learning_time_gain_hours"
CONF_LEARNING_TIME_OFFSET_HOURS = "learning_time_offset_hours"
CONF_NORMALIZED_OFFSET_SLOPE = "normalized_offset_slope"
CONF_NOX = "nox"
CONF_STD_INITIAL = "std_initial"
CONF_TIME_CONSTANT = "time_constant"
CONF_VOC = "voc"
CONF_VOC_BASELINE = "voc_baseline"
# Actions

View File

@@ -76,9 +76,7 @@ StateClass Sensor::get_state_class() {
void Sensor::publish_state(float state) {
this->raw_state = state;
if (this->raw_callback_) {
this->raw_callback_->call(state);
}
this->raw_callback_.call(state);
ESP_LOGV(TAG, "'%s': Received new state %f", this->name_.c_str(), state);
@@ -91,10 +89,7 @@ void Sensor::publish_state(float state) {
void Sensor::add_on_state_callback(std::function<void(float)> &&callback) { this->callback_.add(std::move(callback)); }
void Sensor::add_on_raw_state_callback(std::function<void(float)> &&callback) {
if (!this->raw_callback_) {
this->raw_callback_ = make_unique<CallbackManager<void(float)>>();
}
this->raw_callback_->add(std::move(callback));
this->raw_callback_.add(std::move(callback));
}
void Sensor::add_filter(Filter *filter) {

View File

@@ -125,8 +125,8 @@ class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBa
void internal_send_state_to_frontend(float state);
protected:
std::unique_ptr<CallbackManager<void(float)>> raw_callback_; ///< Storage for raw state callbacks (lazy allocated).
CallbackManager<void(float)> callback_; ///< Storage for filtered state callbacks.
LazyCallbackManager<void(float)> raw_callback_; ///< Storage for raw state callbacks.
LazyCallbackManager<void(float)> callback_; ///< Storage for filtered state callbacks.
Filter *filter_list_{nullptr}; ///< Store all active filters.

View File

@@ -2,11 +2,20 @@ import esphome.codegen as cg
from esphome.components import i2c, sensirion_common, sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_ALGORITHM_TUNING,
CONF_COMPENSATION,
CONF_GAIN_FACTOR,
CONF_GATING_MAX_DURATION_MINUTES,
CONF_ID,
CONF_INDEX_OFFSET,
CONF_LEARNING_TIME_GAIN_HOURS,
CONF_LEARNING_TIME_OFFSET_HOURS,
CONF_NOX,
CONF_STD_INITIAL,
CONF_STORE_BASELINE,
CONF_TEMPERATURE_SOURCE,
CONF_VOC,
CONF_VOC_BASELINE,
DEVICE_CLASS_AQI,
ICON_RADIATOR,
STATE_CLASS_MEASUREMENT,
@@ -24,16 +33,7 @@ SGP4xComponent = sgp4x_ns.class_(
sensirion_common.SensirionI2CDevice,
)
CONF_ALGORITHM_TUNING = "algorithm_tuning"
CONF_GATING_MAX_DURATION_MINUTES = "gating_max_duration_minutes"
CONF_HUMIDITY_SOURCE = "humidity_source"
CONF_INDEX_OFFSET = "index_offset"
CONF_LEARNING_TIME_GAIN_HOURS = "learning_time_gain_hours"
CONF_LEARNING_TIME_OFFSET_HOURS = "learning_time_offset_hours"
CONF_NOX = "nox"
CONF_STD_INITIAL = "std_initial"
CONF_VOC = "voc"
CONF_VOC_BASELINE = "voc_baseline"
def validate_sensors(config):

View File

@@ -134,8 +134,8 @@ class Switch : public EntityBase, public EntityBase_DeviceClass {
// Pointer first (4 bytes)
ESPPreferenceObject rtc_;
// CallbackManager (12 bytes on 32-bit - contains vector)
CallbackManager<void(bool)> state_callback_{};
// LazyCallbackManager (4 bytes on 32-bit - nullptr when empty)
LazyCallbackManager<void(bool)> state_callback_{};
// Small types grouped together
Deduplicator<bool> publish_dedup_; // 2 bytes (bool has_value_ + bool last_value_)

View File

@@ -44,7 +44,7 @@ class Text : public EntityBase {
*/
virtual void control(const std::string &value) = 0;
CallbackManager<void(const std::string &)> state_callback_;
LazyCallbackManager<void(const std::string &)> state_callback_;
};
} // namespace text

View File

@@ -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; }

View File

@@ -55,9 +55,9 @@ class TextSensor : public EntityBase, public EntityBase_DeviceClass {
/// Clear the entire filter chain.
void clear_filters();
void add_on_state_callback(std::function<void(std::string)> callback);
void add_on_state_callback(std::function<void(const std::string &)> callback);
/// Add a callback that will be called every time the sensor sends a raw value.
void add_on_raw_state_callback(std::function<void(std::string)> callback);
void add_on_raw_state_callback(std::function<void(const std::string &)> callback);
// ========== INTERNAL METHODS ==========
// (In most use cases you won't need these)
@@ -65,9 +65,8 @@ class TextSensor : public EntityBase, public EntityBase_DeviceClass {
void internal_send_state_to_frontend(const std::string &state);
protected:
std::unique_ptr<CallbackManager<void(std::string)>>
raw_callback_; ///< Storage for raw state callbacks (lazy allocated).
CallbackManager<void(std::string)> callback_; ///< Storage for filtered state callbacks.
LazyCallbackManager<void(const std::string &)> raw_callback_; ///< Storage for raw state callbacks.
LazyCallbackManager<void(const std::string &)> callback_; ///< Storage for filtered state callbacks.
Filter *filter_list_{nullptr}; ///< Store all active filters.
};

View File

@@ -50,7 +50,7 @@ class UpdateEntity : public EntityBase, public EntityBase_DeviceClass {
UpdateState state_{UPDATE_STATE_UNKNOWN};
UpdateInfo update_info_;
CallbackManager<void()> state_callback_{};
LazyCallbackManager<void()> state_callback_{};
std::unique_ptr<Trigger<const UpdateInfo &>> update_available_trigger_{nullptr};
};

View File

@@ -144,7 +144,7 @@ class Valve : public EntityBase, public EntityBase_DeviceClass {
optional<ValveRestoreState> restore_state_();
CallbackManager<void()> state_callback_{};
LazyCallbackManager<void()> state_callback_{};
ESPPreferenceObject rtc_;
};

View File

@@ -528,6 +528,16 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) {
for (auto *listener : global_wifi_component->connect_state_listeners_) {
listener->on_wifi_connect_state(global_wifi_component->wifi_ssid(), global_wifi_component->wifi_bssid());
}
// For static IP configurations, GOT_IP event may not fire, so notify IP listeners here
#ifdef USE_WIFI_MANUAL_IP
if (const WiFiAP *config = global_wifi_component->get_selected_sta_();
config && config->get_manual_ip().has_value()) {
for (auto *listener : global_wifi_component->ip_state_listeners_) {
listener->on_ip_state(global_wifi_component->wifi_sta_ip_addresses(),
global_wifi_component->get_dns_address(0), global_wifi_component->get_dns_address(1));
}
}
#endif
#endif
break;
}

View File

@@ -739,6 +739,14 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
for (auto *listener : this->connect_state_listeners_) {
listener->on_wifi_connect_state(this->wifi_ssid(), this->wifi_bssid());
}
// For static IP configurations, GOT_IP event may not fire, so notify IP listeners here
#ifdef USE_WIFI_MANUAL_IP
if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value()) {
for (auto *listener : this->ip_state_listeners_) {
listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1));
}
}
#endif
#endif
} else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_DISCONNECTED) {

View File

@@ -305,6 +305,14 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_
for (auto *listener : this->connect_state_listeners_) {
listener->on_wifi_connect_state(this->wifi_ssid(), this->wifi_bssid());
}
// For static IP configurations, GOT_IP event may not fire, so notify IP listeners here
#ifdef USE_WIFI_MANUAL_IP
if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value()) {
for (auto *listener : this->ip_state_listeners_) {
listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1));
}
}
#endif
#endif
break;
}

View File

@@ -259,6 +259,15 @@ void WiFiComponent::wifi_loop_() {
for (auto *listener : this->connect_state_listeners_) {
listener->on_wifi_connect_state(this->wifi_ssid(), this->wifi_bssid());
}
// For static IP configurations, notify IP listeners immediately as the IP is already configured
#ifdef USE_WIFI_MANUAL_IP
if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value()) {
s_sta_had_ip = true;
for (auto *listener : this->ip_state_listeners_) {
listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1));
}
}
#endif
#endif
} else if (!is_connected && s_sta_was_connected) {
// Just disconnected

View File

@@ -123,6 +123,7 @@ CONF_ADDRESS = "address"
CONF_ADDRESSABLE_LIGHT_ID = "addressable_light_id"
CONF_ADVANCED = "advanced"
CONF_AFTER = "after"
CONF_ALGORITHM_TUNING = "algorithm_tuning"
CONF_ALL = "all"
CONF_ALLOW_OTHER_USES = "allow_other_uses"
CONF_ALPHA = "alpha"
@@ -435,6 +436,7 @@ CONF_GAIN_FACTOR = "gain_factor"
CONF_GAMMA_CORRECT = "gamma_correct"
CONF_GAS_RESISTANCE = "gas_resistance"
CONF_GATEWAY = "gateway"
CONF_GATING_MAX_DURATION_MINUTES = "gating_max_duration_minutes"
CONF_GLASS_ATTENUATION_FACTOR = "glass_attenuation_factor"
CONF_GLYPHS = "glyphs"
CONF_GPIO = "gpio"
@@ -497,6 +499,7 @@ CONF_INCLUDE_INTERNAL = "include_internal"
CONF_INCLUDES = "includes"
CONF_INCLUDES_C = "includes_c"
CONF_INDEX = "index"
CONF_INDEX_OFFSET = "index_offset"
CONF_INDOOR = "indoor"
CONF_INFRARED = "infrared"
CONF_INIT_SEQUENCE = "init_sequence"
@@ -534,6 +537,8 @@ CONF_LAMBDA = "lambda"
CONF_LAST_CONFIDENCE = "last_confidence"
CONF_LAST_FINGER_ID = "last_finger_id"
CONF_LATITUDE = "latitude"
CONF_LEARNING_TIME_GAIN_HOURS = "learning_time_gain_hours"
CONF_LEARNING_TIME_OFFSET_HOURS = "learning_time_offset_hours"
CONF_LED = "led"
CONF_LEGEND = "legend"
CONF_LENGTH = "length"
@@ -645,7 +650,9 @@ CONF_NEVER = "never"
CONF_NEW_PASSWORD = "new_password"
CONF_NITROGEN_DIOXIDE = "nitrogen_dioxide"
CONF_NOISE_LEVEL = "noise_level"
CONF_NORMALIZED_OFFSET_SLOPE = "normalized_offset_slope"
CONF_NOTIFY = "notify"
CONF_NOX = "nox"
CONF_NUM_ATTEMPTS = "num_attempts"
CONF_NUM_CHANNELS = "num_channels"
CONF_NUM_CHIPS = "num_chips"
@@ -939,6 +946,7 @@ CONF_STATE_TOPIC = "state_topic"
CONF_STATIC_IP = "static_ip"
CONF_STATUS = "status"
CONF_STB_PIN = "stb_pin"
CONF_STD_INITIAL = "std_initial"
CONF_STEP = "step"
CONF_STEP_DELAY = "step_delay"
CONF_STEP_MODE = "step_mode"
@@ -1006,6 +1014,7 @@ CONF_TILT_COMMAND_TOPIC = "tilt_command_topic"
CONF_TILT_LAMBDA = "tilt_lambda"
CONF_TILT_STATE_TOPIC = "tilt_state_topic"
CONF_TIME = "time"
CONF_TIME_CONSTANT = "time_constant"
CONF_TIME_ID = "time_id"
CONF_TIMEOUT = "timeout"
CONF_TIMES = "times"
@@ -1060,6 +1069,8 @@ CONF_VERSION = "version"
CONF_VIBRATIONS = "vibrations"
CONF_VISIBLE = "visible"
CONF_VISUAL = "visual"
CONF_VOC = "voc"
CONF_VOC_BASELINE = "voc_baseline"
CONF_VOLTAGE = "voltage"
CONF_VOLTAGE_ATTENUATION = "voltage_attenuation"
CONF_VOLTAGE_DIVIDER = "voltage_divider"

View File

@@ -471,10 +471,14 @@ std::string base64_encode(const uint8_t *buf, size_t buf_len) {
}
size_t base64_decode(const std::string &encoded_string, uint8_t *buf, size_t buf_len) {
int in_len = encoded_string.size();
return base64_decode(reinterpret_cast<const uint8_t *>(encoded_string.data()), encoded_string.size(), buf, buf_len);
}
size_t base64_decode(const uint8_t *encoded_data, size_t encoded_len, uint8_t *buf, size_t buf_len) {
size_t in_len = encoded_len;
int i = 0;
int j = 0;
int in = 0;
size_t in = 0;
size_t out = 0;
uint8_t char_array_4[4], char_array_3[3];
bool truncated = false;
@@ -482,8 +486,8 @@ size_t base64_decode(const std::string &encoded_string, uint8_t *buf, size_t buf
// SAFETY: The loop condition checks is_base64() before processing each character.
// This ensures base64_find_char() is only called on valid base64 characters,
// preventing the edge case where invalid chars would return 0 (same as 'A').
while (in_len-- && (encoded_string[in] != '=') && is_base64(encoded_string[in])) {
char_array_4[i++] = encoded_string[in];
while (in_len-- && (encoded_data[in] != '=') && is_base64(encoded_data[in])) {
char_array_4[i++] = encoded_data[in];
in++;
if (i == 4) {
for (i = 0; i < 4; i++)

View File

@@ -885,6 +885,7 @@ std::string base64_encode(const std::vector<uint8_t> &buf);
std::vector<uint8_t> base64_decode(const std::string &encoded_string);
size_t base64_decode(std::string const &encoded_string, uint8_t *buf, size_t buf_len);
size_t base64_decode(const uint8_t *encoded_data, size_t encoded_len, uint8_t *buf, size_t buf_len);
///@}
@@ -941,6 +942,50 @@ template<typename... Ts> class CallbackManager<void(Ts...)> {
std::vector<std::function<void(Ts...)>> callbacks_;
};
template<typename... X> class LazyCallbackManager;
/** Lazy-allocating callback manager that only allocates memory when callbacks are registered.
*
* This is a drop-in replacement for CallbackManager that saves memory when no callbacks
* are registered (common case after the Controller Registry eliminated per-entity callbacks
* from API and web_server components).
*
* Memory overhead comparison (32-bit systems):
* - CallbackManager: 12 bytes (empty std::vector)
* - LazyCallbackManager: 4 bytes (nullptr unique_ptr)
*
* @tparam Ts The arguments for the callbacks, wrapped in void().
*/
template<typename... Ts> class LazyCallbackManager<void(Ts...)> {
public:
/// Add a callback to the list. Allocates the underlying CallbackManager on first use.
void add(std::function<void(Ts...)> &&callback) {
if (!this->callbacks_) {
this->callbacks_ = make_unique<CallbackManager<void(Ts...)>>();
}
this->callbacks_->add(std::move(callback));
}
/// Call all callbacks in this manager. No-op if no callbacks registered.
void call(Ts... args) {
if (this->callbacks_) {
this->callbacks_->call(args...);
}
}
/// Return the number of registered callbacks.
size_t size() const { return this->callbacks_ ? this->callbacks_->size() : 0; }
/// Check if any callbacks are registered.
bool empty() const { return !this->callbacks_ || this->callbacks_->size() == 0; }
/// Call all callbacks in this manager.
void operator()(Ts... args) { this->call(args...); }
protected:
std::unique_ptr<CallbackManager<void(Ts...)>> callbacks_;
};
/// Helper class to deduplicate items in a series of values.
template<typename T> class Deduplicator {
public:

View File

@@ -279,14 +279,30 @@ async def test_alarm_control_panel_state_transitions(
except TimeoutError:
pytest.fail(f"on_chime callback not fired. Log lines: {log_lines[-20:]}")
# Close the chime sensor
# Close the chime sensor and wait for alarm to become ready again
# We need to wait for this transition before testing door sensor,
# otherwise there's a race where the door sensor state change could
# arrive before the chime sensor state change, leaving the alarm in
# a continuous "not ready" state with no on_ready callback fired.
ready_after_chime_close: asyncio.Future[bool] = loop.create_future()
ready_futures.append(ready_after_chime_close)
client.switch_command(chime_switch_info.key, False)
# ===== Test ready state changes =====
# Opening/closing sensors while disarmed affects ready state
# The on_ready callback fires when sensors_ready changes
# Wait for alarm to become ready again (chime sensor closed)
try:
await asyncio.wait_for(ready_after_chime_close, timeout=2.0)
except TimeoutError:
pytest.fail(
f"on_ready callback not fired when chime sensor closed. "
f"Log lines: {log_lines[-20:]}"
)
# Set up futures for ready state changes
# ===== Test ready state changes =====
# Now the alarm is confirmed ready. Opening/closing door sensor
# should trigger on_ready callbacks.
# Set up futures for door sensor state changes
ready_future_1: asyncio.Future[bool] = loop.create_future()
ready_future_2: asyncio.Future[bool] = loop.create_future()
ready_futures.extend([ready_future_1, ready_future_2])