Compare commits

..

1 Commits

Author SHA1 Message Date
J. Nick Koston
abac07d676 Avoid strlen for logger on all platforms that support it 2025-07-26 16:28:48 -10:00
58 changed files with 1242 additions and 4058 deletions

View File

@@ -2,7 +2,6 @@
import argparse
from datetime import datetime
import functools
import getpass
import importlib
import logging
import os
@@ -336,7 +335,7 @@ def check_permissions(port):
raise EsphomeError(
"You do not have read or write permission on the selected serial port. "
"To resolve this issue, you can add your user to the dialout group "
f"by running the following command: sudo usermod -a -G dialout {getpass.getuser()}. "
f"by running the following command: sudo usermod -a -G dialout {os.getlogin()}. "
"You will need to log out & back in or reboot to activate the new group access."
)
@@ -464,13 +463,6 @@ def command_vscode(args):
def command_compile(args, config):
# Set memory analysis options in config
if args.analyze_memory:
config.setdefault(CONF_ESPHOME, {})["analyze_memory"] = True
if args.memory_report:
config.setdefault(CONF_ESPHOME, {})["memory_report_file"] = args.memory_report
exit_code = write_cpp(config)
if exit_code != 0:
return exit_code
@@ -850,17 +842,6 @@ def parse_args(argv):
help="Only generate source code, do not compile.",
action="store_true",
)
parser_compile.add_argument(
"--analyze-memory",
help="Analyze and display memory usage by component after compilation.",
action="store_true",
)
parser_compile.add_argument(
"--memory-report",
help="Save memory analysis report to a file (supports .json or .txt).",
type=str,
metavar="FILE",
)
parser_upload = subparsers.add_parser(
"upload",

File diff suppressed because it is too large Load Diff

View File

@@ -53,7 +53,6 @@ SERVICE_ARG_NATIVE_TYPES = {
CONF_ENCRYPTION = "encryption"
CONF_BATCH_DELAY = "batch_delay"
CONF_CUSTOM_SERVICES = "custom_services"
CONF_HOMEASSISTANT_SERVICES = "homeassistant_services"
CONF_HOMEASSISTANT_STATES = "homeassistant_states"
@@ -120,9 +119,6 @@ CONFIG_SCHEMA = cv.All(
cv.Range(max=cv.TimePeriod(milliseconds=65535)),
),
cv.Optional(CONF_CUSTOM_SERVICES, default=False): cv.boolean,
cv.Optional(CONF_HOMEASSISTANT_SERVICES, default=False): cv.boolean,
cv.Optional(CONF_HOMEASSISTANT_STATES, default=False): cv.boolean,
cv.Optional(CONF_HOMEASSISTANT_SERVICES, default=False): cv.boolean,
cv.Optional(CONF_HOMEASSISTANT_STATES, default=False): cv.boolean,
cv.Optional(CONF_ON_CLIENT_CONNECTED): automation.validate_automation(
single=True
@@ -152,9 +148,6 @@ async def to_code(config):
if config.get(CONF_ACTIONS) or config[CONF_CUSTOM_SERVICES]:
cg.add_define("USE_API_SERVICES")
if config[CONF_HOMEASSISTANT_SERVICES]:
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
if config[CONF_HOMEASSISTANT_STATES]:
cg.add_define("USE_API_HOMEASSISTANT_STATES")
@@ -247,7 +240,6 @@ HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All(
HOMEASSISTANT_ACTION_ACTION_SCHEMA,
)
async def homeassistant_service_to_code(config, action_id, template_arg, args):
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
serv = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, serv, False)
templ = await cg.templatable(config[CONF_ACTION], args, None)
@@ -291,7 +283,6 @@ HOMEASSISTANT_EVENT_ACTION_SCHEMA = cv.Schema(
HOMEASSISTANT_EVENT_ACTION_SCHEMA,
)
async def homeassistant_event_to_code(config, action_id, template_arg, args):
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
serv = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, serv, True)
templ = await cg.templatable(config[CONF_EVENT], args, None)

View File

@@ -755,19 +755,17 @@ message NoiseEncryptionSetKeyResponse {
message SubscribeHomeassistantServicesRequest {
option (id) = 34;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_API_HOMEASSISTANT_SERVICES";
}
message HomeassistantServiceMap {
string key = 1;
string value = 2 [(no_zero_copy) = true];
string value = 2;
}
message HomeassistantServiceResponse {
option (id) = 35;
option (source) = SOURCE_SERVER;
option (no_delay) = true;
option (ifdef) = "USE_API_HOMEASSISTANT_SERVICES";
string service = 1;
repeated HomeassistantServiceMap data = 2;

View File

@@ -244,7 +244,21 @@ void APIConnection::loop() {
#ifdef USE_API_HOMEASSISTANT_STATES
if (state_subs_at_ >= 0) {
this->process_state_subscriptions_();
const auto &subs = this->parent_->get_state_subs();
if (state_subs_at_ < static_cast<int>(subs.size())) {
auto &it = subs[state_subs_at_];
SubscribeHomeAssistantStateResponse resp;
resp.set_entity_id(StringRef(it.entity_id));
// attribute.value() returns temporary - must store it
std::string attribute_value = it.attribute.value();
resp.set_attribute(StringRef(attribute_value));
resp.once = it.once;
if (this->send_message(resp, SubscribeHomeAssistantStateResponse::MESSAGE_TYPE)) {
state_subs_at_++;
}
} else {
state_subs_at_ = -1;
}
}
#endif
}
@@ -276,9 +290,8 @@ uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint8_t mess
#endif
// Calculate size
ProtoSize size_calc;
msg.calculate_size(size_calc);
uint32_t calculated_size = size_calc.get_size();
uint32_t calculated_size = 0;
msg.calculate_size(calculated_size);
// Cache frame sizes to avoid repeated virtual calls
const uint8_t header_padding = conn->helper_->frame_header_padding();
@@ -631,13 +644,17 @@ uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection
if (traits.get_supports_fan_modes() && climate->fan_mode.has_value())
resp.fan_mode = static_cast<enums::ClimateFanMode>(climate->fan_mode.value());
if (!traits.get_supported_custom_fan_modes().empty() && climate->custom_fan_mode.has_value()) {
resp.set_custom_fan_mode(StringRef(climate->custom_fan_mode.value()));
// custom_fan_mode.value() returns temporary - must store it
std::string custom_fan_mode = climate->custom_fan_mode.value();
resp.set_custom_fan_mode(StringRef(custom_fan_mode));
}
if (traits.get_supports_presets() && climate->preset.has_value()) {
resp.preset = static_cast<enums::ClimatePreset>(climate->preset.value());
}
if (!traits.get_supported_custom_presets().empty() && climate->custom_preset.has_value()) {
resp.set_custom_preset(StringRef(climate->custom_preset.value()));
// custom_preset.value() returns temporary - must store it
std::string custom_preset = climate->custom_preset.value();
resp.set_custom_preset(StringRef(custom_preset));
}
if (traits.get_supports_swing_modes())
resp.swing_mode = static_cast<enums::ClimateSwingMode>(climate->swing_mode);
@@ -1521,18 +1538,19 @@ void APIConnection::execute_service(const ExecuteServiceRequest &msg) {
#endif
#ifdef USE_API_NOISE
bool APIConnection::send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyRequest &msg) {
NoiseEncryptionSetKeyResponse resp;
resp.success = false;
psk_t psk{};
NoiseEncryptionSetKeyResponse resp;
if (base64_decode(msg.key, psk.data(), msg.key.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");
} else {
resp.success = true;
resp.success = false;
return this->send_message(resp, NoiseEncryptionSetKeyResponse::MESSAGE_TYPE);
}
if (!this->parent_->save_noise_psk(psk, true)) {
ESP_LOGW(TAG, "Failed to save encryption key");
resp.success = false;
return this->send_message(resp, NoiseEncryptionSetKeyResponse::MESSAGE_TYPE);
}
resp.success = true;
return this->send_message(resp, NoiseEncryptionSetKeyResponse::MESSAGE_TYPE);
}
#endif
@@ -1826,27 +1844,5 @@ uint16_t APIConnection::try_send_ping_request(EntityBase *entity, APIConnection
return encode_message_to_buffer(req, PingRequest::MESSAGE_TYPE, conn, remaining_size, is_single);
}
#ifdef USE_API_HOMEASSISTANT_STATES
void APIConnection::process_state_subscriptions_() {
const auto &subs = this->parent_->get_state_subs();
if (this->state_subs_at_ >= static_cast<int>(subs.size())) {
this->state_subs_at_ = -1;
return;
}
const auto &it = subs[this->state_subs_at_];
SubscribeHomeAssistantStateResponse resp;
resp.set_entity_id(StringRef(it.entity_id));
// Avoid string copy by directly using the optional's value if it exists
resp.set_attribute(it.attribute.has_value() ? StringRef(it.attribute.value()) : StringRef(""));
resp.once = it.once;
if (this->send_message(resp, SubscribeHomeAssistantStateResponse::MESSAGE_TYPE)) {
this->state_subs_at_++;
}
}
#endif // USE_API_HOMEASSISTANT_STATES
} // namespace esphome::api
#endif

View File

@@ -131,13 +131,11 @@ class APIConnection : public APIServerConnection {
void media_player_command(const MediaPlayerCommandRequest &msg) override;
#endif
bool try_send_log_message(int level, const char *tag, const char *line, size_t message_len);
#ifdef USE_API_HOMEASSISTANT_SERVICES
void send_homeassistant_service_call(const HomeassistantServiceResponse &call) {
if (!this->flags_.service_call_subscription)
return;
this->send_message(call, HomeassistantServiceResponse::MESSAGE_TYPE);
}
#endif
#ifdef USE_BLUETOOTH_PROXY
void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) override;
void unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) override;
@@ -211,11 +209,9 @@ class APIConnection : public APIServerConnection {
if (msg.dump_config)
App.schedule_dump_config();
}
#ifdef USE_API_HOMEASSISTANT_SERVICES
void subscribe_homeassistant_services(const SubscribeHomeassistantServicesRequest &msg) override {
this->flags_.service_call_subscription = true;
}
#endif
#ifdef USE_API_HOMEASSISTANT_STATES
void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) override;
#endif
@@ -298,10 +294,6 @@ class APIConnection : public APIServerConnection {
// Helper function to handle authentication completion
void complete_authentication_();
#ifdef USE_API_HOMEASSISTANT_STATES
void process_state_subscriptions_();
#endif // USE_API_HOMEASSISTANT_STATES
// Non-template helper to encode any ProtoMessage
static uint16_t encode_message_to_buffer(ProtoMessage &msg, uint8_t message_type, APIConnection *conn,
uint32_t remaining_size, bool is_single);

View File

@@ -27,5 +27,4 @@ extend google.protobuf.MessageOptions {
extend google.protobuf.FieldOptions {
optional string field_ifdef = 1042;
optional uint32 fixed_array_size = 50007;
optional bool no_zero_copy = 50008 [default=false];
}

File diff suppressed because it is too large Load Diff

View File

@@ -340,7 +340,7 @@ class HelloResponse : public ProtoMessage {
StringRef name_ref_{};
void set_name(const StringRef &ref) { this->name_ref_ = ref; }
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -371,14 +371,14 @@ class ConnectResponse : public ProtoMessage {
#endif
bool invalid_password{false};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
};
class DisconnectRequest : public ProtoMessage {
class DisconnectRequest : public ProtoDecodableMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 5;
static constexpr uint8_t ESTIMATED_SIZE = 0;
@@ -391,7 +391,7 @@ class DisconnectRequest : public ProtoMessage {
protected:
};
class DisconnectResponse : public ProtoMessage {
class DisconnectResponse : public ProtoDecodableMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 6;
static constexpr uint8_t ESTIMATED_SIZE = 0;
@@ -404,7 +404,7 @@ class DisconnectResponse : public ProtoMessage {
protected:
};
class PingRequest : public ProtoMessage {
class PingRequest : public ProtoDecodableMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 7;
static constexpr uint8_t ESTIMATED_SIZE = 0;
@@ -417,7 +417,7 @@ class PingRequest : public ProtoMessage {
protected:
};
class PingResponse : public ProtoMessage {
class PingResponse : public ProtoDecodableMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 8;
static constexpr uint8_t ESTIMATED_SIZE = 0;
@@ -430,7 +430,7 @@ class PingResponse : public ProtoMessage {
protected:
};
class DeviceInfoRequest : public ProtoMessage {
class DeviceInfoRequest : public ProtoDecodableMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 9;
static constexpr uint8_t ESTIMATED_SIZE = 0;
@@ -450,7 +450,7 @@ class AreaInfo : public ProtoMessage {
StringRef name_ref_{};
void set_name(const StringRef &ref) { this->name_ref_ = ref; }
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -466,7 +466,7 @@ class DeviceInfo : public ProtoMessage {
void set_name(const StringRef &ref) { this->name_ref_ = ref; }
uint32_t area_id{0};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -539,14 +539,14 @@ class DeviceInfoResponse : public ProtoMessage {
AreaInfo area{};
#endif
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
};
class ListEntitiesRequest : public ProtoMessage {
class ListEntitiesRequest : public ProtoDecodableMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 11;
static constexpr uint8_t ESTIMATED_SIZE = 0;
@@ -572,7 +572,7 @@ class ListEntitiesDoneResponse : public ProtoMessage {
protected:
};
class SubscribeStatesRequest : public ProtoMessage {
class SubscribeStatesRequest : public ProtoDecodableMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 20;
static constexpr uint8_t ESTIMATED_SIZE = 0;
@@ -597,7 +597,7 @@ class ListEntitiesBinarySensorResponse : public InfoResponseProtoMessage {
void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; }
bool is_status_binary_sensor{false};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -614,7 +614,7 @@ class BinarySensorStateResponse : public StateResponseProtoMessage {
bool state{false};
bool missing_state{false};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -637,7 +637,7 @@ class ListEntitiesCoverResponse : public InfoResponseProtoMessage {
void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; }
bool supports_stop{false};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -655,7 +655,7 @@ class CoverStateResponse : public StateResponseProtoMessage {
float tilt{0.0f};
enums::CoverOperation current_operation{};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -697,7 +697,7 @@ class ListEntitiesFanResponse : public InfoResponseProtoMessage {
int32_t supported_speed_count{0};
std::vector<std::string> supported_preset_modes{};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -718,7 +718,7 @@ class FanStateResponse : public StateResponseProtoMessage {
StringRef preset_mode_ref_{};
void set_preset_mode(const StringRef &ref) { this->preset_mode_ref_ = ref; }
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -765,7 +765,7 @@ class ListEntitiesLightResponse : public InfoResponseProtoMessage {
float max_mireds{0.0f};
std::vector<std::string> effects{};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -793,7 +793,7 @@ class LightStateResponse : public StateResponseProtoMessage {
StringRef effect_ref_{};
void set_effect(const StringRef &ref) { this->effect_ref_ = ref; }
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -859,7 +859,7 @@ class ListEntitiesSensorResponse : public InfoResponseProtoMessage {
void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; }
enums::SensorStateClass state_class{};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -876,7 +876,7 @@ class SensorStateResponse : public StateResponseProtoMessage {
float state{0.0f};
bool missing_state{false};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -896,7 +896,7 @@ class ListEntitiesSwitchResponse : public InfoResponseProtoMessage {
StringRef device_class_ref_{};
void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; }
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -912,7 +912,7 @@ class SwitchStateResponse : public StateResponseProtoMessage {
#endif
bool state{false};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -947,7 +947,7 @@ class ListEntitiesTextSensorResponse : public InfoResponseProtoMessage {
StringRef device_class_ref_{};
void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; }
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -965,7 +965,7 @@ class TextSensorStateResponse : public StateResponseProtoMessage {
void set_state(const StringRef &ref) { this->state_ref_ = ref; }
bool missing_state{false};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -1004,7 +1004,7 @@ class SubscribeLogsResponse : public ProtoMessage {
this->message_len_ = len;
}
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -1036,7 +1036,7 @@ class NoiseEncryptionSetKeyResponse : public ProtoMessage {
#endif
bool success{false};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -1044,8 +1044,7 @@ class NoiseEncryptionSetKeyResponse : public ProtoMessage {
protected:
};
#endif
#ifdef USE_API_HOMEASSISTANT_SERVICES
class SubscribeHomeassistantServicesRequest : public ProtoMessage {
class SubscribeHomeassistantServicesRequest : public ProtoDecodableMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 34;
static constexpr uint8_t ESTIMATED_SIZE = 0;
@@ -1062,9 +1061,10 @@ class HomeassistantServiceMap : public ProtoMessage {
public:
StringRef key_ref_{};
void set_key(const StringRef &ref) { this->key_ref_ = ref; }
std::string value{};
StringRef value_ref_{};
void set_value(const StringRef &ref) { this->value_ref_ = ref; }
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -1085,16 +1085,15 @@ class HomeassistantServiceResponse : public ProtoMessage {
std::vector<HomeassistantServiceMap> variables{};
bool is_event{false};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
};
#endif
#ifdef USE_API_HOMEASSISTANT_STATES
class SubscribeHomeAssistantStatesRequest : public ProtoMessage {
class SubscribeHomeAssistantStatesRequest : public ProtoDecodableMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 38;
static constexpr uint8_t ESTIMATED_SIZE = 0;
@@ -1120,7 +1119,7 @@ class SubscribeHomeAssistantStateResponse : public ProtoMessage {
void set_attribute(const StringRef &ref) { this->attribute_ref_ = ref; }
bool once{false};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -1145,7 +1144,7 @@ class HomeAssistantStateResponse : public ProtoDecodableMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
};
#endif
class GetTimeRequest : public ProtoMessage {
class GetTimeRequest : public ProtoDecodableMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 36;
static constexpr uint8_t ESTIMATED_SIZE = 0;
@@ -1167,7 +1166,7 @@ class GetTimeResponse : public ProtoDecodableMessage {
#endif
uint32_t epoch_seconds{0};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -1182,7 +1181,7 @@ class ListEntitiesServicesArgument : public ProtoMessage {
void set_name(const StringRef &ref) { this->name_ref_ = ref; }
enums::ServiceArgType type{};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -1201,7 +1200,7 @@ class ListEntitiesServicesResponse : public ProtoMessage {
uint32_t key{0};
std::vector<ListEntitiesServicesArgument> args{};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -1255,7 +1254,7 @@ class ListEntitiesCameraResponse : public InfoResponseProtoMessage {
const char *message_name() const override { return "list_entities_camera_response"; }
#endif
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -1277,7 +1276,7 @@ class CameraImageResponse : public StateResponseProtoMessage {
}
bool done{false};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -1327,7 +1326,7 @@ class ListEntitiesClimateResponse : public InfoResponseProtoMessage {
float visual_min_humidity{0.0f};
float visual_max_humidity{0.0f};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -1357,7 +1356,7 @@ class ClimateStateResponse : public StateResponseProtoMessage {
float current_humidity{0.0f};
float target_humidity{0.0f};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -1418,7 +1417,7 @@ class ListEntitiesNumberResponse : public InfoResponseProtoMessage {
StringRef device_class_ref_{};
void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; }
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -1435,7 +1434,7 @@ class NumberStateResponse : public StateResponseProtoMessage {
float state{0.0f};
bool missing_state{false};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -1469,7 +1468,7 @@ class ListEntitiesSelectResponse : public InfoResponseProtoMessage {
#endif
std::vector<std::string> options{};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -1487,7 +1486,7 @@ class SelectStateResponse : public StateResponseProtoMessage {
void set_state(const StringRef &ref) { this->state_ref_ = ref; }
bool missing_state{false};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -1524,7 +1523,7 @@ class ListEntitiesSirenResponse : public InfoResponseProtoMessage {
bool supports_duration{false};
bool supports_volume{false};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -1540,7 +1539,7 @@ class SirenStateResponse : public StateResponseProtoMessage {
#endif
bool state{false};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -1586,7 +1585,7 @@ class ListEntitiesLockResponse : public InfoResponseProtoMessage {
StringRef code_format_ref_{};
void set_code_format(const StringRef &ref) { this->code_format_ref_ = ref; }
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -1602,7 +1601,7 @@ class LockStateResponse : public StateResponseProtoMessage {
#endif
enums::LockState state{};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -1640,7 +1639,7 @@ class ListEntitiesButtonResponse : public InfoResponseProtoMessage {
StringRef device_class_ref_{};
void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; }
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -1673,7 +1672,7 @@ class MediaPlayerSupportedFormat : public ProtoMessage {
enums::MediaPlayerFormatPurpose purpose{};
uint32_t sample_bytes{0};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -1690,7 +1689,7 @@ class ListEntitiesMediaPlayerResponse : public InfoResponseProtoMessage {
bool supports_pause{false};
std::vector<MediaPlayerSupportedFormat> supported_formats{};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -1708,7 +1707,7 @@ class MediaPlayerStateResponse : public StateResponseProtoMessage {
float volume{0.0f};
bool muted{false};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -1764,7 +1763,7 @@ class BluetoothLERawAdvertisement : public ProtoMessage {
uint8_t data[62]{};
uint8_t data_len{0};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -1780,7 +1779,7 @@ class BluetoothLERawAdvertisementsResponse : public ProtoMessage {
#endif
std::vector<BluetoothLERawAdvertisement> advertisements{};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -1817,7 +1816,7 @@ class BluetoothDeviceConnectionResponse : public ProtoMessage {
uint32_t mtu{0};
int32_t error{0};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -1844,7 +1843,7 @@ class BluetoothGATTDescriptor : public ProtoMessage {
std::array<uint64_t, 2> uuid{};
uint32_t handle{0};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -1858,7 +1857,7 @@ class BluetoothGATTCharacteristic : public ProtoMessage {
uint32_t properties{0};
std::vector<BluetoothGATTDescriptor> descriptors{};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -1871,7 +1870,7 @@ class BluetoothGATTService : public ProtoMessage {
uint32_t handle{0};
std::vector<BluetoothGATTCharacteristic> characteristics{};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -1888,7 +1887,7 @@ class BluetoothGATTGetServicesResponse : public ProtoMessage {
uint64_t address{0};
std::array<BluetoothGATTService, 1> services{};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -1904,7 +1903,7 @@ class BluetoothGATTGetServicesDoneResponse : public ProtoMessage {
#endif
uint64_t address{0};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -1943,7 +1942,7 @@ class BluetoothGATTReadResponse : public ProtoMessage {
this->data_len_ = len;
}
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -2036,14 +2035,14 @@ class BluetoothGATTNotifyDataResponse : public ProtoMessage {
this->data_len_ = len;
}
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
};
class SubscribeBluetoothConnectionsFreeRequest : public ProtoMessage {
class SubscribeBluetoothConnectionsFreeRequest : public ProtoDecodableMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 80;
static constexpr uint8_t ESTIMATED_SIZE = 0;
@@ -2067,7 +2066,7 @@ class BluetoothConnectionsFreeResponse : public ProtoMessage {
uint32_t limit{0};
std::vector<uint64_t> allocated{};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -2085,7 +2084,7 @@ class BluetoothGATTErrorResponse : public ProtoMessage {
uint32_t handle{0};
int32_t error{0};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -2102,7 +2101,7 @@ class BluetoothGATTWriteResponse : public ProtoMessage {
uint64_t address{0};
uint32_t handle{0};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -2119,7 +2118,7 @@ class BluetoothGATTNotifyResponse : public ProtoMessage {
uint64_t address{0};
uint32_t handle{0};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -2137,7 +2136,7 @@ class BluetoothDevicePairingResponse : public ProtoMessage {
bool paired{false};
int32_t error{0};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -2155,14 +2154,14 @@ class BluetoothDeviceUnpairingResponse : public ProtoMessage {
bool success{false};
int32_t error{0};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
};
class UnsubscribeBluetoothLEAdvertisementsRequest : public ProtoMessage {
class UnsubscribeBluetoothLEAdvertisementsRequest : public ProtoDecodableMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 87;
static constexpr uint8_t ESTIMATED_SIZE = 0;
@@ -2186,7 +2185,7 @@ class BluetoothDeviceClearCacheResponse : public ProtoMessage {
bool success{false};
int32_t error{0};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -2203,7 +2202,7 @@ class BluetoothScannerStateResponse : public ProtoMessage {
enums::BluetoothScannerState state{};
enums::BluetoothScannerMode mode{};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -2249,7 +2248,7 @@ class VoiceAssistantAudioSettings : public ProtoMessage {
uint32_t auto_gain{0};
float volume_multiplier{0.0f};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -2271,7 +2270,7 @@ class VoiceAssistantRequest : public ProtoMessage {
StringRef wake_word_phrase_ref_{};
void set_wake_word_phrase(const StringRef &ref) { this->wake_word_phrase_ref_ = ref; }
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -2338,7 +2337,7 @@ class VoiceAssistantAudio : public ProtoDecodableMessage {
}
bool end{false};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -2396,7 +2395,7 @@ class VoiceAssistantAnnounceFinished : public ProtoMessage {
#endif
bool success{false};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -2411,14 +2410,14 @@ class VoiceAssistantWakeWord : public ProtoMessage {
void set_wake_word(const StringRef &ref) { this->wake_word_ref_ = ref; }
std::vector<std::string> trained_languages{};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
};
class VoiceAssistantConfigurationRequest : public ProtoMessage {
class VoiceAssistantConfigurationRequest : public ProtoDecodableMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 121;
static constexpr uint8_t ESTIMATED_SIZE = 0;
@@ -2442,7 +2441,7 @@ class VoiceAssistantConfigurationResponse : public ProtoMessage {
std::vector<std::string> active_wake_words{};
uint32_t max_active_wake_words{0};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -2477,7 +2476,7 @@ class ListEntitiesAlarmControlPanelResponse : public InfoResponseProtoMessage {
bool requires_code{false};
bool requires_code_to_arm{false};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -2493,7 +2492,7 @@ class AlarmControlPanelStateResponse : public StateResponseProtoMessage {
#endif
enums::AlarmControlPanelState state{};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -2533,7 +2532,7 @@ class ListEntitiesTextResponse : public InfoResponseProtoMessage {
void set_pattern(const StringRef &ref) { this->pattern_ref_ = ref; }
enums::TextMode mode{};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -2551,7 +2550,7 @@ class TextStateResponse : public StateResponseProtoMessage {
void set_state(const StringRef &ref) { this->state_ref_ = ref; }
bool missing_state{false};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -2585,7 +2584,7 @@ class ListEntitiesDateResponse : public InfoResponseProtoMessage {
const char *message_name() const override { return "list_entities_date_response"; }
#endif
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -2604,7 +2603,7 @@ class DateStateResponse : public StateResponseProtoMessage {
uint32_t month{0};
uint32_t day{0};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -2639,7 +2638,7 @@ class ListEntitiesTimeResponse : public InfoResponseProtoMessage {
const char *message_name() const override { return "list_entities_time_response"; }
#endif
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -2658,7 +2657,7 @@ class TimeStateResponse : public StateResponseProtoMessage {
uint32_t minute{0};
uint32_t second{0};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -2696,7 +2695,7 @@ class ListEntitiesEventResponse : public InfoResponseProtoMessage {
void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; }
std::vector<std::string> event_types{};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -2713,7 +2712,7 @@ class EventResponse : public StateResponseProtoMessage {
StringRef event_type_ref_{};
void set_event_type(const StringRef &ref) { this->event_type_ref_ = ref; }
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -2735,7 +2734,7 @@ class ListEntitiesValveResponse : public InfoResponseProtoMessage {
bool supports_position{false};
bool supports_stop{false};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -2752,7 +2751,7 @@ class ValveStateResponse : public StateResponseProtoMessage {
float position{0.0f};
enums::ValveOperation current_operation{};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -2787,7 +2786,7 @@ class ListEntitiesDateTimeResponse : public InfoResponseProtoMessage {
const char *message_name() const override { return "list_entities_date_time_response"; }
#endif
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -2804,7 +2803,7 @@ class DateTimeStateResponse : public StateResponseProtoMessage {
bool missing_state{false};
uint32_t epoch_seconds{0};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -2839,7 +2838,7 @@ class ListEntitiesUpdateResponse : public InfoResponseProtoMessage {
StringRef device_class_ref_{};
void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; }
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -2868,7 +2867,7 @@ class UpdateStateResponse : public StateResponseProtoMessage {
StringRef release_url_ref_{};
void set_release_url(const StringRef &ref) { this->release_url_ref_ = ref; }
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif

View File

@@ -1038,14 +1038,13 @@ void NoiseEncryptionSetKeyRequest::dump_to(std::string &out) const {
}
void NoiseEncryptionSetKeyResponse::dump_to(std::string &out) const { dump_field(out, "success", this->success); }
#endif
#ifdef USE_API_HOMEASSISTANT_SERVICES
void SubscribeHomeassistantServicesRequest::dump_to(std::string &out) const {
out.append("SubscribeHomeassistantServicesRequest {}");
}
void HomeassistantServiceMap::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "HomeassistantServiceMap");
dump_field(out, "key", this->key_ref_);
dump_field(out, "value", this->value);
dump_field(out, "value", this->value_ref_);
}
void HomeassistantServiceResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "HomeassistantServiceResponse");
@@ -1067,7 +1066,6 @@ void HomeassistantServiceResponse::dump_to(std::string &out) const {
}
dump_field(out, "is_event", this->is_event);
}
#endif
#ifdef USE_API_HOMEASSISTANT_STATES
void SubscribeHomeAssistantStatesRequest::dump_to(std::string &out) const {
out.append("SubscribeHomeAssistantStatesRequest {}");

View File

@@ -35,7 +35,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
}
case DisconnectRequest::MESSAGE_TYPE: {
DisconnectRequest msg;
// Empty message: no decode needed
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "on_disconnect_request: %s", msg.dump().c_str());
#endif
@@ -44,7 +44,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
}
case DisconnectResponse::MESSAGE_TYPE: {
DisconnectResponse msg;
// Empty message: no decode needed
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "on_disconnect_response: %s", msg.dump().c_str());
#endif
@@ -53,7 +53,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
}
case PingRequest::MESSAGE_TYPE: {
PingRequest msg;
// Empty message: no decode needed
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "on_ping_request: %s", msg.dump().c_str());
#endif
@@ -62,7 +62,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
}
case PingResponse::MESSAGE_TYPE: {
PingResponse msg;
// Empty message: no decode needed
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "on_ping_response: %s", msg.dump().c_str());
#endif
@@ -71,7 +71,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
}
case DeviceInfoRequest::MESSAGE_TYPE: {
DeviceInfoRequest msg;
// Empty message: no decode needed
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "on_device_info_request: %s", msg.dump().c_str());
#endif
@@ -80,7 +80,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
}
case ListEntitiesRequest::MESSAGE_TYPE: {
ListEntitiesRequest msg;
// Empty message: no decode needed
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "on_list_entities_request: %s", msg.dump().c_str());
#endif
@@ -89,7 +89,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
}
case SubscribeStatesRequest::MESSAGE_TYPE: {
SubscribeStatesRequest msg;
// Empty message: no decode needed
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "on_subscribe_states_request: %s", msg.dump().c_str());
#endif
@@ -149,20 +149,18 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
break;
}
#endif
#ifdef USE_API_HOMEASSISTANT_SERVICES
case SubscribeHomeassistantServicesRequest::MESSAGE_TYPE: {
SubscribeHomeassistantServicesRequest msg;
// Empty message: no decode needed
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "on_subscribe_homeassistant_services_request: %s", msg.dump().c_str());
#endif
this->on_subscribe_homeassistant_services_request(msg);
break;
}
#endif
case GetTimeRequest::MESSAGE_TYPE: {
GetTimeRequest msg;
// Empty message: no decode needed
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "on_get_time_request: %s", msg.dump().c_str());
#endif
@@ -181,7 +179,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
#ifdef USE_API_HOMEASSISTANT_STATES
case SubscribeHomeAssistantStatesRequest::MESSAGE_TYPE: {
SubscribeHomeAssistantStatesRequest msg;
// Empty message: no decode needed
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "on_subscribe_home_assistant_states_request: %s", msg.dump().c_str());
#endif
@@ -390,7 +388,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
#ifdef USE_BLUETOOTH_PROXY
case SubscribeBluetoothConnectionsFreeRequest::MESSAGE_TYPE: {
SubscribeBluetoothConnectionsFreeRequest msg;
// Empty message: no decode needed
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "on_subscribe_bluetooth_connections_free_request: %s", msg.dump().c_str());
#endif
@@ -401,7 +399,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
#ifdef USE_BLUETOOTH_PROXY
case UnsubscribeBluetoothLEAdvertisementsRequest::MESSAGE_TYPE: {
UnsubscribeBluetoothLEAdvertisementsRequest msg;
// Empty message: no decode needed
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "on_unsubscribe_bluetooth_le_advertisements_request: %s", msg.dump().c_str());
#endif
@@ -555,7 +553,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
#ifdef USE_VOICE_ASSISTANT
case VoiceAssistantConfigurationRequest::MESSAGE_TYPE: {
VoiceAssistantConfigurationRequest msg;
// Empty message: no decode needed
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "on_voice_assistant_configuration_request: %s", msg.dump().c_str());
#endif
@@ -641,14 +639,12 @@ void APIServerConnection::on_subscribe_logs_request(const SubscribeLogsRequest &
this->subscribe_logs(msg);
}
}
#ifdef USE_API_HOMEASSISTANT_SERVICES
void APIServerConnection::on_subscribe_homeassistant_services_request(
const SubscribeHomeassistantServicesRequest &msg) {
if (this->check_authenticated_()) {
this->subscribe_homeassistant_services(msg);
}
}
#endif
#ifdef USE_API_HOMEASSISTANT_STATES
void APIServerConnection::on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) {
if (this->check_authenticated_()) {

View File

@@ -60,9 +60,7 @@ class APIServerConnectionBase : public ProtoService {
virtual void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &value){};
#endif
#ifdef USE_API_HOMEASSISTANT_SERVICES
virtual void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &value){};
#endif
#ifdef USE_API_HOMEASSISTANT_STATES
virtual void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &value){};
@@ -220,9 +218,7 @@ class APIServerConnection : public APIServerConnectionBase {
virtual void list_entities(const ListEntitiesRequest &msg) = 0;
virtual void subscribe_states(const SubscribeStatesRequest &msg) = 0;
virtual void subscribe_logs(const SubscribeLogsRequest &msg) = 0;
#ifdef USE_API_HOMEASSISTANT_SERVICES
virtual void subscribe_homeassistant_services(const SubscribeHomeassistantServicesRequest &msg) = 0;
#endif
#ifdef USE_API_HOMEASSISTANT_STATES
virtual void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) = 0;
#endif
@@ -342,9 +338,7 @@ class APIServerConnection : public APIServerConnectionBase {
void on_list_entities_request(const ListEntitiesRequest &msg) override;
void on_subscribe_states_request(const SubscribeStatesRequest &msg) override;
void on_subscribe_logs_request(const SubscribeLogsRequest &msg) override;
#ifdef USE_API_HOMEASSISTANT_SERVICES
void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &msg) override;
#endif
#ifdef USE_API_HOMEASSISTANT_STATES
void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) override;
#endif

View File

@@ -369,13 +369,11 @@ void APIServer::set_password(const std::string &password) { this->password_ = pa
void APIServer::set_batch_delay(uint16_t batch_delay) { this->batch_delay_ = batch_delay; }
#ifdef USE_API_HOMEASSISTANT_SERVICES
void APIServer::send_homeassistant_service_call(const HomeassistantServiceResponse &call) {
for (auto &client : this->clients_) {
client->send_homeassistant_service_call(call);
}
}
#endif
#ifdef USE_API_HOMEASSISTANT_STATES
void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,

View File

@@ -106,9 +106,7 @@ class APIServer : public Component, public Controller {
#ifdef USE_MEDIA_PLAYER
void on_media_player_update(media_player::MediaPlayer *obj) override;
#endif
#ifdef USE_API_HOMEASSISTANT_SERVICES
void send_homeassistant_service_call(const HomeassistantServiceResponse &call);
#endif
#ifdef USE_API_SERVICES
void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); }
#endif

View File

@@ -137,7 +137,6 @@ class CustomAPIDevice {
}
#endif
#ifdef USE_API_HOMEASSISTANT_SERVICES
/** Call a Home Assistant service from ESPHome.
*
* Usage:
@@ -175,7 +174,7 @@ class CustomAPIDevice {
resp.data.emplace_back();
auto &kv = resp.data.back();
kv.set_key(StringRef(it.first));
kv.value = it.second;
kv.set_value(StringRef(it.second));
}
global_api_server->send_homeassistant_service_call(resp);
}
@@ -218,11 +217,10 @@ class CustomAPIDevice {
resp.data.emplace_back();
auto &kv = resp.data.back();
kv.set_key(StringRef(it.first));
kv.value = it.second;
kv.set_value(StringRef(it.second));
}
global_api_server->send_homeassistant_service_call(resp);
}
#endif
};
} // namespace esphome::api

View File

@@ -2,7 +2,6 @@
#include "api_server.h"
#ifdef USE_API
#ifdef USE_API_HOMEASSISTANT_SERVICES
#include "api_pb2.h"
#include "esphome/core/automation.h"
#include "esphome/core/helpers.h"
@@ -70,19 +69,22 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
resp.data.emplace_back();
auto &kv = resp.data.back();
kv.set_key(StringRef(it.key));
kv.value = it.value.value(x...);
std::string value = it.value.value(x...);
kv.set_value(StringRef(value));
}
for (auto &it : this->data_template_) {
resp.data_template.emplace_back();
auto &kv = resp.data_template.back();
kv.set_key(StringRef(it.key));
kv.value = it.value.value(x...);
std::string value = it.value.value(x...);
kv.set_value(StringRef(value));
}
for (auto &it : this->variables_) {
resp.variables.emplace_back();
auto &kv = resp.variables.back();
kv.set_key(StringRef(it.key));
kv.value = it.value.value(x...);
std::string value = it.value.value(x...);
kv.set_value(StringRef(value));
}
this->parent_->send_homeassistant_service_call(resp);
}
@@ -98,4 +100,3 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
} // namespace esphome::api
#endif
#endif

View File

@@ -35,10 +35,11 @@ namespace esphome::api {
*
* Unsafe Patterns (WILL cause crashes/corruption):
* 1. Temporaries: msg.set_field(StringRef(obj.get_string())) // get_string() returns by value
* 2. Concatenation: msg.set_field(StringRef(str1 + str2)) // Result is temporary
* 2. Optional values: msg.set_field(StringRef(optional.value())) // value() returns a copy
* 3. Concatenation: msg.set_field(StringRef(str1 + str2)) // Result is temporary
*
* For unsafe patterns, store in a local variable first:
* std::string temp = get_string(); // or str1 + str2
* std::string temp = optional.value(); // or get_string() or str1 + str2
* msg.set_field(StringRef(temp));
*
* The send_*_response pattern ensures proper lifetime management by encoding
@@ -333,16 +334,13 @@ class ProtoWriteBuffer {
std::vector<uint8_t> *buffer_;
};
// Forward declaration
class ProtoSize;
class ProtoMessage {
public:
virtual ~ProtoMessage() = default;
// Default implementation for messages with no fields
virtual void encode(ProtoWriteBuffer buffer) const {}
// Default implementation for messages with no fields
virtual void calculate_size(ProtoSize &size) const {}
virtual void calculate_size(uint32_t &total_size) const {}
#ifdef HAS_PROTO_MESSAGE_DUMP
std::string dump() const;
virtual void dump_to(std::string &out) const = 0;
@@ -363,32 +361,24 @@ class ProtoDecodableMessage : public ProtoMessage {
};
class ProtoSize {
private:
uint32_t total_size_ = 0;
public:
/**
* @brief ProtoSize class for Protocol Buffer serialization size calculation
*
* This class provides methods to calculate the exact byte counts needed
* for encoding various Protocol Buffer field types. The class now uses an
* object-based approach to reduce parameter passing overhead while keeping
* varint calculation methods static for external use.
* This class provides static methods to calculate the exact byte counts needed
* for encoding various Protocol Buffer field types. All methods are designed to be
* efficient for the common case where many fields have default values.
*
* Implements Protocol Buffer encoding size calculation according to:
* https://protobuf.dev/programming-guides/encoding/
*
* Key features:
* - Object-based approach reduces flash usage by eliminating parameter passing
* - Early-return optimization for zero/default values
* - Static varint methods for external callers
* - Direct total_size updates to avoid unnecessary additions
* - Specialized handling for different field types according to protobuf spec
* - Templated helpers for repeated fields and messages
*/
ProtoSize() = default;
uint32_t get_size() const { return total_size_; }
/**
* @brief Calculates the size in bytes needed to encode a uint32_t value as a varint
*
@@ -489,7 +479,9 @@ class ProtoSize {
* @brief Common parameters for all add_*_field methods
*
* All add_*_field methods follow these common patterns:
* * @param field_id_size Pre-calculated size of the field ID in bytes
*
* @param total_size Reference to the total message size to update
* @param field_id_size Pre-calculated size of the field ID in bytes
* @param value The value to calculate size for (type varies)
* @param force Whether to calculate size even if the value is default/zero/empty
*
@@ -502,63 +494,85 @@ class ProtoSize {
/**
* @brief Calculates and adds the size of an int32 field to the total message size
*/
inline void add_int32(uint32_t field_id_size, int32_t value) {
if (value != 0) {
add_int32_force(field_id_size, value);
static inline void add_int32_field(uint32_t &total_size, uint32_t field_id_size, int32_t value) {
// Skip calculation if value is zero
if (value == 0) {
return; // No need to update total_size
}
// Calculate and directly add to total_size
if (value < 0) {
// Negative values are encoded as 10-byte varints in protobuf
total_size += field_id_size + 10;
} else {
// For non-negative values, use the standard varint size
total_size += field_id_size + varint(static_cast<uint32_t>(value));
}
}
/**
* @brief Calculates and adds the size of an int32 field to the total message size (force version)
* @brief Calculates and adds the size of an int32 field to the total message size (repeated field version)
*/
inline void add_int32_force(uint32_t field_id_size, int32_t value) {
// Always calculate size when forced
// Negative values are encoded as 10-byte varints in protobuf
total_size_ += field_id_size + (value < 0 ? 10 : varint(static_cast<uint32_t>(value)));
static inline void add_int32_field_repeated(uint32_t &total_size, uint32_t field_id_size, int32_t value) {
// Always calculate size for repeated fields
if (value < 0) {
// Negative values are encoded as 10-byte varints in protobuf
total_size += field_id_size + 10;
} else {
// For non-negative values, use the standard varint size
total_size += field_id_size + varint(static_cast<uint32_t>(value));
}
}
/**
* @brief Calculates and adds the size of a uint32 field to the total message size
*/
inline void add_uint32(uint32_t field_id_size, uint32_t value) {
if (value != 0) {
add_uint32_force(field_id_size, value);
static inline void add_uint32_field(uint32_t &total_size, uint32_t field_id_size, uint32_t value) {
// Skip calculation if value is zero
if (value == 0) {
return; // No need to update total_size
}
// Calculate and directly add to total_size
total_size += field_id_size + varint(value);
}
/**
* @brief Calculates and adds the size of a uint32 field to the total message size (force version)
* @brief Calculates and adds the size of a uint32 field to the total message size (repeated field version)
*/
inline void add_uint32_force(uint32_t field_id_size, uint32_t value) {
// Always calculate size when force is true
total_size_ += field_id_size + varint(value);
static inline void add_uint32_field_repeated(uint32_t &total_size, uint32_t field_id_size, uint32_t value) {
// Always calculate size for repeated fields
total_size += field_id_size + varint(value);
}
/**
* @brief Calculates and adds the size of a boolean field to the total message size
*/
inline void add_bool(uint32_t field_id_size, bool value) {
if (value) {
// Boolean fields always use 1 byte when true
total_size_ += field_id_size + 1;
static inline void add_bool_field(uint32_t &total_size, uint32_t field_id_size, bool value) {
// Skip calculation if value is false
if (!value) {
return; // No need to update total_size
}
// Boolean fields always use 1 byte when true
total_size += field_id_size + 1;
}
/**
* @brief Calculates and adds the size of a boolean field to the total message size (force version)
* @brief Calculates and adds the size of a boolean field to the total message size (repeated field version)
*/
inline void add_bool_force(uint32_t field_id_size, bool value) {
// Always calculate size when force is true
static inline void add_bool_field_repeated(uint32_t &total_size, uint32_t field_id_size, bool value) {
// Always calculate size for repeated fields
// Boolean fields always use 1 byte
total_size_ += field_id_size + 1;
total_size += field_id_size + 1;
}
/**
* @brief Calculates and adds the size of a float field to the total message size
*/
inline void add_float(uint32_t field_id_size, float value) {
static inline void add_float_field(uint32_t &total_size, uint32_t field_id_size, float value) {
if (value != 0.0f) {
total_size_ += field_id_size + 4;
total_size += field_id_size + 4;
}
}
@@ -568,9 +582,9 @@ class ProtoSize {
/**
* @brief Calculates and adds the size of a fixed32 field to the total message size
*/
inline void add_fixed32(uint32_t field_id_size, uint32_t value) {
static inline void add_fixed32_field(uint32_t &total_size, uint32_t field_id_size, uint32_t value) {
if (value != 0) {
total_size_ += field_id_size + 4;
total_size += field_id_size + 4;
}
}
@@ -580,104 +594,149 @@ class ProtoSize {
/**
* @brief Calculates and adds the size of a sfixed32 field to the total message size
*/
inline void add_sfixed32(uint32_t field_id_size, int32_t value) {
static inline void add_sfixed32_field(uint32_t &total_size, uint32_t field_id_size, int32_t value) {
if (value != 0) {
total_size_ += field_id_size + 4;
total_size += field_id_size + 4;
}
}
// NOTE: add_sfixed64_field removed - wire type 1 (64-bit: sfixed64) not supported
// to reduce overhead on embedded systems
/**
* @brief Calculates and adds the size of an enum field to the total message size
*
* Enum fields are encoded as uint32 varints.
*/
static inline void add_enum_field(uint32_t &total_size, uint32_t field_id_size, uint32_t value) {
// Skip calculation if value is zero
if (value == 0) {
return; // No need to update total_size
}
// Enums are encoded as uint32
total_size += field_id_size + varint(value);
}
/**
* @brief Calculates and adds the size of an enum field to the total message size (repeated field version)
*
* Enum fields are encoded as uint32 varints.
*/
static inline void add_enum_field_repeated(uint32_t &total_size, uint32_t field_id_size, uint32_t value) {
// Always calculate size for repeated fields
// Enums are encoded as uint32
total_size += field_id_size + varint(value);
}
/**
* @brief Calculates and adds the size of a sint32 field to the total message size
*
* Sint32 fields use ZigZag encoding, which is more efficient for negative values.
*/
inline void add_sint32(uint32_t field_id_size, int32_t value) {
if (value != 0) {
add_sint32_force(field_id_size, value);
static inline void add_sint32_field(uint32_t &total_size, uint32_t field_id_size, int32_t value) {
// Skip calculation if value is zero
if (value == 0) {
return; // No need to update total_size
}
// ZigZag encoding for sint32: (n << 1) ^ (n >> 31)
uint32_t zigzag = (static_cast<uint32_t>(value) << 1) ^ (static_cast<uint32_t>(value >> 31));
total_size += field_id_size + varint(zigzag);
}
/**
* @brief Calculates and adds the size of a sint32 field to the total message size (force version)
* @brief Calculates and adds the size of a sint32 field to the total message size (repeated field version)
*
* Sint32 fields use ZigZag encoding, which is more efficient for negative values.
*/
inline void add_sint32_force(uint32_t field_id_size, int32_t value) {
// Always calculate size when force is true
static inline void add_sint32_field_repeated(uint32_t &total_size, uint32_t field_id_size, int32_t value) {
// Always calculate size for repeated fields
// ZigZag encoding for sint32: (n << 1) ^ (n >> 31)
uint32_t zigzag = (static_cast<uint32_t>(value) << 1) ^ (static_cast<uint32_t>(value >> 31));
total_size_ += field_id_size + varint(zigzag);
total_size += field_id_size + varint(zigzag);
}
/**
* @brief Calculates and adds the size of an int64 field to the total message size
*/
inline void add_int64(uint32_t field_id_size, int64_t value) {
if (value != 0) {
add_int64_force(field_id_size, value);
static inline void add_int64_field(uint32_t &total_size, uint32_t field_id_size, int64_t value) {
// Skip calculation if value is zero
if (value == 0) {
return; // No need to update total_size
}
// Calculate and directly add to total_size
total_size += field_id_size + varint(value);
}
/**
* @brief Calculates and adds the size of an int64 field to the total message size (force version)
* @brief Calculates and adds the size of an int64 field to the total message size (repeated field version)
*/
inline void add_int64_force(uint32_t field_id_size, int64_t value) {
// Always calculate size when force is true
total_size_ += field_id_size + varint(value);
static inline void add_int64_field_repeated(uint32_t &total_size, uint32_t field_id_size, int64_t value) {
// Always calculate size for repeated fields
total_size += field_id_size + varint(value);
}
/**
* @brief Calculates and adds the size of a uint64 field to the total message size
*/
inline void add_uint64(uint32_t field_id_size, uint64_t value) {
if (value != 0) {
add_uint64_force(field_id_size, value);
static inline void add_uint64_field(uint32_t &total_size, uint32_t field_id_size, uint64_t value) {
// Skip calculation if value is zero
if (value == 0) {
return; // No need to update total_size
}
// Calculate and directly add to total_size
total_size += field_id_size + varint(value);
}
/**
* @brief Calculates and adds the size of a uint64 field to the total message size (force version)
* @brief Calculates and adds the size of a uint64 field to the total message size (repeated field version)
*/
inline void add_uint64_force(uint32_t field_id_size, uint64_t value) {
// Always calculate size when force is true
total_size_ += field_id_size + varint(value);
static inline void add_uint64_field_repeated(uint32_t &total_size, uint32_t field_id_size, uint64_t value) {
// Always calculate size for repeated fields
total_size += field_id_size + varint(value);
}
// NOTE: sint64 support functions (add_sint64_field, add_sint64_field_force) removed
// NOTE: sint64 support functions (add_sint64_field, add_sint64_field_repeated) removed
// sint64 type is not supported by ESPHome API to reduce overhead on embedded systems
/**
* @brief Calculates and adds the size of a length-delimited field (string/bytes) to the total message size
* @brief Calculates and adds the size of a string field using length
*/
inline void add_length(uint32_t field_id_size, size_t len) {
if (len != 0) {
add_length_force(field_id_size, len);
static inline void add_string_field(uint32_t &total_size, uint32_t field_id_size, size_t len) {
// Skip calculation if string is empty
if (len == 0) {
return; // No need to update total_size
}
// Field ID + length varint + string bytes
total_size += field_id_size + varint(static_cast<uint32_t>(len)) + static_cast<uint32_t>(len);
}
/**
* @brief Calculates and adds the size of a length-delimited field (string/bytes) to the total message size (repeated
* field version)
* @brief Calculates and adds the size of a string/bytes field to the total message size (repeated field version)
*/
inline void add_length_force(uint32_t field_id_size, size_t len) {
// Always calculate size when force is true
static inline void add_string_field_repeated(uint32_t &total_size, uint32_t field_id_size, const std::string &str) {
// Always calculate size for repeated fields
const uint32_t str_size = static_cast<uint32_t>(str.size());
total_size += field_id_size + varint(str_size) + str_size;
}
/**
* @brief Calculates and adds the size of a bytes field to the total message size
*/
static inline void add_bytes_field(uint32_t &total_size, uint32_t field_id_size, size_t len) {
// Skip calculation if bytes is empty
if (len == 0) {
return; // No need to update total_size
}
// Field ID + length varint + data bytes
total_size_ += field_id_size + varint(static_cast<uint32_t>(len)) + static_cast<uint32_t>(len);
total_size += field_id_size + varint(static_cast<uint32_t>(len)) + static_cast<uint32_t>(len);
}
/**
* @brief Adds a pre-calculated size directly to the total
*
* This is used when we can calculate the total size by multiplying the number
* of elements by the bytes per element (for repeated fixed-size types like float, fixed32, etc.)
*
* @param size The pre-calculated total size to add
*/
inline void add_precalculated_size(uint32_t size) { total_size_ += size; }
/**
* @brief Calculates and adds the size of a nested message field to the total message size
*
@@ -686,21 +745,26 @@ class ProtoSize {
*
* @param nested_size The pre-calculated size of the nested message
*/
inline void add_message_field(uint32_t field_id_size, uint32_t nested_size) {
if (nested_size != 0) {
add_message_field_force(field_id_size, nested_size);
static inline void add_message_field(uint32_t &total_size, uint32_t field_id_size, uint32_t nested_size) {
// Skip calculation if nested message is empty
if (nested_size == 0) {
return; // No need to update total_size
}
// Calculate and directly add to total_size
// Field ID + length varint + nested message content
total_size += field_id_size + varint(nested_size) + nested_size;
}
/**
* @brief Calculates and adds the size of a nested message field to the total message size (force version)
* @brief Calculates and adds the size of a nested message field to the total message size (repeated field version)
*
* @param nested_size The pre-calculated size of the nested message
*/
inline void add_message_field_force(uint32_t field_id_size, uint32_t nested_size) {
// Always calculate size when force is true
static inline void add_message_field_repeated(uint32_t &total_size, uint32_t field_id_size, uint32_t nested_size) {
// Always calculate size for repeated fields
// Field ID + length varint + nested message content
total_size_ += field_id_size + varint(nested_size) + nested_size;
total_size += field_id_size + varint(nested_size) + nested_size;
}
/**
@@ -712,29 +776,26 @@ class ProtoSize {
*
* @param message The nested message object
*/
inline void add_message_object(uint32_t field_id_size, const ProtoMessage &message) {
// Calculate nested message size by creating a temporary ProtoSize
ProtoSize nested_calc;
message.calculate_size(nested_calc);
uint32_t nested_size = nested_calc.get_size();
static inline void add_message_object(uint32_t &total_size, uint32_t field_id_size, const ProtoMessage &message) {
uint32_t nested_size = 0;
message.calculate_size(nested_size);
// Use the base implementation with the calculated nested_size
add_message_field(field_id_size, nested_size);
add_message_field(total_size, field_id_size, nested_size);
}
/**
* @brief Calculates and adds the size of a nested message field to the total message size (force version)
* @brief Calculates and adds the size of a nested message field to the total message size (repeated field version)
*
* @param message The nested message object
*/
inline void add_message_object_force(uint32_t field_id_size, const ProtoMessage &message) {
// Calculate nested message size by creating a temporary ProtoSize
ProtoSize nested_calc;
message.calculate_size(nested_calc);
uint32_t nested_size = nested_calc.get_size();
static inline void add_message_object_repeated(uint32_t &total_size, uint32_t field_id_size,
const ProtoMessage &message) {
uint32_t nested_size = 0;
message.calculate_size(nested_size);
// Use the base implementation with the calculated nested_size
add_message_field_force(field_id_size, nested_size);
add_message_field_repeated(total_size, field_id_size, nested_size);
}
/**
@@ -747,15 +808,16 @@ class ProtoSize {
* @param messages Vector of message objects
*/
template<typename MessageType>
inline void add_repeated_message(uint32_t field_id_size, const std::vector<MessageType> &messages) {
static inline void add_repeated_message(uint32_t &total_size, uint32_t field_id_size,
const std::vector<MessageType> &messages) {
// Skip if the vector is empty
if (messages.empty()) {
return;
}
// Use the force version for all messages in the repeated field
// Use the repeated field version for all messages
for (const auto &message : messages) {
add_message_object_force(field_id_size, message);
add_message_object_repeated(total_size, field_id_size, message);
}
}
};
@@ -765,9 +827,8 @@ inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const ProtoMessa
this->encode_field_raw(field_id, 2); // type 2: Length-delimited message
// Calculate the message size first
ProtoSize msg_size;
value.calculate_size(msg_size);
uint32_t msg_length_bytes = msg_size.get_size();
uint32_t msg_length_bytes = 0;
value.calculate_size(msg_length_bytes);
// Calculate how many bytes the length varint needs
uint32_t varint_length_bytes = ProtoSize::varint(msg_length_bytes);
@@ -816,9 +877,8 @@ class ProtoService {
// Optimized method that pre-allocates buffer based on message size
bool send_message_(const ProtoMessage &msg, uint8_t message_type) {
ProtoSize size;
msg.calculate_size(size);
uint32_t msg_size = size.get_size();
uint32_t msg_size = 0;
msg.calculate_size(msg_size);
// Create a pre-sized buffer
auto buffer = this->create_buffer(msg_size);

View File

@@ -80,10 +80,15 @@ void BluetoothConnection::send_service_for_discovery_() {
&service_result, &service_count, this->send_service_);
this->send_service_++;
if (service_status != ESP_GATT_OK || service_count == 0) {
ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_service %s, status=%d, service_count=%d, offset=%d",
this->connection_index_, this->address_str().c_str(), service_status != ESP_GATT_OK ? "error" : "missing",
service_status, service_count, this->send_service_ - 1);
if (service_status != ESP_GATT_OK) {
ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_service error at offset=%d, status=%d", this->connection_index_,
this->address_str().c_str(), this->send_service_ - 1, service_status);
return;
}
if (service_count == 0) {
ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_service missing, service_count=%d", this->connection_index_,
this->address_str().c_str(), service_count);
return;
}
@@ -99,20 +104,15 @@ void BluetoothConnection::send_service_for_discovery_() {
esp_ble_gattc_get_attr_count(this->gattc_if_, this->conn_id_, ESP_GATT_DB_CHARACTERISTIC,
service_result.start_handle, service_result.end_handle, 0, &total_char_count);
if (char_count_status != ESP_GATT_OK) {
if (char_count_status == ESP_GATT_OK && total_char_count > 0) {
// Only reserve if we successfully got a count
service_resp.characteristics.reserve(total_char_count);
} else if (char_count_status != ESP_GATT_OK) {
ESP_LOGW(TAG, "[%d] [%s] Error getting characteristic count, status=%d", this->connection_index_,
this->address_str().c_str(), char_count_status);
return;
}
if (total_char_count == 0) {
// No characteristics, just send the service response
api_conn->send_message(resp, api::BluetoothGATTGetServicesResponse::MESSAGE_TYPE);
return;
}
// Reserve space and process characteristics
service_resp.characteristics.reserve(total_char_count);
// Now process characteristics
uint16_t char_offset = 0;
esp_gattc_char_elem_t char_result;
while (true) { // characteristics
@@ -126,7 +126,7 @@ void BluetoothConnection::send_service_for_discovery_() {
if (char_status != ESP_GATT_OK) {
ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_all_char error, status=%d", this->connection_index_,
this->address_str().c_str(), char_status);
return;
break;
}
if (char_count == 0) {
break;
@@ -141,21 +141,19 @@ void BluetoothConnection::send_service_for_discovery_() {
// Get the number of descriptors directly with one call
uint16_t total_desc_count = 0;
esp_gatt_status_t desc_count_status = esp_ble_gattc_get_attr_count(
this->gattc_if_, this->conn_id_, ESP_GATT_DB_DESCRIPTOR, 0, 0, char_result.char_handle, &total_desc_count);
esp_gatt_status_t desc_count_status =
esp_ble_gattc_get_attr_count(this->gattc_if_, this->conn_id_, ESP_GATT_DB_DESCRIPTOR, char_result.char_handle,
service_result.end_handle, 0, &total_desc_count);
if (desc_count_status != ESP_GATT_OK) {
if (desc_count_status == ESP_GATT_OK && total_desc_count > 0) {
// Only reserve if we successfully got a count
characteristic_resp.descriptors.reserve(total_desc_count);
} else if (desc_count_status != ESP_GATT_OK) {
ESP_LOGW(TAG, "[%d] [%s] Error getting descriptor count for char handle %d, status=%d", this->connection_index_,
this->address_str().c_str(), char_result.char_handle, desc_count_status);
return;
}
if (total_desc_count == 0) {
// No descriptors, continue to next characteristic
continue;
}
// Reserve space and process descriptors
characteristic_resp.descriptors.reserve(total_desc_count);
// Now process descriptors
uint16_t desc_offset = 0;
esp_gattc_descr_elem_t desc_result;
while (true) { // descriptors
@@ -168,10 +166,10 @@ void BluetoothConnection::send_service_for_discovery_() {
if (desc_status != ESP_GATT_OK) {
ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_all_descr error, status=%d", this->connection_index_,
this->address_str().c_str(), desc_status);
return;
break;
}
if (desc_count == 0) {
break; // No more descriptors
break;
}
characteristic_resp.descriptors.emplace_back();

View File

@@ -571,8 +571,6 @@ CONF_SDKCONFIG_OPTIONS = "sdkconfig_options"
CONF_ENABLE_LWIP_DHCP_SERVER = "enable_lwip_dhcp_server"
CONF_ENABLE_LWIP_MDNS_QUERIES = "enable_lwip_mdns_queries"
CONF_ENABLE_LWIP_BRIDGE_INTERFACE = "enable_lwip_bridge_interface"
CONF_ENABLE_LWIP_TCPIP_CORE_LOCKING = "enable_lwip_tcpip_core_locking"
CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY = "enable_lwip_check_thread_safety"
def _validate_idf_component(config: ConfigType) -> ConfigType:
@@ -621,12 +619,6 @@ ESP_IDF_FRAMEWORK_SCHEMA = cv.All(
cv.Optional(
CONF_ENABLE_LWIP_BRIDGE_INTERFACE, default=False
): cv.boolean,
cv.Optional(
CONF_ENABLE_LWIP_TCPIP_CORE_LOCKING, default=True
): cv.boolean,
cv.Optional(
CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY, default=True
): cv.boolean,
}
),
cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list(
@@ -793,18 +785,6 @@ async def to_code(config):
if not advanced.get(CONF_ENABLE_LWIP_BRIDGE_INTERFACE, False):
add_idf_sdkconfig_option("CONFIG_LWIP_BRIDGEIF_MAX_PORTS", 0)
# Apply LWIP core locking for better socket performance
# This is already enabled by default in Arduino framework, where it provides
# significant performance benefits. Our benchmarks show socket operations are
# 24-200% faster with core locking enabled:
# - select() on 4 sockets: ~190μs (Arduino/core locking) vs ~235μs (ESP-IDF default)
# - Up to 200% slower under load when all operations queue through tcpip_thread
# Enabling this makes ESP-IDF socket performance match Arduino framework.
if advanced.get(CONF_ENABLE_LWIP_TCPIP_CORE_LOCKING, True):
add_idf_sdkconfig_option("CONFIG_LWIP_TCPIP_CORE_LOCKING", True)
if advanced.get(CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY, True):
add_idf_sdkconfig_option("CONFIG_LWIP_CHECK_THREAD_SAFETY", True)
cg.add_platformio_option("board_build.partitions", "partitions.csv")
if CONF_PARTITIONS in config:
add_extra_build_file(

View File

@@ -23,7 +23,6 @@ CONFIG_SCHEMA = (
async def to_code(config):
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
var = await number.new_number(
config,
min_value=0,

View File

@@ -93,12 +93,14 @@ void HomeassistantNumber::control(float value) {
resp.data.emplace_back();
auto &entity_id = resp.data.back();
entity_id.set_key(ENTITY_ID_KEY);
entity_id.value = this->entity_id_;
entity_id.set_value(StringRef(this->entity_id_));
resp.data.emplace_back();
auto &entity_value = resp.data.back();
entity_value.set_key(VALUE_KEY);
entity_value.value = to_string(value);
// to_string() returns a temporary - must store it to avoid dangling reference
std::string value_str = to_string(value);
entity_value.set_value(StringRef(value_str));
api::global_api_server->send_homeassistant_service_call(resp);
}

View File

@@ -37,7 +37,6 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config):
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await switch.register_switch(var, config)

View File

@@ -54,7 +54,7 @@ void HomeassistantSwitch::write_state(bool state) {
resp.data.emplace_back();
auto &entity_id_kv = resp.data.back();
entity_id_kv.set_key(ENTITY_ID_KEY);
entity_id_kv.value = this->entity_id_;
entity_id_kv.set_value(StringRef(this->entity_id_));
api::global_api_server->send_homeassistant_service_call(resp);
}

View File

@@ -159,7 +159,7 @@ void IDFI2CBus::dump_config() {
if (s.second) {
ESP_LOGCONFIG(TAG, "Found device at address 0x%02X", s.first);
} else {
ESP_LOGCONFIG(TAG, "Unknown error at address 0x%02X", s.first);
ESP_LOGE(TAG, "Unknown error at address 0x%02X", s.first);
}
}
}

View File

@@ -9,28 +9,6 @@ namespace light {
static const char *const TAG = "light";
// Helper functions to reduce code size for logging
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_WARN
static void log_validation_warning(const char *name, const char *param_name, float val, float min, float max) {
ESP_LOGW(TAG, "'%s': %s value %.2f is out of range [%.1f - %.1f]", name, param_name, val, min, max);
}
static void log_feature_not_supported(const char *name, const char *feature) {
ESP_LOGW(TAG, "'%s': %s not supported", name, feature);
}
static void log_color_mode_not_supported(const char *name, const char *feature) {
ESP_LOGW(TAG, "'%s': color mode does not support setting %s", name, feature);
}
static void log_invalid_parameter(const char *name, const char *message) { ESP_LOGW(TAG, "'%s': %s", name, message); }
#else
#define log_validation_warning(name, param_name, val, min, max)
#define log_feature_not_supported(name, feature)
#define log_color_mode_not_supported(name, feature)
#define log_invalid_parameter(name, message)
#endif
// Macro to reduce repetitive setter code
#define IMPLEMENT_LIGHT_CALL_SETTER(name, type, flag) \
LightCall &LightCall::set_##name(optional<type>(name)) { \
@@ -66,21 +44,11 @@ static const LogString *color_mode_to_human(ColorMode color_mode) {
return LOG_STR("");
}
// Helper to log percentage values
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG
static void log_percent(const char *name, const char *param, float value) {
ESP_LOGD(TAG, " %s: %.0f%%", param, value * 100.0f);
}
#else
#define log_percent(name, param, value)
#endif
void LightCall::perform() {
const char *name = this->parent_->get_name().c_str();
LightColorValues v = this->validate_();
const bool publish = this->get_publish_();
if (publish) {
if (this->get_publish_()) {
ESP_LOGD(TAG, "'%s' Setting:", name);
// Only print color mode when it's being changed
@@ -98,11 +66,11 @@ void LightCall::perform() {
}
if (this->has_brightness()) {
log_percent(name, "Brightness", v.get_brightness());
ESP_LOGD(TAG, " Brightness: %.0f%%", v.get_brightness() * 100.0f);
}
if (this->has_color_brightness()) {
log_percent(name, "Color brightness", v.get_color_brightness());
ESP_LOGD(TAG, " Color brightness: %.0f%%", v.get_color_brightness() * 100.0f);
}
if (this->has_red() || this->has_green() || this->has_blue()) {
ESP_LOGD(TAG, " Red: %.0f%%, Green: %.0f%%, Blue: %.0f%%", v.get_red() * 100.0f, v.get_green() * 100.0f,
@@ -110,7 +78,7 @@ void LightCall::perform() {
}
if (this->has_white()) {
log_percent(name, "White", v.get_white());
ESP_LOGD(TAG, " White: %.0f%%", v.get_white() * 100.0f);
}
if (this->has_color_temperature()) {
ESP_LOGD(TAG, " Color temperature: %.1f mireds", v.get_color_temperature());
@@ -124,26 +92,26 @@ void LightCall::perform() {
if (this->has_flash_()) {
// FLASH
if (publish) {
if (this->get_publish_()) {
ESP_LOGD(TAG, " Flash length: %.1fs", this->flash_length_ / 1e3f);
}
this->parent_->start_flash_(v, this->flash_length_, publish);
this->parent_->start_flash_(v, this->flash_length_, this->get_publish_());
} else if (this->has_transition_()) {
// TRANSITION
if (publish) {
if (this->get_publish_()) {
ESP_LOGD(TAG, " Transition length: %.1fs", this->transition_length_ / 1e3f);
}
// Special case: Transition and effect can be set when turning off
if (this->has_effect_()) {
if (publish) {
if (this->get_publish_()) {
ESP_LOGD(TAG, " Effect: 'None'");
}
this->parent_->stop_effect_();
}
this->parent_->start_transition_(v, this->transition_length_, publish);
this->parent_->start_transition_(v, this->transition_length_, this->get_publish_());
} else if (this->has_effect_()) {
// EFFECT
@@ -154,7 +122,7 @@ void LightCall::perform() {
effect_s = this->parent_->effects_[this->effect_ - 1]->get_name().c_str();
}
if (publish) {
if (this->get_publish_()) {
ESP_LOGD(TAG, " Effect: '%s'", effect_s);
}
@@ -165,13 +133,13 @@ void LightCall::perform() {
this->parent_->set_immediately_(v, true);
} else {
// INSTANT CHANGE
this->parent_->set_immediately_(v, publish);
this->parent_->set_immediately_(v, this->get_publish_());
}
if (!this->has_transition_()) {
this->parent_->target_state_reached_callback_.call();
}
if (publish) {
if (this->get_publish_()) {
this->parent_->publish_state();
}
if (this->get_save_()) {
@@ -201,19 +169,19 @@ LightColorValues LightCall::validate_() {
// Brightness exists check
if (this->has_brightness() && this->brightness_ > 0.0f && !(color_mode & ColorCapability::BRIGHTNESS)) {
log_feature_not_supported(name, "brightness");
ESP_LOGW(TAG, "'%s': setting brightness not supported", name);
this->set_flag_(FLAG_HAS_BRIGHTNESS, false);
}
// Transition length possible check
if (this->has_transition_() && this->transition_length_ != 0 && !(color_mode & ColorCapability::BRIGHTNESS)) {
log_feature_not_supported(name, "transitions");
ESP_LOGW(TAG, "'%s': transitions not supported", name);
this->set_flag_(FLAG_HAS_TRANSITION, false);
}
// Color brightness exists check
if (this->has_color_brightness() && this->color_brightness_ > 0.0f && !(color_mode & ColorCapability::RGB)) {
log_color_mode_not_supported(name, "RGB brightness");
ESP_LOGW(TAG, "'%s': color mode does not support setting RGB brightness", name);
this->set_flag_(FLAG_HAS_COLOR_BRIGHTNESS, false);
}
@@ -221,7 +189,7 @@ LightColorValues LightCall::validate_() {
if ((this->has_red() && this->red_ > 0.0f) || (this->has_green() && this->green_ > 0.0f) ||
(this->has_blue() && this->blue_ > 0.0f)) {
if (!(color_mode & ColorCapability::RGB)) {
log_color_mode_not_supported(name, "RGB color");
ESP_LOGW(TAG, "'%s': color mode does not support setting RGB color", name);
this->set_flag_(FLAG_HAS_RED, false);
this->set_flag_(FLAG_HAS_GREEN, false);
this->set_flag_(FLAG_HAS_BLUE, false);
@@ -231,21 +199,21 @@ LightColorValues LightCall::validate_() {
// White value exists check
if (this->has_white() && this->white_ > 0.0f &&
!(color_mode & ColorCapability::WHITE || color_mode & ColorCapability::COLD_WARM_WHITE)) {
log_color_mode_not_supported(name, "white value");
ESP_LOGW(TAG, "'%s': color mode does not support setting white value", name);
this->set_flag_(FLAG_HAS_WHITE, false);
}
// Color temperature exists check
if (this->has_color_temperature() &&
!(color_mode & ColorCapability::COLOR_TEMPERATURE || color_mode & ColorCapability::COLD_WARM_WHITE)) {
log_color_mode_not_supported(name, "color temperature");
ESP_LOGW(TAG, "'%s': color mode does not support setting color temperature", name);
this->set_flag_(FLAG_HAS_COLOR_TEMPERATURE, false);
}
// Cold/warm white value exists check
if ((this->has_cold_white() && this->cold_white_ > 0.0f) || (this->has_warm_white() && this->warm_white_ > 0.0f)) {
if (!(color_mode & ColorCapability::COLD_WARM_WHITE)) {
log_color_mode_not_supported(name, "cold/warm white value");
ESP_LOGW(TAG, "'%s': color mode does not support setting cold/warm white value", name);
this->set_flag_(FLAG_HAS_COLD_WHITE, false);
this->set_flag_(FLAG_HAS_WARM_WHITE, false);
}
@@ -255,7 +223,8 @@ LightColorValues LightCall::validate_() {
if (this->has_##name_()) { \
auto val = this->name_##_; \
if (val < (min) || val > (max)) { \
log_validation_warning(name, LOG_STR_LITERAL(upper_name), val, (min), (max)); \
ESP_LOGW(TAG, "'%s': %s value %.2f is out of range [%.1f - %.1f]", name, LOG_STR_LITERAL(upper_name), val, \
(min), (max)); \
this->name_##_ = clamp(val, (min), (max)); \
} \
}
@@ -319,7 +288,7 @@ LightColorValues LightCall::validate_() {
// Flash length check
if (this->has_flash_() && this->flash_length_ == 0) {
log_invalid_parameter(name, "flash length must be greater than zero");
ESP_LOGW(TAG, "'%s': flash length must be greater than zero", name);
this->set_flag_(FLAG_HAS_FLASH, false);
}
@@ -338,13 +307,13 @@ LightColorValues LightCall::validate_() {
}
if (this->has_effect_() && (this->has_transition_() || this->has_flash_())) {
log_invalid_parameter(name, "effect cannot be used with transition/flash");
ESP_LOGW(TAG, "'%s': effect cannot be used with transition/flash", name);
this->set_flag_(FLAG_HAS_TRANSITION, false);
this->set_flag_(FLAG_HAS_FLASH, false);
}
if (this->has_flash_() && this->has_transition_()) {
log_invalid_parameter(name, "flash cannot be used with transition");
ESP_LOGW(TAG, "'%s': flash cannot be used with transition", name);
this->set_flag_(FLAG_HAS_TRANSITION, false);
}
@@ -361,7 +330,7 @@ LightColorValues LightCall::validate_() {
}
if (this->has_transition_() && !supports_transition) {
log_feature_not_supported(name, "transitions");
ESP_LOGW(TAG, "'%s': transitions not supported", name);
this->set_flag_(FLAG_HAS_TRANSITION, false);
}
@@ -371,7 +340,7 @@ LightColorValues LightCall::validate_() {
bool target_state = this->has_state() ? this->state_ : v.is_on();
if (!this->has_flash_() && !target_state) {
if (this->has_effect_()) {
log_invalid_parameter(name, "cannot start effect when turning off");
ESP_LOGW(TAG, "'%s': cannot start effect when turning off", name);
this->set_flag_(FLAG_HAS_EFFECT, false);
} else if (this->parent_->active_effect_index_ != 0 && explicit_turn_off_request) {
// Auto turn off effect
@@ -395,27 +364,21 @@ void LightCall::transform_parameters_() {
// - RGBWW lights with color_interlock=true, which also sets "brightness" and
// "color_temperature" (without color_interlock, CW/WW are set directly)
// - Legacy Home Assistant (pre-colormode), which sets "white" and "color_temperature"
// Cache min/max mireds to avoid repeated calls
const float min_mireds = traits.get_min_mireds();
const float max_mireds = traits.get_max_mireds();
if (((this->has_white() && this->white_ > 0.0f) || this->has_color_temperature()) && //
(this->color_mode_ & ColorCapability::COLD_WARM_WHITE) && //
!(this->color_mode_ & ColorCapability::WHITE) && //
!(this->color_mode_ & ColorCapability::COLOR_TEMPERATURE) && //
min_mireds > 0.0f && max_mireds > 0.0f) {
traits.get_min_mireds() > 0.0f && traits.get_max_mireds() > 0.0f) {
ESP_LOGD(TAG, "'%s': setting cold/warm white channels using white/color temperature values",
this->parent_->get_name().c_str());
if (this->has_color_temperature()) {
const float color_temp = clamp(this->color_temperature_, min_mireds, max_mireds);
const float range = max_mireds - min_mireds;
const float ww_fraction = (color_temp - min_mireds) / range;
const float color_temp = clamp(this->color_temperature_, traits.get_min_mireds(), traits.get_max_mireds());
const float ww_fraction =
(color_temp - traits.get_min_mireds()) / (traits.get_max_mireds() - traits.get_min_mireds());
const float cw_fraction = 1.0f - ww_fraction;
const float max_cw_ww = std::max(ww_fraction, cw_fraction);
const float gamma = this->parent_->get_gamma_correct();
this->cold_white_ = gamma_uncorrect(cw_fraction / max_cw_ww, gamma);
this->warm_white_ = gamma_uncorrect(ww_fraction / max_cw_ww, gamma);
this->cold_white_ = gamma_uncorrect(cw_fraction / max_cw_ww, this->parent_->get_gamma_correct());
this->warm_white_ = gamma_uncorrect(ww_fraction / max_cw_ww, this->parent_->get_gamma_correct());
this->set_flag_(FLAG_HAS_COLD_WHITE, true);
this->set_flag_(FLAG_HAS_WARM_WHITE, true);
}
@@ -479,39 +442,41 @@ std::set<ColorMode> LightCall::get_suitable_color_modes_() {
bool has_rgb = (this->has_color_brightness() && this->color_brightness_ > 0.0f) ||
(this->has_red() || this->has_green() || this->has_blue());
// Build key from flags: [rgb][cwww][ct][white]
#define KEY(white, ct, cwww, rgb) ((white) << 0 | (ct) << 1 | (cwww) << 2 | (rgb) << 3)
#define ENTRY(white, ct, cwww, rgb, ...) \
std::make_tuple<uint8_t, std::set<ColorMode>>(KEY(white, ct, cwww, rgb), __VA_ARGS__)
uint8_t key = KEY(has_white, has_ct, has_cwww, has_rgb);
// Flag order: white, color temperature, cwww, rgb
std::array<std::tuple<uint8_t, std::set<ColorMode>>, 10> lookup_table{
ENTRY(true, false, false, false,
{ColorMode::WHITE, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE,
ColorMode::RGB_COLD_WARM_WHITE}),
ENTRY(false, true, false, false,
{ColorMode::COLOR_TEMPERATURE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE,
ColorMode::RGB_COLD_WARM_WHITE}),
ENTRY(true, true, false, false,
{ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}),
ENTRY(false, false, true, false, {ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLD_WARM_WHITE}),
ENTRY(false, false, false, false,
{ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE, ColorMode::RGB,
ColorMode::WHITE, ColorMode::COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE}),
ENTRY(true, false, false, true,
{ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}),
ENTRY(false, true, false, true, {ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}),
ENTRY(true, true, false, true, {ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}),
ENTRY(false, false, true, true, {ColorMode::RGB_COLD_WARM_WHITE}),
ENTRY(false, false, false, true,
{ColorMode::RGB, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}),
};
switch (key) {
case KEY(true, false, false, false): // white only
return {ColorMode::WHITE, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE,
ColorMode::RGB_COLD_WARM_WHITE};
case KEY(false, true, false, false): // ct only
return {ColorMode::COLOR_TEMPERATURE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE,
ColorMode::RGB_COLD_WARM_WHITE};
case KEY(true, true, false, false): // white + ct
return {ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE};
case KEY(false, false, true, false): // cwww only
return {ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLD_WARM_WHITE};
case KEY(false, false, false, false): // none
return {ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE, ColorMode::RGB,
ColorMode::WHITE, ColorMode::COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE};
case KEY(true, false, false, true): // rgb + white
return {ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE};
case KEY(false, true, false, true): // rgb + ct
case KEY(true, true, false, true): // rgb + white + ct
return {ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE};
case KEY(false, false, true, true): // rgb + cwww
return {ColorMode::RGB_COLD_WARM_WHITE};
case KEY(false, false, false, true): // rgb only
return {ColorMode::RGB, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE};
default:
return {}; // conflicting flags
auto key = KEY(has_white, has_ct, has_cwww, has_rgb);
for (auto &item : lookup_table) {
if (std::get<0>(item) == key)
return std::get<1>(item);
}
#undef KEY
// This happens if there are conflicting flags given.
return {};
}
LightCall &LightCall::set_effect(const std::string &effect) {

View File

@@ -84,23 +84,18 @@ class LightColorValues {
* @return The linearly interpolated LightColorValues.
*/
static LightColorValues lerp(const LightColorValues &start, const LightColorValues &end, float completion) {
// Directly interpolate the raw values to avoid getter/setter overhead.
// This is safe because:
// - All LightColorValues have their values clamped when set via the setters
// - std::lerp guarantees output is in the same range as inputs
// - Therefore the output doesn't need clamping, so we can skip the setters
LightColorValues v;
v.color_mode_ = end.color_mode_;
v.state_ = std::lerp(start.state_, end.state_, completion);
v.brightness_ = std::lerp(start.brightness_, end.brightness_, completion);
v.color_brightness_ = std::lerp(start.color_brightness_, end.color_brightness_, completion);
v.red_ = std::lerp(start.red_, end.red_, completion);
v.green_ = std::lerp(start.green_, end.green_, completion);
v.blue_ = std::lerp(start.blue_, end.blue_, completion);
v.white_ = std::lerp(start.white_, end.white_, completion);
v.color_temperature_ = std::lerp(start.color_temperature_, end.color_temperature_, completion);
v.cold_white_ = std::lerp(start.cold_white_, end.cold_white_, completion);
v.warm_white_ = std::lerp(start.warm_white_, end.warm_white_, completion);
v.set_color_mode(end.color_mode_);
v.set_state(std::lerp(start.get_state(), end.get_state(), completion));
v.set_brightness(std::lerp(start.get_brightness(), end.get_brightness(), completion));
v.set_color_brightness(std::lerp(start.get_color_brightness(), end.get_color_brightness(), completion));
v.set_red(std::lerp(start.get_red(), end.get_red(), completion));
v.set_green(std::lerp(start.get_green(), end.get_green(), completion));
v.set_blue(std::lerp(start.get_blue(), end.get_blue(), completion));
v.set_white(std::lerp(start.get_white(), end.get_white(), completion));
v.set_color_temperature(std::lerp(start.get_color_temperature(), end.get_color_temperature(), completion));
v.set_cold_white(std::lerp(start.get_cold_white(), end.get_cold_white(), completion));
v.set_warm_white(std::lerp(start.get_warm_white(), end.get_warm_white(), completion));
return v;
}

View File

@@ -8,32 +8,6 @@ namespace light {
// See https://www.home-assistant.io/integrations/light.mqtt/#json-schema for documentation on the schema
// Lookup table for color mode strings
static constexpr const char *get_color_mode_json_str(ColorMode mode) {
switch (mode) {
case ColorMode::ON_OFF:
return "onoff";
case ColorMode::BRIGHTNESS:
return "brightness";
case ColorMode::WHITE:
return "white"; // not supported by HA in MQTT
case ColorMode::COLOR_TEMPERATURE:
return "color_temp";
case ColorMode::COLD_WARM_WHITE:
return "cwww"; // not supported by HA
case ColorMode::RGB:
return "rgb";
case ColorMode::RGB_WHITE:
return "rgbw";
case ColorMode::RGB_COLOR_TEMPERATURE:
return "rgbct"; // not supported by HA
case ColorMode::RGB_COLD_WARM_WHITE:
return "rgbww";
default:
return nullptr;
}
}
void LightJSONSchema::dump_json(LightState &state, JsonObject root) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
if (state.supports_effects())
@@ -42,36 +16,60 @@ void LightJSONSchema::dump_json(LightState &state, JsonObject root) {
auto values = state.remote_values;
auto traits = state.get_output()->get_traits();
const auto color_mode = values.get_color_mode();
const char *mode_str = get_color_mode_json_str(color_mode);
if (mode_str != nullptr) {
root["color_mode"] = mode_str;
switch (values.get_color_mode()) {
case ColorMode::UNKNOWN: // don't need to set color mode if we don't know it
break;
case ColorMode::ON_OFF:
root["color_mode"] = "onoff";
break;
case ColorMode::BRIGHTNESS:
root["color_mode"] = "brightness";
break;
case ColorMode::WHITE: // not supported by HA in MQTT
root["color_mode"] = "white";
break;
case ColorMode::COLOR_TEMPERATURE:
root["color_mode"] = "color_temp";
break;
case ColorMode::COLD_WARM_WHITE: // not supported by HA
root["color_mode"] = "cwww";
break;
case ColorMode::RGB:
root["color_mode"] = "rgb";
break;
case ColorMode::RGB_WHITE:
root["color_mode"] = "rgbw";
break;
case ColorMode::RGB_COLOR_TEMPERATURE: // not supported by HA
root["color_mode"] = "rgbct";
break;
case ColorMode::RGB_COLD_WARM_WHITE:
root["color_mode"] = "rgbww";
break;
}
if (color_mode & ColorCapability::ON_OFF)
if (values.get_color_mode() & ColorCapability::ON_OFF)
root["state"] = (values.get_state() != 0.0f) ? "ON" : "OFF";
if (color_mode & ColorCapability::BRIGHTNESS)
root["brightness"] = to_uint8_scale(values.get_brightness());
if (values.get_color_mode() & ColorCapability::BRIGHTNESS)
root["brightness"] = uint8_t(values.get_brightness() * 255);
JsonObject color = root["color"].to<JsonObject>();
if (color_mode & ColorCapability::RGB) {
float color_brightness = values.get_color_brightness();
color["r"] = to_uint8_scale(color_brightness * values.get_red());
color["g"] = to_uint8_scale(color_brightness * values.get_green());
color["b"] = to_uint8_scale(color_brightness * values.get_blue());
if (values.get_color_mode() & ColorCapability::RGB) {
color["r"] = uint8_t(values.get_color_brightness() * values.get_red() * 255);
color["g"] = uint8_t(values.get_color_brightness() * values.get_green() * 255);
color["b"] = uint8_t(values.get_color_brightness() * values.get_blue() * 255);
}
if (color_mode & ColorCapability::WHITE) {
uint8_t white_val = to_uint8_scale(values.get_white());
color["w"] = white_val;
root["white_value"] = white_val; // legacy API
if (values.get_color_mode() & ColorCapability::WHITE) {
color["w"] = uint8_t(values.get_white() * 255);
root["white_value"] = uint8_t(values.get_white() * 255); // legacy API
}
if (color_mode & ColorCapability::COLOR_TEMPERATURE) {
if (values.get_color_mode() & ColorCapability::COLOR_TEMPERATURE) {
// this one isn't under the color subkey for some reason
root["color_temp"] = uint32_t(values.get_color_temperature());
}
if (color_mode & ColorCapability::COLD_WARM_WHITE) {
color["c"] = to_uint8_scale(values.get_cold_white());
color["w"] = to_uint8_scale(values.get_warm_white());
if (values.get_color_mode() & ColorCapability::COLD_WARM_WHITE) {
color["c"] = uint8_t(values.get_cold_white() * 255);
color["w"] = uint8_t(values.get_warm_white() * 255);
}
}

View File

@@ -24,8 +24,7 @@ void LightState::setup() {
}
// When supported color temperature range is known, initialize color temperature setting within bounds.
auto traits = this->get_traits();
float min_mireds = traits.get_min_mireds();
float min_mireds = this->get_traits().get_min_mireds();
if (min_mireds > 0) {
this->remote_values.set_color_temperature(min_mireds);
this->current_values.set_color_temperature(min_mireds);
@@ -44,8 +43,11 @@ void LightState::setup() {
this->rtc_ = global_preferences->make_preference<LightStateRTCState>(this->get_object_id_hash());
// Attempt to load from preferences, else fall back to default values
if (!this->rtc_.load(&recovered)) {
recovered.state = (this->restore_mode_ == LIGHT_RESTORE_DEFAULT_ON ||
this->restore_mode_ == LIGHT_RESTORE_INVERTED_DEFAULT_ON);
recovered.state = false;
if (this->restore_mode_ == LIGHT_RESTORE_DEFAULT_ON ||
this->restore_mode_ == LIGHT_RESTORE_INVERTED_DEFAULT_ON) {
recovered.state = true;
}
} else if (this->restore_mode_ == LIGHT_RESTORE_INVERTED_DEFAULT_OFF ||
this->restore_mode_ == LIGHT_RESTORE_INVERTED_DEFAULT_ON) {
// Inverted restore state
@@ -86,18 +88,17 @@ void LightState::setup() {
}
void LightState::dump_config() {
ESP_LOGCONFIG(TAG, "Light '%s'", this->get_name().c_str());
auto traits = this->get_traits();
if (traits.supports_color_capability(ColorCapability::BRIGHTNESS)) {
if (this->get_traits().supports_color_capability(ColorCapability::BRIGHTNESS)) {
ESP_LOGCONFIG(TAG,
" Default Transition Length: %.1fs\n"
" Gamma Correct: %.2f",
this->default_transition_length_ / 1e3f, this->gamma_correct_);
}
if (traits.supports_color_capability(ColorCapability::COLOR_TEMPERATURE)) {
if (this->get_traits().supports_color_capability(ColorCapability::COLOR_TEMPERATURE)) {
ESP_LOGCONFIG(TAG,
" Min Mireds: %.1f\n"
" Max Mireds: %.1f",
traits.get_min_mireds(), traits.get_max_mireds());
this->get_traits().get_min_mireds(), this->get_traits().get_max_mireds());
}
}
void LightState::loop() {

View File

@@ -65,7 +65,7 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch
uint16_t buffer_at = 0; // Initialize buffer position
this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, console_buffer, &buffer_at,
MAX_CONSOLE_LOG_MSG_SIZE);
this->write_msg_(console_buffer);
this->write_msg_(console_buffer, buffer_at);
}
// Reset the recursion guard for this task
@@ -136,11 +136,11 @@ void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __Flas
&this->tx_buffer_at_, this->tx_buffer_size_);
// Write to console and send callback starting at the msg_start
if (this->baud_rate_ > 0) {
this->write_msg_(this->tx_buffer_ + msg_start);
}
size_t msg_length =
this->tx_buffer_at_ - msg_start; // Don't subtract 1 - tx_buffer_at_ is already at the null terminator position
if (this->baud_rate_ > 0) {
this->write_msg_(this->tx_buffer_ + msg_start, msg_length);
}
this->log_callback_.call(level, tag, this->tx_buffer_ + msg_start, msg_length);
global_recursion_guard_ = false;
@@ -224,7 +224,7 @@ void Logger::process_messages_() {
// Note: Messages may appear slightly out of order due to async processing, but
// this is preferred over corrupted/interleaved console output
if (this->baud_rate_ > 0) {
this->write_msg_(this->tx_buffer_);
this->write_msg_(this->tx_buffer_, msg_len);
}
}
} else {

View File

@@ -161,7 +161,7 @@ class Logger : public Component {
protected:
void process_messages_();
void write_msg_(const char *msg);
void write_msg_(const char *msg, size_t len);
// Format a log message with printf-style arguments and write it to a buffer with header, footer, and null terminator
// It's the caller's responsibility to initialize buffer_at (typically to 0)
@@ -194,7 +194,7 @@ class Logger : public Component {
this->tx_buffer_size_);
if (this->baud_rate_ > 0) {
this->write_msg_(this->tx_buffer_); // If logging is enabled, write to console
this->write_msg_(this->tx_buffer_, this->tx_buffer_at_); // If logging is enabled, write to console
}
this->log_callback_.call(level, tag, this->tx_buffer_, this->tx_buffer_at_);
}

View File

@@ -166,7 +166,7 @@ void Logger::pre_setup() {
}
#ifdef USE_ESP_IDF
void HOT Logger::write_msg_(const char *msg) {
void HOT Logger::write_msg_(const char *msg, size_t len) {
if (
#if defined(USE_LOGGER_USB_CDC) && !defined(USE_LOGGER_USB_SERIAL_JTAG)
this->uart_ == UART_SELECTION_USB_CDC
@@ -178,16 +178,26 @@ void HOT Logger::write_msg_(const char *msg) {
/* DISABLES CODE */ (false) // NOLINT
#endif
) {
// For USB CDC/Serial JTAG, we use puts() which adds '\n' automatically.
// This is safe because the buffer is always null-terminated by format_log_to_buffer_with_terminator_.
// The VFS layer handles newline conversion (adding '\r' if configured) in its write function.
// While puts() likely calculates strlen internally, the VFS write function already processes
// character-by-character for newline conversion, so using puts() is efficient here.
puts(msg);
} else {
// Use tx_buffer_at_ if msg points to tx_buffer_, otherwise fall back to strlen
size_t len = (msg == this->tx_buffer_) ? this->tx_buffer_at_ : strlen(msg);
uart_write_bytes(this->uart_num_, msg, len);
// ESP-IDF uses only '\n' for historical reasons, while Arduino platforms use '\r\n'
uart_write_bytes(this->uart_num_, "\n", 1);
}
}
#else
void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); }
void HOT Logger::write_msg_(const char *msg, size_t len) {
// Arduino's println() writes the message followed by "\r\n" (CRLF).
// Previously, println() would call write(msg) which uses strlen() internally.
// By using write(buffer, size) directly, we avoid the strlen() call.
this->hw_serial_->write(reinterpret_cast<const uint8_t *>(msg), len);
this->hw_serial_->write(reinterpret_cast<const uint8_t *>("\r\n"), 2);
}
#endif
const char *const UART_SELECTIONS[] = {

View File

@@ -33,7 +33,13 @@ void Logger::pre_setup() {
ESP_LOGI(TAG, "Log initialized");
}
void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); }
void HOT Logger::write_msg_(const char *msg, size_t len) {
// Arduino's println() writes the message followed by "\r\n" (CRLF).
// Previously, println() would call write(msg) which uses strlen() internally.
// By using write(buffer, size) directly, we avoid the strlen() call.
this->hw_serial_->write(reinterpret_cast<const uint8_t *>(msg), len);
this->hw_serial_->write(reinterpret_cast<const uint8_t *>("\r\n"), 2);
}
const char *const UART_SELECTIONS[] = {"UART0", "UART1", "UART0_SWAP"};

View File

@@ -3,7 +3,7 @@
namespace esphome::logger {
void HOT Logger::write_msg_(const char *msg) {
void HOT Logger::write_msg_(const char *msg, size_t len) {
time_t rawtime;
struct tm *timeinfo;
char buffer[80];

View File

@@ -49,7 +49,13 @@ void Logger::pre_setup() {
ESP_LOGI(TAG, "Log initialized");
}
void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); }
void HOT Logger::write_msg_(const char *msg, size_t len) {
// Arduino's println() writes the message followed by "\r\n" (CRLF).
// Previously, println() would call write(msg) which uses strlen() internally.
// By using write(buffer, size) directly, we avoid the strlen() call.
this->hw_serial_->write(reinterpret_cast<const uint8_t *>(msg), len);
this->hw_serial_->write(reinterpret_cast<const uint8_t *>("\r\n"), 2);
}
const char *const UART_SELECTIONS[] = {"DEFAULT", "UART0", "UART1", "UART2"};

View File

@@ -27,7 +27,13 @@ void Logger::pre_setup() {
ESP_LOGI(TAG, "Log initialized");
}
void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); }
void HOT Logger::write_msg_(const char *msg, size_t len) {
// Arduino's println() writes the message followed by "\r\n" (CRLF).
// Previously, println() would call write(msg) which uses strlen() internally.
// By using write(buffer, size) directly, we avoid the strlen() call.
this->hw_serial_->write(reinterpret_cast<const uint8_t *>(msg), len);
this->hw_serial_->write(reinterpret_cast<const uint8_t *>("\r\n"), 2);
}
const char *const UART_SELECTIONS[] = {"UART0", "UART1", "USB_CDC"};

View File

@@ -63,16 +63,15 @@ void Logger::pre_setup() {
ESP_LOGI(TAG, "Log initialized");
}
void HOT Logger::write_msg_(const char *msg) {
void HOT Logger::write_msg_(const char *msg, size_t len) {
#ifdef CONFIG_PRINTK
printk("%s\n", msg);
#endif
if (nullptr == this->uart_dev_) {
return;
}
while (*msg) {
uart_poll_out(this->uart_dev_, *msg);
++msg;
for (size_t i = 0; i < len; i++) {
uart_poll_out(this->uart_dev_, msg[i]);
}
uart_poll_out(this->uart_dev_, '\n');
}

View File

@@ -13,13 +13,14 @@ void PowerSupply::setup() {
this->request_high_power();
}
void PowerSupply::dump_config() {
ESP_LOGCONFIG(TAG,
"Power Supply:\n"
" Time to enable: %" PRIu32 " ms\n"
" Keep on time: %" PRIu32 " s\n"
" Enable at startup: %s",
this->enable_time_, this->keep_on_time_ / 1000u, YESNO(this->enable_on_boot_));
ESP_LOGCONFIG(TAG, "Power Supply:");
LOG_PIN(" Pin: ", this->pin_);
ESP_LOGCONFIG(TAG,
" Time to enable: %" PRIu32 " ms\n"
" Keep on time: %.1f s",
this->enable_time_, this->keep_on_time_ / 1000.0f);
if (this->enable_on_boot_)
ESP_LOGCONFIG(TAG, " Enabled at startup: True");
}
float PowerSupply::get_setup_priority() const { return setup_priority::IO; }
@@ -29,7 +30,7 @@ bool PowerSupply::is_enabled() const { return this->active_requests_ != 0; }
void PowerSupply::request_high_power() {
if (this->active_requests_ == 0) {
this->cancel_timeout("power-supply-off");
ESP_LOGV(TAG, "Enabling");
ESP_LOGD(TAG, "Enabling power supply.");
this->pin_->digital_write(true);
delay(this->enable_time_);
}
@@ -44,7 +45,7 @@ void PowerSupply::unrequest_high_power() {
this->active_requests_--;
if (this->active_requests_ == 0) {
this->set_timeout("power-supply-off", this->keep_on_time_, [this]() {
ESP_LOGV(TAG, "Disabling");
ESP_LOGD(TAG, "Disabling power supply.");
this->pin_->digital_write(false);
});
}

View File

@@ -36,10 +36,10 @@ class PowerSupply : public Component {
protected:
GPIOPin *pin_;
bool enable_on_boot_{false};
uint32_t enable_time_;
uint32_t keep_on_time_;
int16_t active_requests_{0}; // use signed integer to make catching negative requests easier.
bool enable_on_boot_{false};
};
class PowerSupplyRequester {

View File

@@ -6,7 +6,6 @@ from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_
from esphome.components.network import IPAddress
from esphome.config_helpers import filter_source_files_from_platform
import esphome.config_validation as cv
from esphome.config_validation import only_with_esp_idf
from esphome.const import (
CONF_AP,
CONF_BSSID,
@@ -337,7 +336,7 @@ CONFIG_SCHEMA = cv.All(
single=True
),
cv.Optional(CONF_USE_PSRAM): cv.All(
only_with_esp_idf, cv.requires_component("psram"), cv.boolean
cv.requires_component("psram"), cv.boolean
),
}
),

View File

@@ -16,7 +16,6 @@
namespace esphome {
static const char *const TAG = "component";
static const char *const UNSPECIFIED_MESSAGE = "unspecified";
// Global vectors for component data that doesn't belong in every instance.
// Using vector instead of unordered_map for both because:
@@ -133,7 +132,7 @@ void Component::call_dump_config() {
this->dump_config();
if (this->is_failed()) {
// Look up error message from global vector
const char *error_msg = nullptr;
const char *error_msg = "unspecified";
if (component_error_messages) {
for (const auto &pair : *component_error_messages) {
if (pair.first == this) {
@@ -142,8 +141,7 @@ void Component::call_dump_config() {
}
}
}
ESP_LOGE(TAG, " %s is marked FAILED: %s", this->get_component_source(),
error_msg ? error_msg : UNSPECIFIED_MESSAGE);
ESP_LOGE(TAG, " %s is marked FAILED: %s", this->get_component_source(), error_msg);
}
}
@@ -286,15 +284,15 @@ void Component::status_set_warning(const char *message) {
return;
this->component_state_ |= STATUS_LED_WARNING;
App.app_state_ |= STATUS_LED_WARNING;
ESP_LOGW(TAG, "%s set Warning flag: %s", this->get_component_source(), message ? message : UNSPECIFIED_MESSAGE);
ESP_LOGW(TAG, "%s set Warning flag: %s", this->get_component_source(), message);
}
void Component::status_set_error(const char *message) {
if ((this->component_state_ & STATUS_LED_ERROR) != 0)
return;
this->component_state_ |= STATUS_LED_ERROR;
App.app_state_ |= STATUS_LED_ERROR;
ESP_LOGE(TAG, "%s set Error flag: %s", this->get_component_source(), message ? message : UNSPECIFIED_MESSAGE);
if (message != nullptr) {
ESP_LOGE(TAG, "%s set Error flag: %s", this->get_component_source(), message);
if (strcmp(message, "unspecified") != 0) {
// Lazy allocate the error messages vector if needed
if (!component_error_messages) {
component_error_messages = std::make_unique<std::vector<std::pair<const Component *, const char *>>>();

View File

@@ -202,9 +202,9 @@ class Component {
bool status_has_error() const;
void status_set_warning(const char *message = nullptr);
void status_set_warning(const char *message = "unspecified");
void status_set_error(const char *message = nullptr);
void status_set_error(const char *message = "unspecified");
void status_clear_warning();

View File

@@ -109,7 +109,6 @@
#define USE_API
#define USE_API_CLIENT_CONNECTED_TRIGGER
#define USE_API_CLIENT_DISCONNECTED_TRIGGER
#define USE_API_HOMEASSISTANT_SERVICES
#define USE_API_HOMEASSISTANT_STATES
#define USE_API_NOISE
#define USE_API_PLAINTEXT

View File

@@ -68,10 +68,7 @@ To bit_cast(const From &src) {
return dst;
}
#endif
// clang-format off
inline float lerp(float completion, float start, float end) = delete; // Please use std::lerp. Notice that it has different order on arguments!
// clang-format on
using std::lerp;
// std::byteswap from C++23
template<typename T> constexpr T byteswap(T n) {

View File

@@ -76,40 +76,29 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
return;
}
// Get fresh timestamp BEFORE taking lock - millis_64_ may need to acquire lock itself
const uint64_t now = this->millis_64_(millis());
// Take lock early to protect scheduler_item_pool_ access
LockGuard guard{this->lock_};
// Create and populate the scheduler item
std::unique_ptr<SchedulerItem> item;
if (!this->scheduler_item_pool_.empty()) {
// Reuse from pool
item = std::move(this->scheduler_item_pool_.back());
this->scheduler_item_pool_.pop_back();
} else {
// Allocate new if pool is empty
item = make_unique<SchedulerItem>();
}
auto item = make_unique<SchedulerItem>();
item->component = component;
item->set_name(name_cstr, !is_static_string);
item->type = type;
item->callback = std::move(func);
item->remove = false;
item->is_retry = is_retry;
#ifndef ESPHOME_THREAD_SINGLE
// Special handling for defer() (delay = 0, type = TIMEOUT)
// Single-core platforms don't need thread-safe defer handling
if (delay == 0 && type == SchedulerItem::TIMEOUT) {
// Put in defer queue for guaranteed FIFO execution
LockGuard guard{this->lock_};
this->cancel_item_locked_(component, name_cstr, type);
this->defer_queue_.push_back(std::move(item));
return;
}
#endif /* not ESPHOME_THREAD_SINGLE */
// Get fresh timestamp for new timer/interval - ensures accurate scheduling
const auto now = this->millis_64_(millis()); // Fresh millis() call
// Type-specific setup
if (type == SchedulerItem::INTERVAL) {
item->interval = delay;
@@ -141,10 +130,12 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
}
#endif /* ESPHOME_DEBUG_SCHEDULER */
LockGuard guard{this->lock_};
// For retries, check if there's a cancelled timeout first
if (is_retry && name_cstr != nullptr && type == SchedulerItem::TIMEOUT &&
(has_cancelled_timeout_in_container_(this->items_, component, name_cstr, /* match_retry= */ true) ||
has_cancelled_timeout_in_container_(this->to_add_, component, name_cstr, /* match_retry= */ true))) {
(has_cancelled_timeout_in_container_(this->items_, component, name_cstr) ||
has_cancelled_timeout_in_container_(this->to_add_, component, name_cstr))) {
// Skip scheduling - the retry was cancelled
#ifdef ESPHOME_DEBUG_SCHEDULER
ESP_LOGD(TAG, "Skipping retry '%s' - found cancelled item", name_cstr);
@@ -207,27 +198,25 @@ void retry_handler(const std::shared_ptr<RetryArgs> &args) {
// second execution of `func` happens after `initial_wait_time`
args->scheduler->set_timer_common_(
args->component, Scheduler::SchedulerItem::TIMEOUT, false, &args->name, args->current_interval,
[args]() { retry_handler(args); }, /* is_retry= */ true);
[args]() { retry_handler(args); }, true);
// backoff_increase_factor applied to third & later executions
args->current_interval *= args->backoff_increase_factor;
}
void HOT Scheduler::set_retry_common_(Component *component, bool is_static_string, const void *name_ptr,
uint32_t initial_wait_time, uint8_t max_attempts,
std::function<RetryResult(uint8_t)> func, float backoff_increase_factor) {
const char *name_cstr = this->get_name_cstr_(is_static_string, name_ptr);
if (name_cstr != nullptr)
this->cancel_retry(component, name_cstr);
void HOT Scheduler::set_retry(Component *component, const std::string &name, uint32_t initial_wait_time,
uint8_t max_attempts, std::function<RetryResult(uint8_t)> func,
float backoff_increase_factor) {
if (!name.empty())
this->cancel_retry(component, name);
if (initial_wait_time == SCHEDULER_DONT_RUN)
return;
ESP_LOGVV(TAG, "set_retry(name='%s', initial_wait_time=%" PRIu32 ", max_attempts=%u, backoff_factor=%0.1f)",
name_cstr ? name_cstr : "", initial_wait_time, max_attempts, backoff_increase_factor);
name.c_str(), initial_wait_time, max_attempts, backoff_increase_factor);
if (backoff_increase_factor < 0.0001) {
ESP_LOGE(TAG, "backoff_factor %0.1f too small, using 1.0: %s", backoff_increase_factor, name_cstr ? name_cstr : "");
ESP_LOGE(TAG, "backoff_factor %0.1f too small, using 1.0: %s", backoff_increase_factor, name.c_str());
backoff_increase_factor = 1;
}
@@ -236,36 +225,15 @@ void HOT Scheduler::set_retry_common_(Component *component, bool is_static_strin
args->retry_countdown = max_attempts;
args->current_interval = initial_wait_time;
args->component = component;
args->name = name_cstr ? name_cstr : ""; // Convert to std::string for RetryArgs
args->name = "retry$" + name;
args->backoff_increase_factor = backoff_increase_factor;
args->scheduler = this;
// First execution of `func` immediately - use set_timer_common_ with is_retry=true
this->set_timer_common_(
component, SchedulerItem::TIMEOUT, false, &args->name, 0, [args]() { retry_handler(args); },
/* is_retry= */ true);
}
void HOT Scheduler::set_retry(Component *component, const std::string &name, uint32_t initial_wait_time,
uint8_t max_attempts, std::function<RetryResult(uint8_t)> func,
float backoff_increase_factor) {
this->set_retry_common_(component, false, &name, initial_wait_time, max_attempts, std::move(func),
backoff_increase_factor);
}
void HOT Scheduler::set_retry(Component *component, const char *name, uint32_t initial_wait_time, uint8_t max_attempts,
std::function<RetryResult(uint8_t)> func, float backoff_increase_factor) {
this->set_retry_common_(component, true, name, initial_wait_time, max_attempts, std::move(func),
backoff_increase_factor);
// First execution of `func` immediately
this->set_timeout(component, args->name, 0, [args]() { retry_handler(args); });
}
bool HOT Scheduler::cancel_retry(Component *component, const std::string &name) {
return this->cancel_retry(component, name.c_str());
}
bool HOT Scheduler::cancel_retry(Component *component, const char *name) {
// Cancel timeouts that have is_retry flag set
LockGuard guard{this->lock_};
return this->cancel_item_locked_(component, name, SchedulerItem::TIMEOUT, /* match_retry= */ true);
return this->cancel_timeout(component, "retry$" + name);
}
optional<uint32_t> HOT Scheduler::next_schedule_in(uint32_t now) {
@@ -330,11 +298,11 @@ void HOT Scheduler::call(uint32_t now) {
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
const auto last_dbg = this->last_millis_.load(std::memory_order_relaxed);
const auto major_dbg = this->millis_major_.load(std::memory_order_relaxed);
ESP_LOGD(TAG, "Items: count=%zu, pool=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(),
this->scheduler_item_pool_.size(), now_64, major_dbg, last_dbg);
ESP_LOGD(TAG, "Items: count=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(), now_64,
major_dbg, last_dbg);
#else /* not ESPHOME_THREAD_MULTI_ATOMICS */
ESP_LOGD(TAG, "Items: count=%zu, pool=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(),
this->scheduler_item_pool_.size(), now_64, this->millis_major_, this->last_millis_);
ESP_LOGD(TAG, "Items: count=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(), now_64,
this->millis_major_, this->last_millis_);
#endif /* else ESPHOME_THREAD_MULTI_ATOMICS */
// Cleanup before debug output
this->cleanup_();
@@ -366,7 +334,6 @@ void HOT Scheduler::call(uint32_t now) {
// If we have too many items to remove
if (this->to_remove_ > MAX_LOGICALLY_DELETED_ITEMS) {
// ESP_LOGD(TAG, "Starting cleanup of %u removed items", this->to_remove_);
// We hold the lock for the entire cleanup operation because:
// 1. We're rebuilding the entire items_ list, so we need exclusive access throughout
// 2. Other threads must see either the old state or the new state, not intermediate states
@@ -376,13 +343,10 @@ void HOT Scheduler::call(uint32_t now) {
std::vector<std::unique_ptr<SchedulerItem>> valid_items;
// Move all non-removed items to valid_items, recycle removed ones
// Move all non-removed items to valid_items
for (auto &item : this->items_) {
if (!item->remove) {
valid_items.push_back(std::move(item));
} else {
// Recycle removed items
this->recycle_item_(std::move(item));
}
}
@@ -390,7 +354,6 @@ void HOT Scheduler::call(uint32_t now) {
this->items_ = std::move(valid_items);
// Rebuild the heap structure since items are no longer in heap order
std::make_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp);
// ESP_LOGD(TAG, "Cleanup complete - pool size now: %zu", this->scheduler_item_pool_.size());
this->to_remove_ = 0;
}
@@ -444,9 +407,6 @@ void HOT Scheduler::call(uint32_t now) {
// Add new item directly to to_add_
// since we have the lock held
this->to_add_.push_back(std::move(item));
} else {
// Timeout completed - recycle it
this->recycle_item_(std::move(item));
}
}
}
@@ -496,10 +456,6 @@ size_t HOT Scheduler::cleanup_() {
}
void HOT Scheduler::pop_raw_() {
std::pop_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp);
// Instead of destroying, recycle the item
this->recycle_item_(std::move(this->items_.back()));
this->items_.pop_back();
}
@@ -523,8 +479,7 @@ bool HOT Scheduler::cancel_item_(Component *component, bool is_static_string, co
}
// Helper to cancel items by name - must be called with lock held
bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_cstr, SchedulerItem::Type type,
bool match_retry) {
bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_cstr, SchedulerItem::Type type) {
// Early return if name is invalid - no items to cancel
if (name_cstr == nullptr) {
return false;
@@ -534,24 +489,34 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c
// Check all containers for matching items
#ifndef ESPHOME_THREAD_SINGLE
// Cancel and immediately recycle items in defer queue
// Only check defer queue for timeouts (intervals never go there)
if (type == SchedulerItem::TIMEOUT) {
total_cancelled +=
this->cancel_and_recycle_from_container_(this->defer_queue_, component, name_cstr, type, match_retry);
for (auto &item : this->defer_queue_) {
if (this->matches_item_(item, component, name_cstr, type)) {
item->remove = true;
total_cancelled++;
}
}
}
#endif /* not ESPHOME_THREAD_SINGLE */
// Cancel items in the main heap (can't recycle immediately due to heap structure)
// Cancel items in the main heap
for (auto &item : this->items_) {
if (this->matches_item_(item, component, name_cstr, type, match_retry)) {
if (this->matches_item_(item, component, name_cstr, type)) {
item->remove = true;
total_cancelled++;
this->to_remove_++; // Track removals for heap items
}
}
// Cancel and immediately recycle items in to_add_ since they're not in heap yet
total_cancelled += this->cancel_and_recycle_from_container_(this->to_add_, component, name_cstr, type, match_retry);
// Cancel items in to_add_
for (auto &item : this->to_add_) {
if (this->matches_item_(item, component, name_cstr, type)) {
item->remove = true;
total_cancelled++;
// Don't track removals for to_add_ items
}
}
return total_cancelled > 0;
}
@@ -719,22 +684,4 @@ bool HOT Scheduler::SchedulerItem::cmp(const std::unique_ptr<SchedulerItem> &a,
return a->next_execution_ > b->next_execution_;
}
void Scheduler::recycle_item_(std::unique_ptr<SchedulerItem> item) {
if (!item)
return;
static constexpr size_t MAX_POOL_SIZE = 16;
if (this->scheduler_item_pool_.size() < MAX_POOL_SIZE) {
// Clear callback to release captured resources
item->callback = nullptr;
// Clear dynamic name if any
item->clear_dynamic_name();
this->scheduler_item_pool_.push_back(std::move(item));
// ESP_LOGD(TAG, "Recycled SchedulerItem to pool (pool size now: %zu)", this->scheduler_item_pool_.size());
} else {
// ESP_LOGD(TAG, "Discarding SchedulerItem (pool full at %zu items)", this->scheduler_item_pool_.size());
// unique_ptr will delete the item when it goes out of scope
}
}
} // namespace esphome

View File

@@ -5,7 +5,6 @@
#include <memory>
#include <cstring>
#include <deque>
#include <array>
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
#include <atomic>
#endif
@@ -62,10 +61,7 @@ class Scheduler {
bool cancel_interval(Component *component, const char *name);
void set_retry(Component *component, const std::string &name, uint32_t initial_wait_time, uint8_t max_attempts,
std::function<RetryResult(uint8_t)> func, float backoff_increase_factor = 1.0f);
void set_retry(Component *component, const char *name, uint32_t initial_wait_time, uint8_t max_attempts,
std::function<RetryResult(uint8_t)> func, float backoff_increase_factor = 1.0f);
bool cancel_retry(Component *component, const std::string &name);
bool cancel_retry(Component *component, const char *name);
// Calculate when the next scheduled item should run
// @param now Fresh timestamp from millis() - must not be stale/cached
@@ -102,23 +98,20 @@ class Scheduler {
enum Type : uint8_t { TIMEOUT, INTERVAL } type : 1;
bool remove : 1;
bool name_is_dynamic : 1; // True if name was dynamically allocated (needs delete[])
bool is_retry : 1; // True if this is a retry timeout
// 4 bits padding
// 5 bits padding
// Constructor
SchedulerItem()
: component(nullptr),
interval(0),
next_execution_(0),
type(TIMEOUT),
remove(false),
name_is_dynamic(false),
is_retry(false) {
: component(nullptr), interval(0), next_execution_(0), type(TIMEOUT), remove(false), name_is_dynamic(false) {
name_.static_name = nullptr;
}
// Destructor to clean up dynamic names
~SchedulerItem() { clear_dynamic_name(); }
~SchedulerItem() {
if (name_is_dynamic) {
delete[] name_.dynamic_name;
}
}
// Delete copy operations to prevent accidental copies
SchedulerItem(const SchedulerItem &) = delete;
@@ -131,19 +124,13 @@ class Scheduler {
// Helper to get the name regardless of storage type
const char *get_name() const { return name_is_dynamic ? name_.dynamic_name : name_.static_name; }
// Helper to clear dynamic name if allocated
void clear_dynamic_name() {
if (name_is_dynamic && name_.dynamic_name) {
delete[] name_.dynamic_name;
name_.dynamic_name = nullptr;
name_is_dynamic = false;
}
}
// Helper to set name with proper ownership
void set_name(const char *name, bool make_copy = false) {
// Clean up old dynamic name if any
clear_dynamic_name();
if (name_is_dynamic && name_.dynamic_name) {
delete[] name_.dynamic_name;
name_is_dynamic = false;
}
if (!name) {
// nullptr case - no name provided
@@ -169,10 +156,6 @@ class Scheduler {
void set_timer_common_(Component *component, SchedulerItem::Type type, bool is_static_string, const void *name_ptr,
uint32_t delay, std::function<void()> func, bool is_retry = false);
// Common implementation for retry
void set_retry_common_(Component *component, bool is_static_string, const void *name_ptr, uint32_t initial_wait_time,
uint8_t max_attempts, std::function<RetryResult(uint8_t)> func, float backoff_increase_factor);
uint64_t millis_64_(uint32_t now);
// Cleanup logically deleted items from the scheduler
// Returns the number of items remaining after cleanup
@@ -182,7 +165,7 @@ class Scheduler {
private:
// Helper to cancel items by name - must be called with lock held
bool cancel_item_locked_(Component *component, const char *name, SchedulerItem::Type type, bool match_retry = false);
bool cancel_item_locked_(Component *component, const char *name, SchedulerItem::Type type);
// Helper to extract name as const char* from either static string or std::string
inline const char *get_name_cstr_(bool is_static_string, const void *name_ptr) {
@@ -194,9 +177,8 @@ class Scheduler {
// Helper function to check if item matches criteria for cancellation
inline bool HOT matches_item_(const std::unique_ptr<SchedulerItem> &item, Component *component, const char *name_cstr,
SchedulerItem::Type type, bool match_retry, bool skip_removed = true) const {
if (item->component != component || item->type != type || (skip_removed && item->remove) ||
(match_retry && !item->is_retry)) {
SchedulerItem::Type type, bool skip_removed = true) const {
if (item->component != component || item->type != type || (skip_removed && item->remove)) {
return false;
}
const char *item_name = item->get_name();
@@ -222,40 +204,18 @@ class Scheduler {
return item->remove || (item->component != nullptr && item->component->is_failed());
}
// Helper to recycle a SchedulerItem
void recycle_item_(std::unique_ptr<SchedulerItem> item);
// Template helper to check if any item in a container matches our criteria
template<typename Container>
bool has_cancelled_timeout_in_container_(const Container &container, Component *component, const char *name_cstr,
bool match_retry) const {
bool has_cancelled_timeout_in_container_(const Container &container, Component *component,
const char *name_cstr) const {
for (const auto &item : container) {
if (item->remove && this->matches_item_(item, component, name_cstr, SchedulerItem::TIMEOUT, match_retry,
/* skip_removed= */ false)) {
if (item->remove && this->matches_item_(item, component, name_cstr, SchedulerItem::TIMEOUT, false)) {
return true;
}
}
return false;
}
// Template helper to cancel and recycle items from a container
template<typename Container>
size_t cancel_and_recycle_from_container_(Container &container, Component *component, const char *name_cstr,
SchedulerItem::Type type, bool match_retry) {
size_t cancelled = 0;
for (auto it = container.begin(); it != container.end();) {
if (this->matches_item_(*it, component, name_cstr, type, match_retry)) {
// Recycle the cancelled item immediately
this->recycle_item_(std::move(*it));
it = container.erase(it);
cancelled++;
} else {
++it;
}
}
return cancelled;
}
Mutex lock_;
std::vector<std::unique_ptr<SchedulerItem>> items_;
std::vector<std::unique_ptr<SchedulerItem>> to_add_;
@@ -265,9 +225,6 @@ class Scheduler {
#endif /* ESPHOME_THREAD_SINGLE */
uint32_t to_remove_{0};
// Memory pool for recycling SchedulerItem objects
std::vector<std::unique_ptr<SchedulerItem>> scheduler_item_pool_;
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
/*
* Multi-threaded platforms with atomic support: last_millis_ needs atomic for lock-free updates

View File

@@ -5,7 +5,6 @@ import os
from pathlib import Path
import re
import subprocess
from typing import Any
from esphome.const import CONF_COMPILE_PROCESS_LIMIT, CONF_ESPHOME, KEY_CORE
from esphome.core import CORE, EsphomeError
@@ -107,16 +106,7 @@ def run_compile(config, verbose):
args = []
if CONF_COMPILE_PROCESS_LIMIT in config[CONF_ESPHOME]:
args += [f"-j{config[CONF_ESPHOME][CONF_COMPILE_PROCESS_LIMIT]}"]
result = run_platformio_cli_run(config, verbose, *args)
# Run memory analysis if enabled
if config.get(CONF_ESPHOME, {}).get("analyze_memory", False):
try:
analyze_memory_usage(config)
except Exception as e:
_LOGGER.warning("Failed to analyze memory usage: %s", e)
return result
return run_platformio_cli_run(config, verbose, *args)
def _run_idedata(config):
@@ -345,93 +335,3 @@ class IDEData:
return f"{self.cc_path[:-7]}addr2line.exe"
return f"{self.cc_path[:-3]}addr2line"
@property
def objdump_path(self) -> str:
# replace gcc at end with objdump
# Windows
if self.cc_path.endswith(".exe"):
return f"{self.cc_path[:-7]}objdump.exe"
return f"{self.cc_path[:-3]}objdump"
@property
def readelf_path(self) -> str:
# replace gcc at end with readelf
# Windows
if self.cc_path.endswith(".exe"):
return f"{self.cc_path[:-7]}readelf.exe"
return f"{self.cc_path[:-3]}readelf"
def analyze_memory_usage(config: dict[str, Any]) -> None:
"""Analyze memory usage by component after compilation."""
# Lazy import to avoid overhead when not needed
from esphome.analyze_memory import MemoryAnalyzer
idedata = get_idedata(config)
# Get paths to tools
elf_path = idedata.firmware_elf_path
objdump_path = idedata.objdump_path
readelf_path = idedata.readelf_path
# Debug logging
_LOGGER.debug("ELF path from idedata: %s", elf_path)
# Check if file exists
if not Path(elf_path).exists():
# Try alternate path
alt_path = Path(CORE.relative_build_path(".pioenvs", CORE.name, "firmware.elf"))
if alt_path.exists():
elf_path = str(alt_path)
_LOGGER.debug("Using alternate ELF path: %s", elf_path)
else:
_LOGGER.warning("ELF file not found at %s or %s", elf_path, alt_path)
return
# Extract external components from config
external_components = set()
# Get the list of built-in ESPHome components
from esphome.analyze_memory import get_esphome_components
builtin_components = get_esphome_components()
# Special non-component keys that appear in configs
NON_COMPONENT_KEYS = {
CONF_ESPHOME,
"substitutions",
"packages",
"globals",
"<<",
}
# Check all top-level keys in config
for key in config:
if key not in builtin_components and key not in NON_COMPONENT_KEYS:
# This is an external component
external_components.add(key)
_LOGGER.debug("Detected external components: %s", external_components)
# Create analyzer and run analysis
analyzer = MemoryAnalyzer(elf_path, objdump_path, readelf_path, external_components)
analyzer.analyze()
# Generate and print report
report = analyzer.generate_report()
_LOGGER.info("\n%s", report)
# Optionally save to file
if config.get(CONF_ESPHOME, {}).get("memory_report_file"):
report_file = Path(config[CONF_ESPHOME]["memory_report_file"])
if report_file.suffix == ".json":
report_file.write_text(analyzer.to_json())
_LOGGER.info("Memory report saved to %s", report_file)
else:
report_file.write_text(report)
_LOGGER.info("Memory report saved to %s", report_file)

View File

@@ -275,13 +275,13 @@ class TypeInfo(ABC):
Args:
name: Field name
force: Whether this is for a repeated field
base_method: Base method name (e.g., "add_int32")
base_method: Base method name (e.g., "add_int32_field")
value_expr: Optional value expression (defaults to name)
"""
field_id_size = self.calculate_field_id_size()
method = f"{base_method}_force" if force else base_method
method = f"{base_method}_repeated" if force else base_method
value = value_expr if value_expr else name
return f"size.{method}({field_id_size}, {value});"
return f"ProtoSize::{method}(total_size, {field_id_size}, {value});"
@abstractmethod
def get_size_calculation(self, name: str, force: bool = False) -> str:
@@ -389,7 +389,7 @@ class DoubleType(TypeInfo):
def get_size_calculation(self, name: str, force: bool = False) -> str:
field_id_size = self.calculate_field_id_size()
return f"size.add_double({field_id_size}, {name});"
return f"ProtoSize::add_double_field(total_size, {field_id_size}, {name});"
def get_fixed_size_bytes(self) -> int:
return 8
@@ -413,7 +413,7 @@ class FloatType(TypeInfo):
def get_size_calculation(self, name: str, force: bool = False) -> str:
field_id_size = self.calculate_field_id_size()
return f"size.add_float({field_id_size}, {name});"
return f"ProtoSize::add_float_field(total_size, {field_id_size}, {name});"
def get_fixed_size_bytes(self) -> int:
return 4
@@ -436,7 +436,7 @@ class Int64Type(TypeInfo):
return o
def get_size_calculation(self, name: str, force: bool = False) -> str:
return self._get_simple_size_calculation(name, force, "add_int64")
return self._get_simple_size_calculation(name, force, "add_int64_field")
def get_estimated_size(self) -> int:
return self.calculate_field_id_size() + 3 # field ID + 3 bytes typical varint
@@ -456,7 +456,7 @@ class UInt64Type(TypeInfo):
return o
def get_size_calculation(self, name: str, force: bool = False) -> str:
return self._get_simple_size_calculation(name, force, "add_uint64")
return self._get_simple_size_calculation(name, force, "add_uint64_field")
def get_estimated_size(self) -> int:
return self.calculate_field_id_size() + 3 # field ID + 3 bytes typical varint
@@ -476,7 +476,7 @@ class Int32Type(TypeInfo):
return o
def get_size_calculation(self, name: str, force: bool = False) -> str:
return self._get_simple_size_calculation(name, force, "add_int32")
return self._get_simple_size_calculation(name, force, "add_int32_field")
def get_estimated_size(self) -> int:
return self.calculate_field_id_size() + 3 # field ID + 3 bytes typical varint
@@ -497,7 +497,7 @@ class Fixed64Type(TypeInfo):
def get_size_calculation(self, name: str, force: bool = False) -> str:
field_id_size = self.calculate_field_id_size()
return f"size.add_fixed64({field_id_size}, {name});"
return f"ProtoSize::add_fixed64_field(total_size, {field_id_size}, {name});"
def get_fixed_size_bytes(self) -> int:
return 8
@@ -521,7 +521,7 @@ class Fixed32Type(TypeInfo):
def get_size_calculation(self, name: str, force: bool = False) -> str:
field_id_size = self.calculate_field_id_size()
return f"size.add_fixed32({field_id_size}, {name});"
return f"ProtoSize::add_fixed32_field(total_size, {field_id_size}, {name});"
def get_fixed_size_bytes(self) -> int:
return 4
@@ -543,7 +543,7 @@ class BoolType(TypeInfo):
return o
def get_size_calculation(self, name: str, force: bool = False) -> str:
return self._get_simple_size_calculation(name, force, "add_bool")
return self._get_simple_size_calculation(name, force, "add_bool_field")
def get_estimated_size(self) -> int:
return self.calculate_field_id_size() + 1 # field ID + 1 byte
@@ -562,16 +562,11 @@ class StringType(TypeInfo):
@property
def public_content(self) -> list[str]:
content: list[str] = []
# Check if no_zero_copy option is set
no_zero_copy = get_field_opt(self._field, pb.no_zero_copy, False)
# Add std::string storage if message needs decoding OR if no_zero_copy is set
if self._needs_decode or no_zero_copy:
# Add std::string storage if message needs decoding
if self._needs_decode:
content.append(f"std::string {self.field_name}{{}};")
# Only add StringRef if encoding is needed AND no_zero_copy is not set
if self._needs_encode and not no_zero_copy:
if self._needs_encode:
content.extend(
[
# Add StringRef field if message needs encoding
@@ -586,28 +581,13 @@ class StringType(TypeInfo):
@property
def encode_content(self) -> str:
# Check if no_zero_copy option is set
no_zero_copy = get_field_opt(self._field, pb.no_zero_copy, False)
if no_zero_copy:
# Use the std::string directly
return f"buffer.encode_string({self.number}, this->{self.field_name});"
else:
# Use the StringRef
return f"buffer.encode_string({self.number}, this->{self.field_name}_ref_);"
return f"buffer.encode_string({self.number}, this->{self.field_name}_ref_);"
def dump(self, name):
# Check if no_zero_copy option is set
no_zero_copy = get_field_opt(self._field, pb.no_zero_copy, False)
# If name is 'it', this is a repeated field element - always use string
if name == "it":
return "append_quoted_string(out, StringRef(it));"
# If no_zero_copy is set, always use std::string
if no_zero_copy:
return f'out.append("\'").append(this->{self.field_name}).append("\'");'
# For SOURCE_CLIENT only, always use std::string
if not self._needs_encode:
return f'out.append("\'").append(this->{self.field_name}).append("\'");'
@@ -627,13 +607,6 @@ class StringType(TypeInfo):
@property
def dump_content(self) -> str:
# Check if no_zero_copy option is set
no_zero_copy = get_field_opt(self._field, pb.no_zero_copy, False)
# If no_zero_copy is set, always use std::string
if no_zero_copy:
return f'dump_field(out, "{self.name}", this->{self.field_name});'
# For SOURCE_CLIENT only, use std::string
if not self._needs_encode:
return f'dump_field(out, "{self.name}", this->{self.field_name});'
@@ -649,29 +622,20 @@ class StringType(TypeInfo):
return o
def get_size_calculation(self, name: str, force: bool = False) -> str:
# Check if no_zero_copy option is set
no_zero_copy = get_field_opt(self._field, pb.no_zero_copy, False)
# For SOURCE_CLIENT only messages or no_zero_copy, use the string field directly
if not self._needs_encode or no_zero_copy:
# For no_zero_copy, we need to use .size() on the string
if no_zero_copy and name != "it":
field_id_size = self.calculate_field_id_size()
return (
f"size.add_length({field_id_size}, this->{self.field_name}.size());"
)
return self._get_simple_size_calculation(name, force, "add_length")
# For SOURCE_CLIENT only messages, use the string field directly
if not self._needs_encode:
return self._get_simple_size_calculation(name, force, "add_string_field")
# Check if this is being called from a repeated field context
# In that case, 'name' will be 'it' and we need to use the repeated version
if name == "it":
# For repeated fields, we need to use add_length_force which includes field ID
# For repeated fields, we need to use add_string_field_repeated which includes field ID
field_id_size = self.calculate_field_id_size()
return f"size.add_length_force({field_id_size}, it.size());"
return f"ProtoSize::add_string_field_repeated(total_size, {field_id_size}, it);"
# For messages that need encoding, use the StringRef size
field_id_size = self.calculate_field_id_size()
return f"size.add_length({field_id_size}, this->{self.field_name}_ref_.size());"
return f"ProtoSize::add_string_field(total_size, {field_id_size}, this->{self.field_name}_ref_.size());"
def get_estimated_size(self) -> int:
return self.calculate_field_id_size() + 8 # field ID + 8 bytes typical string
@@ -806,7 +770,7 @@ class BytesType(TypeInfo):
return o
def get_size_calculation(self, name: str, force: bool = False) -> str:
return f"size.add_length({self.calculate_field_id_size()}, this->{self.field_name}_len_);"
return f"ProtoSize::add_bytes_field(total_size, {self.calculate_field_id_size()}, this->{self.field_name}_len_);"
def get_estimated_size(self) -> int:
return self.calculate_field_id_size() + 8 # field ID + 8 bytes typical bytes
@@ -881,11 +845,15 @@ class FixedArrayBytesType(TypeInfo):
field_id_size = self.calculate_field_id_size()
if force:
# For repeated fields, always calculate size (no zero check)
return f"size.add_length_force({field_id_size}, {length_field});"
# For repeated fields, always calculate size
return f"total_size += {field_id_size} + ProtoSize::varint(static_cast<uint32_t>({length_field})) + {length_field};"
else:
# For non-repeated fields, add_length already checks for zero
return f"size.add_length({field_id_size}, {length_field});"
# For non-repeated fields, skip if length is 0 (matching encode_string behavior)
return (
f"if ({length_field} != 0) {{\n"
f" total_size += {field_id_size} + ProtoSize::varint(static_cast<uint32_t>({length_field})) + {length_field};\n"
f"}}"
)
def get_estimated_size(self) -> int:
# Estimate based on typical BLE advertisement size
@@ -912,7 +880,7 @@ class UInt32Type(TypeInfo):
return o
def get_size_calculation(self, name: str, force: bool = False) -> str:
return self._get_simple_size_calculation(name, force, "add_uint32")
return self._get_simple_size_calculation(name, force, "add_uint32_field")
def get_estimated_size(self) -> int:
return self.calculate_field_id_size() + 3 # field ID + 3 bytes typical varint
@@ -949,7 +917,7 @@ class EnumType(TypeInfo):
def get_size_calculation(self, name: str, force: bool = False) -> str:
return self._get_simple_size_calculation(
name, force, "add_uint32", f"static_cast<uint32_t>({name})"
name, force, "add_enum_field", f"static_cast<uint32_t>({name})"
)
def get_estimated_size(self) -> int:
@@ -971,7 +939,7 @@ class SFixed32Type(TypeInfo):
def get_size_calculation(self, name: str, force: bool = False) -> str:
field_id_size = self.calculate_field_id_size()
return f"size.add_sfixed32({field_id_size}, {name});"
return f"ProtoSize::add_sfixed32_field(total_size, {field_id_size}, {name});"
def get_fixed_size_bytes(self) -> int:
return 4
@@ -995,7 +963,7 @@ class SFixed64Type(TypeInfo):
def get_size_calculation(self, name: str, force: bool = False) -> str:
field_id_size = self.calculate_field_id_size()
return f"size.add_sfixed64({field_id_size}, {name});"
return f"ProtoSize::add_sfixed64_field(total_size, {field_id_size}, {name});"
def get_fixed_size_bytes(self) -> int:
return 8
@@ -1018,7 +986,7 @@ class SInt32Type(TypeInfo):
return o
def get_size_calculation(self, name: str, force: bool = False) -> str:
return self._get_simple_size_calculation(name, force, "add_sint32")
return self._get_simple_size_calculation(name, force, "add_sint32_field")
def get_estimated_size(self) -> int:
return self.calculate_field_id_size() + 3 # field ID + 3 bytes typical varint
@@ -1038,7 +1006,7 @@ class SInt64Type(TypeInfo):
return o
def get_size_calculation(self, name: str, force: bool = False) -> str:
return self._get_simple_size_calculation(name, force, "add_sint64")
return self._get_simple_size_calculation(name, force, "add_sint64_field")
def get_estimated_size(self) -> int:
return self.calculate_field_id_size() + 3 # field ID + 3 bytes typical varint
@@ -1272,7 +1240,7 @@ class RepeatedTypeInfo(TypeInfo):
if isinstance(self._ti, MessageType):
# For repeated messages, use the dedicated helper that handles iteration internally
field_id_size = self._ti.calculate_field_id_size()
o = f"size.add_repeated_message({field_id_size}, {name});"
o = f"ProtoSize::add_repeated_message(total_size, {field_id_size}, {name});"
return o
# For other repeated types, use the underlying type's size calculation with force=True
@@ -1285,9 +1253,7 @@ class RepeatedTypeInfo(TypeInfo):
field_id_size = self._ti.calculate_field_id_size()
# Pre-calculate the total bytes per element
bytes_per_element = field_id_size + num_bytes
o += (
f" size.add_precalculated_size({name}.size() * {bytes_per_element});\n"
)
o += f" total_size += {name}.size() * {bytes_per_element};\n"
else:
# Other types need the actual value
o += f" for (const auto {'' if self._ti_is_bool else '&'}it : {name}) {{\n"
@@ -1362,9 +1328,6 @@ def build_type_usage_map(
field_ifdef = get_field_opt(field, pb.field_ifdef)
message_field_ifdefs.setdefault(type_name, set()).add(field_ifdef)
used_messages.add(type_name)
# Also track the field_ifdef if present
field_ifdef = get_field_opt(field, pb.field_ifdef)
message_field_ifdefs.setdefault(type_name, set()).add(field_ifdef)
# Helper to get unique ifdef from a set of messages
def get_unique_ifdef(message_names: set[str]) -> str | None:
@@ -1722,11 +1685,11 @@ def build_message_type(
if needs_encode and encode:
o = f"void {desc.name}::encode(ProtoWriteBuffer buffer) const {{"
if len(encode) == 1 and len(encode[0]) + len(o) + 3 < 120:
o += f" {encode[0]} }}\n"
o += f" {encode[0]} "
else:
o += "\n"
o += indent("\n".join(encode)) + "\n"
o += "}\n"
o += "}\n"
cpp += o
prot = "void encode(ProtoWriteBuffer buffer) const override;"
public_content.append(prot)
@@ -1734,17 +1697,17 @@ def build_message_type(
# Add calculate_size method only if this message needs encoding and has fields
if needs_encode and size_calc:
o = f"void {desc.name}::calculate_size(ProtoSize &size) const {{"
o = f"void {desc.name}::calculate_size(uint32_t &total_size) const {{"
# For a single field, just inline it for simplicity
if len(size_calc) == 1 and len(size_calc[0]) + len(o) + 3 < 120:
o += f" {size_calc[0]} }}\n"
o += f" {size_calc[0]} "
else:
# For multiple fields
o += "\n"
o += indent("\n".join(size_calc)) + "\n"
o += "}\n"
o += "}\n"
cpp += o
prot = "void calculate_size(ProtoSize &size) const override;"
prot = "void calculate_size(uint32_t &total_size) const override;"
public_content.append(prot)
# If no fields to calculate size for or message doesn't need encoding, the default implementation in ProtoMessage will be used
@@ -1775,13 +1738,8 @@ def build_message_type(
if base_class:
out = f"class {desc.name} : public {base_class} {{\n"
else:
# Check if message has any non-deprecated fields
has_fields = any(not field.options.deprecated for field in desc.field)
# Determine inheritance based on whether the message needs decoding and has fields
if needs_decode and has_fields:
base_class = "ProtoDecodableMessage"
else:
base_class = "ProtoMessage"
# Determine inheritance based on whether the message needs decoding
base_class = "ProtoDecodableMessage" if needs_decode else "ProtoMessage"
out = f"class {desc.name} : public {base_class} {{\n"
out += " public:\n"
out += indent("\n".join(public_content)) + "\n"
@@ -2047,14 +2005,7 @@ def build_service_message_type(
hout += f"virtual void {func}(const {mt.name} &value){{}};\n"
case = ""
case += f"{mt.name} msg;\n"
# Check if this message has any fields (excluding deprecated ones)
has_fields = any(not field.options.deprecated for field in mt.field)
if has_fields:
# Normal case: decode the message
case += "msg.decode(msg_data, msg_size);\n"
else:
# Empty message optimization: skip decode since there are no fields
case += "// Empty message: no decode needed\n"
case += "msg.decode(msg_data, msg_size);\n"
if log:
case += "#ifdef HAS_PROTO_MESSAGE_DUMP\n"
case += f'ESP_LOGVV(TAG, "{func}: %s", msg.dump().c_str());\n'

View File

@@ -197,7 +197,7 @@ def lint_content_find_check(find, only_first=False, **kwargs):
find_ = find(fname, content)
errs = []
for line, col in find_all(content, find_):
err = func(fname, line, col, content)
err = func(fname)
errs.append((line + 1, col + 1, err))
if only_first:
break
@@ -264,12 +264,12 @@ def lint_executable_bit(fname):
"esphome/dashboard/static/ext-searchbox.js",
],
)
def lint_tabs(fname, line, col, content):
def lint_tabs(fname):
return "File contains tab character. Please convert tabs to spaces."
@lint_content_find_check("\r", only_first=True)
def lint_newline(fname, line, col, content):
def lint_newline(fname):
return "File contains Windows newline. Please set your editor to Unix newline mode."
@@ -512,7 +512,7 @@ def relative_cpp_search_text(fname, content):
@lint_content_find_check(relative_cpp_search_text, include=["esphome/components/*.cpp"])
def lint_relative_cpp_import(fname, line, col, content):
def lint_relative_cpp_import(fname):
return (
"Component contains absolute import - Components must always use "
"relative imports.\n"
@@ -529,20 +529,6 @@ def relative_py_search_text(fname, content):
return f"esphome.components.{integration}"
def convert_path_to_relative(abspath, current):
"""Convert an absolute path to a relative import path."""
if abspath == current:
return "."
absparts = abspath.split(".")
curparts = current.split(".")
uplen = len(curparts)
while absparts and curparts and absparts[0] == curparts[0]:
absparts.pop(0)
curparts.pop(0)
uplen -= 1
return "." * uplen + ".".join(absparts)
@lint_content_find_check(
relative_py_search_text,
include=["esphome/components/*.py"],
@@ -551,19 +537,14 @@ def convert_path_to_relative(abspath, current):
"esphome/components/web_server/__init__.py",
],
)
def lint_relative_py_import(fname, line, col, content):
import_line = content.splitlines()[line]
abspath = import_line[col:].split(" ")[0]
current = fname.removesuffix(".py").replace(os.path.sep, ".")
replacement = convert_path_to_relative(abspath, current)
newline = import_line.replace(abspath, replacement)
def lint_relative_py_import(fname):
return (
"Component contains absolute import - Components must always use "
"relative imports within the integration.\n"
"Change:\n"
f" {import_line}\n"
' from esphome.components.abc import abc_ns"\n'
"to:\n"
f" {newline}\n"
" from . import abc_ns\n\n"
)
@@ -607,7 +588,7 @@ def lint_namespace(fname, content):
@lint_content_find_check('"esphome.h"', include=cpp_include, exclude=["tests/custom.h"])
def lint_esphome_h(fname, line, col, content):
def lint_esphome_h(fname):
return (
"File contains reference to 'esphome.h' - This file is "
"auto-generated and should only be used for *custom* "
@@ -698,7 +679,7 @@ def lint_trailing_whitespace(fname, match):
"tests/custom.h",
],
)
def lint_log_in_header(fname, line, col, content):
def lint_log_in_header(fname):
return (
"Found reference to ESP_LOG in header file. Using ESP_LOG* in header files "
"is currently not possible - please move the definition to a source file (.cpp)"

View File

@@ -6,7 +6,7 @@ set -e
cd "$(dirname "$0")/.."
if [ ! -n "$VIRTUAL_ENV" ]; then
if [ -x "$(command -v uv)" ]; then
uv venv --seed venv
uv venv venv
else
python3 -m venv venv
fi

View File

@@ -1,5 +0,0 @@
switch:
- name: ${door_name} Garage Door Switch
platform: gpio
pin: ${door_pin}
id: ${door_id}

View File

@@ -1,19 +0,0 @@
packages:
left_garage_door: !include
file: garage-door.yaml
vars:
door_name: Left
door_pin: 1
door_id: left_garage_door
middle_garage_door: !include
file: garage-door.yaml
vars:
door_name: Middle
door_pin: 2
door_id: middle_garage_door
right_garage_door: !include
file: garage-door.yaml
vars:
door_name: Right
door_pin: 3
door_id: right_garage_door

View File

@@ -1,311 +0,0 @@
esphome:
name: test-ha-api
friendly_name: Home Assistant API Test
host:
api:
services:
- service: trigger_all_tests
then:
- logger.log: "=== Starting Home Assistant API Tests ==="
- button.press: test_basic_service
- button.press: test_templated_service
- button.press: test_empty_string_service
- button.press: test_multiple_fields_service
- button.press: test_complex_lambda_service
- button.press: test_all_empty_service
- button.press: test_rapid_service_calls
- button.press: test_read_ha_states
- number.set:
id: ha_number
value: 42.5
- switch.turn_on: ha_switch
- switch.turn_off: ha_switch
- logger.log: "=== All tests completed ==="
logger:
level: DEBUG
# Time component for templated values
time:
- platform: homeassistant
id: homeassistant_time
# Global variables for testing
globals:
- id: test_brightness
type: int
initial_value: '75'
- id: test_string
type: std::string
initial_value: '"test_value"'
# Sensors for testing state reading
sensor:
- platform: template
name: "Test Sensor"
id: test_sensor
lambda: return 42.0;
update_interval: 0.1s
# Home Assistant sensor that reads external state
- platform: homeassistant
name: "HA Temperature"
entity_id: sensor.external_temperature
id: ha_temperature
on_value:
then:
- logger.log:
format: "HA Temperature state updated: %.1f"
args: ['x']
# Test multiple HA state sensors
- platform: homeassistant
name: "HA Humidity"
entity_id: sensor.external_humidity
id: ha_humidity
on_value:
then:
- logger.log:
format: "HA Humidity state updated: %.1f"
args: ['x']
# Binary sensor from Home Assistant
binary_sensor:
- platform: homeassistant
name: "HA Motion"
entity_id: binary_sensor.external_motion
id: ha_motion
on_state:
then:
- logger.log:
format: "HA Motion state changed: %s"
args: ['x ? "ON" : "OFF"']
# Text sensor from Home Assistant
text_sensor:
- platform: homeassistant
name: "HA Weather"
entity_id: weather.home
attribute: condition
id: ha_weather
on_value:
then:
- logger.log:
format: "HA Weather condition updated: %s"
args: ['x.c_str()']
# Test empty state handling
- platform: homeassistant
name: "HA Empty State"
entity_id: sensor.nonexistent_sensor
id: ha_empty_state
on_value:
then:
- logger.log:
format: "HA Empty state updated: %s"
args: ['x.c_str()']
# Number component for testing HA number control
number:
- platform: template
name: "HA Controlled Number"
id: ha_number
min_value: 0
max_value: 100
step: 1
optimistic: true
set_action:
- logger.log:
format: "Setting HA number to: %.1f"
args: ['x']
- homeassistant.action:
action: input_number.set_value
data:
entity_id: input_number.test_number
value: !lambda 'return to_string(x);'
# Switch component for testing HA switch control
switch:
- platform: template
name: "HA Controlled Switch"
id: ha_switch
optimistic: true
turn_on_action:
- logger.log: "Toggling HA switch: switch.test_switch ON"
- homeassistant.action:
action: switch.turn_on
data:
entity_id: switch.test_switch
turn_off_action:
- logger.log: "Toggling HA switch: switch.test_switch OFF"
- homeassistant.action:
action: switch.turn_off
data:
entity_id: switch.test_switch
# Buttons for testing various service call scenarios
button:
# Test 1: Basic service call with static values
- platform: template
name: "Test Basic Service"
id: test_basic_service
on_press:
- logger.log: "Sending HomeAssistant service call: light.turn_off"
- homeassistant.action:
action: light.turn_off
data:
entity_id: light.test_light
- logger.log: "Service data: entity_id=light.test_light"
# Test 2: Service call with templated/lambda values (main bug fix test)
- platform: template
name: "Test Templated Service"
id: test_templated_service
on_press:
- logger.log: "Testing templated service call"
- lambda: |-
int brightness_percent = id(test_brightness);
std::string computed = to_string(brightness_percent * 255 / 100);
ESP_LOGI("test", "Lambda computed value: %s", computed.c_str());
- homeassistant.action:
action: light.turn_on
data:
entity_id: light.test_light
# This creates a temporary string - the main test case
brightness: !lambda 'return to_string(id(test_brightness) * 255 / 100);'
data_template:
color_name: !lambda 'return id(test_string);'
variables:
transition: !lambda 'return "2.5";'
# Test 3: Service call with empty string values
- platform: template
name: "Test Empty String Service"
id: test_empty_string_service
on_press:
- logger.log: "Testing empty string values"
- homeassistant.action:
action: notify.test
data:
message: "Test message"
title: ""
data_template:
target: !lambda 'return "";'
variables:
sound: !lambda 'return "";'
- logger.log: "Empty value for key: title"
- logger.log: "Empty value for key: target"
- logger.log: "Empty value for key: sound"
# Test 4: Service call with multiple data fields
- platform: template
name: "Test Multiple Fields Service"
id: test_multiple_fields_service
on_press:
- logger.log: "Testing multiple data fields"
- homeassistant.action:
action: climate.set_temperature
data:
entity_id: climate.test_climate
temperature: "22"
hvac_mode: "heat"
data_template:
target_temp_high: !lambda 'return "24";'
target_temp_low: !lambda 'return "20";'
variables:
preset_mode: !lambda 'return "comfort";'
# Test 5: Complex lambda with string operations
- platform: template
name: "Test Complex Lambda Service"
id: test_complex_lambda_service
on_press:
- logger.log: "Testing complex lambda expressions"
- homeassistant.action:
action: script.test_script
data:
entity_id: !lambda |-
std::string base = "light.";
std::string room = "living_room";
return base + room;
brightness_pct: !lambda |-
float sensor_val = id(test_sensor).state;
int pct = (int)(sensor_val * 2.38); // 42 * 2.38 ≈ 100
return to_string(pct);
data_template:
message: !lambda |-
char buffer[50];
snprintf(buffer, sizeof(buffer), "Sensor: %.1f, Time: %02d:%02d",
id(test_sensor).state,
id(homeassistant_time).now().hour,
id(homeassistant_time).now().minute);
return std::string(buffer);
# Test 6: Service with only empty strings to verify size calculation
- platform: template
name: "Test All Empty Service"
id: test_all_empty_service
on_press:
- logger.log: "Testing all empty string values"
- homeassistant.action:
action: test.empty
data:
field1: ""
field2: ""
data_template:
field3: !lambda 'return "";'
variables:
field4: !lambda 'return "";'
- logger.log: "All empty service call completed"
# Test 7: Rapid successive service calls
- platform: template
name: "Test Rapid Service Calls"
id: test_rapid_service_calls
on_press:
- logger.log: "Testing rapid service calls"
- repeat:
count: 5
then:
- homeassistant.action:
action: counter.increment
data:
entity_id: counter.test_counter
- delay: 10ms
- logger.log: "Rapid service calls completed"
# Test 8: Log current HA states
- platform: template
name: "Test Read HA States"
id: test_read_ha_states
on_press:
- logger.log: "Reading current HA states"
- lambda: |-
if (id(ha_temperature).has_state()) {
ESP_LOGI("test", "Current HA Temperature: %.1f", id(ha_temperature).state);
} else {
ESP_LOGI("test", "HA Temperature has no state");
}
if (id(ha_humidity).has_state()) {
ESP_LOGI("test", "Current HA Humidity: %.1f", id(ha_humidity).state);
} else {
ESP_LOGI("test", "HA Humidity has no state");
}
ESP_LOGI("test", "Current HA Motion: %s", id(ha_motion).state ? "ON" : "OFF");
if (id(ha_weather).has_state()) {
ESP_LOGI("test", "Current HA Weather: %s", id(ha_weather).state.c_str());
} else {
ESP_LOGI("test", "HA Weather has no state");
}
if (id(ha_empty_state).has_state()) {
ESP_LOGI("test", "HA Empty State value: %s", id(ha_empty_state).state.c_str());
} else {
ESP_LOGI("test", "HA Empty State has no value (expected)");
}

View File

@@ -210,15 +210,6 @@ sensor:
name: "Test Sensor 50"
lambda: return 50.0;
update_interval: 0.1s
# Temperature sensor for the thermostat
- platform: template
name: "Temperature Sensor"
id: temp_sensor
lambda: return 22.5;
unit_of_measurement: "°C"
device_class: temperature
state_class: measurement
update_interval: 5s
# Mixed entity types for comprehensive batching test
binary_sensor:
@@ -294,50 +285,6 @@ valve:
stop_action:
- logger.log: "Valve stopping"
output:
- platform: template
id: heater_output
type: binary
write_action:
- logger.log: "Heater output changed"
- platform: template
id: cooler_output
type: binary
write_action:
- logger.log: "Cooler output changed"
climate:
- platform: thermostat
name: "Test Thermostat"
sensor: temp_sensor
default_preset: Home
on_boot_restore_from: default_preset
min_heating_off_time: 1s
min_heating_run_time: 1s
min_cooling_off_time: 1s
min_cooling_run_time: 1s
min_idle_time: 1s
heat_action:
- output.turn_on: heater_output
cool_action:
- output.turn_on: cooler_output
idle_action:
- output.turn_off: heater_output
- output.turn_off: cooler_output
preset:
- name: Home
default_target_temperature_low: 20
default_target_temperature_high: 24
mode: heat_cool
- name: Away
default_target_temperature_low: 16
default_target_temperature_high: 26
mode: heat_cool
- name: Sleep
default_target_temperature_low: 18
default_target_temperature_high: 22
mode: heat_cool
alarm_control_panel:
- platform: template
name: "Test Alarm"

View File

@@ -37,15 +37,6 @@ globals:
- id: multiple_same_name_counter
type: int
initial_value: '0'
- id: const_char_retry_counter
type: int
initial_value: '0'
- id: static_char_retry_counter
type: int
initial_value: '0'
- id: mixed_cancel_result
type: bool
initial_value: 'false'
# Using different component types for each test to ensure isolation
sensor:
@@ -238,56 +229,6 @@ script:
return RetryResult::RETRY;
});
# Test 8: Const char* overloads
- logger.log: "=== Test 8: Const char* overloads ==="
- lambda: |-
auto *component = id(simple_retry_sensor);
// Test 8a: Direct string literal
App.scheduler.set_retry(component, "const_char_test", 30, 2,
[](uint8_t retry_countdown) {
id(const_char_retry_counter)++;
ESP_LOGI("test", "Const char retry %d", id(const_char_retry_counter));
return RetryResult::DONE;
});
# Test 9: Static const char* variable
- logger.log: "=== Test 9: Static const char* ==="
- lambda: |-
auto *component = id(backoff_retry_sensor);
static const char* STATIC_NAME = "static_retry_test";
App.scheduler.set_retry(component, STATIC_NAME, 20, 1,
[](uint8_t retry_countdown) {
id(static_char_retry_counter)++;
ESP_LOGI("test", "Static const char retry %d", id(static_char_retry_counter));
return RetryResult::DONE;
});
// Cancel with same static const char*
App.scheduler.set_timeout(component, "static_cancel", 10, []() {
static const char* STATIC_NAME = "static_retry_test";
bool result = App.scheduler.cancel_retry(id(backoff_retry_sensor), STATIC_NAME);
ESP_LOGI("test", "Static cancel result: %s", result ? "true" : "false");
});
# Test 10: Mix string and const char* cancel
- logger.log: "=== Test 10: Mixed string/const char* ==="
- lambda: |-
auto *component = id(immediate_done_sensor);
// Set with std::string
std::string str_name = "mixed_retry";
App.scheduler.set_retry(component, str_name, 40, 3,
[](uint8_t retry_countdown) {
ESP_LOGI("test", "Mixed retry - should be cancelled");
return RetryResult::RETRY;
});
// Cancel with const char*
id(mixed_cancel_result) = App.scheduler.cancel_retry(component, "mixed_retry");
ESP_LOGI("test", "Mixed cancel result: %s", id(mixed_cancel_result) ? "true" : "false");
# Wait for all tests to complete before reporting
- delay: 500ms
@@ -301,7 +242,4 @@ script:
ESP_LOGI("test", "Empty name retry counter: %d (expected 1-2)", id(empty_name_retry_counter));
ESP_LOGI("test", "Component retry counter: %d (expected 2)", id(script_retry_counter));
ESP_LOGI("test", "Multiple same name counter: %d (expected 20+)", id(multiple_same_name_counter));
ESP_LOGI("test", "Const char retry counter: %d (expected 1)", id(const_char_retry_counter));
ESP_LOGI("test", "Static char retry counter: %d (expected 1)", id(static_char_retry_counter));
ESP_LOGI("test", "Mixed cancel result: %s (expected true)", id(mixed_cancel_result) ? "true" : "false");
ESP_LOGI("test", "All retry tests completed");

View File

@@ -1,305 +0,0 @@
"""Integration test for Home Assistant API functionality.
Tests:
- Home Assistant service calls with templated values (main bug fix)
- Service calls with empty string values
- Home Assistant state reading (sensors, binary sensors, text sensors)
- Home Assistant number and switch component control
- Complex lambda expressions and string handling
"""
from __future__ import annotations
import asyncio
import re
from aioesphomeapi import HomeassistantServiceCall
import pytest
from .types import APIClientConnectedFactory, RunCompiledFunction
@pytest.mark.asyncio
async def test_api_homeassistant(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""Comprehensive test for Home Assistant API functionality."""
loop = asyncio.get_running_loop()
# Create futures for patterns that capture values
lambda_computed_future = loop.create_future()
ha_temp_state_future = loop.create_future()
ha_humidity_state_future = loop.create_future()
ha_motion_state_future = loop.create_future()
ha_weather_state_future = loop.create_future()
# State update futures
temp_update_future = loop.create_future()
humidity_update_future = loop.create_future()
motion_update_future = loop.create_future()
weather_update_future = loop.create_future()
# Number future
ha_number_future = loop.create_future()
tests_complete_future = loop.create_future()
# Patterns to match in logs - only keeping patterns that capture values
lambda_computed_pattern = re.compile(r"Lambda computed value: (\d+)")
ha_temp_state_pattern = re.compile(r"Current HA Temperature: ([\d.]+)")
ha_humidity_state_pattern = re.compile(r"Current HA Humidity: ([\d.]+)")
ha_motion_state_pattern = re.compile(r"Current HA Motion: (ON|OFF)")
ha_weather_state_pattern = re.compile(r"Current HA Weather: (\w+)")
# State update patterns
temp_update_pattern = re.compile(r"HA Temperature state updated: ([\d.]+)")
humidity_update_pattern = re.compile(r"HA Humidity state updated: ([\d.]+)")
motion_update_pattern = re.compile(r"HA Motion state changed: (ON|OFF)")
weather_update_pattern = re.compile(r"HA Weather condition updated: (\w+)")
# Number pattern
ha_number_pattern = re.compile(r"Setting HA number to: ([\d.]+)")
tests_complete_pattern = re.compile(r"=== All tests completed ===")
# Track all log lines for debugging
log_lines: list[str] = []
# Track HomeAssistant service calls
ha_service_calls: list[HomeassistantServiceCall] = []
# Service call futures organized by service name
service_call_futures = {
"light.turn_off": loop.create_future(), # basic_service_call
"light.turn_on": loop.create_future(), # templated_service_call
"notify.test": loop.create_future(), # empty_string_service_call
"climate.set_temperature": loop.create_future(), # multiple_fields_service_call
"script.test_script": loop.create_future(), # complex_lambda_service_call
"test.empty": loop.create_future(), # all_empty_service_call
"input_number.set_value": loop.create_future(), # ha_number_service_call
"switch.turn_on": loop.create_future(), # ha_switch_on_service_call
"switch.turn_off": loop.create_future(), # ha_switch_off_service_call
}
def on_service_call(service_call: HomeassistantServiceCall) -> None:
"""Capture HomeAssistant service calls."""
ha_service_calls.append(service_call)
# Check if this service call is one we're waiting for
if service_call.service in service_call_futures:
future = service_call_futures[service_call.service]
if not future.done():
future.set_result(service_call)
def check_output(line: str) -> None:
"""Check log output for expected messages."""
log_lines.append(line)
# Check for patterns that capture values
if not lambda_computed_future.done():
match = lambda_computed_pattern.search(line)
if match:
lambda_computed_future.set_result(match.group(1))
elif not ha_temp_state_future.done() and ha_temp_state_pattern.search(line):
ha_temp_state_future.set_result(line)
elif not ha_humidity_state_future.done() and ha_humidity_state_pattern.search(
line
):
ha_humidity_state_future.set_result(line)
elif not ha_motion_state_future.done() and ha_motion_state_pattern.search(line):
ha_motion_state_future.set_result(line)
elif not ha_weather_state_future.done() and ha_weather_state_pattern.search(
line
):
ha_weather_state_future.set_result(line)
# Check state update patterns
elif not temp_update_future.done() and temp_update_pattern.search(line):
temp_update_future.set_result(line)
elif not humidity_update_future.done() and humidity_update_pattern.search(line):
humidity_update_future.set_result(line)
elif not motion_update_future.done() and motion_update_pattern.search(line):
motion_update_future.set_result(line)
elif not weather_update_future.done() and weather_update_pattern.search(line):
weather_update_future.set_result(line)
# Check number pattern
elif not ha_number_future.done() and ha_number_pattern.search(line):
match = ha_number_pattern.search(line)
if match:
ha_number_future.set_result(match.group(1))
elif not tests_complete_future.done() and tests_complete_pattern.search(line):
tests_complete_future.set_result(True)
# Run with log monitoring
async with (
run_compiled(yaml_config, line_callback=check_output),
api_client_connected() as client,
):
# Verify device info
device_info = await client.device_info()
assert device_info is not None
assert device_info.name == "test-ha-api"
# Subscribe to HomeAssistant service calls
client.subscribe_service_calls(on_service_call)
# Send some Home Assistant states for our sensors to read
client.send_home_assistant_state("sensor.external_temperature", "", "22.5")
client.send_home_assistant_state("sensor.external_humidity", "", "65.0")
client.send_home_assistant_state("binary_sensor.external_motion", "", "ON")
client.send_home_assistant_state("weather.home", "condition", "sunny")
# List entities and services
_, services = await client.list_entities_services()
# Find the trigger service
trigger_service = next(
(s for s in services if s.name == "trigger_all_tests"), None
)
assert trigger_service is not None, "trigger_all_tests service not found"
# Execute all tests
client.execute_service(trigger_service, {})
# Wait for all tests to complete with appropriate timeouts
try:
# Templated service test - the main bug fix
computed_value = await asyncio.wait_for(lambda_computed_future, timeout=5.0)
# Verify the computed value is reasonable (75 * 255 / 100 = 191.25 -> 191)
assert computed_value in ["191", "192"], (
f"Unexpected computed value: {computed_value}"
)
# Check state reads - verify we received the mocked values
temp_line = await asyncio.wait_for(ha_temp_state_future, timeout=5.0)
assert "Current HA Temperature: 22.5" in temp_line
humidity_line = await asyncio.wait_for(
ha_humidity_state_future, timeout=5.0
)
assert "Current HA Humidity: 65.0" in humidity_line
motion_line = await asyncio.wait_for(ha_motion_state_future, timeout=5.0)
assert "Current HA Motion: ON" in motion_line
weather_line = await asyncio.wait_for(ha_weather_state_future, timeout=5.0)
assert "Current HA Weather: sunny" in weather_line
# Number test
number_value = await asyncio.wait_for(ha_number_future, timeout=5.0)
assert number_value == "42.5", f"Unexpected number value: {number_value}"
# Wait for completion
await asyncio.wait_for(tests_complete_future, timeout=5.0)
# Now verify the protobuf messages
# 1. Basic service call
basic_call = await asyncio.wait_for(
service_call_futures["light.turn_off"], timeout=2.0
)
assert basic_call.service == "light.turn_off"
assert "entity_id" in basic_call.data, (
f"entity_id not found in data: {basic_call.data}"
)
assert basic_call.data["entity_id"] == "light.test_light", (
f"Wrong entity_id: {basic_call.data['entity_id']}"
)
# 2. Templated service call - verify the temporary string issue is fixed
templated_call = await asyncio.wait_for(
service_call_futures["light.turn_on"], timeout=2.0
)
assert templated_call.service == "light.turn_on"
# Check the computed brightness value
assert "brightness" in templated_call.data
assert templated_call.data["brightness"] in ["191", "192"] # 75 * 255 / 100
# Check data_template
assert "color_name" in templated_call.data_template
assert templated_call.data_template["color_name"] == "test_value"
# Check variables
assert "transition" in templated_call.variables
assert templated_call.variables["transition"] == "2.5"
# 3. Empty string service call
empty_call = await asyncio.wait_for(
service_call_futures["notify.test"], timeout=2.0
)
assert empty_call.service == "notify.test"
# Verify empty strings are properly handled
assert "title" in empty_call.data and empty_call.data["title"] == ""
assert (
"target" in empty_call.data_template
and empty_call.data_template["target"] == ""
)
assert (
"sound" in empty_call.variables and empty_call.variables["sound"] == ""
)
# 4. Multiple fields service call
multi_call = await asyncio.wait_for(
service_call_futures["climate.set_temperature"], timeout=2.0
)
assert multi_call.service == "climate.set_temperature"
assert multi_call.data["temperature"] == "22"
assert multi_call.data["hvac_mode"] == "heat"
assert multi_call.data_template["target_temp_high"] == "24"
assert multi_call.variables["preset_mode"] == "comfort"
# 5. Complex lambda service call
complex_call = await asyncio.wait_for(
service_call_futures["script.test_script"], timeout=2.0
)
assert complex_call.service == "script.test_script"
assert complex_call.data["entity_id"] == "light.living_room"
assert complex_call.data["brightness_pct"] == "99" # 42 * 2.38 ≈ 99
# Check message includes sensor value
assert "message" in complex_call.data_template
assert "Sensor: 42.0" in complex_call.data_template["message"]
# 6. All empty service call
all_empty_call = await asyncio.wait_for(
service_call_futures["test.empty"], timeout=2.0
)
assert all_empty_call.service == "test.empty"
# All fields should be empty strings
assert all(v == "" for v in all_empty_call.data.values())
assert all(v == "" for v in all_empty_call.data_template.values())
assert all(v == "" for v in all_empty_call.variables.values())
# 7. HA Number service call
number_call = await asyncio.wait_for(
service_call_futures["input_number.set_value"], timeout=2.0
)
assert number_call.service == "input_number.set_value"
assert number_call.data["entity_id"] == "input_number.test_number"
# The value might be formatted with trailing zeros
assert float(number_call.data["value"]) == 42.5
# 8. HA Switch service calls
switch_on_call = await asyncio.wait_for(
service_call_futures["switch.turn_on"], timeout=2.0
)
assert switch_on_call.service == "switch.turn_on"
assert switch_on_call.data["entity_id"] == "switch.test_switch"
switch_off_call = await asyncio.wait_for(
service_call_futures["switch.turn_off"], timeout=2.0
)
assert switch_off_call.service == "switch.turn_off"
assert switch_off_call.data["entity_id"] == "switch.test_switch"
except TimeoutError as e:
# Show recent log lines for debugging
recent_logs = "\n".join(log_lines[-20:])
service_calls_summary = "\n".join(
f"- {call.service}" for call in ha_service_calls
)
pytest.fail(
f"Test timed out waiting for expected log pattern or service call. Error: {e}\n\n"
f"Recent log lines:\n{recent_logs}\n\n"
f"Received service calls:\n{service_calls_summary}"
)

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
import asyncio
from aioesphomeapi import ClimateInfo, EntityState, SensorState
from aioesphomeapi import EntityState, SensorState
import pytest
from .types import APIClientConnectedFactory, RunCompiledFunction
@@ -70,22 +70,3 @@ async def test_host_mode_many_entities(
assert len(sensor_states) >= 50, (
f"Expected at least 50 sensor states, got {len(sensor_states)}"
)
# Get entity info to verify climate entity details
entities = await client.list_entities_services()
climate_infos = [e for e in entities[0] if isinstance(e, ClimateInfo)]
assert len(climate_infos) >= 1, "Expected at least 1 climate entity"
climate_info = climate_infos[0]
# Verify the thermostat has presets
assert len(climate_info.supported_presets) > 0, (
"Expected climate to have presets"
)
# The thermostat platform uses standard presets (Home, Away, Sleep)
# which should be transmitted properly without string copies
# Verify specific presets exist
preset_names = [p.name for p in climate_info.supported_presets]
assert "HOME" in preset_names, f"Expected 'HOME' preset, got {preset_names}"
assert "AWAY" in preset_names, f"Expected 'AWAY' preset, got {preset_names}"
assert "SLEEP" in preset_names, f"Expected 'SLEEP' preset, got {preset_names}"

View File

@@ -180,69 +180,6 @@ async def test_light_calls(
state = await wait_for_state_change(rgb_light.key)
assert state.state is False
# Test color mode combinations to verify get_suitable_color_modes optimization
# Test 22: White only mode
client.light_command(key=rgbcw_light.key, state=True, white=0.5)
state = await wait_for_state_change(rgbcw_light.key)
assert state.state is True
# Test 23: Color temperature only mode
client.light_command(key=rgbcw_light.key, state=True, color_temperature=300)
state = await wait_for_state_change(rgbcw_light.key)
assert state.color_temperature == pytest.approx(300)
# Test 24: Cold/warm white only mode
client.light_command(
key=rgbcw_light.key, state=True, cold_white=0.6, warm_white=0.4
)
state = await wait_for_state_change(rgbcw_light.key)
assert state.cold_white == pytest.approx(0.6)
assert state.warm_white == pytest.approx(0.4)
# Test 25: RGB only mode
client.light_command(key=rgb_light.key, state=True, rgb=(0.5, 0.5, 0.5))
state = await wait_for_state_change(rgb_light.key)
assert state.state is True
# Test 26: RGB + white combination
client.light_command(
key=rgbcw_light.key, state=True, rgb=(0.3, 0.3, 0.3), white=0.5
)
state = await wait_for_state_change(rgbcw_light.key)
assert state.state is True
# Test 27: RGB + color temperature combination
client.light_command(
key=rgbcw_light.key, state=True, rgb=(0.4, 0.4, 0.4), color_temperature=280
)
state = await wait_for_state_change(rgbcw_light.key)
assert state.state is True
# Test 28: RGB + cold/warm white combination
client.light_command(
key=rgbcw_light.key,
state=True,
rgb=(0.2, 0.2, 0.2),
cold_white=0.5,
warm_white=0.5,
)
state = await wait_for_state_change(rgbcw_light.key)
assert state.state is True
# Test 29: White + color temperature combination
client.light_command(
key=rgbcw_light.key, state=True, white=0.6, color_temperature=320
)
state = await wait_for_state_change(rgbcw_light.key)
assert state.state is True
# Test 30: No specific color parameters (tests default mode selection)
client.light_command(key=rgbcw_light.key, state=True, brightness=0.75)
state = await wait_for_state_change(rgbcw_light.key)
assert state.state is True
assert state.brightness == pytest.approx(0.75)
# Final cleanup - turn all lights off
for light in lights:
client.light_command(

View File

@@ -23,9 +23,6 @@ async def test_scheduler_retry_test(
empty_name_retry_done = asyncio.Event()
component_retry_done = asyncio.Event()
multiple_name_done = asyncio.Event()
const_char_done = asyncio.Event()
static_char_done = asyncio.Event()
mixed_cancel_done = asyncio.Event()
test_complete = asyncio.Event()
# Track retry counts
@@ -36,20 +33,16 @@ async def test_scheduler_retry_test(
empty_name_retry_count = 0
component_retry_count = 0
multiple_name_count = 0
const_char_retry_count = 0
static_char_retry_count = 0
# Track specific test results
cancel_result = None
empty_cancel_result = None
mixed_cancel_result = None
backoff_intervals = []
def on_log_line(line: str) -> None:
nonlocal simple_retry_count, backoff_retry_count, immediate_done_count
nonlocal cancel_retry_count, empty_name_retry_count, component_retry_count
nonlocal multiple_name_count, const_char_retry_count, static_char_retry_count
nonlocal cancel_result, empty_cancel_result, mixed_cancel_result
nonlocal multiple_name_count, cancel_result, empty_cancel_result
# Strip ANSI color codes
clean_line = re.sub(r"\x1b\[[0-9;]*m", "", line)
@@ -113,27 +106,6 @@ async def test_scheduler_retry_test(
if multiple_name_count >= 20:
multiple_name_done.set()
# Const char retry test
elif "Const char retry" in clean_line:
if match := re.search(r"Const char retry (\d+)", clean_line):
const_char_retry_count = int(match.group(1))
const_char_done.set()
# Static const char retry test
elif "Static const char retry" in clean_line:
if match := re.search(r"Static const char retry (\d+)", clean_line):
static_char_retry_count = int(match.group(1))
static_char_done.set()
elif "Static cancel result:" in clean_line:
# This is part of test 9, but we don't track it separately
pass
# Mixed cancel test
elif "Mixed cancel result:" in clean_line:
mixed_cancel_result = "true" in clean_line
mixed_cancel_done.set()
# Test completion
elif "All retry tests completed" in clean_line:
test_complete.set()
@@ -255,40 +227,6 @@ async def test_scheduler_retry_test(
f"Expected multiple name count >= 20 (second retry only), got {multiple_name_count}"
)
# Wait for const char retry test
try:
await asyncio.wait_for(const_char_done.wait(), timeout=1.0)
except TimeoutError:
pytest.fail(
f"Const char retry test did not complete. Count: {const_char_retry_count}"
)
assert const_char_retry_count == 1, (
f"Expected 1 const char retry call, got {const_char_retry_count}"
)
# Wait for static char retry test
try:
await asyncio.wait_for(static_char_done.wait(), timeout=1.0)
except TimeoutError:
pytest.fail(
f"Static char retry test did not complete. Count: {static_char_retry_count}"
)
assert static_char_retry_count == 1, (
f"Expected 1 static char retry call, got {static_char_retry_count}"
)
# Wait for mixed cancel test
try:
await asyncio.wait_for(mixed_cancel_done.wait(), timeout=1.0)
except TimeoutError:
pytest.fail("Mixed cancel test did not complete")
assert mixed_cancel_result is True, (
"Mixed string/const char cancel should have succeeded"
)
# Wait for test completion
try:
await asyncio.wait_for(test_complete.wait(), timeout=1.0)