mirror of
https://github.com/esphome/esphome.git
synced 2025-08-10 20:29:24 +00:00
Compare commits
1 Commits
revert-943
...
logger_no_
Author | SHA1 | Date | |
---|---|---|---|
![]() |
abac07d676 |
@@ -1 +1 @@
|
||||
4df2fc55e977ba821978fac5f1e721ce2338e23647050b7005b4c801b1770739
|
||||
32b0db73b3ae01ba18c9cbb1dabbd8156bc14dded500471919bd0a3dc33916e0
|
||||
|
@@ -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."
|
||||
)
|
||||
|
||||
|
@@ -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,7 +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_ON_CLIENT_CONNECTED): automation.validate_automation(
|
||||
single=True
|
||||
@@ -150,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")
|
||||
|
||||
@@ -245,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)
|
||||
@@ -289,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)
|
||||
|
@@ -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;
|
||||
|
@@ -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
|
||||
}
|
||||
@@ -630,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);
|
||||
@@ -1520,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
|
||||
@@ -1825,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
|
||||
|
@@ -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
|
||||
|
||||
// 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);
|
||||
|
@@ -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];
|
||||
}
|
||||
|
@@ -843,14 +843,13 @@ void NoiseEncryptionSetKeyResponse::calculate_size(uint32_t &total_size) const {
|
||||
ProtoSize::add_bool_field(total_size, 1, this->success);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
void HomeassistantServiceMap::encode(ProtoWriteBuffer buffer) const {
|
||||
buffer.encode_string(1, this->key_ref_);
|
||||
buffer.encode_string(2, this->value);
|
||||
buffer.encode_string(2, this->value_ref_);
|
||||
}
|
||||
void HomeassistantServiceMap::calculate_size(uint32_t &total_size) const {
|
||||
ProtoSize::add_string_field(total_size, 1, this->key_ref_.size());
|
||||
ProtoSize::add_string_field(total_size, 1, this->value.size());
|
||||
ProtoSize::add_string_field(total_size, 1, this->value_ref_.size());
|
||||
}
|
||||
void HomeassistantServiceResponse::encode(ProtoWriteBuffer buffer) const {
|
||||
buffer.encode_string(1, this->service_ref_);
|
||||
@@ -872,7 +871,6 @@ void HomeassistantServiceResponse::calculate_size(uint32_t &total_size) const {
|
||||
ProtoSize::add_repeated_message(total_size, 1, this->variables);
|
||||
ProtoSize::add_bool_field(total_size, 1, this->is_event);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
void SubscribeHomeAssistantStateResponse::encode(ProtoWriteBuffer buffer) const {
|
||||
buffer.encode_string(1, this->entity_id_ref_);
|
||||
|
@@ -1044,7 +1044,6 @@ class NoiseEncryptionSetKeyResponse : public ProtoMessage {
|
||||
protected:
|
||||
};
|
||||
#endif
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
class SubscribeHomeassistantServicesRequest : public ProtoDecodableMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 34;
|
||||
@@ -1062,7 +1061,8 @@ 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(uint32_t &total_size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@@ -1092,7 +1092,6 @@ class HomeassistantServiceResponse : public ProtoMessage {
|
||||
|
||||
protected:
|
||||
};
|
||||
#endif
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
class SubscribeHomeAssistantStatesRequest : public ProtoDecodableMessage {
|
||||
public:
|
||||
|
@@ -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 {}");
|
||||
|
@@ -149,7 +149,6 @@ 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;
|
||||
msg.decode(msg_data, msg_size);
|
||||
@@ -159,7 +158,6 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
this->on_subscribe_homeassistant_services_request(msg);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
case GetTimeRequest::MESSAGE_TYPE: {
|
||||
GetTimeRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
@@ -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_()) {
|
||||
|
@@ -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
|
||||
|
@@ -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,
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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();
|
||||
|
@@ -126,6 +126,6 @@ async def to_code(config):
|
||||
cg.add(var.set_max_temperature(config[CONF_MAX_TEMPERATURE]))
|
||||
cg.add(var.set_min_temperature(config[CONF_MIN_TEMPERATURE]))
|
||||
|
||||
cg.add_library("tonia/HeatpumpIR", "1.0.37")
|
||||
cg.add_library("tonia/HeatpumpIR", "1.0.35")
|
||||
if CORE.is_libretiny:
|
||||
CORE.add_platformio_option("lib_ignore", "IRremoteESP8266")
|
||||
|
@@ -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,
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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)
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -270,7 +270,7 @@ async def component_to_code(config):
|
||||
|
||||
# disable library compatibility checks
|
||||
cg.add_platformio_option("lib_ldf_mode", "off")
|
||||
cg.add_platformio_option("lib_compat_mode", "strict")
|
||||
cg.add_platformio_option("lib_compat_mode", "soft")
|
||||
# include <Arduino.h> in every file
|
||||
cg.add_platformio_option("build_src_flags", "-include Arduino.h")
|
||||
# dummy version code
|
||||
|
@@ -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) {
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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() {
|
||||
|
@@ -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 {
|
||||
|
@@ -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_);
|
||||
}
|
||||
|
@@ -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[] = {
|
||||
|
@@ -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"};
|
||||
|
||||
|
@@ -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];
|
||||
|
@@ -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"};
|
||||
|
||||
|
@@ -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"};
|
||||
|
||||
|
@@ -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');
|
||||
}
|
||||
|
@@ -15,7 +15,7 @@ from ..defines import (
|
||||
TILE_DIRECTIONS,
|
||||
literal,
|
||||
)
|
||||
from ..lv_validation import animated, lv_int, lv_pct
|
||||
from ..lv_validation import animated, lv_int
|
||||
from ..lvcode import lv, lv_assign, lv_expr, lv_obj, lv_Pvariable
|
||||
from ..schemas import container_schema
|
||||
from ..types import LV_EVENT, LvType, ObjUpdateAction, lv_obj_t, lv_obj_t_ptr
|
||||
@@ -41,8 +41,8 @@ TILEVIEW_SCHEMA = cv.Schema(
|
||||
container_schema(
|
||||
obj_spec,
|
||||
{
|
||||
cv.Required(CONF_ROW): cv.positive_int,
|
||||
cv.Required(CONF_COLUMN): cv.positive_int,
|
||||
cv.Required(CONF_ROW): lv_int,
|
||||
cv.Required(CONF_COLUMN): lv_int,
|
||||
cv.GenerateID(): cv.declare_id(lv_tile_t),
|
||||
cv.Optional(CONF_DIR, default="ALL"): TILE_DIRECTIONS.several_of,
|
||||
},
|
||||
@@ -63,29 +63,21 @@ class TileviewType(WidgetType):
|
||||
)
|
||||
|
||||
async def to_code(self, w: Widget, config: dict):
|
||||
tiles = config[CONF_TILES]
|
||||
for tile_conf in tiles:
|
||||
for tile_conf in config.get(CONF_TILES, ()):
|
||||
w_id = tile_conf[CONF_ID]
|
||||
tile_obj = lv_Pvariable(lv_obj_t, w_id)
|
||||
tile = Widget.create(w_id, tile_obj, tile_spec, tile_conf)
|
||||
dirs = tile_conf[CONF_DIR]
|
||||
if isinstance(dirs, list):
|
||||
dirs = "|".join(dirs)
|
||||
row_pos = tile_conf[CONF_ROW]
|
||||
col_pos = tile_conf[CONF_COLUMN]
|
||||
lv_assign(
|
||||
tile_obj,
|
||||
lv_expr.tileview_add_tile(w.obj, col_pos, row_pos, literal(dirs)),
|
||||
lv_expr.tileview_add_tile(
|
||||
w.obj, tile_conf[CONF_COLUMN], tile_conf[CONF_ROW], literal(dirs)
|
||||
),
|
||||
)
|
||||
# Bugfix for LVGL 8.x
|
||||
lv_obj.set_pos(tile_obj, lv_pct(col_pos * 100), lv_pct(row_pos * 100))
|
||||
await set_obj_properties(tile, tile_conf)
|
||||
await add_widgets(tile, tile_conf)
|
||||
if tiles:
|
||||
# Set the first tile as active
|
||||
lv_obj.set_tile_id(
|
||||
w.obj, tiles[0][CONF_COLUMN], tiles[0][CONF_ROW], literal("LV_ANIM_OFF")
|
||||
)
|
||||
|
||||
|
||||
tileview_spec = TileviewType()
|
||||
|
@@ -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);
|
||||
});
|
||||
}
|
||||
|
@@ -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 {
|
||||
|
@@ -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
|
||||
),
|
||||
}
|
||||
),
|
||||
|
@@ -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 *>>>();
|
||||
|
@@ -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();
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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) {
|
||||
|
@@ -83,7 +83,6 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
|
||||
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)
|
||||
@@ -135,8 +134,8 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
|
||||
|
||||
// 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);
|
||||
@@ -199,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;
|
||||
}
|
||||
|
||||
@@ -228,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) {
|
||||
@@ -503,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;
|
||||
@@ -517,7 +492,7 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c
|
||||
// Only check defer queue for timeouts (intervals never go there)
|
||||
if (type == SchedulerItem::TIMEOUT) {
|
||||
for (auto &item : this->defer_queue_) {
|
||||
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++;
|
||||
}
|
||||
@@ -527,7 +502,7 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c
|
||||
|
||||
// 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
|
||||
@@ -536,7 +511,7 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c
|
||||
|
||||
// Cancel items in to_add_
|
||||
for (auto &item : this->to_add_) {
|
||||
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++;
|
||||
// Don't track removals for to_add_ items
|
||||
|
@@ -61,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
|
||||
@@ -101,18 +98,11 @@ 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;
|
||||
}
|
||||
|
||||
@@ -166,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
|
||||
@@ -179,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) {
|
||||
@@ -191,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();
|
||||
@@ -221,11 +206,10 @@ class Scheduler {
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
@@ -225,10 +225,9 @@ class _Schema(vol.Schema):
|
||||
return ret
|
||||
|
||||
schema = schemas[0]
|
||||
extra_schemas = self._extra_schemas.copy()
|
||||
if isinstance(schema, _Schema):
|
||||
extra_schemas.extend(schema._extra_schemas)
|
||||
if isinstance(schema, vol.Schema):
|
||||
schema = schema.schema
|
||||
ret = super().extend(schema, extra=extra)
|
||||
return _Schema(ret.schema, extra=ret.extra, extra_schemas=extra_schemas)
|
||||
return _Schema(
|
||||
ret.schema, extra=ret.extra, extra_schemas=self._extra_schemas.copy()
|
||||
)
|
||||
|
@@ -78,7 +78,7 @@ lib_deps =
|
||||
glmnet/Dsmr@0.7 ; dsmr
|
||||
rweather/Crypto@0.4.0 ; dsmr
|
||||
dudanov/MideaUART@1.1.9 ; midea
|
||||
tonia/HeatpumpIR@1.0.37 ; heatpumpir
|
||||
tonia/HeatpumpIR@1.0.35 ; heatpumpir
|
||||
build_flags =
|
||||
${common.build_flags}
|
||||
-DUSE_ARDUINO
|
||||
@@ -205,6 +205,7 @@ build_unflags =
|
||||
extends = common:arduino
|
||||
platform = libretiny@1.9.1
|
||||
framework = arduino
|
||||
lib_compat_mode = soft
|
||||
lib_deps =
|
||||
droscy/esp_wireguard@0.4.2 ; wireguard
|
||||
build_flags =
|
||||
|
@@ -12,7 +12,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile
|
||||
esptool==4.9.0
|
||||
click==8.1.7
|
||||
esphome-dashboard==20250514.0
|
||||
aioesphomeapi==37.1.3
|
||||
aioesphomeapi==37.1.2
|
||||
zeroconf==0.147.0
|
||||
puremagic==1.30
|
||||
ruamel.yaml==0.18.14 # dashboard_import
|
||||
|
@@ -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,15 +622,8 @@ 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"ProtoSize::add_string_field(total_size, {field_id_size}, this->{self.field_name}.size());"
|
||||
# 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
|
||||
|
@@ -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)"
|
||||
|
@@ -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
|
||||
|
@@ -1,51 +0,0 @@
|
||||
"""
|
||||
Test schema.extend functionality in esphome.config_validation.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
import esphome.config_validation as cv
|
||||
|
||||
|
||||
def test_config_extend() -> None:
|
||||
"""Test that schema.extend correctly merges schemas with extras."""
|
||||
|
||||
def func1(data: dict[str, Any]) -> dict[str, Any]:
|
||||
data["extra_1"] = "value1"
|
||||
return data
|
||||
|
||||
def func2(data: dict[str, Any]) -> dict[str, Any]:
|
||||
data["extra_2"] = "value2"
|
||||
return data
|
||||
|
||||
schema1 = cv.Schema(
|
||||
{
|
||||
cv.Required("key1"): cv.string,
|
||||
}
|
||||
)
|
||||
schema1.add_extra(func1)
|
||||
schema2 = cv.Schema(
|
||||
{
|
||||
cv.Required("key2"): cv.string,
|
||||
}
|
||||
)
|
||||
schema2.add_extra(func2)
|
||||
extended_schema = schema1.extend(schema2)
|
||||
config = {
|
||||
"key1": "initial_value1",
|
||||
"key2": "initial_value2",
|
||||
}
|
||||
validated = extended_schema(config)
|
||||
assert validated["key1"] == "initial_value1"
|
||||
assert validated["key2"] == "initial_value2"
|
||||
assert validated["extra_1"] == "value1"
|
||||
assert validated["extra_2"] == "value2"
|
||||
|
||||
# Check the opposite order of extension
|
||||
extended_schema = schema2.extend(schema1)
|
||||
|
||||
validated = extended_schema(config)
|
||||
assert validated["key1"] == "initial_value1"
|
||||
assert validated["key2"] == "initial_value2"
|
||||
assert validated["extra_1"] == "value1"
|
||||
assert validated["extra_2"] == "value2"
|
@@ -738,7 +738,7 @@ lvgl:
|
||||
id: bar_id
|
||||
value: !lambda return (int)((float)rand() / RAND_MAX * 100);
|
||||
start_value: !lambda return (int)((float)rand() / RAND_MAX * 100);
|
||||
mode: range
|
||||
mode: symmetrical
|
||||
- logger.log:
|
||||
format: "bar value %f"
|
||||
args: [x]
|
||||
|
@@ -1,5 +0,0 @@
|
||||
switch:
|
||||
- name: ${door_name} Garage Door Switch
|
||||
platform: gpio
|
||||
pin: ${door_pin}
|
||||
id: ${door_id}
|
@@ -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
|
@@ -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)");
|
||||
}
|
@@ -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"
|
||||
|
@@ -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");
|
||||
|
@@ -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}"
|
||||
)
|
@@ -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}"
|
||||
|
@@ -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(
|
||||
|
@@ -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)
|
||||
|
Reference in New Issue
Block a user