1
0
mirror of https://github.com/esphome/esphome.git synced 2025-09-13 16:52:18 +01:00

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

This commit is contained in:
J. Nick Koston
2025-06-25 23:39:45 +02:00
108 changed files with 3097 additions and 406 deletions

View File

@@ -14,8 +14,8 @@ from esphome.const import (
CONF_WEB_SERVER,
)
from esphome.core import CORE, coroutine_with_priority
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
from esphome.cpp_generator import MockObjClass
from esphome.cpp_helpers import setup_entity
CODEOWNERS = ["@grahambrown11", "@hwstar"]
IS_PLATFORM_COMPONENT = True
@@ -149,6 +149,9 @@ _ALARM_CONTROL_PANEL_SCHEMA = (
)
_ALARM_CONTROL_PANEL_SCHEMA.add_extra(entity_duplicate_validator("alarm_control_panel"))
def alarm_control_panel_schema(
class_: MockObjClass,
*,
@@ -190,7 +193,7 @@ ALARM_CONTROL_PANEL_CONDITION_SCHEMA = maybe_simple_id(
async def setup_alarm_control_panel_core_(var, config):
await setup_entity(var, config)
await setup_entity(var, config, "alarm_control_panel")
for conf in config.get(CONF_ON_STATE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)

View File

@@ -188,6 +188,17 @@ message DeviceInfoRequest {
// Empty
}
message AreaInfo {
uint32 area_id = 1;
string name = 2;
}
message DeviceInfo {
uint32 device_id = 1;
string name = 2;
uint32 area_id = 3;
}
message DeviceInfoResponse {
option (id) = 10;
option (source) = SOURCE_SERVER;
@@ -236,6 +247,12 @@ message DeviceInfoResponse {
// Supports receiving and saving api encryption key
bool api_encryption_supported = 19;
repeated DeviceInfo devices = 20;
repeated AreaInfo areas = 21;
// Top-level area info to phase out suggested_area
AreaInfo area = 22;
}
message ListEntitiesRequest {
@@ -280,6 +297,7 @@ message ListEntitiesBinarySensorResponse {
bool disabled_by_default = 7;
string icon = 8;
EntityCategory entity_category = 9;
uint32 device_id = 10;
}
message BinarySensorStateResponse {
option (id) = 21;
@@ -315,6 +333,7 @@ message ListEntitiesCoverResponse {
string icon = 10;
EntityCategory entity_category = 11;
bool supports_stop = 12;
uint32 device_id = 13;
}
enum LegacyCoverState {
@@ -388,6 +407,7 @@ message ListEntitiesFanResponse {
string icon = 10;
EntityCategory entity_category = 11;
repeated string supported_preset_modes = 12;
uint32 device_id = 13;
}
enum FanSpeed {
FAN_SPEED_LOW = 0;
@@ -471,6 +491,7 @@ message ListEntitiesLightResponse {
bool disabled_by_default = 13;
string icon = 14;
EntityCategory entity_category = 15;
uint32 device_id = 16;
}
message LightStateResponse {
option (id) = 24;
@@ -563,6 +584,7 @@ message ListEntitiesSensorResponse {
SensorLastResetType legacy_last_reset_type = 11;
bool disabled_by_default = 12;
EntityCategory entity_category = 13;
uint32 device_id = 14;
}
message SensorStateResponse {
option (id) = 25;
@@ -595,6 +617,7 @@ message ListEntitiesSwitchResponse {
bool disabled_by_default = 7;
EntityCategory entity_category = 8;
string device_class = 9;
uint32 device_id = 10;
}
message SwitchStateResponse {
option (id) = 26;
@@ -632,6 +655,7 @@ message ListEntitiesTextSensorResponse {
bool disabled_by_default = 6;
EntityCategory entity_category = 7;
string device_class = 8;
uint32 device_id = 9;
}
message TextSensorStateResponse {
option (id) = 27;
@@ -814,6 +838,7 @@ message ListEntitiesCameraResponse {
bool disabled_by_default = 5;
string icon = 6;
EntityCategory entity_category = 7;
uint32 device_id = 8;
}
message CameraImageResponse {
@@ -916,6 +941,7 @@ message ListEntitiesClimateResponse {
bool supports_target_humidity = 23;
float visual_min_humidity = 24;
float visual_max_humidity = 25;
uint32 device_id = 26;
}
message ClimateStateResponse {
option (id) = 47;
@@ -999,6 +1025,7 @@ message ListEntitiesNumberResponse {
string unit_of_measurement = 11;
NumberMode mode = 12;
string device_class = 13;
uint32 device_id = 14;
}
message NumberStateResponse {
option (id) = 50;
@@ -1039,6 +1066,7 @@ message ListEntitiesSelectResponse {
repeated string options = 6;
bool disabled_by_default = 7;
EntityCategory entity_category = 8;
uint32 device_id = 9;
}
message SelectStateResponse {
option (id) = 53;
@@ -1081,6 +1109,7 @@ message ListEntitiesSirenResponse {
bool supports_duration = 8;
bool supports_volume = 9;
EntityCategory entity_category = 10;
uint32 device_id = 11;
}
message SirenStateResponse {
option (id) = 56;
@@ -1144,6 +1173,7 @@ message ListEntitiesLockResponse {
// Not yet implemented:
string code_format = 11;
uint32 device_id = 12;
}
message LockStateResponse {
option (id) = 59;
@@ -1183,6 +1213,7 @@ message ListEntitiesButtonResponse {
bool disabled_by_default = 6;
EntityCategory entity_category = 7;
string device_class = 8;
uint32 device_id = 9;
}
message ButtonCommandRequest {
option (id) = 62;
@@ -1238,6 +1269,8 @@ message ListEntitiesMediaPlayerResponse {
bool supports_pause = 8;
repeated MediaPlayerSupportedFormat supported_formats = 9;
uint32 device_id = 10;
}
message MediaPlayerStateResponse {
option (id) = 64;
@@ -1778,6 +1811,7 @@ message ListEntitiesAlarmControlPanelResponse {
uint32 supported_features = 8;
bool requires_code = 9;
bool requires_code_to_arm = 10;
uint32 device_id = 11;
}
message AlarmControlPanelStateResponse {
@@ -1823,6 +1857,7 @@ message ListEntitiesTextResponse {
uint32 max_length = 9;
string pattern = 10;
TextMode mode = 11;
uint32 device_id = 12;
}
message TextStateResponse {
option (id) = 98;
@@ -1863,6 +1898,7 @@ message ListEntitiesDateResponse {
string icon = 5;
bool disabled_by_default = 6;
EntityCategory entity_category = 7;
uint32 device_id = 8;
}
message DateStateResponse {
option (id) = 101;
@@ -1906,6 +1942,7 @@ message ListEntitiesTimeResponse {
string icon = 5;
bool disabled_by_default = 6;
EntityCategory entity_category = 7;
uint32 device_id = 8;
}
message TimeStateResponse {
option (id) = 104;
@@ -1952,6 +1989,7 @@ message ListEntitiesEventResponse {
string device_class = 8;
repeated string event_types = 9;
uint32 device_id = 10;
}
message EventResponse {
option (id) = 108;
@@ -1983,6 +2021,7 @@ message ListEntitiesValveResponse {
bool assumed_state = 9;
bool supports_position = 10;
bool supports_stop = 11;
uint32 device_id = 12;
}
enum ValveOperation {
@@ -2029,6 +2068,7 @@ message ListEntitiesDateTimeResponse {
string icon = 5;
bool disabled_by_default = 6;
EntityCategory entity_category = 7;
uint32 device_id = 8;
}
message DateTimeStateResponse {
option (id) = 113;
@@ -2069,6 +2109,7 @@ message ListEntitiesUpdateResponse {
bool disabled_by_default = 6;
EntityCategory entity_category = 7;
string device_class = 8;
uint32 device_id = 9;
}
message UpdateStateResponse {
option (id) = 117;

View File

@@ -1629,6 +1629,23 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) {
#endif
#ifdef USE_API_NOISE
resp.api_encryption_supported = true;
#endif
#ifdef USE_DEVICES
for (auto const &device : App.get_devices()) {
DeviceInfo device_info;
device_info.device_id = device->get_device_id();
device_info.name = device->get_name();
device_info.area_id = device->get_area_id();
resp.devices.push_back(device_info);
}
#endif
#ifdef USE_AREAS
for (auto const &area : App.get_areas()) {
AreaInfo area_info;
area_info.area_id = area->get_area_id();
area_info.name = area->get_name();
resp.areas.push_back(area_info);
}
#endif
return resp;
}

View File

@@ -301,6 +301,9 @@ class APIConnection : public APIServerConnection {
response.icon = entity->get_icon();
response.disabled_by_default = entity->is_disabled_by_default();
response.entity_category = static_cast<enums::EntityCategory>(entity->get_entity_category());
#ifdef USE_DEVICES
response.device_id = entity->get_device_id();
#endif
}
// Helper function to fill common entity state fields

View File

@@ -812,6 +812,103 @@ void PingResponse::dump_to(std::string &out) const { out.append("PingResponse {}
#ifdef HAS_PROTO_MESSAGE_DUMP
void DeviceInfoRequest::dump_to(std::string &out) const { out.append("DeviceInfoRequest {}"); }
#endif
bool AreaInfo::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
case 1: {
this->area_id = value.as_uint32();
return true;
}
default:
return false;
}
}
bool AreaInfo::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 2: {
this->name = value.as_string();
return true;
}
default:
return false;
}
}
void AreaInfo::encode(ProtoWriteBuffer buffer) const {
buffer.encode_uint32(1, this->area_id);
buffer.encode_string(2, this->name);
}
void AreaInfo::calculate_size(uint32_t &total_size) const {
ProtoSize::add_uint32_field(total_size, 1, this->area_id, false);
ProtoSize::add_string_field(total_size, 1, this->name, false);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void AreaInfo::dump_to(std::string &out) const {
__attribute__((unused)) char buffer[64];
out.append("AreaInfo {\n");
out.append(" area_id: ");
sprintf(buffer, "%" PRIu32, this->area_id);
out.append(buffer);
out.append("\n");
out.append(" name: ");
out.append("'").append(this->name).append("'");
out.append("\n");
out.append("}");
}
#endif
bool DeviceInfo::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
case 1: {
this->device_id = value.as_uint32();
return true;
}
case 3: {
this->area_id = value.as_uint32();
return true;
}
default:
return false;
}
}
bool DeviceInfo::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 2: {
this->name = value.as_string();
return true;
}
default:
return false;
}
}
void DeviceInfo::encode(ProtoWriteBuffer buffer) const {
buffer.encode_uint32(1, this->device_id);
buffer.encode_string(2, this->name);
buffer.encode_uint32(3, this->area_id);
}
void DeviceInfo::calculate_size(uint32_t &total_size) const {
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
ProtoSize::add_string_field(total_size, 1, this->name, false);
ProtoSize::add_uint32_field(total_size, 1, this->area_id, false);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void DeviceInfo::dump_to(std::string &out) const {
__attribute__((unused)) char buffer[64];
out.append("DeviceInfo {\n");
out.append(" device_id: ");
sprintf(buffer, "%" PRIu32, this->device_id);
out.append(buffer);
out.append("\n");
out.append(" name: ");
out.append("'").append(this->name).append("'");
out.append("\n");
out.append(" area_id: ");
sprintf(buffer, "%" PRIu32, this->area_id);
out.append(buffer);
out.append("\n");
out.append("}");
}
#endif
bool DeviceInfoResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
case 1: {
@@ -896,6 +993,18 @@ bool DeviceInfoResponse::decode_length(uint32_t field_id, ProtoLengthDelimited v
this->bluetooth_mac_address = value.as_string();
return true;
}
case 20: {
this->devices.push_back(value.as_message<DeviceInfo>());
return true;
}
case 21: {
this->areas.push_back(value.as_message<AreaInfo>());
return true;
}
case 22: {
this->area = value.as_message<AreaInfo>();
return true;
}
default:
return false;
}
@@ -920,6 +1029,13 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_string(16, this->suggested_area);
buffer.encode_string(18, this->bluetooth_mac_address);
buffer.encode_bool(19, this->api_encryption_supported);
for (auto &it : this->devices) {
buffer.encode_message<DeviceInfo>(20, it, true);
}
for (auto &it : this->areas) {
buffer.encode_message<AreaInfo>(21, it, true);
}
buffer.encode_message<AreaInfo>(22, this->area);
}
void DeviceInfoResponse::calculate_size(uint32_t &total_size) const {
ProtoSize::add_bool_field(total_size, 1, this->uses_password, false);
@@ -941,6 +1057,9 @@ void DeviceInfoResponse::calculate_size(uint32_t &total_size) const {
ProtoSize::add_string_field(total_size, 2, this->suggested_area, false);
ProtoSize::add_string_field(total_size, 2, this->bluetooth_mac_address, false);
ProtoSize::add_bool_field(total_size, 2, this->api_encryption_supported, false);
ProtoSize::add_repeated_message(total_size, 2, this->devices);
ProtoSize::add_repeated_message(total_size, 2, this->areas);
ProtoSize::add_message_object(total_size, 2, this->area, false);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void DeviceInfoResponse::dump_to(std::string &out) const {
@@ -1026,6 +1145,22 @@ void DeviceInfoResponse::dump_to(std::string &out) const {
out.append(" api_encryption_supported: ");
out.append(YESNO(this->api_encryption_supported));
out.append("\n");
for (const auto &it : this->devices) {
out.append(" devices: ");
it.dump_to(out);
out.append("\n");
}
for (const auto &it : this->areas) {
out.append(" areas: ");
it.dump_to(out);
out.append("\n");
}
out.append(" area: ");
this->area.dump_to(out);
out.append("\n");
out.append("}");
}
#endif
@@ -1052,6 +1187,10 @@ bool ListEntitiesBinarySensorResponse::decode_varint(uint32_t field_id, ProtoVar
this->entity_category = value.as_enum<enums::EntityCategory>();
return true;
}
case 10: {
this->device_id = value.as_uint32();
return true;
}
default:
return false;
}
@@ -1102,6 +1241,7 @@ void ListEntitiesBinarySensorResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_bool(7, this->disabled_by_default);
buffer.encode_string(8, this->icon);
buffer.encode_enum<enums::EntityCategory>(9, this->entity_category);
buffer.encode_uint32(10, this->device_id);
}
void ListEntitiesBinarySensorResponse::calculate_size(uint32_t &total_size) const {
ProtoSize::add_string_field(total_size, 1, this->object_id, false);
@@ -1113,6 +1253,7 @@ void ListEntitiesBinarySensorResponse::calculate_size(uint32_t &total_size) cons
ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false);
ProtoSize::add_string_field(total_size, 1, this->icon, false);
ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->entity_category), false);
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void ListEntitiesBinarySensorResponse::dump_to(std::string &out) const {
@@ -1154,6 +1295,11 @@ void ListEntitiesBinarySensorResponse::dump_to(std::string &out) const {
out.append(" entity_category: ");
out.append(proto_enum_to_string<enums::EntityCategory>(this->entity_category));
out.append("\n");
out.append(" device_id: ");
sprintf(buffer, "%" PRIu32, this->device_id);
out.append(buffer);
out.append("\n");
out.append("}");
}
#endif
@@ -1236,6 +1382,10 @@ bool ListEntitiesCoverResponse::decode_varint(uint32_t field_id, ProtoVarInt val
this->supports_stop = value.as_bool();
return true;
}
case 13: {
this->device_id = value.as_uint32();
return true;
}
default:
return false;
}
@@ -1289,6 +1439,7 @@ void ListEntitiesCoverResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_string(10, this->icon);
buffer.encode_enum<enums::EntityCategory>(11, this->entity_category);
buffer.encode_bool(12, this->supports_stop);
buffer.encode_uint32(13, this->device_id);
}
void ListEntitiesCoverResponse::calculate_size(uint32_t &total_size) const {
ProtoSize::add_string_field(total_size, 1, this->object_id, false);
@@ -1303,6 +1454,7 @@ void ListEntitiesCoverResponse::calculate_size(uint32_t &total_size) const {
ProtoSize::add_string_field(total_size, 1, this->icon, false);
ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->entity_category), false);
ProtoSize::add_bool_field(total_size, 1, this->supports_stop, false);
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void ListEntitiesCoverResponse::dump_to(std::string &out) const {
@@ -1356,6 +1508,11 @@ void ListEntitiesCoverResponse::dump_to(std::string &out) const {
out.append(" supports_stop: ");
out.append(YESNO(this->supports_stop));
out.append("\n");
out.append(" device_id: ");
sprintf(buffer, "%" PRIu32, this->device_id);
out.append(buffer);
out.append("\n");
out.append("}");
}
#endif
@@ -1565,6 +1722,10 @@ bool ListEntitiesFanResponse::decode_varint(uint32_t field_id, ProtoVarInt value
this->entity_category = value.as_enum<enums::EntityCategory>();
return true;
}
case 13: {
this->device_id = value.as_uint32();
return true;
}
default:
return false;
}
@@ -1620,6 +1781,7 @@ void ListEntitiesFanResponse::encode(ProtoWriteBuffer buffer) const {
for (auto &it : this->supported_preset_modes) {
buffer.encode_string(12, it, true);
}
buffer.encode_uint32(13, this->device_id);
}
void ListEntitiesFanResponse::calculate_size(uint32_t &total_size) const {
ProtoSize::add_string_field(total_size, 1, this->object_id, false);
@@ -1638,6 +1800,7 @@ void ListEntitiesFanResponse::calculate_size(uint32_t &total_size) const {
ProtoSize::add_string_field(total_size, 1, it, true);
}
}
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void ListEntitiesFanResponse::dump_to(std::string &out) const {
@@ -1694,6 +1857,11 @@ void ListEntitiesFanResponse::dump_to(std::string &out) const {
out.append("'").append(it).append("'");
out.append("\n");
}
out.append(" device_id: ");
sprintf(buffer, "%" PRIu32, this->device_id);
out.append(buffer);
out.append("\n");
out.append("}");
}
#endif
@@ -1987,6 +2155,10 @@ bool ListEntitiesLightResponse::decode_varint(uint32_t field_id, ProtoVarInt val
this->entity_category = value.as_enum<enums::EntityCategory>();
return true;
}
case 16: {
this->device_id = value.as_uint32();
return true;
}
default:
return false;
}
@@ -2055,6 +2227,7 @@ void ListEntitiesLightResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_bool(13, this->disabled_by_default);
buffer.encode_string(14, this->icon);
buffer.encode_enum<enums::EntityCategory>(15, this->entity_category);
buffer.encode_uint32(16, this->device_id);
}
void ListEntitiesLightResponse::calculate_size(uint32_t &total_size) const {
ProtoSize::add_string_field(total_size, 1, this->object_id, false);
@@ -2080,6 +2253,7 @@ void ListEntitiesLightResponse::calculate_size(uint32_t &total_size) const {
ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false);
ProtoSize::add_string_field(total_size, 1, this->icon, false);
ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->entity_category), false);
ProtoSize::add_uint32_field(total_size, 2, this->device_id, false);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void ListEntitiesLightResponse::dump_to(std::string &out) const {
@@ -2151,6 +2325,11 @@ void ListEntitiesLightResponse::dump_to(std::string &out) const {
out.append(" entity_category: ");
out.append(proto_enum_to_string<enums::EntityCategory>(this->entity_category));
out.append("\n");
out.append(" device_id: ");
sprintf(buffer, "%" PRIu32, this->device_id);
out.append(buffer);
out.append("\n");
out.append("}");
}
#endif
@@ -2658,6 +2837,10 @@ bool ListEntitiesSensorResponse::decode_varint(uint32_t field_id, ProtoVarInt va
this->entity_category = value.as_enum<enums::EntityCategory>();
return true;
}
case 14: {
this->device_id = value.as_uint32();
return true;
}
default:
return false;
}
@@ -2716,6 +2899,7 @@ void ListEntitiesSensorResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_enum<enums::SensorLastResetType>(11, this->legacy_last_reset_type);
buffer.encode_bool(12, this->disabled_by_default);
buffer.encode_enum<enums::EntityCategory>(13, this->entity_category);
buffer.encode_uint32(14, this->device_id);
}
void ListEntitiesSensorResponse::calculate_size(uint32_t &total_size) const {
ProtoSize::add_string_field(total_size, 1, this->object_id, false);
@@ -2731,6 +2915,7 @@ void ListEntitiesSensorResponse::calculate_size(uint32_t &total_size) const {
ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->legacy_last_reset_type), false);
ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false);
ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->entity_category), false);
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void ListEntitiesSensorResponse::dump_to(std::string &out) const {
@@ -2789,6 +2974,11 @@ void ListEntitiesSensorResponse::dump_to(std::string &out) const {
out.append(" entity_category: ");
out.append(proto_enum_to_string<enums::EntityCategory>(this->entity_category));
out.append("\n");
out.append(" device_id: ");
sprintf(buffer, "%" PRIu32, this->device_id);
out.append(buffer);
out.append("\n");
out.append("}");
}
#endif
@@ -2860,6 +3050,10 @@ bool ListEntitiesSwitchResponse::decode_varint(uint32_t field_id, ProtoVarInt va
this->entity_category = value.as_enum<enums::EntityCategory>();
return true;
}
case 10: {
this->device_id = value.as_uint32();
return true;
}
default:
return false;
}
@@ -2910,6 +3104,7 @@ void ListEntitiesSwitchResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_bool(7, this->disabled_by_default);
buffer.encode_enum<enums::EntityCategory>(8, this->entity_category);
buffer.encode_string(9, this->device_class);
buffer.encode_uint32(10, this->device_id);
}
void ListEntitiesSwitchResponse::calculate_size(uint32_t &total_size) const {
ProtoSize::add_string_field(total_size, 1, this->object_id, false);
@@ -2921,6 +3116,7 @@ void ListEntitiesSwitchResponse::calculate_size(uint32_t &total_size) const {
ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false);
ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->entity_category), false);
ProtoSize::add_string_field(total_size, 1, this->device_class, false);
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void ListEntitiesSwitchResponse::dump_to(std::string &out) const {
@@ -2962,6 +3158,11 @@ void ListEntitiesSwitchResponse::dump_to(std::string &out) const {
out.append(" device_class: ");
out.append("'").append(this->device_class).append("'");
out.append("\n");
out.append(" device_id: ");
sprintf(buffer, "%" PRIu32, this->device_id);
out.append(buffer);
out.append("\n");
out.append("}");
}
#endif
@@ -3061,6 +3262,10 @@ bool ListEntitiesTextSensorResponse::decode_varint(uint32_t field_id, ProtoVarIn
this->entity_category = value.as_enum<enums::EntityCategory>();
return true;
}
case 9: {
this->device_id = value.as_uint32();
return true;
}
default:
return false;
}
@@ -3110,6 +3315,7 @@ void ListEntitiesTextSensorResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_bool(6, this->disabled_by_default);
buffer.encode_enum<enums::EntityCategory>(7, this->entity_category);
buffer.encode_string(8, this->device_class);
buffer.encode_uint32(9, this->device_id);
}
void ListEntitiesTextSensorResponse::calculate_size(uint32_t &total_size) const {
ProtoSize::add_string_field(total_size, 1, this->object_id, false);
@@ -3120,6 +3326,7 @@ void ListEntitiesTextSensorResponse::calculate_size(uint32_t &total_size) const
ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false);
ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->entity_category), false);
ProtoSize::add_string_field(total_size, 1, this->device_class, false);
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void ListEntitiesTextSensorResponse::dump_to(std::string &out) const {
@@ -3157,6 +3364,11 @@ void ListEntitiesTextSensorResponse::dump_to(std::string &out) const {
out.append(" device_class: ");
out.append("'").append(this->device_class).append("'");
out.append("\n");
out.append(" device_id: ");
sprintf(buffer, "%" PRIu32, this->device_id);
out.append(buffer);
out.append("\n");
out.append("}");
}
#endif
@@ -3922,6 +4134,10 @@ bool ListEntitiesCameraResponse::decode_varint(uint32_t field_id, ProtoVarInt va
this->entity_category = value.as_enum<enums::EntityCategory>();
return true;
}
case 8: {
this->device_id = value.as_uint32();
return true;
}
default:
return false;
}
@@ -3966,6 +4182,7 @@ void ListEntitiesCameraResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_bool(5, this->disabled_by_default);
buffer.encode_string(6, this->icon);
buffer.encode_enum<enums::EntityCategory>(7, this->entity_category);
buffer.encode_uint32(8, this->device_id);
}
void ListEntitiesCameraResponse::calculate_size(uint32_t &total_size) const {
ProtoSize::add_string_field(total_size, 1, this->object_id, false);
@@ -3975,6 +4192,7 @@ void ListEntitiesCameraResponse::calculate_size(uint32_t &total_size) const {
ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false);
ProtoSize::add_string_field(total_size, 1, this->icon, false);
ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->entity_category), false);
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void ListEntitiesCameraResponse::dump_to(std::string &out) const {
@@ -4008,6 +4226,11 @@ void ListEntitiesCameraResponse::dump_to(std::string &out) const {
out.append(" entity_category: ");
out.append(proto_enum_to_string<enums::EntityCategory>(this->entity_category));
out.append("\n");
out.append(" device_id: ");
sprintf(buffer, "%" PRIu32, this->device_id);
out.append(buffer);
out.append("\n");
out.append("}");
}
#endif
@@ -4156,6 +4379,10 @@ bool ListEntitiesClimateResponse::decode_varint(uint32_t field_id, ProtoVarInt v
this->supports_target_humidity = value.as_bool();
return true;
}
case 26: {
this->device_id = value.as_uint32();
return true;
}
default:
return false;
}
@@ -4262,6 +4489,7 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_bool(23, this->supports_target_humidity);
buffer.encode_float(24, this->visual_min_humidity);
buffer.encode_float(25, this->visual_max_humidity);
buffer.encode_uint32(26, this->device_id);
}
void ListEntitiesClimateResponse::calculate_size(uint32_t &total_size) const {
ProtoSize::add_string_field(total_size, 1, this->object_id, false);
@@ -4313,6 +4541,7 @@ void ListEntitiesClimateResponse::calculate_size(uint32_t &total_size) const {
ProtoSize::add_bool_field(total_size, 2, this->supports_target_humidity, false);
ProtoSize::add_fixed_field<4>(total_size, 2, this->visual_min_humidity != 0.0f, false);
ProtoSize::add_fixed_field<4>(total_size, 2, this->visual_max_humidity != 0.0f, false);
ProtoSize::add_uint32_field(total_size, 2, this->device_id, false);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void ListEntitiesClimateResponse::dump_to(std::string &out) const {
@@ -4436,6 +4665,11 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const {
sprintf(buffer, "%g", this->visual_max_humidity);
out.append(buffer);
out.append("\n");
out.append(" device_id: ");
sprintf(buffer, "%" PRIu32, this->device_id);
out.append(buffer);
out.append("\n");
out.append("}");
}
#endif
@@ -4901,6 +5135,10 @@ bool ListEntitiesNumberResponse::decode_varint(uint32_t field_id, ProtoVarInt va
this->mode = value.as_enum<enums::NumberMode>();
return true;
}
case 14: {
this->device_id = value.as_uint32();
return true;
}
default:
return false;
}
@@ -4971,6 +5209,7 @@ void ListEntitiesNumberResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_string(11, this->unit_of_measurement);
buffer.encode_enum<enums::NumberMode>(12, this->mode);
buffer.encode_string(13, this->device_class);
buffer.encode_uint32(14, this->device_id);
}
void ListEntitiesNumberResponse::calculate_size(uint32_t &total_size) const {
ProtoSize::add_string_field(total_size, 1, this->object_id, false);
@@ -4986,6 +5225,7 @@ void ListEntitiesNumberResponse::calculate_size(uint32_t &total_size) const {
ProtoSize::add_string_field(total_size, 1, this->unit_of_measurement, false);
ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->mode), false);
ProtoSize::add_string_field(total_size, 1, this->device_class, false);
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void ListEntitiesNumberResponse::dump_to(std::string &out) const {
@@ -5046,6 +5286,11 @@ void ListEntitiesNumberResponse::dump_to(std::string &out) const {
out.append(" device_class: ");
out.append("'").append(this->device_class).append("'");
out.append("\n");
out.append(" device_id: ");
sprintf(buffer, "%" PRIu32, this->device_id);
out.append(buffer);
out.append("\n");
out.append("}");
}
#endif
@@ -5151,6 +5396,10 @@ bool ListEntitiesSelectResponse::decode_varint(uint32_t field_id, ProtoVarInt va
this->entity_category = value.as_enum<enums::EntityCategory>();
return true;
}
case 9: {
this->device_id = value.as_uint32();
return true;
}
default:
return false;
}
@@ -5202,6 +5451,7 @@ void ListEntitiesSelectResponse::encode(ProtoWriteBuffer buffer) const {
}
buffer.encode_bool(7, this->disabled_by_default);
buffer.encode_enum<enums::EntityCategory>(8, this->entity_category);
buffer.encode_uint32(9, this->device_id);
}
void ListEntitiesSelectResponse::calculate_size(uint32_t &total_size) const {
ProtoSize::add_string_field(total_size, 1, this->object_id, false);
@@ -5216,6 +5466,7 @@ void ListEntitiesSelectResponse::calculate_size(uint32_t &total_size) const {
}
ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false);
ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->entity_category), false);
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void ListEntitiesSelectResponse::dump_to(std::string &out) const {
@@ -5255,6 +5506,11 @@ void ListEntitiesSelectResponse::dump_to(std::string &out) const {
out.append(" entity_category: ");
out.append(proto_enum_to_string<enums::EntityCategory>(this->entity_category));
out.append("\n");
out.append(" device_id: ");
sprintf(buffer, "%" PRIu32, this->device_id);
out.append(buffer);
out.append("\n");
out.append("}");
}
#endif
@@ -5378,6 +5634,10 @@ bool ListEntitiesSirenResponse::decode_varint(uint32_t field_id, ProtoVarInt val
this->entity_category = value.as_enum<enums::EntityCategory>();
return true;
}
case 11: {
this->device_id = value.as_uint32();
return true;
}
default:
return false;
}
@@ -5431,6 +5691,7 @@ void ListEntitiesSirenResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_bool(8, this->supports_duration);
buffer.encode_bool(9, this->supports_volume);
buffer.encode_enum<enums::EntityCategory>(10, this->entity_category);
buffer.encode_uint32(11, this->device_id);
}
void ListEntitiesSirenResponse::calculate_size(uint32_t &total_size) const {
ProtoSize::add_string_field(total_size, 1, this->object_id, false);
@@ -5447,6 +5708,7 @@ void ListEntitiesSirenResponse::calculate_size(uint32_t &total_size) const {
ProtoSize::add_bool_field(total_size, 1, this->supports_duration, false);
ProtoSize::add_bool_field(total_size, 1, this->supports_volume, false);
ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->entity_category), false);
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void ListEntitiesSirenResponse::dump_to(std::string &out) const {
@@ -5494,6 +5756,11 @@ void ListEntitiesSirenResponse::dump_to(std::string &out) const {
out.append(" entity_category: ");
out.append(proto_enum_to_string<enums::EntityCategory>(this->entity_category));
out.append("\n");
out.append(" device_id: ");
sprintf(buffer, "%" PRIu32, this->device_id);
out.append(buffer);
out.append("\n");
out.append("}");
}
#endif
@@ -5683,6 +5950,10 @@ bool ListEntitiesLockResponse::decode_varint(uint32_t field_id, ProtoVarInt valu
this->requires_code = value.as_bool();
return true;
}
case 12: {
this->device_id = value.as_uint32();
return true;
}
default:
return false;
}
@@ -5735,6 +6006,7 @@ void ListEntitiesLockResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_bool(9, this->supports_open);
buffer.encode_bool(10, this->requires_code);
buffer.encode_string(11, this->code_format);
buffer.encode_uint32(12, this->device_id);
}
void ListEntitiesLockResponse::calculate_size(uint32_t &total_size) const {
ProtoSize::add_string_field(total_size, 1, this->object_id, false);
@@ -5748,6 +6020,7 @@ void ListEntitiesLockResponse::calculate_size(uint32_t &total_size) const {
ProtoSize::add_bool_field(total_size, 1, this->supports_open, false);
ProtoSize::add_bool_field(total_size, 1, this->requires_code, false);
ProtoSize::add_string_field(total_size, 1, this->code_format, false);
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void ListEntitiesLockResponse::dump_to(std::string &out) const {
@@ -5797,6 +6070,11 @@ void ListEntitiesLockResponse::dump_to(std::string &out) const {
out.append(" code_format: ");
out.append("'").append(this->code_format).append("'");
out.append("\n");
out.append(" device_id: ");
sprintf(buffer, "%" PRIu32, this->device_id);
out.append(buffer);
out.append("\n");
out.append("}");
}
#endif
@@ -5922,6 +6200,10 @@ bool ListEntitiesButtonResponse::decode_varint(uint32_t field_id, ProtoVarInt va
this->entity_category = value.as_enum<enums::EntityCategory>();
return true;
}
case 9: {
this->device_id = value.as_uint32();
return true;
}
default:
return false;
}
@@ -5971,6 +6253,7 @@ void ListEntitiesButtonResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_bool(6, this->disabled_by_default);
buffer.encode_enum<enums::EntityCategory>(7, this->entity_category);
buffer.encode_string(8, this->device_class);
buffer.encode_uint32(9, this->device_id);
}
void ListEntitiesButtonResponse::calculate_size(uint32_t &total_size) const {
ProtoSize::add_string_field(total_size, 1, this->object_id, false);
@@ -5981,6 +6264,7 @@ void ListEntitiesButtonResponse::calculate_size(uint32_t &total_size) const {
ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false);
ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->entity_category), false);
ProtoSize::add_string_field(total_size, 1, this->device_class, false);
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void ListEntitiesButtonResponse::dump_to(std::string &out) const {
@@ -6018,6 +6302,11 @@ void ListEntitiesButtonResponse::dump_to(std::string &out) const {
out.append(" device_class: ");
out.append("'").append(this->device_class).append("'");
out.append("\n");
out.append(" device_id: ");
sprintf(buffer, "%" PRIu32, this->device_id);
out.append(buffer);
out.append("\n");
out.append("}");
}
#endif
@@ -6135,6 +6424,10 @@ bool ListEntitiesMediaPlayerResponse::decode_varint(uint32_t field_id, ProtoVarI
this->supports_pause = value.as_bool();
return true;
}
case 10: {
this->device_id = value.as_uint32();
return true;
}
default:
return false;
}
@@ -6187,6 +6480,7 @@ void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer buffer) const {
for (auto &it : this->supported_formats) {
buffer.encode_message<MediaPlayerSupportedFormat>(9, it, true);
}
buffer.encode_uint32(10, this->device_id);
}
void ListEntitiesMediaPlayerResponse::calculate_size(uint32_t &total_size) const {
ProtoSize::add_string_field(total_size, 1, this->object_id, false);
@@ -6198,6 +6492,7 @@ void ListEntitiesMediaPlayerResponse::calculate_size(uint32_t &total_size) const
ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->entity_category), false);
ProtoSize::add_bool_field(total_size, 1, this->supports_pause, false);
ProtoSize::add_repeated_message(total_size, 1, this->supported_formats);
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void ListEntitiesMediaPlayerResponse::dump_to(std::string &out) const {
@@ -6241,6 +6536,11 @@ void ListEntitiesMediaPlayerResponse::dump_to(std::string &out) const {
it.dump_to(out);
out.append("\n");
}
out.append(" device_id: ");
sprintf(buffer, "%" PRIu32, this->device_id);
out.append(buffer);
out.append("\n");
out.append("}");
}
#endif
@@ -8551,6 +8851,10 @@ bool ListEntitiesAlarmControlPanelResponse::decode_varint(uint32_t field_id, Pro
this->requires_code_to_arm = value.as_bool();
return true;
}
case 11: {
this->device_id = value.as_uint32();
return true;
}
default:
return false;
}
@@ -8598,6 +8902,7 @@ void ListEntitiesAlarmControlPanelResponse::encode(ProtoWriteBuffer buffer) cons
buffer.encode_uint32(8, this->supported_features);
buffer.encode_bool(9, this->requires_code);
buffer.encode_bool(10, this->requires_code_to_arm);
buffer.encode_uint32(11, this->device_id);
}
void ListEntitiesAlarmControlPanelResponse::calculate_size(uint32_t &total_size) const {
ProtoSize::add_string_field(total_size, 1, this->object_id, false);
@@ -8610,6 +8915,7 @@ void ListEntitiesAlarmControlPanelResponse::calculate_size(uint32_t &total_size)
ProtoSize::add_uint32_field(total_size, 1, this->supported_features, false);
ProtoSize::add_bool_field(total_size, 1, this->requires_code, false);
ProtoSize::add_bool_field(total_size, 1, this->requires_code_to_arm, false);
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void ListEntitiesAlarmControlPanelResponse::dump_to(std::string &out) const {
@@ -8656,6 +8962,11 @@ void ListEntitiesAlarmControlPanelResponse::dump_to(std::string &out) const {
out.append(" requires_code_to_arm: ");
out.append(YESNO(this->requires_code_to_arm));
out.append("\n");
out.append(" device_id: ");
sprintf(buffer, "%" PRIu32, this->device_id);
out.append(buffer);
out.append("\n");
out.append("}");
}
#endif
@@ -8783,6 +9094,10 @@ bool ListEntitiesTextResponse::decode_varint(uint32_t field_id, ProtoVarInt valu
this->mode = value.as_enum<enums::TextMode>();
return true;
}
case 12: {
this->device_id = value.as_uint32();
return true;
}
default:
return false;
}
@@ -8835,6 +9150,7 @@ void ListEntitiesTextResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_uint32(9, this->max_length);
buffer.encode_string(10, this->pattern);
buffer.encode_enum<enums::TextMode>(11, this->mode);
buffer.encode_uint32(12, this->device_id);
}
void ListEntitiesTextResponse::calculate_size(uint32_t &total_size) const {
ProtoSize::add_string_field(total_size, 1, this->object_id, false);
@@ -8848,6 +9164,7 @@ void ListEntitiesTextResponse::calculate_size(uint32_t &total_size) const {
ProtoSize::add_uint32_field(total_size, 1, this->max_length, false);
ProtoSize::add_string_field(total_size, 1, this->pattern, false);
ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->mode), false);
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void ListEntitiesTextResponse::dump_to(std::string &out) const {
@@ -8899,6 +9216,11 @@ void ListEntitiesTextResponse::dump_to(std::string &out) const {
out.append(" mode: ");
out.append(proto_enum_to_string<enums::TextMode>(this->mode));
out.append("\n");
out.append(" device_id: ");
sprintf(buffer, "%" PRIu32, this->device_id);
out.append(buffer);
out.append("\n");
out.append("}");
}
#endif
@@ -9014,6 +9336,10 @@ bool ListEntitiesDateResponse::decode_varint(uint32_t field_id, ProtoVarInt valu
this->entity_category = value.as_enum<enums::EntityCategory>();
return true;
}
case 8: {
this->device_id = value.as_uint32();
return true;
}
default:
return false;
}
@@ -9058,6 +9384,7 @@ void ListEntitiesDateResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_string(5, this->icon);
buffer.encode_bool(6, this->disabled_by_default);
buffer.encode_enum<enums::EntityCategory>(7, this->entity_category);
buffer.encode_uint32(8, this->device_id);
}
void ListEntitiesDateResponse::calculate_size(uint32_t &total_size) const {
ProtoSize::add_string_field(total_size, 1, this->object_id, false);
@@ -9067,6 +9394,7 @@ void ListEntitiesDateResponse::calculate_size(uint32_t &total_size) const {
ProtoSize::add_string_field(total_size, 1, this->icon, false);
ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false);
ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->entity_category), false);
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void ListEntitiesDateResponse::dump_to(std::string &out) const {
@@ -9100,6 +9428,11 @@ void ListEntitiesDateResponse::dump_to(std::string &out) const {
out.append(" entity_category: ");
out.append(proto_enum_to_string<enums::EntityCategory>(this->entity_category));
out.append("\n");
out.append(" device_id: ");
sprintf(buffer, "%" PRIu32, this->device_id);
out.append(buffer);
out.append("\n");
out.append("}");
}
#endif
@@ -9255,6 +9588,10 @@ bool ListEntitiesTimeResponse::decode_varint(uint32_t field_id, ProtoVarInt valu
this->entity_category = value.as_enum<enums::EntityCategory>();
return true;
}
case 8: {
this->device_id = value.as_uint32();
return true;
}
default:
return false;
}
@@ -9299,6 +9636,7 @@ void ListEntitiesTimeResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_string(5, this->icon);
buffer.encode_bool(6, this->disabled_by_default);
buffer.encode_enum<enums::EntityCategory>(7, this->entity_category);
buffer.encode_uint32(8, this->device_id);
}
void ListEntitiesTimeResponse::calculate_size(uint32_t &total_size) const {
ProtoSize::add_string_field(total_size, 1, this->object_id, false);
@@ -9308,6 +9646,7 @@ void ListEntitiesTimeResponse::calculate_size(uint32_t &total_size) const {
ProtoSize::add_string_field(total_size, 1, this->icon, false);
ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false);
ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->entity_category), false);
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void ListEntitiesTimeResponse::dump_to(std::string &out) const {
@@ -9341,6 +9680,11 @@ void ListEntitiesTimeResponse::dump_to(std::string &out) const {
out.append(" entity_category: ");
out.append(proto_enum_to_string<enums::EntityCategory>(this->entity_category));
out.append("\n");
out.append(" device_id: ");
sprintf(buffer, "%" PRIu32, this->device_id);
out.append(buffer);
out.append("\n");
out.append("}");
}
#endif
@@ -9496,6 +9840,10 @@ bool ListEntitiesEventResponse::decode_varint(uint32_t field_id, ProtoVarInt val
this->entity_category = value.as_enum<enums::EntityCategory>();
return true;
}
case 10: {
this->device_id = value.as_uint32();
return true;
}
default:
return false;
}
@@ -9552,6 +9900,7 @@ void ListEntitiesEventResponse::encode(ProtoWriteBuffer buffer) const {
for (auto &it : this->event_types) {
buffer.encode_string(9, it, true);
}
buffer.encode_uint32(10, this->device_id);
}
void ListEntitiesEventResponse::calculate_size(uint32_t &total_size) const {
ProtoSize::add_string_field(total_size, 1, this->object_id, false);
@@ -9567,6 +9916,7 @@ void ListEntitiesEventResponse::calculate_size(uint32_t &total_size) const {
ProtoSize::add_string_field(total_size, 1, it, true);
}
}
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void ListEntitiesEventResponse::dump_to(std::string &out) const {
@@ -9610,6 +9960,11 @@ void ListEntitiesEventResponse::dump_to(std::string &out) const {
out.append("'").append(it).append("'");
out.append("\n");
}
out.append(" device_id: ");
sprintf(buffer, "%" PRIu32, this->device_id);
out.append(buffer);
out.append("\n");
out.append("}");
}
#endif
@@ -9678,6 +10033,10 @@ bool ListEntitiesValveResponse::decode_varint(uint32_t field_id, ProtoVarInt val
this->supports_stop = value.as_bool();
return true;
}
case 12: {
this->device_id = value.as_uint32();
return true;
}
default:
return false;
}
@@ -9730,6 +10089,7 @@ void ListEntitiesValveResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_bool(9, this->assumed_state);
buffer.encode_bool(10, this->supports_position);
buffer.encode_bool(11, this->supports_stop);
buffer.encode_uint32(12, this->device_id);
}
void ListEntitiesValveResponse::calculate_size(uint32_t &total_size) const {
ProtoSize::add_string_field(total_size, 1, this->object_id, false);
@@ -9743,6 +10103,7 @@ void ListEntitiesValveResponse::calculate_size(uint32_t &total_size) const {
ProtoSize::add_bool_field(total_size, 1, this->assumed_state, false);
ProtoSize::add_bool_field(total_size, 1, this->supports_position, false);
ProtoSize::add_bool_field(total_size, 1, this->supports_stop, false);
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void ListEntitiesValveResponse::dump_to(std::string &out) const {
@@ -9792,6 +10153,11 @@ void ListEntitiesValveResponse::dump_to(std::string &out) const {
out.append(" supports_stop: ");
out.append(YESNO(this->supports_stop));
out.append("\n");
out.append(" device_id: ");
sprintf(buffer, "%" PRIu32, this->device_id);
out.append(buffer);
out.append("\n");
out.append("}");
}
#endif
@@ -9923,6 +10289,10 @@ bool ListEntitiesDateTimeResponse::decode_varint(uint32_t field_id, ProtoVarInt
this->entity_category = value.as_enum<enums::EntityCategory>();
return true;
}
case 8: {
this->device_id = value.as_uint32();
return true;
}
default:
return false;
}
@@ -9967,6 +10337,7 @@ void ListEntitiesDateTimeResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_string(5, this->icon);
buffer.encode_bool(6, this->disabled_by_default);
buffer.encode_enum<enums::EntityCategory>(7, this->entity_category);
buffer.encode_uint32(8, this->device_id);
}
void ListEntitiesDateTimeResponse::calculate_size(uint32_t &total_size) const {
ProtoSize::add_string_field(total_size, 1, this->object_id, false);
@@ -9976,6 +10347,7 @@ void ListEntitiesDateTimeResponse::calculate_size(uint32_t &total_size) const {
ProtoSize::add_string_field(total_size, 1, this->icon, false);
ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false);
ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->entity_category), false);
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void ListEntitiesDateTimeResponse::dump_to(std::string &out) const {
@@ -10009,6 +10381,11 @@ void ListEntitiesDateTimeResponse::dump_to(std::string &out) const {
out.append(" entity_category: ");
out.append(proto_enum_to_string<enums::EntityCategory>(this->entity_category));
out.append("\n");
out.append(" device_id: ");
sprintf(buffer, "%" PRIu32, this->device_id);
out.append(buffer);
out.append("\n");
out.append("}");
}
#endif
@@ -10114,6 +10491,10 @@ bool ListEntitiesUpdateResponse::decode_varint(uint32_t field_id, ProtoVarInt va
this->entity_category = value.as_enum<enums::EntityCategory>();
return true;
}
case 9: {
this->device_id = value.as_uint32();
return true;
}
default:
return false;
}
@@ -10163,6 +10544,7 @@ void ListEntitiesUpdateResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_bool(6, this->disabled_by_default);
buffer.encode_enum<enums::EntityCategory>(7, this->entity_category);
buffer.encode_string(8, this->device_class);
buffer.encode_uint32(9, this->device_id);
}
void ListEntitiesUpdateResponse::calculate_size(uint32_t &total_size) const {
ProtoSize::add_string_field(total_size, 1, this->object_id, false);
@@ -10173,6 +10555,7 @@ void ListEntitiesUpdateResponse::calculate_size(uint32_t &total_size) const {
ProtoSize::add_bool_field(total_size, 1, this->disabled_by_default, false);
ProtoSize::add_enum_field(total_size, 1, static_cast<uint32_t>(this->entity_category), false);
ProtoSize::add_string_field(total_size, 1, this->device_class, false);
ProtoSize::add_uint32_field(total_size, 1, this->device_id, false);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void ListEntitiesUpdateResponse::dump_to(std::string &out) const {
@@ -10210,6 +10593,11 @@ void ListEntitiesUpdateResponse::dump_to(std::string &out) const {
out.append(" device_class: ");
out.append("'").append(this->device_class).append("'");
out.append("\n");
out.append(" device_id: ");
sprintf(buffer, "%" PRIu32, this->device_id);
out.append(buffer);
out.append("\n");
out.append("}");
}
#endif

View File

@@ -264,6 +264,7 @@ class InfoResponseProtoMessage : public ProtoMessage {
bool disabled_by_default{false};
std::string icon{};
enums::EntityCategory entity_category{};
uint32_t device_id{0};
protected:
};
@@ -415,10 +416,39 @@ class DeviceInfoRequest : public ProtoMessage {
protected:
};
class AreaInfo : public ProtoMessage {
public:
uint32_t area_id{0};
std::string name{};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class DeviceInfo : public ProtoMessage {
public:
uint32_t device_id{0};
std::string name{};
uint32_t area_id{0};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class DeviceInfoResponse : public ProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 10;
static constexpr uint16_t ESTIMATED_SIZE = 129;
static constexpr uint16_t ESTIMATED_SIZE = 219;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "device_info_response"; }
#endif
@@ -441,6 +471,9 @@ class DeviceInfoResponse : public ProtoMessage {
std::string suggested_area{};
std::string bluetooth_mac_address{};
bool api_encryption_supported{false};
std::vector<DeviceInfo> devices{};
std::vector<AreaInfo> areas{};
AreaInfo area{};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
@@ -493,7 +526,7 @@ class SubscribeStatesRequest : public ProtoMessage {
class ListEntitiesBinarySensorResponse : public InfoResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 12;
static constexpr uint16_t ESTIMATED_SIZE = 56;
static constexpr uint16_t ESTIMATED_SIZE = 60;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_binary_sensor_response"; }
#endif
@@ -532,7 +565,7 @@ class BinarySensorStateResponse : public StateResponseProtoMessage {
class ListEntitiesCoverResponse : public InfoResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 13;
static constexpr uint16_t ESTIMATED_SIZE = 62;
static constexpr uint16_t ESTIMATED_SIZE = 66;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_cover_response"; }
#endif
@@ -601,7 +634,7 @@ class CoverCommandRequest : public ProtoMessage {
class ListEntitiesFanResponse : public InfoResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 14;
static constexpr uint16_t ESTIMATED_SIZE = 73;
static constexpr uint16_t ESTIMATED_SIZE = 77;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_fan_response"; }
#endif
@@ -679,7 +712,7 @@ class FanCommandRequest : public ProtoMessage {
class ListEntitiesLightResponse : public InfoResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 15;
static constexpr uint16_t ESTIMATED_SIZE = 85;
static constexpr uint16_t ESTIMATED_SIZE = 90;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_light_response"; }
#endif
@@ -780,7 +813,7 @@ class LightCommandRequest : public ProtoMessage {
class ListEntitiesSensorResponse : public InfoResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 16;
static constexpr uint16_t ESTIMATED_SIZE = 73;
static constexpr uint16_t ESTIMATED_SIZE = 77;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_sensor_response"; }
#endif
@@ -823,7 +856,7 @@ class SensorStateResponse : public StateResponseProtoMessage {
class ListEntitiesSwitchResponse : public InfoResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 17;
static constexpr uint16_t ESTIMATED_SIZE = 56;
static constexpr uint16_t ESTIMATED_SIZE = 60;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_switch_response"; }
#endif
@@ -880,7 +913,7 @@ class SwitchCommandRequest : public ProtoMessage {
class ListEntitiesTextSensorResponse : public InfoResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 18;
static constexpr uint16_t ESTIMATED_SIZE = 54;
static constexpr uint16_t ESTIMATED_SIZE = 58;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_text_sensor_response"; }
#endif
@@ -1196,7 +1229,7 @@ class ExecuteServiceRequest : public ProtoMessage {
class ListEntitiesCameraResponse : public InfoResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 43;
static constexpr uint16_t ESTIMATED_SIZE = 45;
static constexpr uint16_t ESTIMATED_SIZE = 49;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_camera_response"; }
#endif
@@ -1253,7 +1286,7 @@ class CameraImageRequest : public ProtoMessage {
class ListEntitiesClimateResponse : public InfoResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 46;
static constexpr uint16_t ESTIMATED_SIZE = 151;
static constexpr uint16_t ESTIMATED_SIZE = 156;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_climate_response"; }
#endif
@@ -1362,7 +1395,7 @@ class ClimateCommandRequest : public ProtoMessage {
class ListEntitiesNumberResponse : public InfoResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 49;
static constexpr uint16_t ESTIMATED_SIZE = 80;
static constexpr uint16_t ESTIMATED_SIZE = 84;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_number_response"; }
#endif
@@ -1423,7 +1456,7 @@ class NumberCommandRequest : public ProtoMessage {
class ListEntitiesSelectResponse : public InfoResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 52;
static constexpr uint16_t ESTIMATED_SIZE = 63;
static constexpr uint16_t ESTIMATED_SIZE = 67;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_select_response"; }
#endif
@@ -1481,7 +1514,7 @@ class SelectCommandRequest : public ProtoMessage {
class ListEntitiesSirenResponse : public InfoResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 55;
static constexpr uint16_t ESTIMATED_SIZE = 67;
static constexpr uint16_t ESTIMATED_SIZE = 71;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_siren_response"; }
#endif
@@ -1547,7 +1580,7 @@ class SirenCommandRequest : public ProtoMessage {
class ListEntitiesLockResponse : public InfoResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 58;
static constexpr uint16_t ESTIMATED_SIZE = 60;
static constexpr uint16_t ESTIMATED_SIZE = 64;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_lock_response"; }
#endif
@@ -1609,7 +1642,7 @@ class LockCommandRequest : public ProtoMessage {
class ListEntitiesButtonResponse : public InfoResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 61;
static constexpr uint16_t ESTIMATED_SIZE = 54;
static constexpr uint16_t ESTIMATED_SIZE = 58;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_button_response"; }
#endif
@@ -1662,7 +1695,7 @@ class MediaPlayerSupportedFormat : public ProtoMessage {
class ListEntitiesMediaPlayerResponse : public InfoResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 63;
static constexpr uint16_t ESTIMATED_SIZE = 81;
static constexpr uint16_t ESTIMATED_SIZE = 85;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_media_player_response"; }
#endif
@@ -2532,7 +2565,7 @@ class VoiceAssistantSetConfiguration : public ProtoMessage {
class ListEntitiesAlarmControlPanelResponse : public InfoResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 94;
static constexpr uint16_t ESTIMATED_SIZE = 53;
static constexpr uint16_t ESTIMATED_SIZE = 57;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_alarm_control_panel_response"; }
#endif
@@ -2592,7 +2625,7 @@ class AlarmControlPanelCommandRequest : public ProtoMessage {
class ListEntitiesTextResponse : public InfoResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 97;
static constexpr uint16_t ESTIMATED_SIZE = 64;
static constexpr uint16_t ESTIMATED_SIZE = 68;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_text_response"; }
#endif
@@ -2653,7 +2686,7 @@ class TextCommandRequest : public ProtoMessage {
class ListEntitiesDateResponse : public InfoResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 100;
static constexpr uint16_t ESTIMATED_SIZE = 45;
static constexpr uint16_t ESTIMATED_SIZE = 49;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_date_response"; }
#endif
@@ -2713,7 +2746,7 @@ class DateCommandRequest : public ProtoMessage {
class ListEntitiesTimeResponse : public InfoResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 103;
static constexpr uint16_t ESTIMATED_SIZE = 45;
static constexpr uint16_t ESTIMATED_SIZE = 49;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_time_response"; }
#endif
@@ -2773,7 +2806,7 @@ class TimeCommandRequest : public ProtoMessage {
class ListEntitiesEventResponse : public InfoResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 107;
static constexpr uint16_t ESTIMATED_SIZE = 72;
static constexpr uint16_t ESTIMATED_SIZE = 76;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_event_response"; }
#endif
@@ -2811,7 +2844,7 @@ class EventResponse : public StateResponseProtoMessage {
class ListEntitiesValveResponse : public InfoResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 109;
static constexpr uint16_t ESTIMATED_SIZE = 60;
static constexpr uint16_t ESTIMATED_SIZE = 64;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_valve_response"; }
#endif
@@ -2873,7 +2906,7 @@ class ValveCommandRequest : public ProtoMessage {
class ListEntitiesDateTimeResponse : public InfoResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 112;
static constexpr uint16_t ESTIMATED_SIZE = 45;
static constexpr uint16_t ESTIMATED_SIZE = 49;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_date_time_response"; }
#endif
@@ -2928,7 +2961,7 @@ class DateTimeCommandRequest : public ProtoMessage {
class ListEntitiesUpdateResponse : public InfoResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 116;
static constexpr uint16_t ESTIMATED_SIZE = 54;
static constexpr uint16_t ESTIMATED_SIZE = 58;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_update_response"; }
#endif

View File

@@ -60,8 +60,8 @@ from esphome.const import (
DEVICE_CLASS_WINDOW,
)
from esphome.core import CORE, coroutine_with_priority
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
from esphome.cpp_generator import MockObjClass
from esphome.cpp_helpers import setup_entity
from esphome.util import Registry
CODEOWNERS = ["@esphome/core"]
@@ -148,6 +148,7 @@ BinarySensorCondition = binary_sensor_ns.class_("BinarySensorCondition", Conditi
# Filters
Filter = binary_sensor_ns.class_("Filter")
TimeoutFilter = binary_sensor_ns.class_("TimeoutFilter", Filter, cg.Component)
DelayedOnOffFilter = binary_sensor_ns.class_("DelayedOnOffFilter", Filter, cg.Component)
DelayedOnFilter = binary_sensor_ns.class_("DelayedOnFilter", Filter, cg.Component)
DelayedOffFilter = binary_sensor_ns.class_("DelayedOffFilter", Filter, cg.Component)
@@ -171,6 +172,19 @@ async def invert_filter_to_code(config, filter_id):
return cg.new_Pvariable(filter_id)
@register_filter(
"timeout",
TimeoutFilter,
cv.templatable(cv.positive_time_period_milliseconds),
)
async def timeout_filter_to_code(config, filter_id):
var = cg.new_Pvariable(filter_id)
await cg.register_component(var, {})
template_ = await cg.templatable(config, [], cg.uint32)
cg.add(var.set_timeout_value(template_))
return var
@register_filter(
"delayed_on_off",
DelayedOnOffFilter,
@@ -491,6 +505,9 @@ _BINARY_SENSOR_SCHEMA = (
)
_BINARY_SENSOR_SCHEMA.add_extra(entity_duplicate_validator("binary_sensor"))
def binary_sensor_schema(
class_: MockObjClass = cv.UNDEFINED,
*,
@@ -521,7 +538,7 @@ BINARY_SENSOR_SCHEMA.add_extra(cv.deprecated_schema_constant("binary_sensor"))
async def setup_binary_sensor_core_(var, config):
await setup_entity(var, config)
await setup_entity(var, config, "binary_sensor")
if (device_class := config.get(CONF_DEVICE_CLASS)) is not None:
cg.add(var.set_device_class(device_class))

View File

@@ -25,6 +25,12 @@ void Filter::input(bool value) {
}
}
void TimeoutFilter::input(bool value) {
this->set_timeout("timeout", this->timeout_delay_.value(), [this]() { this->parent_->invalidate_state(); });
// we do not de-dup here otherwise changes from invalid to valid state will not be output
this->output(value);
}
optional<bool> DelayedOnOffFilter::new_value(bool value) {
if (value) {
this->set_timeout("ON_OFF", this->on_delay_.value(), [this]() { this->output(true); });

View File

@@ -16,7 +16,7 @@ class Filter {
public:
virtual optional<bool> new_value(bool value) = 0;
void input(bool value);
virtual void input(bool value);
void output(bool value);
@@ -28,6 +28,16 @@ class Filter {
Deduplicator<bool> dedup_;
};
class TimeoutFilter : public Filter, public Component {
public:
optional<bool> new_value(bool value) override { return value; }
void input(bool value) override;
template<typename T> void set_timeout_value(T timeout) { this->timeout_delay_ = timeout; }
protected:
TemplatableValue<uint32_t> timeout_delay_{};
};
class DelayedOnOffFilter : public Filter, public Component {
public:
optional<bool> new_value(bool value) override;

View File

@@ -18,8 +18,8 @@ from esphome.const import (
DEVICE_CLASS_UPDATE,
)
from esphome.core import CORE, coroutine_with_priority
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
from esphome.cpp_generator import MockObjClass
from esphome.cpp_helpers import setup_entity
CODEOWNERS = ["@esphome/core"]
IS_PLATFORM_COMPONENT = True
@@ -61,6 +61,9 @@ _BUTTON_SCHEMA = (
)
_BUTTON_SCHEMA.add_extra(entity_duplicate_validator("button"))
def button_schema(
class_: MockObjClass,
*,
@@ -87,7 +90,7 @@ BUTTON_SCHEMA.add_extra(cv.deprecated_schema_constant("button"))
async def setup_button_core_(var, config):
await setup_entity(var, config)
await setup_entity(var, config, "button")
for conf in config.get(CONF_ON_PRESS, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)

View File

@@ -48,8 +48,8 @@ from esphome.const import (
CONF_WEB_SERVER,
)
from esphome.core import CORE, coroutine_with_priority
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
from esphome.cpp_generator import MockObjClass
from esphome.cpp_helpers import setup_entity
IS_PLATFORM_COMPONENT = True
@@ -247,6 +247,9 @@ _CLIMATE_SCHEMA = (
)
_CLIMATE_SCHEMA.add_extra(entity_duplicate_validator("climate"))
def climate_schema(
class_: MockObjClass,
*,
@@ -273,7 +276,7 @@ CLIMATE_SCHEMA.add_extra(cv.deprecated_schema_constant("climate"))
async def setup_climate_core_(var, config):
await setup_entity(var, config)
await setup_entity(var, config, "climate")
visual = config[CONF_VISUAL]
if (min_temp := visual.get(CONF_MIN_TEMPERATURE)) is not None:

View File

@@ -33,8 +33,8 @@ from esphome.const import (
DEVICE_CLASS_WINDOW,
)
from esphome.core import CORE, coroutine_with_priority
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
from esphome.cpp_generator import MockObjClass
from esphome.cpp_helpers import setup_entity
IS_PLATFORM_COMPONENT = True
@@ -126,6 +126,9 @@ _COVER_SCHEMA = (
)
_COVER_SCHEMA.add_extra(entity_duplicate_validator("cover"))
def cover_schema(
class_: MockObjClass,
*,
@@ -154,7 +157,7 @@ COVER_SCHEMA.add_extra(cv.deprecated_schema_constant("cover"))
async def setup_cover_core_(var, config):
await setup_entity(var, config)
await setup_entity(var, config, "cover")
if (device_class := config.get(CONF_DEVICE_CLASS)) is not None:
cg.add(var.set_device_class(device_class))

View File

@@ -22,8 +22,8 @@ from esphome.const import (
CONF_YEAR,
)
from esphome.core import CORE, coroutine_with_priority
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
from esphome.cpp_generator import MockObjClass
from esphome.cpp_helpers import setup_entity
CODEOWNERS = ["@rfdarter", "@jesserockz"]
@@ -84,6 +84,8 @@ _DATETIME_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(
.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
).add_extra(_validate_time_present)
_DATETIME_SCHEMA.add_extra(entity_duplicate_validator("datetime"))
def date_schema(class_: MockObjClass) -> cv.Schema:
schema = cv.Schema(
@@ -133,7 +135,7 @@ def datetime_schema(class_: MockObjClass) -> cv.Schema:
async def setup_datetime_core_(var, config):
await setup_entity(var, config)
await setup_entity(var, config, "datetime")
if (mqtt_id := config.get(CONF_MQTT_ID)) is not None:
mqtt_ = cg.new_Pvariable(mqtt_id, var)

View File

@@ -455,7 +455,7 @@ CONFIG_SCHEMA = cv.Schema(
CONF_NAME: "Demo Plain Sensor",
},
{
CONF_NAME: "Demo Temperature Sensor",
CONF_NAME: "Demo Temperature Sensor 1",
CONF_UNIT_OF_MEASUREMENT: UNIT_CELSIUS,
CONF_ICON: ICON_THERMOMETER,
CONF_ACCURACY_DECIMALS: 1,
@@ -463,7 +463,7 @@ CONFIG_SCHEMA = cv.Schema(
CONF_STATE_CLASS: STATE_CLASS_MEASUREMENT,
},
{
CONF_NAME: "Demo Temperature Sensor",
CONF_NAME: "Demo Temperature Sensor 2",
CONF_UNIT_OF_MEASUREMENT: UNIT_CELSIUS,
CONF_ICON: ICON_THERMOMETER,
CONF_ACCURACY_DECIMALS: 1,

View File

@@ -4,6 +4,7 @@
#include "ble_event_pool.h"
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <esp_bt.h>
@@ -516,13 +517,12 @@ void ESP32BLE::dump_config() {
break;
}
ESP_LOGCONFIG(TAG,
"ESP32 BLE:\n"
" MAC address: %02X:%02X:%02X:%02X:%02X:%02X\n"
"BLE:\n"
" MAC address: %s\n"
" IO Capability: %s",
mac_address[0], mac_address[1], mac_address[2], mac_address[3], mac_address[4], mac_address[5],
io_capability_s);
format_mac_address_pretty(mac_address).c_str(), io_capability_s);
} else {
ESP_LOGCONFIG(TAG, "ESP32 BLE: bluetooth stack is not enabled");
ESP_LOGCONFIG(TAG, "Bluetooth stack is not enabled");
}
}

View File

@@ -19,7 +19,7 @@ from esphome.const import (
CONF_VSYNC_PIN,
)
from esphome.core import CORE
from esphome.cpp_helpers import setup_entity
from esphome.core.entity_helpers import setup_entity
DEPENDENCIES = ["esp32"]
@@ -284,7 +284,7 @@ SETTERS = {
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await setup_entity(var, config)
await setup_entity(var, config, "camera")
await cg.register_component(var, config)
for key, setter in SETTERS.items():

View File

@@ -0,0 +1,5 @@
import esphome.config_validation as cv
CONFIG_SCHEMA = cv.invalid(
"The esp32_hall component has been removed as of ESPHome 2025.7.0. See https://github.com/esphome/esphome/pull/9117 for details."
)

View File

@@ -18,8 +18,8 @@ from esphome.const import (
DEVICE_CLASS_MOTION,
)
from esphome.core import CORE, coroutine_with_priority
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
from esphome.cpp_generator import MockObjClass
from esphome.cpp_helpers import setup_entity
CODEOWNERS = ["@nohat"]
IS_PLATFORM_COMPONENT = True
@@ -59,6 +59,9 @@ _EVENT_SCHEMA = (
)
_EVENT_SCHEMA.add_extra(entity_duplicate_validator("event"))
def event_schema(
class_: MockObjClass = cv.UNDEFINED,
*,
@@ -88,7 +91,7 @@ EVENT_SCHEMA.add_extra(cv.deprecated_schema_constant("event"))
async def setup_event_core_(var, config, *, event_types: list[str]):
await setup_entity(var, config)
await setup_entity(var, config, "event")
for conf in config.get(CONF_ON_EVENT, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)

View File

@@ -32,7 +32,7 @@ from esphome.const import (
CONF_WEB_SERVER,
)
from esphome.core import CORE, coroutine_with_priority
from esphome.cpp_helpers import setup_entity
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
IS_PLATFORM_COMPONENT = True
@@ -161,6 +161,9 @@ _FAN_SCHEMA = (
)
_FAN_SCHEMA.add_extra(entity_duplicate_validator("fan"))
def fan_schema(
class_: cg.Pvariable,
*,
@@ -225,7 +228,7 @@ def validate_preset_modes(value):
async def setup_fan_core_(var, config):
await setup_entity(var, config)
await setup_entity(var, config, "fan")
cg.add(var.set_restore_mode(config[CONF_RESTORE_MODE]))

View File

@@ -8,6 +8,8 @@
#include "esphome/components/sensor/sensor.h"
#endif
#include "esphome/core/application.h"
#define highbyte(val) (uint8_t)((val) >> 8)
#define lowbyte(val) (uint8_t)((val) &0xff)
@@ -73,9 +75,9 @@ void LD2410Component::dump_config() {
#endif
this->read_all_info();
ESP_LOGCONFIG(TAG,
" Throttle_ : %ums\n"
" MAC Address : %s\n"
" Firmware Version : %s",
" Throttle: %ums\n"
" MAC address: %s\n"
" Firmware version: %s",
this->throttle_, const_cast<char *>(this->mac_.c_str()), const_cast<char *>(this->version_.c_str()));
}
@@ -153,7 +155,7 @@ void LD2410Component::handle_periodic_data_(uint8_t *buffer, int len) {
/*
Reduce data update rate to prevent home assistant database size grow fast
*/
int32_t current_millis = millis();
int32_t current_millis = App.get_loop_component_start_time();
if (current_millis - last_periodic_millis_ < this->throttle_)
return;
last_periodic_millis_ = current_millis;
@@ -299,21 +301,6 @@ const char MAC_FMT[] = "%02X:%02X:%02X:%02X:%02X:%02X";
const std::string UNKNOWN_MAC("unknown");
const std::string NO_MAC("08:05:04:03:02:01");
std::string format_mac(uint8_t *buffer) {
std::string::size_type mac_size = 256;
std::string mac;
do {
mac.resize(mac_size + 1);
mac_size = std::snprintf(&mac[0], mac.size(), MAC_FMT, buffer[10], buffer[11], buffer[12], buffer[13], buffer[14],
buffer[15]);
} while (mac_size + 1 > mac.size());
mac.resize(mac_size);
if (mac == NO_MAC) {
return UNKNOWN_MAC;
}
return mac;
}
#ifdef USE_NUMBER
std::function<void(void)> set_number_value(number::Number *n, float value) {
float normalized_value = value * 1.0;
@@ -328,40 +315,40 @@ std::function<void(void)> set_number_value(number::Number *n, float value) {
bool LD2410Component::handle_ack_data_(uint8_t *buffer, int len) {
ESP_LOGV(TAG, "Handling ACK DATA for COMMAND %02X", buffer[COMMAND]);
if (len < 10) {
ESP_LOGE(TAG, "Error with last command : incorrect length");
ESP_LOGE(TAG, "Invalid length");
return true;
}
if (buffer[0] != 0xFD || buffer[1] != 0xFC || buffer[2] != 0xFB || buffer[3] != 0xFA) { // check 4 frame start bytes
ESP_LOGE(TAG, "Error with last command : incorrect Header");
ESP_LOGE(TAG, "Invalid header");
return true;
}
if (buffer[COMMAND_STATUS] != 0x01) {
ESP_LOGE(TAG, "Error with last command : status != 0x01");
ESP_LOGE(TAG, "Invalid status");
return true;
}
if (this->two_byte_to_int_(buffer[8], buffer[9]) != 0x00) {
ESP_LOGE(TAG, "Error with last command , last buffer was: %u , %u", buffer[8], buffer[9]);
ESP_LOGE(TAG, "Invalid command: %u, %u", buffer[8], buffer[9]);
return true;
}
switch (buffer[COMMAND]) {
case lowbyte(CMD_ENABLE_CONF):
ESP_LOGV(TAG, "Handled Enable conf command");
ESP_LOGV(TAG, "Enable conf");
break;
case lowbyte(CMD_DISABLE_CONF):
ESP_LOGV(TAG, "Handled Disabled conf command");
ESP_LOGV(TAG, "Disabled conf");
break;
case lowbyte(CMD_SET_BAUD_RATE):
ESP_LOGV(TAG, "Handled baud rate change command");
ESP_LOGV(TAG, "Baud rate change");
#ifdef USE_SELECT
if (this->baud_rate_select_ != nullptr) {
ESP_LOGE(TAG, "Change baud rate component config to %s and reinstall", this->baud_rate_select_->state.c_str());
ESP_LOGE(TAG, "Configure baud rate to %s and reinstall", this->baud_rate_select_->state.c_str());
}
#endif
break;
case lowbyte(CMD_VERSION):
this->version_ = format_version(buffer);
ESP_LOGV(TAG, "FW Version is: %s", const_cast<char *>(this->version_.c_str()));
ESP_LOGV(TAG, "Firmware version: %s", const_cast<char *>(this->version_.c_str()));
#ifdef USE_TEXT_SENSOR
if (this->version_text_sensor_ != nullptr) {
this->version_text_sensor_->publish_state(this->version_);
@@ -371,7 +358,7 @@ bool LD2410Component::handle_ack_data_(uint8_t *buffer, int len) {
case lowbyte(CMD_QUERY_DISTANCE_RESOLUTION): {
std::string distance_resolution =
DISTANCE_RESOLUTION_INT_TO_ENUM.at(this->two_byte_to_int_(buffer[10], buffer[11]));
ESP_LOGV(TAG, "Distance resolution is: %s", const_cast<char *>(distance_resolution.c_str()));
ESP_LOGV(TAG, "Distance resolution: %s", const_cast<char *>(distance_resolution.c_str()));
#ifdef USE_SELECT
if (this->distance_resolution_select_ != nullptr &&
this->distance_resolution_select_->state != distance_resolution) {
@@ -383,9 +370,9 @@ bool LD2410Component::handle_ack_data_(uint8_t *buffer, int len) {
this->light_function_ = LIGHT_FUNCTION_INT_TO_ENUM.at(buffer[10]);
this->light_threshold_ = buffer[11] * 1.0;
this->out_pin_level_ = OUT_PIN_LEVEL_INT_TO_ENUM.at(buffer[12]);
ESP_LOGV(TAG, "Light function is: %s", const_cast<char *>(this->light_function_.c_str()));
ESP_LOGV(TAG, "Light threshold is: %f", this->light_threshold_);
ESP_LOGV(TAG, "Out pin level is: %s", const_cast<char *>(this->out_pin_level_.c_str()));
ESP_LOGV(TAG, "Light function: %s", const_cast<char *>(this->light_function_.c_str()));
ESP_LOGV(TAG, "Light threshold: %f", this->light_threshold_);
ESP_LOGV(TAG, "Out pin level: %s", const_cast<char *>(this->out_pin_level_.c_str()));
#ifdef USE_SELECT
if (this->light_function_select_ != nullptr && this->light_function_select_->state != this->light_function_) {
this->light_function_select_->publish_state(this->light_function_);
@@ -406,11 +393,11 @@ bool LD2410Component::handle_ack_data_(uint8_t *buffer, int len) {
if (len < 20) {
return false;
}
this->mac_ = format_mac(buffer);
ESP_LOGV(TAG, "MAC Address is: %s", const_cast<char *>(this->mac_.c_str()));
this->mac_ = format_mac_address_pretty(&buffer[10]);
ESP_LOGV(TAG, "MAC address: %s", this->mac_.c_str());
#ifdef USE_TEXT_SENSOR
if (this->mac_text_sensor_ != nullptr) {
this->mac_text_sensor_->publish_state(this->mac_);
this->mac_text_sensor_->publish_state(this->mac_ == NO_MAC ? UNKNOWN_MAC : this->mac_);
}
#endif
#ifdef USE_SWITCH
@@ -420,19 +407,19 @@ bool LD2410Component::handle_ack_data_(uint8_t *buffer, int len) {
#endif
break;
case lowbyte(CMD_GATE_SENS):
ESP_LOGV(TAG, "Handled sensitivity command");
ESP_LOGV(TAG, "Sensitivity");
break;
case lowbyte(CMD_BLUETOOTH):
ESP_LOGV(TAG, "Handled bluetooth command");
ESP_LOGV(TAG, "Bluetooth");
break;
case lowbyte(CMD_SET_DISTANCE_RESOLUTION):
ESP_LOGV(TAG, "Handled set distance resolution command");
ESP_LOGV(TAG, "Set distance resolution");
break;
case lowbyte(CMD_SET_LIGHT_CONTROL):
ESP_LOGV(TAG, "Handled set light control command");
ESP_LOGV(TAG, "Set light control");
break;
case lowbyte(CMD_BT_PASSWORD):
ESP_LOGV(TAG, "Handled set bluetooth password command");
ESP_LOGV(TAG, "Set bluetooth password");
break;
case lowbyte(CMD_QUERY): // Query parameters response
{
@@ -532,7 +519,7 @@ void LD2410Component::set_baud_rate(const std::string &state) {
void LD2410Component::set_bluetooth_password(const std::string &password) {
if (password.length() != 6) {
ESP_LOGE(TAG, "set_bluetooth_password(): invalid password length, must be exactly 6 chars '%s'", password.c_str());
ESP_LOGE(TAG, "Password must be exactly 6 chars");
return;
}
this->set_config_mode_(true);
@@ -544,7 +531,7 @@ void LD2410Component::set_bluetooth_password(const std::string &password) {
void LD2410Component::set_engineering_mode(bool enable) {
this->set_config_mode_(true);
last_engineering_mode_change_millis_ = millis();
last_engineering_mode_change_millis_ = App.get_loop_component_start_time();
uint8_t cmd = enable ? CMD_ENABLE_ENG : CMD_DISABLE_ENG;
this->send_command_(cmd, nullptr, 0);
this->set_config_mode_(false);

View File

@@ -1,4 +1,5 @@
#include "ld2420.h"
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
/*
@@ -40,7 +41,7 @@ There are three documented parameters for modes:
00 04 = Energy output mode
This mode outputs detailed signal energy values for each gate and the target distance.
The data format consist of the following.
Header HH, Length LL, Persence PP, Distance DD, 16 Gate Energies EE, Footer FF
Header HH, Length LL, Presence PP, Distance DD, 16 Gate Energies EE, Footer FF
HH HH HH HH LL LL PP DD DD EE EE .. 16x .. FF FF FF FF
F4 F3 F2 F1 23 00 00 00 00 00 00 .. .. .. .. F8 F7 F6 F5
00 00 = debug output mode
@@ -67,10 +68,10 @@ float LD2420Component::get_setup_priority() const { return setup_priority::BUS;
void LD2420Component::dump_config() {
ESP_LOGCONFIG(TAG,
"LD2420:\n"
" Firmware Version : %7s\n"
"LD2420 Number:",
" Firmware version: %7s",
this->ld2420_firmware_ver_);
#ifdef USE_NUMBER
ESP_LOGCONFIG(TAG, "Number:");
LOG_NUMBER(TAG, " Gate Timeout:", this->gate_timeout_number_);
LOG_NUMBER(TAG, " Gate Max Distance:", this->max_gate_distance_number_);
LOG_NUMBER(TAG, " Gate Min Distance:", this->min_gate_distance_number_);
@@ -86,10 +87,10 @@ void LD2420Component::dump_config() {
LOG_BUTTON(TAG, " Factory Reset:", this->factory_reset_button_);
LOG_BUTTON(TAG, " Restart Module:", this->restart_module_button_);
#endif
ESP_LOGCONFIG(TAG, "LD2420 Select:");
ESP_LOGCONFIG(TAG, "Select:");
LOG_SELECT(TAG, " Operating Mode", this->operating_selector_);
if (this->get_firmware_int_(ld2420_firmware_ver_) < CALIBRATE_VERSION_MIN) {
ESP_LOGW(TAG, "LD2420 Firmware Version %s and older are only supported in Simple Mode", ld2420_firmware_ver_);
if (LD2420Component::get_firmware_int(this->ld2420_firmware_ver_) < CALIBRATE_VERSION_MIN) {
ESP_LOGW(TAG, "Firmware version %s and older supports Simple Mode only", this->ld2420_firmware_ver_);
}
}
@@ -102,7 +103,7 @@ uint8_t LD2420Component::calc_checksum(void *data, size_t size) {
return checksum;
}
int LD2420Component::get_firmware_int_(const char *version_string) {
int LD2420Component::get_firmware_int(const char *version_string) {
std::string version_str = version_string;
if (version_str[0] == 'v') {
version_str = version_str.substr(1);
@@ -115,7 +116,7 @@ int LD2420Component::get_firmware_int_(const char *version_string) {
void LD2420Component::setup() {
ESP_LOGCONFIG(TAG, "Running setup");
if (this->set_config_mode(true) == LD2420_ERROR_TIMEOUT) {
ESP_LOGE(TAG, "LD2420 module has failed to respond, check baud rate and serial connections.");
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
this->mark_failed();
return;
}
@@ -127,7 +128,7 @@ void LD2420Component::setup() {
const char *pfw = this->ld2420_firmware_ver_;
std::string fw_str(pfw);
for (auto &listener : listeners_) {
for (auto &listener : this->listeners_) {
listener->on_fw_version(fw_str);
}
@@ -137,11 +138,11 @@ void LD2420Component::setup() {
}
memcpy(&this->new_config, &this->current_config, sizeof(this->current_config));
if (get_firmware_int_(ld2420_firmware_ver_) < CALIBRATE_VERSION_MIN) {
if (LD2420Component::get_firmware_int(this->ld2420_firmware_ver_) < CALIBRATE_VERSION_MIN) {
this->set_operating_mode(OP_SIMPLE_MODE_STRING);
this->operating_selector_->publish_state(OP_SIMPLE_MODE_STRING);
this->set_mode_(CMD_SYSTEM_MODE_SIMPLE);
ESP_LOGW(TAG, "LD2420 Frimware Version %s and older are only supported in Simple Mode", ld2420_firmware_ver_);
ESP_LOGW(TAG, "Firmware version %s and older supports Simple Mode only", this->ld2420_firmware_ver_);
} else {
this->set_mode_(CMD_SYSTEM_MODE_ENERGY);
this->operating_selector_->publish_state(OP_NORMAL_MODE_STRING);
@@ -151,18 +152,17 @@ void LD2420Component::setup() {
#endif
this->set_system_mode(this->system_mode_);
this->set_config_mode(false);
ESP_LOGCONFIG(TAG, "LD2420 setup complete.");
}
void LD2420Component::apply_config_action() {
const uint8_t checksum = calc_checksum(&this->new_config, sizeof(this->new_config));
if (checksum == calc_checksum(&this->current_config, sizeof(this->current_config))) {
ESP_LOGCONFIG(TAG, "No configuration change detected");
ESP_LOGD(TAG, "No configuration change detected");
return;
}
ESP_LOGCONFIG(TAG, "Reconfiguring LD2420");
ESP_LOGD(TAG, "Reconfiguring");
if (this->set_config_mode(true) == LD2420_ERROR_TIMEOUT) {
ESP_LOGE(TAG, "LD2420 module has failed to respond, check baud rate and serial connections.");
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
this->mark_failed();
return;
}
@@ -178,13 +178,12 @@ void LD2420Component::apply_config_action() {
this->set_system_mode(this->system_mode_);
this->set_config_mode(false); // Disable config mode to save new values in LD2420 nvm
this->set_operating_mode(OP_NORMAL_MODE_STRING);
ESP_LOGCONFIG(TAG, "LD2420 reconfig complete.");
}
void LD2420Component::factory_reset_action() {
ESP_LOGCONFIG(TAG, "Setting factory defaults");
ESP_LOGD(TAG, "Setting factory defaults");
if (this->set_config_mode(true) == LD2420_ERROR_TIMEOUT) {
ESP_LOGE(TAG, "LD2420 module has failed to respond, check baud rate and serial connections.");
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
this->mark_failed();
return;
}
@@ -207,18 +206,16 @@ void LD2420Component::factory_reset_action() {
this->init_gate_config_numbers();
this->refresh_gate_config_numbers();
#endif
ESP_LOGCONFIG(TAG, "LD2420 factory reset complete.");
}
void LD2420Component::restart_module_action() {
ESP_LOGCONFIG(TAG, "Restarting LD2420 module");
ESP_LOGD(TAG, "Restarting");
this->send_module_restart();
this->set_timeout(250, [this]() {
this->set_config_mode(true);
this->set_system_mode(system_mode_);
this->set_system_mode(this->system_mode_);
this->set_config_mode(false);
});
ESP_LOGCONFIG(TAG, "LD2420 Restarted.");
}
void LD2420Component::revert_config_action() {
@@ -226,18 +223,18 @@ void LD2420Component::revert_config_action() {
#ifdef USE_NUMBER
this->init_gate_config_numbers();
#endif
ESP_LOGCONFIG(TAG, "Reverted config number edits.");
ESP_LOGD(TAG, "Reverted config number edits");
}
void LD2420Component::loop() {
// If there is a active send command do not process it here, the send command call will handle it.
if (!get_cmd_active_()) {
if (!available())
if (!this->get_cmd_active_()) {
if (!this->available())
return;
static uint8_t buffer[2048];
static uint8_t rx_data;
while (available()) {
rx_data = read();
while (this->available()) {
rx_data = this->read();
this->readline_(rx_data, buffer, sizeof(buffer));
}
}
@@ -292,7 +289,7 @@ void LD2420Component::report_gate_data() {
void LD2420Component::set_operating_mode(const std::string &state) {
// If unsupported firmware ignore mode select
if (get_firmware_int_(ld2420_firmware_ver_) >= CALIBRATE_VERSION_MIN) {
if (LD2420Component::get_firmware_int(ld2420_firmware_ver_) >= CALIBRATE_VERSION_MIN) {
this->current_operating_mode = OP_MODE_TO_UINT.at(state);
// Entering Auto Calibrate we need to clear the privoiuos data collection
this->operating_selector_->publish_state(state);
@@ -365,13 +362,13 @@ void LD2420Component::handle_energy_mode_(uint8_t *buffer, int len) {
}
// Resonable refresh rate for home assistant database size health
const int32_t current_millis = millis();
const int32_t current_millis = App.get_loop_component_start_time();
if (current_millis - this->last_periodic_millis < REFRESH_RATE_MS)
return;
this->last_periodic_millis = current_millis;
for (auto &listener : this->listeners_) {
listener->on_distance(get_distance_());
listener->on_presence(get_presence_());
listener->on_distance(this->get_distance_());
listener->on_presence(this->get_presence_());
listener->on_energy(this->gate_energy_, sizeof(this->gate_energy_) / sizeof(this->gate_energy_[0]));
}
@@ -392,9 +389,9 @@ void LD2420Component::handle_simple_mode_(const uint8_t *inbuf, int len) {
char outbuf[bufsize]{0};
while (true) {
if (inbuf[pos - 2] == 'O' && inbuf[pos - 1] == 'F' && inbuf[pos] == 'F') {
set_presence_(false);
this->set_presence_(false);
} else if (inbuf[pos - 1] == 'O' && inbuf[pos] == 'N') {
set_presence_(true);
this->set_presence_(true);
}
if (inbuf[pos] >= '0' && inbuf[pos] <= '9') {
if (index < bufsize - 1) {
@@ -411,18 +408,18 @@ void LD2420Component::handle_simple_mode_(const uint8_t *inbuf, int len) {
}
outbuf[index] = '\0';
if (index > 1)
set_distance_(strtol(outbuf, &endptr, 10));
this->set_distance_(strtol(outbuf, &endptr, 10));
if (get_mode_() == CMD_SYSTEM_MODE_SIMPLE) {
if (this->get_mode_() == CMD_SYSTEM_MODE_SIMPLE) {
// Resonable refresh rate for home assistant database size health
const int32_t current_millis = millis();
const int32_t current_millis = App.get_loop_component_start_time();
if (current_millis - this->last_normal_periodic_millis < REFRESH_RATE_MS)
return;
this->last_normal_periodic_millis = current_millis;
for (auto &listener : this->listeners_)
listener->on_distance(get_distance_());
listener->on_distance(this->get_distance_());
for (auto &listener : this->listeners_)
listener->on_presence(get_presence_());
listener->on_presence(this->get_presence_());
}
}
@@ -433,10 +430,10 @@ void LD2420Component::handle_ack_data_(uint8_t *buffer, int len) {
uint8_t data_element = 0;
uint16_t data_pos = 0;
if (this->cmd_reply_.length > CMD_MAX_BYTES) {
ESP_LOGW(TAG, "LD2420 reply - received command reply frame is corrupt, length exceeds %d bytes.", CMD_MAX_BYTES);
ESP_LOGW(TAG, "Reply frame too long");
return;
} else if (this->cmd_reply_.length < 2) {
ESP_LOGW(TAG, "LD2420 reply - received command frame is corrupt, length is less than 2 bytes.");
ESP_LOGW(TAG, "Command frame too short");
return;
}
memcpy(&this->cmd_reply_.error, &buffer[CMD_ERROR_WORD], sizeof(this->cmd_reply_.error));
@@ -447,13 +444,13 @@ void LD2420Component::handle_ack_data_(uint8_t *buffer, int len) {
this->cmd_reply_.ack = true;
switch ((uint16_t) this->cmd_reply_.command) {
case (CMD_ENABLE_CONF):
ESP_LOGD(TAG, "LD2420 reply - set config enable: CMD = %2X %s", CMD_ENABLE_CONF, result);
ESP_LOGV(TAG, "Set config enable: CMD = %2X %s", CMD_ENABLE_CONF, result);
break;
case (CMD_DISABLE_CONF):
ESP_LOGD(TAG, "LD2420 reply - set config disable: CMD = %2X %s", CMD_DISABLE_CONF, result);
ESP_LOGV(TAG, "Set config disable: CMD = %2X %s", CMD_DISABLE_CONF, result);
break;
case (CMD_READ_REGISTER):
ESP_LOGD(TAG, "LD2420 reply - read register: CMD = %2X %s", CMD_READ_REGISTER, result);
ESP_LOGV(TAG, "Read register: CMD = %2X %s", CMD_READ_REGISTER, result);
// TODO Read/Write register is not implemented yet, this will get flushed out to a proper header file
data_pos = 0x0A;
for (uint16_t index = 0; index < (CMD_REG_DATA_REPLY_SIZE * // NOLINT
@@ -465,13 +462,13 @@ void LD2420Component::handle_ack_data_(uint8_t *buffer, int len) {
}
break;
case (CMD_WRITE_REGISTER):
ESP_LOGD(TAG, "LD2420 reply - write register: CMD = %2X %s", CMD_WRITE_REGISTER, result);
ESP_LOGV(TAG, "Write register: CMD = %2X %s", CMD_WRITE_REGISTER, result);
break;
case (CMD_WRITE_ABD_PARAM):
ESP_LOGD(TAG, "LD2420 reply - write gate parameter(s): %2X %s", CMD_WRITE_ABD_PARAM, result);
ESP_LOGV(TAG, "Write gate parameter(s): %2X %s", CMD_WRITE_ABD_PARAM, result);
break;
case (CMD_READ_ABD_PARAM):
ESP_LOGD(TAG, "LD2420 reply - read gate parameter(s): %2X %s", CMD_READ_ABD_PARAM, result);
ESP_LOGV(TAG, "Read gate parameter(s): %2X %s", CMD_READ_ABD_PARAM, result);
data_pos = CMD_ABD_DATA_REPLY_START;
for (uint16_t index = 0; index < (CMD_ABD_DATA_REPLY_SIZE * // NOLINT
((buffer[CMD_FRAME_DATA_LENGTH] - 4) / CMD_ABD_DATA_REPLY_SIZE));
@@ -483,11 +480,11 @@ void LD2420Component::handle_ack_data_(uint8_t *buffer, int len) {
}
break;
case (CMD_WRITE_SYS_PARAM):
ESP_LOGD(TAG, "LD2420 reply - set system parameter(s): %2X %s", CMD_WRITE_SYS_PARAM, result);
ESP_LOGV(TAG, "Set system parameter(s): %2X %s", CMD_WRITE_SYS_PARAM, result);
break;
case (CMD_READ_VERSION):
memcpy(this->ld2420_firmware_ver_, &buffer[12], buffer[10]);
ESP_LOGD(TAG, "LD2420 reply - module firmware version: %7s %s", this->ld2420_firmware_ver_, result);
ESP_LOGV(TAG, "Firmware version: %7s %s", this->ld2420_firmware_ver_, result);
break;
default:
break;
@@ -533,7 +530,7 @@ int LD2420Component::send_cmd_from_array(CmdFrameT frame) {
}
while (!this->cmd_reply_.ack) {
while (available()) {
while (this->available()) {
this->readline_(read(), ack_buffer, sizeof(ack_buffer));
}
delay_microseconds_safe(1450);
@@ -548,7 +545,7 @@ int LD2420Component::send_cmd_from_array(CmdFrameT frame) {
if (this->cmd_reply_.ack)
retry = 0;
if (this->cmd_reply_.error > 0)
handle_cmd_error(error);
this->handle_cmd_error(error);
}
return error;
}
@@ -563,7 +560,7 @@ uint8_t LD2420Component::set_config_mode(bool enable) {
cmd_frame.data_length += sizeof(CMD_PROTOCOL_VER);
}
cmd_frame.footer = CMD_FRAME_FOOTER;
ESP_LOGD(TAG, "Sending set config %s command: %2X", enable ? "enable" : "disable", cmd_frame.command);
ESP_LOGV(TAG, "Sending set config %s command: %2X", enable ? "enable" : "disable", cmd_frame.command);
return this->send_cmd_from_array(cmd_frame);
}
@@ -576,7 +573,7 @@ void LD2420Component::ld2420_restart() {
cmd_frame.header = CMD_FRAME_HEADER;
cmd_frame.command = CMD_RESTART;
cmd_frame.footer = CMD_FRAME_FOOTER;
ESP_LOGD(TAG, "Sending restart command: %2X", cmd_frame.command);
ESP_LOGV(TAG, "Sending restart command: %2X", cmd_frame.command);
this->send_cmd_from_array(cmd_frame);
}
@@ -588,7 +585,7 @@ void LD2420Component::get_reg_value_(uint16_t reg) {
cmd_frame.data[1] = reg;
cmd_frame.data_length += 2;
cmd_frame.footer = CMD_FRAME_FOOTER;
ESP_LOGD(TAG, "Sending read register %4X command: %2X", reg, cmd_frame.command);
ESP_LOGV(TAG, "Sending read register %4X command: %2X", reg, cmd_frame.command);
this->send_cmd_from_array(cmd_frame);
}
@@ -602,11 +599,11 @@ void LD2420Component::set_reg_value(uint16_t reg, uint16_t value) {
memcpy(&cmd_frame.data[cmd_frame.data_length], &value, sizeof(CMD_REG_DATA_REPLY_SIZE));
cmd_frame.data_length += 2;
cmd_frame.footer = CMD_FRAME_FOOTER;
ESP_LOGD(TAG, "Sending write register %4X command: %2X data = %4X", reg, cmd_frame.command, value);
ESP_LOGV(TAG, "Sending write register %4X command: %2X data = %4X", reg, cmd_frame.command, value);
this->send_cmd_from_array(cmd_frame);
}
void LD2420Component::handle_cmd_error(uint8_t error) { ESP_LOGI(TAG, "Command failed: %s", ERR_MESSAGE[error]); }
void LD2420Component::handle_cmd_error(uint8_t error) { ESP_LOGE(TAG, "Command failed: %s", ERR_MESSAGE[error]); }
int LD2420Component::get_gate_threshold_(uint8_t gate) {
uint8_t error;
@@ -619,7 +616,7 @@ int LD2420Component::get_gate_threshold_(uint8_t gate) {
memcpy(&cmd_frame.data[cmd_frame.data_length], &CMD_GATE_STILL_THRESH[gate], sizeof(CMD_GATE_STILL_THRESH[gate]));
cmd_frame.data_length += 2;
cmd_frame.footer = CMD_FRAME_FOOTER;
ESP_LOGD(TAG, "Sending read gate %d high/low theshold command: %2X", gate, cmd_frame.command);
ESP_LOGV(TAG, "Sending read gate %d high/low threshold command: %2X", gate, cmd_frame.command);
error = this->send_cmd_from_array(cmd_frame);
if (error == 0) {
this->current_config.move_thresh[gate] = cmd_reply_.data[0];
@@ -644,7 +641,7 @@ int LD2420Component::get_min_max_distances_timeout_() {
sizeof(CMD_TIMEOUT_REG)); // Register: global delay time
cmd_frame.data_length += sizeof(CMD_TIMEOUT_REG);
cmd_frame.footer = CMD_FRAME_FOOTER;
ESP_LOGD(TAG, "Sending read gate min max and timeout command: %2X", cmd_frame.command);
ESP_LOGV(TAG, "Sending read gate min max and timeout command: %2X", cmd_frame.command);
error = this->send_cmd_from_array(cmd_frame);
if (error == 0) {
this->current_config.min_gate = (uint16_t) cmd_reply_.data[0];
@@ -667,9 +664,9 @@ void LD2420Component::set_system_mode(uint16_t mode) {
memcpy(&cmd_frame.data[cmd_frame.data_length], &unknown_parm, sizeof(unknown_parm));
cmd_frame.data_length += sizeof(unknown_parm);
cmd_frame.footer = CMD_FRAME_FOOTER;
ESP_LOGD(TAG, "Sending write system mode command: %2X", cmd_frame.command);
ESP_LOGV(TAG, "Sending write system mode command: %2X", cmd_frame.command);
if (this->send_cmd_from_array(cmd_frame) == 0)
set_mode_(mode);
this->set_mode_(mode);
}
void LD2420Component::get_firmware_version_() {
@@ -679,7 +676,7 @@ void LD2420Component::get_firmware_version_() {
cmd_frame.command = CMD_READ_VERSION;
cmd_frame.footer = CMD_FRAME_FOOTER;
ESP_LOGD(TAG, "Sending read firmware version command: %2X", cmd_frame.command);
ESP_LOGV(TAG, "Sending read firmware version command: %2X", cmd_frame.command);
this->send_cmd_from_array(cmd_frame);
}
@@ -712,7 +709,7 @@ void LD2420Component::set_min_max_distances_timeout(uint32_t max_gate_distance,
cmd_frame.data_length += sizeof(timeout);
cmd_frame.footer = CMD_FRAME_FOOTER;
ESP_LOGD(TAG, "Sending write gate min max and timeout command: %2X", cmd_frame.command);
ESP_LOGV(TAG, "Sending write gate min max and timeout command: %2X", cmd_frame.command);
this->send_cmd_from_array(cmd_frame);
}
@@ -738,7 +735,7 @@ void LD2420Component::set_gate_threshold(uint8_t gate) {
sizeof(this->new_config.still_thresh[gate]));
cmd_frame.data_length += sizeof(this->new_config.still_thresh[gate]);
cmd_frame.footer = CMD_FRAME_FOOTER;
ESP_LOGD(TAG, "Sending set gate %4X sensitivity command: %2X", gate, cmd_frame.command);
ESP_LOGV(TAG, "Sending set gate %4X sensitivity command: %2X", gate, cmd_frame.command);
this->send_cmd_from_array(cmd_frame);
}

View File

@@ -179,7 +179,7 @@ class LD2420Component : public Component, public uart::UARTDevice {
void set_operating_mode(const std::string &state);
void auto_calibrate_sensitivity();
void update_radar_data(uint16_t const *gate_energy, uint8_t sample_number);
uint8_t calc_checksum(void *data, size_t size);
static uint8_t calc_checksum(void *data, size_t size);
RegConfigT current_config;
RegConfigT new_config;
@@ -222,7 +222,7 @@ class LD2420Component : public Component, public uart::UARTDevice {
volatile bool ack;
};
int get_firmware_int_(const char *version_string);
static int get_firmware_int(const char *version_string);
void get_firmware_version_();
int get_gate_threshold_(uint8_t gate);
void get_reg_value_(uint16_t reg);

View File

@@ -6,7 +6,9 @@
#ifdef USE_SENSOR
#include "esphome/components/sensor/sensor.h"
#endif
#include "esphome/core/application.h"
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#define highbyte(val) (uint8_t)((val) >> 8)
#define lowbyte(val) (uint8_t)((val) &0xff)
@@ -96,11 +98,6 @@ static inline std::string get_direction(int16_t speed) {
return STATIONARY;
}
static inline std::string format_mac(uint8_t *buffer) {
return str_snprintf("%02X:%02X:%02X:%02X:%02X:%02X", 17, buffer[10], buffer[11], buffer[12], buffer[13], buffer[14],
buffer[15]);
}
static inline std::string format_version(uint8_t *buffer) {
return str_sprintf("%u.%02X.%02X%02X%02X%02X", buffer[13], buffer[12], buffer[17], buffer[16], buffer[15],
buffer[14]);
@@ -120,7 +117,7 @@ void LD2450Component::setup() {
}
void LD2450Component::dump_config() {
ESP_LOGCONFIG(TAG, "HLK-LD2450 Human motion tracking radar module:");
ESP_LOGCONFIG(TAG, "LD2450:");
#ifdef USE_BINARY_SENSOR
LOG_BINARY_SENSOR(" ", "TargetBinarySensor", this->target_binary_sensor_);
LOG_BINARY_SENSOR(" ", "MovingTargetBinarySensor", this->moving_target_binary_sensor_);
@@ -189,9 +186,9 @@ void LD2450Component::dump_config() {
LOG_NUMBER(" ", "PresenceTimeoutNumber", this->presence_timeout_number_);
#endif
ESP_LOGCONFIG(TAG,
" Throttle : %ums\n"
" MAC Address : %s\n"
" Firmware version : %s",
" Throttle: %ums\n"
" MAC Address: %s\n"
" Firmware version: %s",
this->throttle_, const_cast<char *>(this->mac_.c_str()), const_cast<char *>(this->version_.c_str()));
}
@@ -266,8 +263,7 @@ bool LD2450Component::get_timeout_status_(uint32_t check_millis) {
if (this->timeout_ == 0) {
this->timeout_ = ld2450::convert_seconds_to_ms(DEFAULT_PRESENCE_TIMEOUT);
}
auto current_millis = millis();
return current_millis - check_millis >= this->timeout_;
return App.get_loop_component_start_time() - check_millis >= this->timeout_;
}
// Extract, store and publish zone details LD2450 buffer
@@ -354,25 +350,24 @@ void LD2450Component::send_command_(uint8_t command, const uint8_t *command_valu
// Header Target 1 Target 2 Target 3 End
void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) {
if (len < 29) { // header (4 bytes) + 8 x 3 target data + footer (2 bytes)
ESP_LOGE(TAG, "Periodic data: invalid message length");
ESP_LOGE(TAG, "Invalid message length");
return;
}
if (buffer[0] != 0xAA || buffer[1] != 0xFF || buffer[2] != 0x03 || buffer[3] != 0x00) { // header
ESP_LOGE(TAG, "Periodic data: invalid message header");
ESP_LOGE(TAG, "Invalid message header");
return;
}
if (buffer[len - 2] != 0x55 || buffer[len - 1] != 0xCC) { // footer
ESP_LOGE(TAG, "Periodic data: invalid message footer");
ESP_LOGE(TAG, "Invalid message footer");
return;
}
auto current_millis = millis();
if (current_millis - this->last_periodic_millis_ < this->throttle_) {
if (App.get_loop_component_start_time() - this->last_periodic_millis_ < this->throttle_) {
ESP_LOGV(TAG, "Throttling: %d", this->throttle_);
return;
}
this->last_periodic_millis_ = current_millis;
this->last_periodic_millis_ = App.get_loop_component_start_time();
int16_t target_count = 0;
int16_t still_target_count = 0;
@@ -555,13 +550,13 @@ void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) {
#ifdef USE_SENSOR
// For presence timeout check
if (target_count > 0) {
this->presence_millis_ = millis();
this->presence_millis_ = App.get_loop_component_start_time();
}
if (moving_target_count > 0) {
this->moving_presence_millis_ = millis();
this->moving_presence_millis_ = App.get_loop_component_start_time();
}
if (still_target_count > 0) {
this->still_presence_millis_ = millis();
this->still_presence_millis_ = App.get_loop_component_start_time();
}
#endif
}
@@ -569,31 +564,31 @@ void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) {
bool LD2450Component::handle_ack_data_(uint8_t *buffer, uint8_t len) {
ESP_LOGV(TAG, "Handling ack data for command %02X", buffer[COMMAND]);
if (len < 10) {
ESP_LOGE(TAG, "Ack data: invalid length");
ESP_LOGE(TAG, "Invalid ack length");
return true;
}
if (buffer[0] != 0xFD || buffer[1] != 0xFC || buffer[2] != 0xFB || buffer[3] != 0xFA) { // frame header
ESP_LOGE(TAG, "Ack data: invalid header (command %02X)", buffer[COMMAND]);
ESP_LOGE(TAG, "Invalid ack header (command %02X)", buffer[COMMAND]);
return true;
}
if (buffer[COMMAND_STATUS] != 0x01) {
ESP_LOGE(TAG, "Ack data: invalid status");
ESP_LOGE(TAG, "Invalid ack status");
return true;
}
if (buffer[8] || buffer[9]) {
ESP_LOGE(TAG, "Ack data: last buffer was %u, %u", buffer[8], buffer[9]);
ESP_LOGE(TAG, "Last buffer was %u, %u", buffer[8], buffer[9]);
return true;
}
switch (buffer[COMMAND]) {
case lowbyte(CMD_ENABLE_CONF):
ESP_LOGV(TAG, "Got enable conf command");
ESP_LOGV(TAG, "Enable conf command");
break;
case lowbyte(CMD_DISABLE_CONF):
ESP_LOGV(TAG, "Got disable conf command");
ESP_LOGV(TAG, "Disable conf command");
break;
case lowbyte(CMD_SET_BAUD_RATE):
ESP_LOGV(TAG, "Got baud rate change command");
ESP_LOGV(TAG, "Baud rate change command");
#ifdef USE_SELECT
if (this->baud_rate_select_ != nullptr) {
ESP_LOGV(TAG, "Change baud rate to %s", this->baud_rate_select_->state.c_str());
@@ -613,7 +608,7 @@ bool LD2450Component::handle_ack_data_(uint8_t *buffer, uint8_t len) {
if (len < 20) {
return false;
}
this->mac_ = ld2450::format_mac(buffer);
this->mac_ = format_mac_address_pretty(&buffer[10]);
ESP_LOGV(TAG, "MAC address: %s", this->mac_.c_str());
#ifdef USE_TEXT_SENSOR
if (this->mac_text_sensor_ != nullptr) {
@@ -622,15 +617,15 @@ bool LD2450Component::handle_ack_data_(uint8_t *buffer, uint8_t len) {
#endif
#ifdef USE_SWITCH
if (this->bluetooth_switch_ != nullptr) {
this->bluetooth_switch_->publish_state(this->mac_ != NO_MAC);
this->bluetooth_switch_->publish_state(this->mac_ != UNKNOWN_MAC);
}
#endif
break;
case lowbyte(CMD_BLUETOOTH):
ESP_LOGV(TAG, "Got Bluetooth command");
ESP_LOGV(TAG, "Bluetooth command");
break;
case lowbyte(CMD_SINGLE_TARGET_MODE):
ESP_LOGV(TAG, "Got single target conf command");
ESP_LOGV(TAG, "Single target conf command");
#ifdef USE_SWITCH
if (this->multi_target_switch_ != nullptr) {
this->multi_target_switch_->publish_state(false);
@@ -638,7 +633,7 @@ bool LD2450Component::handle_ack_data_(uint8_t *buffer, uint8_t len) {
#endif
break;
case lowbyte(CMD_MULTI_TARGET_MODE):
ESP_LOGV(TAG, "Got multi target conf command");
ESP_LOGV(TAG, "Multi target conf command");
#ifdef USE_SWITCH
if (this->multi_target_switch_ != nullptr) {
this->multi_target_switch_->publish_state(true);
@@ -646,7 +641,7 @@ bool LD2450Component::handle_ack_data_(uint8_t *buffer, uint8_t len) {
#endif
break;
case lowbyte(CMD_QUERY_TARGET_MODE):
ESP_LOGV(TAG, "Got query target tracking mode command");
ESP_LOGV(TAG, "Query target tracking mode command");
#ifdef USE_SWITCH
if (this->multi_target_switch_ != nullptr) {
this->multi_target_switch_->publish_state(buffer[10] == 0x02);
@@ -654,7 +649,7 @@ bool LD2450Component::handle_ack_data_(uint8_t *buffer, uint8_t len) {
#endif
break;
case lowbyte(CMD_QUERY_ZONE):
ESP_LOGV(TAG, "Got query zone conf command");
ESP_LOGV(TAG, "Query zone conf command");
this->zone_type_ = std::stoi(std::to_string(buffer[10]), nullptr, 16);
this->publish_zone_type();
#ifdef USE_SELECT
@@ -674,7 +669,7 @@ bool LD2450Component::handle_ack_data_(uint8_t *buffer, uint8_t len) {
this->process_zone_(buffer);
break;
case lowbyte(CMD_SET_ZONE):
ESP_LOGV(TAG, "Got set zone conf command");
ESP_LOGV(TAG, "Set zone conf command");
this->query_zone_info();
break;
default:

View File

@@ -38,8 +38,8 @@ from esphome.const import (
CONF_WHITE,
)
from esphome.core import CORE, coroutine_with_priority
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
from esphome.cpp_generator import MockObjClass
from esphome.cpp_helpers import setup_entity
from .automation import LIGHT_STATE_SCHEMA
from .effects import (
@@ -110,6 +110,8 @@ LIGHT_SCHEMA = (
)
)
LIGHT_SCHEMA.add_extra(entity_duplicate_validator("light"))
BINARY_LIGHT_SCHEMA = LIGHT_SCHEMA.extend(
{
cv.Optional(CONF_EFFECTS): validate_effects(BINARY_EFFECTS),
@@ -207,7 +209,7 @@ def validate_color_temperature_channels(value):
async def setup_light_core_(light_var, output_var, config):
await setup_entity(light_var, config)
await setup_entity(light_var, config, "light")
cg.add(light_var.set_restore_mode(config[CONF_RESTORE_MODE]))

View File

@@ -14,8 +14,8 @@ from esphome.const import (
CONF_WEB_SERVER,
)
from esphome.core import CORE, coroutine_with_priority
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
from esphome.cpp_generator import MockObjClass
from esphome.cpp_helpers import setup_entity
CODEOWNERS = ["@esphome/core"]
IS_PLATFORM_COMPONENT = True
@@ -67,6 +67,9 @@ _LOCK_SCHEMA = (
)
_LOCK_SCHEMA.add_extra(entity_duplicate_validator("lock"))
def lock_schema(
class_: MockObjClass = cv.UNDEFINED,
*,
@@ -94,7 +97,7 @@ LOCK_SCHEMA.add_extra(cv.deprecated_schema_constant("lock"))
async def _setup_lock_core(var, config):
await setup_entity(var, config)
await setup_entity(var, config, "lock")
for conf in config.get(CONF_ON_LOCK, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)

View File

@@ -3,7 +3,7 @@ import esphome.config_validation as cv
from esphome.const import CONF_SIZE, CONF_TEXT
from esphome.cpp_generator import MockObjClass
from ..defines import CONF_MAIN, literal
from ..defines import CONF_MAIN
from ..lv_validation import color, color_retmapper, lv_text
from ..lvcode import LocalVariable, lv, lv_expr
from ..schemas import TEXT_SCHEMA
@@ -34,7 +34,7 @@ class QrCodeType(WidgetType):
)
def get_uses(self):
return ("canvas", "img")
return ("canvas", "img", "label")
def obj_creator(self, parent: MockObjClass, config: dict):
dark_color = color_retmapper(config[CONF_DARK_COLOR])
@@ -45,10 +45,8 @@ class QrCodeType(WidgetType):
async def to_code(self, w: Widget, config):
if (value := config.get(CONF_TEXT)) is not None:
value = await lv_text.process(value)
with LocalVariable(
"qr_text", cg.const_char_ptr, value, modifier=""
) as str_obj:
lv.qrcode_update(w.obj, str_obj, literal(f"strlen({str_obj})"))
with LocalVariable("qr_text", cg.std_string, value, modifier="") as str_obj:
lv.qrcode_update(w.obj, str_obj.c_str(), str_obj.size())
qr_code_spec = QrCodeType()

View File

@@ -11,9 +11,9 @@ from esphome.const import (
CONF_VOLUME,
)
from esphome.core import CORE
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
from esphome.coroutine import coroutine_with_priority
from esphome.cpp_generator import MockObjClass
from esphome.cpp_helpers import setup_entity
CODEOWNERS = ["@jesserockz"]
@@ -81,7 +81,7 @@ IsAnnouncingCondition = media_player_ns.class_(
async def setup_media_player_core_(var, config):
await setup_entity(var, config)
await setup_entity(var, config, "media_player")
for conf in config.get(CONF_ON_STATE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
@@ -143,6 +143,8 @@ _MEDIA_PLAYER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(
}
)
_MEDIA_PLAYER_SCHEMA.add_extra(entity_duplicate_validator("media_player"))
def media_player_schema(
class_: MockObjClass,
@@ -166,7 +168,6 @@ def media_player_schema(
MEDIA_PLAYER_SCHEMA = media_player_schema(MediaPlayer)
MEDIA_PLAYER_SCHEMA.add_extra(cv.deprecated_schema_constant("media_player"))
MEDIA_PLAYER_ACTION_SCHEMA = automation.maybe_simple_id(
cv.Schema(
{

View File

@@ -64,6 +64,14 @@ class ModbusDevice {
this->parent_->send(this->address_, function, start_address, number_of_entities, payload_len, payload);
}
void send_raw(const std::vector<uint8_t> &payload) { this->parent_->send_raw(payload); }
void send_error(uint8_t function_code, uint8_t exception_code) {
std::vector<uint8_t> error_response;
error_response.reserve(3);
error_response.push_back(this->address_);
error_response.push_back(function_code | 0x80);
error_response.push_back(exception_code);
this->send_raw(error_response);
}
// If more than one device is connected block sending a new command before a response is received
bool waiting_for_response() { return parent_->waiting_for_response != 0; }

View File

@@ -112,6 +112,22 @@ TYPE_REGISTER_MAP = {
"FP32_R": 2,
}
CPP_TYPE_REGISTER_MAP = {
"RAW": cg.uint16,
"U_WORD": cg.uint16,
"S_WORD": cg.int16,
"U_DWORD": cg.uint32,
"U_DWORD_R": cg.uint32,
"S_DWORD": cg.int32,
"S_DWORD_R": cg.int32,
"U_QWORD": cg.uint64,
"U_QWORD_R": cg.uint64,
"S_QWORD": cg.int64,
"S_QWORD_R": cg.int64,
"FP32": cg.float_,
"FP32_R": cg.float_,
}
ModbusCommandSentTrigger = modbus_controller_ns.class_(
"ModbusCommandSentTrigger", automation.Trigger.template(cg.int_, cg.int_)
)
@@ -285,21 +301,24 @@ async def to_code(config):
cg.add(var.set_offline_skip_updates(config[CONF_OFFLINE_SKIP_UPDATES]))
if CONF_SERVER_REGISTERS in config:
for server_register in config[CONF_SERVER_REGISTERS]:
cg.add(
var.add_server_register(
cg.new_Pvariable(
server_register_var = cg.new_Pvariable(
server_register[CONF_ID],
server_register[CONF_ADDRESS],
server_register[CONF_VALUE_TYPE],
TYPE_REGISTER_MAP[server_register[CONF_VALUE_TYPE]],
)
cpp_type = CPP_TYPE_REGISTER_MAP[server_register[CONF_VALUE_TYPE]]
cg.add(
server_register_var.set_read_lambda(
cg.TemplateArguments(cpp_type),
await cg.process_lambda(
server_register[CONF_READ_LAMBDA],
[],
return_type=cg.float_,
[(cg.uint16, "address")],
return_type=cpp_type,
),
)
)
)
cg.add(var.add_server_register(server_register_var))
await register_modbus_device(var, config)
for conf in config.get(CONF_ON_COMMAND_SENT, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)

View File

@@ -117,12 +117,17 @@ void ModbusController::on_modbus_read_registers(uint8_t function_code, uint16_t
bool found = false;
for (auto *server_register : this->server_registers_) {
if (server_register->address == current_address) {
float value = server_register->read_lambda();
if (!server_register->read_lambda) {
break;
}
int64_t value = server_register->read_lambda();
ESP_LOGD(TAG, "Matched register. Address: 0x%02X. Value type: %zu. Register count: %u. Value: %s.",
server_register->address, static_cast<size_t>(server_register->value_type),
server_register->register_count, server_register->format_value(value).c_str());
ESP_LOGD(TAG, "Matched register. Address: 0x%02X. Value type: %zu. Register count: %u. Value: %0.1f.",
server_register->address, static_cast<uint8_t>(server_register->value_type),
server_register->register_count, value);
std::vector<uint16_t> payload = float_to_payload(value, server_register->value_type);
std::vector<uint16_t> payload;
payload.reserve(server_register->register_count * 2);
number_to_payload(payload, value, server_register->value_type);
sixteen_bit_response.insert(sixteen_bit_response.end(), payload.cbegin(), payload.cend());
current_address += server_register->register_count;
found = true;
@@ -132,11 +137,7 @@ void ModbusController::on_modbus_read_registers(uint8_t function_code, uint16_t
if (!found) {
ESP_LOGW(TAG, "Could not match any register to address %02X. Sending exception response.", current_address);
std::vector<uint8_t> error_response;
error_response.push_back(this->address_);
error_response.push_back(0x81);
error_response.push_back(0x02);
this->send_raw(error_response);
send_error(function_code, 0x02);
return;
}
}

View File

@@ -63,6 +63,10 @@ enum class SensorValueType : uint8_t {
FP32_R = 0xD
};
inline bool value_type_is_float(SensorValueType v) {
return v == SensorValueType::FP32 || v == SensorValueType::FP32_R;
}
inline ModbusFunctionCode modbus_register_read_function(ModbusRegisterType reg_type) {
switch (reg_type) {
case ModbusRegisterType::COIL:
@@ -253,18 +257,53 @@ class SensorItem {
};
class ServerRegister {
using ReadLambda = std::function<int64_t()>;
public:
ServerRegister(uint16_t address, SensorValueType value_type, uint8_t register_count,
std::function<float()> read_lambda) {
ServerRegister(uint16_t address, SensorValueType value_type, uint8_t register_count) {
this->address = address;
this->value_type = value_type;
this->register_count = register_count;
this->read_lambda = std::move(read_lambda);
}
template<typename T> void set_read_lambda(const std::function<T(uint16_t address)> &&user_read_lambda) {
this->read_lambda = [this, user_read_lambda]() -> int64_t {
T user_value = user_read_lambda(this->address);
if constexpr (std::is_same_v<T, float>) {
return bit_cast<uint32_t>(user_value);
} else {
return static_cast<int64_t>(user_value);
}
};
}
// Formats a raw value into a string representation based on the value type for debugging
std::string format_value(int64_t value) const {
switch (this->value_type) {
case SensorValueType::U_WORD:
case SensorValueType::U_DWORD:
case SensorValueType::U_DWORD_R:
case SensorValueType::U_QWORD:
case SensorValueType::U_QWORD_R:
return std::to_string(static_cast<uint64_t>(value));
case SensorValueType::S_WORD:
case SensorValueType::S_DWORD:
case SensorValueType::S_DWORD_R:
case SensorValueType::S_QWORD:
case SensorValueType::S_QWORD_R:
return std::to_string(value);
case SensorValueType::FP32_R:
case SensorValueType::FP32:
return str_sprintf("%.1f", bit_cast<float>(static_cast<uint32_t>(value)));
default:
return std::to_string(value);
}
}
uint16_t address{0};
SensorValueType value_type{SensorValueType::RAW};
uint8_t register_count{0};
std::function<float()> read_lambda;
ReadLambda read_lambda;
};
// ModbusController::create_register_ranges_ tries to optimize register range
@@ -444,7 +483,7 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice {
void on_modbus_data(const std::vector<uint8_t> &data) override;
/// called when a modbus error response was received
void on_modbus_error(uint8_t function_code, uint8_t exception_code) override;
/// called when a modbus request (function code 3 or 4) was parsed without errors
/// called when a modbus request (function code 0x03 or 0x04) was parsed without errors
void on_modbus_read_registers(uint8_t function_code, uint16_t start_address, uint16_t number_of_registers) final;
/// default delegate called by process_modbus_data when a response has retrieved from the incoming queue
void on_register_data(ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data);
@@ -529,7 +568,7 @@ inline float payload_to_float(const std::vector<uint8_t> &data, const SensorItem
int64_t number = payload_to_number(data, item.sensor_value_type, item.offset, item.bitmask);
float float_value;
if (item.sensor_value_type == SensorValueType::FP32 || item.sensor_value_type == SensorValueType::FP32_R) {
if (value_type_is_float(item.sensor_value_type)) {
float_value = bit_cast<float>(static_cast<uint32_t>(number));
} else {
float_value = static_cast<float>(number);
@@ -541,7 +580,7 @@ inline float payload_to_float(const std::vector<uint8_t> &data, const SensorItem
inline std::vector<uint16_t> float_to_payload(float value, SensorValueType value_type) {
int64_t val;
if (value_type == SensorValueType::FP32 || value_type == SensorValueType::FP32_R) {
if (value_type_is_float(value_type)) {
val = bit_cast<uint32_t>(value);
} else {
val = llroundf(value);

View File

@@ -68,6 +68,7 @@ def AUTO_LOAD():
CONF_DISCOVER_IP = "discover_ip"
CONF_IDF_SEND_ASYNC = "idf_send_async"
CONF_WAIT_FOR_CONNECTION = "wait_for_connection"
def validate_message_just_topic(value):
@@ -298,6 +299,7 @@ CONFIG_SCHEMA = cv.All(
}
),
cv.Optional(CONF_PUBLISH_NAN_AS_NONE, default=False): cv.boolean,
cv.Optional(CONF_WAIT_FOR_CONNECTION, default=False): cv.boolean,
}
),
validate_config,
@@ -453,6 +455,8 @@ async def to_code(config):
cg.add(var.set_publish_nan_as_none(config[CONF_PUBLISH_NAN_AS_NONE]))
cg.add(var.set_wait_for_connection(config[CONF_WAIT_FOR_CONNECTION]))
MQTT_PUBLISH_ACTION_SCHEMA = cv.Schema(
{

View File

@@ -17,7 +17,8 @@ enum class MQTTClientDisconnectReason : int8_t {
MQTT_MALFORMED_CREDENTIALS = 4,
MQTT_NOT_AUTHORIZED = 5,
ESP8266_NOT_ENOUGH_SPACE = 6,
TLS_BAD_FINGERPRINT = 7
TLS_BAD_FINGERPRINT = 7,
DNS_RESOLVE_ERROR = 8
};
/// internal struct for MQTT messages.

View File

@@ -176,7 +176,8 @@ void MQTTClientComponent::dump_config() {
}
}
bool MQTTClientComponent::can_proceed() {
return network::is_disabled() || this->state_ == MQTT_CLIENT_DISABLED || this->is_connected();
return network::is_disabled() || this->state_ == MQTT_CLIENT_DISABLED || this->is_connected() ||
!this->wait_for_connection_;
}
void MQTTClientComponent::start_dnslookup_() {
@@ -228,6 +229,8 @@ void MQTTClientComponent::check_dnslookup_() {
if (this->dns_resolve_error_) {
ESP_LOGW(TAG, "Couldn't resolve IP address for '%s'", this->credentials_.address.c_str());
this->state_ = MQTT_CLIENT_DISCONNECTED;
this->disconnect_reason_ = MQTTClientDisconnectReason::DNS_RESOLVE_ERROR;
this->on_disconnect_.call(MQTTClientDisconnectReason::DNS_RESOLVE_ERROR);
return;
}
@@ -697,7 +700,9 @@ void MQTTClientComponent::set_on_connect(mqtt_on_connect_callback_t &&callback)
}
void MQTTClientComponent::set_on_disconnect(mqtt_on_disconnect_callback_t &&callback) {
auto callback_copy = callback;
this->mqtt_backend_.set_on_disconnect(std::forward<mqtt_on_disconnect_callback_t>(callback));
this->on_disconnect_.add(std::move(callback_copy));
}
#if ASYNC_TCP_SSL_ENABLED

View File

@@ -4,11 +4,12 @@
#ifdef USE_MQTT
#include "esphome/core/component.h"
#include "esphome/core/automation.h"
#include "esphome/core/log.h"
#include "esphome/components/json/json_util.h"
#include "esphome/components/network/ip_address.h"
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#if defined(USE_ESP32)
#include "mqtt_backend_esp32.h"
#elif defined(USE_ESP8266)
@@ -267,6 +268,8 @@ class MQTTClientComponent : public Component {
void set_publish_nan_as_none(bool publish_nan_as_none);
bool is_publish_nan_as_none() const;
void set_wait_for_connection(bool wait_for_connection) { this->wait_for_connection_ = wait_for_connection; }
protected:
void send_device_info_();
@@ -332,8 +335,10 @@ class MQTTClientComponent : public Component {
uint32_t connect_begin_;
uint32_t last_connected_{0};
optional<MQTTClientDisconnectReason> disconnect_reason_{};
CallbackManager<MQTTBackend::on_disconnect_callback_t> on_disconnect_;
bool publish_nan_as_none_{false};
bool wait_for_connection_{false};
};
extern MQTTClientComponent *global_mqtt_client; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)

View File

@@ -76,8 +76,8 @@ from esphome.const import (
DEVICE_CLASS_WIND_SPEED,
)
from esphome.core import CORE, coroutine_with_priority
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
from esphome.cpp_generator import MockObjClass
from esphome.cpp_helpers import setup_entity
CODEOWNERS = ["@esphome/core"]
DEVICE_CLASSES = [
@@ -207,6 +207,9 @@ _NUMBER_SCHEMA = (
)
_NUMBER_SCHEMA.add_extra(entity_duplicate_validator("number"))
def number_schema(
class_: MockObjClass,
*,
@@ -237,7 +240,7 @@ NUMBER_SCHEMA.add_extra(cv.deprecated_schema_constant("number"))
async def setup_number_core_(
var, config, *, min_value: float, max_value: float, step: float
):
await setup_entity(var, config)
await setup_entity(var, config, "number")
cg.add(var.traits.set_min_value(min_value))
cg.add(var.traits.set_max_value(max_value))

View File

@@ -17,8 +17,8 @@ from esphome.const import (
CONF_WEB_SERVER,
)
from esphome.core import CORE, coroutine_with_priority
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
from esphome.cpp_generator import MockObjClass
from esphome.cpp_helpers import setup_entity
CODEOWNERS = ["@esphome/core"]
IS_PLATFORM_COMPONENT = True
@@ -65,6 +65,9 @@ _SELECT_SCHEMA = (
)
_SELECT_SCHEMA.add_extra(entity_duplicate_validator("select"))
def select_schema(
class_: MockObjClass,
*,
@@ -89,7 +92,7 @@ SELECT_SCHEMA.add_extra(cv.deprecated_schema_constant("select"))
async def setup_select_core_(var, config, *, options: list[str]):
await setup_entity(var, config)
await setup_entity(var, config, "select")
cg.add(var.traits.set_options(options))

View File

@@ -101,8 +101,8 @@ from esphome.const import (
ENTITY_CATEGORY_CONFIG,
)
from esphome.core import CORE, coroutine_with_priority
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
from esphome.cpp_generator import MockObjClass
from esphome.cpp_helpers import setup_entity
from esphome.util import Registry
CODEOWNERS = ["@esphome/core"]
@@ -318,6 +318,8 @@ _SENSOR_SCHEMA = (
)
)
_SENSOR_SCHEMA.add_extra(entity_duplicate_validator("sensor"))
def sensor_schema(
class_: MockObjClass = cv.UNDEFINED,
@@ -787,7 +789,7 @@ async def build_filters(config):
async def setup_sensor_core_(var, config):
await setup_entity(var, config)
await setup_entity(var, config, "sensor")
if (device_class := config.get(CONF_DEVICE_CLASS)) is not None:
cg.add(var.set_device_class(device_class))

View File

@@ -20,8 +20,8 @@ from esphome.const import (
DEVICE_CLASS_SWITCH,
)
from esphome.core import CORE, coroutine_with_priority
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
from esphome.cpp_generator import MockObjClass
from esphome.cpp_helpers import setup_entity
CODEOWNERS = ["@esphome/core"]
IS_PLATFORM_COMPONENT = True
@@ -91,6 +91,9 @@ _SWITCH_SCHEMA = (
)
_SWITCH_SCHEMA.add_extra(entity_duplicate_validator("switch"))
def switch_schema(
class_: MockObjClass,
*,
@@ -131,7 +134,7 @@ SWITCH_SCHEMA.add_extra(cv.deprecated_schema_constant("switch"))
async def setup_switch_core_(var, config):
await setup_entity(var, config)
await setup_entity(var, config, "switch")
if (inverted := config.get(CONF_INVERTED)) is not None:
cg.add(var.set_inverted(inverted))

View File

@@ -14,8 +14,8 @@ from esphome.const import (
CONF_WEB_SERVER,
)
from esphome.core import CORE, coroutine_with_priority
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
from esphome.cpp_generator import MockObjClass
from esphome.cpp_helpers import setup_entity
CODEOWNERS = ["@mauritskorse"]
IS_PLATFORM_COMPONENT = True
@@ -58,6 +58,9 @@ _TEXT_SCHEMA = (
)
_TEXT_SCHEMA.add_extra(entity_duplicate_validator("text"))
def text_schema(
class_: MockObjClass = cv.UNDEFINED,
*,
@@ -94,7 +97,7 @@ async def setup_text_core_(
max_length: int | None,
pattern: str | None,
):
await setup_entity(var, config)
await setup_entity(var, config, "text")
cg.add(var.traits.set_min_length(min_length))
cg.add(var.traits.set_max_length(max_length))

View File

@@ -21,8 +21,8 @@ from esphome.const import (
DEVICE_CLASS_TIMESTAMP,
)
from esphome.core import CORE, coroutine_with_priority
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
from esphome.cpp_generator import MockObjClass
from esphome.cpp_helpers import setup_entity
from esphome.util import Registry
DEVICE_CLASSES = [
@@ -153,6 +153,9 @@ _TEXT_SENSOR_SCHEMA = (
)
_TEXT_SENSOR_SCHEMA.add_extra(entity_duplicate_validator("text_sensor"))
def text_sensor_schema(
class_: MockObjClass = cv.UNDEFINED,
*,
@@ -186,7 +189,7 @@ async def build_filters(config):
async def setup_text_sensor_core_(var, config):
await setup_entity(var, config)
await setup_entity(var, config, "text_sensor")
if (device_class := config.get(CONF_DEVICE_CLASS)) is not None:
cg.add(var.set_device_class(device_class))

View File

@@ -15,8 +15,8 @@ from esphome.const import (
ENTITY_CATEGORY_CONFIG,
)
from esphome.core import CORE, coroutine_with_priority
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
from esphome.cpp_generator import MockObjClass
from esphome.cpp_helpers import setup_entity
CODEOWNERS = ["@jesserockz"]
IS_PLATFORM_COMPONENT = True
@@ -58,6 +58,9 @@ _UPDATE_SCHEMA = (
)
_UPDATE_SCHEMA.add_extra(entity_duplicate_validator("update"))
def update_schema(
class_: MockObjClass = cv.UNDEFINED,
*,
@@ -87,7 +90,7 @@ UPDATE_SCHEMA.add_extra(cv.deprecated_schema_constant("update"))
async def setup_update_core_(var, config):
await setup_entity(var, config)
await setup_entity(var, config, "update")
if device_class_config := config.get(CONF_DEVICE_CLASS):
cg.add(var.set_device_class(device_class_config))

View File

@@ -22,8 +22,8 @@ from esphome.const import (
DEVICE_CLASS_WATER,
)
from esphome.core import CORE, coroutine_with_priority
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
from esphome.cpp_generator import MockObjClass
from esphome.cpp_helpers import setup_entity
IS_PLATFORM_COMPONENT = True
@@ -103,6 +103,9 @@ _VALVE_SCHEMA = (
)
_VALVE_SCHEMA.add_extra(entity_duplicate_validator("valve"))
def valve_schema(
class_: MockObjClass = cv.UNDEFINED,
*,
@@ -132,7 +135,7 @@ VALVE_SCHEMA.add_extra(cv.deprecated_schema_constant("valve"))
async def _setup_valve_core(var, config):
await setup_entity(var, config)
await setup_entity(var, config, "valve")
if device_class_config := config.get(CONF_DEVICE_CLASS):
cg.add(var.set_device_class(device_class_config))

View File

@@ -741,11 +741,6 @@ void WiFiComponent::set_power_save_mode(WiFiPowerSaveMode power_save) { this->po
void WiFiComponent::set_passive_scan(bool passive) { this->passive_scan_ = passive; }
std::string WiFiComponent::format_mac_addr(const uint8_t *mac) {
char buf[20];
sprintf(buf, "%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
return buf;
}
bool WiFiComponent::is_captive_portal_active_() {
#ifdef USE_CAPTIVE_PORTAL
return captive_portal::global_captive_portal != nullptr && captive_portal::global_captive_portal->is_active();

View File

@@ -321,8 +321,6 @@ class WiFiComponent : public Component {
int32_t get_wifi_channel();
protected:
static std::string format_mac_addr(const uint8_t mac[6]);
#ifdef USE_WIFI_AP
void setup_ap_config_();
#endif // USE_WIFI_AP

View File

@@ -550,7 +550,7 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_
memcpy(buf, it.ssid, it.ssid_len);
buf[it.ssid_len] = '\0';
ESP_LOGV(TAG, "Connected ssid='%s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", buf,
format_mac_addr(it.bssid).c_str(), it.channel, get_auth_mode_str(it.authmode));
format_mac_address_pretty(it.bssid).c_str(), it.channel, get_auth_mode_str(it.authmode));
#if USE_NETWORK_IPV6
this->set_timeout(100, [] { WiFi.enableIPv6(); });
#endif /* USE_NETWORK_IPV6 */
@@ -566,7 +566,7 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_
ESP_LOGW(TAG, "Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf);
} else {
ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf,
format_mac_addr(it.bssid).c_str(), get_disconnect_reason_str(it.reason));
format_mac_address_pretty(it.bssid).c_str(), get_disconnect_reason_str(it.reason));
}
uint8_t reason = it.reason;
@@ -636,13 +636,13 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_
case ESPHOME_EVENT_ID_WIFI_AP_STACONNECTED: {
auto it = info.wifi_sta_connected;
auto &mac = it.bssid;
ESP_LOGV(TAG, "AP client connected MAC=%s", format_mac_addr(mac).c_str());
ESP_LOGV(TAG, "AP client connected MAC=%s", format_mac_address_pretty(mac).c_str());
break;
}
case ESPHOME_EVENT_ID_WIFI_AP_STADISCONNECTED: {
auto it = info.wifi_sta_disconnected;
auto &mac = it.bssid;
ESP_LOGV(TAG, "AP client disconnected MAC=%s", format_mac_addr(mac).c_str());
ESP_LOGV(TAG, "AP client disconnected MAC=%s", format_mac_address_pretty(mac).c_str());
break;
}
case ESPHOME_EVENT_ID_WIFI_AP_STAIPASSIGNED: {
@@ -651,7 +651,7 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_
}
case ESPHOME_EVENT_ID_WIFI_AP_PROBEREQRECVED: {
auto it = info.wifi_ap_probereqrecved;
ESP_LOGVV(TAG, "AP receive Probe Request MAC=%s RSSI=%d", format_mac_addr(it.mac).c_str(), it.rssi);
ESP_LOGVV(TAG, "AP receive Probe Request MAC=%s RSSI=%d", format_mac_address_pretty(it.mac).c_str(), it.rssi);
break;
}
default:

View File

@@ -496,7 +496,8 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) {
char buf[33];
memcpy(buf, it.ssid, it.ssid_len);
buf[it.ssid_len] = '\0';
ESP_LOGV(TAG, "Connected ssid='%s' bssid=%s channel=%u", buf, format_mac_addr(it.bssid).c_str(), it.channel);
ESP_LOGV(TAG, "Connected ssid='%s' bssid=%s channel=%u", buf, format_mac_address_pretty(it.bssid).c_str(),
it.channel);
s_sta_connected = true;
break;
}
@@ -510,7 +511,7 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) {
s_sta_connect_not_found = true;
} else {
ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf,
format_mac_addr(it.bssid).c_str(), LOG_STR_ARG(get_disconnect_reason_str(it.reason)));
format_mac_address_pretty(it.bssid).c_str(), LOG_STR_ARG(get_disconnect_reason_str(it.reason)));
s_sta_connect_error = true;
}
s_sta_connected = false;
@@ -545,17 +546,17 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) {
}
case EVENT_SOFTAPMODE_STACONNECTED: {
auto it = event->event_info.sta_connected;
ESP_LOGV(TAG, "AP client connected MAC=%s aid=%u", format_mac_addr(it.mac).c_str(), it.aid);
ESP_LOGV(TAG, "AP client connected MAC=%s aid=%u", format_mac_address_pretty(it.mac).c_str(), it.aid);
break;
}
case EVENT_SOFTAPMODE_STADISCONNECTED: {
auto it = event->event_info.sta_disconnected;
ESP_LOGV(TAG, "AP client disconnected MAC=%s aid=%u", format_mac_addr(it.mac).c_str(), it.aid);
ESP_LOGV(TAG, "AP client disconnected MAC=%s aid=%u", format_mac_address_pretty(it.mac).c_str(), it.aid);
break;
}
case EVENT_SOFTAPMODE_PROBEREQRECVED: {
auto it = event->event_info.ap_probereqrecved;
ESP_LOGVV(TAG, "AP receive Probe Request MAC=%s RSSI=%d", format_mac_addr(it.mac).c_str(), it.rssi);
ESP_LOGVV(TAG, "AP receive Probe Request MAC=%s RSSI=%d", format_mac_address_pretty(it.mac).c_str(), it.rssi);
break;
}
#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 4, 0)
@@ -567,7 +568,7 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) {
}
case EVENT_SOFTAPMODE_DISTRIBUTE_STA_IP: {
auto it = event->event_info.distribute_sta_ip;
ESP_LOGV(TAG, "AP Distribute Station IP MAC=%s IP=%s aid=%u", format_mac_addr(it.mac).c_str(),
ESP_LOGV(TAG, "AP Distribute Station IP MAC=%s IP=%s aid=%u", format_mac_address_pretty(it.mac).c_str(),
format_ip_addr(it.ip).c_str(), it.aid);
break;
}

View File

@@ -691,7 +691,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
memcpy(buf, it.ssid, it.ssid_len);
buf[it.ssid_len] = '\0';
ESP_LOGV(TAG, "Connected ssid='%s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", buf,
format_mac_addr(it.bssid).c_str(), it.channel, get_auth_mode_str(it.authmode));
format_mac_address_pretty(it.bssid).c_str(), it.channel, get_auth_mode_str(it.authmode));
s_sta_connected = true;
} else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_DISCONNECTED) {
@@ -708,7 +708,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
return;
} else {
ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf,
format_mac_addr(it.bssid).c_str(), get_disconnect_reason_str(it.reason));
format_mac_address_pretty(it.bssid).c_str(), get_disconnect_reason_str(it.reason));
s_sta_connect_error = true;
}
s_sta_connected = false;
@@ -780,15 +780,15 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
} else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_PROBEREQRECVED) {
const auto &it = data->data.ap_probe_req_rx;
ESP_LOGVV(TAG, "AP receive Probe Request MAC=%s RSSI=%d", format_mac_addr(it.mac).c_str(), it.rssi);
ESP_LOGVV(TAG, "AP receive Probe Request MAC=%s RSSI=%d", format_mac_address_pretty(it.mac).c_str(), it.rssi);
} else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_STACONNECTED) {
const auto &it = data->data.ap_staconnected;
ESP_LOGV(TAG, "AP client connected MAC=%s", format_mac_addr(it.mac).c_str());
ESP_LOGV(TAG, "AP client connected MAC=%s", format_mac_address_pretty(it.mac).c_str());
} else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_STADISCONNECTED) {
const auto &it = data->data.ap_stadisconnected;
ESP_LOGV(TAG, "AP client disconnected MAC=%s", format_mac_addr(it.mac).c_str());
ESP_LOGV(TAG, "AP client disconnected MAC=%s", format_mac_address_pretty(it.mac).c_str());
} else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_AP_STAIPASSIGNED) {
const auto &it = data->data.ip_ap_staipassigned;

View File

@@ -281,7 +281,7 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_
memcpy(buf, it.ssid, it.ssid_len);
buf[it.ssid_len] = '\0';
ESP_LOGV(TAG, "Connected ssid='%s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", buf,
format_mac_addr(it.bssid).c_str(), it.channel, get_auth_mode_str(it.authmode));
format_mac_address_pretty(it.bssid).c_str(), it.channel, get_auth_mode_str(it.authmode));
break;
}
@@ -294,7 +294,7 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_
ESP_LOGW(TAG, "Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf);
} else {
ESP_LOGW(TAG, "Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf,
format_mac_addr(it.bssid).c_str(), get_disconnect_reason_str(it.reason));
format_mac_address_pretty(it.bssid).c_str(), get_disconnect_reason_str(it.reason));
}
uint8_t reason = it.reason;
@@ -349,13 +349,13 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_
case ESPHOME_EVENT_ID_WIFI_AP_STACONNECTED: {
auto it = info.wifi_sta_connected;
auto &mac = it.bssid;
ESP_LOGV(TAG, "AP client connected MAC=%s", format_mac_addr(mac).c_str());
ESP_LOGV(TAG, "AP client connected MAC=%s", format_mac_address_pretty(mac).c_str());
break;
}
case ESPHOME_EVENT_ID_WIFI_AP_STADISCONNECTED: {
auto it = info.wifi_sta_disconnected;
auto &mac = it.bssid;
ESP_LOGV(TAG, "AP client disconnected MAC=%s", format_mac_addr(mac).c_str());
ESP_LOGV(TAG, "AP client disconnected MAC=%s", format_mac_address_pretty(mac).c_str());
break;
}
case ESPHOME_EVENT_ID_WIFI_AP_STAIPASSIGNED: {
@@ -364,7 +364,7 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_
}
case ESPHOME_EVENT_ID_WIFI_AP_PROBEREQRECVED: {
auto it = info.wifi_ap_probereqrecved;
ESP_LOGVV(TAG, "AP receive Probe Request MAC=%s RSSI=%d", format_mac_addr(it.mac).c_str(), it.rssi);
ESP_LOGVV(TAG, "AP receive Probe Request MAC=%s RSSI=%d", format_mac_address_pretty(it.mac).c_str(), it.rssi);
break;
}
default:

View File

@@ -320,7 +320,7 @@ bool decrypt_xiaomi_payload(std::vector<uint8_t> &raw, const uint8_t *bindkey, c
memcpy(mac_address + 4, mac_reverse + 1, 1);
memcpy(mac_address + 5, mac_reverse, 1);
ESP_LOGVV(TAG, "decrypt_xiaomi_payload(): authenticated decryption failed.");
ESP_LOGVV(TAG, " MAC address : %s", format_hex_pretty(mac_address, 6).c_str());
ESP_LOGVV(TAG, " MAC address : %s", format_mac_address_pretty(mac_address).c_str());
ESP_LOGVV(TAG, " Packet : %s", format_hex_pretty(raw.data(), raw.size()).c_str());
ESP_LOGVV(TAG, " Key : %s", format_hex_pretty(vector.key, vector.keysize).c_str());
ESP_LOGVV(TAG, " Iv : %s", format_hex_pretty(vector.iv, vector.ivsize).c_str());

View File

@@ -1,5 +1,7 @@
"""Helpers for config validation using voluptuous."""
from __future__ import annotations
from contextlib import contextmanager
from dataclasses import dataclass
from datetime import datetime
@@ -29,6 +31,7 @@ from esphome.const import (
CONF_COMMAND_RETAIN,
CONF_COMMAND_TOPIC,
CONF_DAY,
CONF_DEVICE_ID,
CONF_DISABLED_BY_DEFAULT,
CONF_DISCOVERY,
CONF_ENTITY_CATEGORY,
@@ -355,6 +358,13 @@ def icon(value):
)
def sub_device_id(value: str | None) -> core.ID:
# Lazy import to avoid circular imports
from esphome.core.config import Device
return use_id(Device)(value)
def boolean(value):
"""Validate the given config option to be a boolean.
@@ -1896,6 +1906,7 @@ ENTITY_BASE_SCHEMA = Schema(
Optional(CONF_DISABLED_BY_DEFAULT, default=False): boolean,
Optional(CONF_ICON): icon,
Optional(CONF_ENTITY_CATEGORY): entity_category,
Optional(CONF_DEVICE_ID): sub_device_id,
}
)
@@ -1964,7 +1975,7 @@ class Version:
return f"{self.major}.{self.minor}.{self.patch}"
@classmethod
def parse(cls, value: str) -> "Version":
def parse(cls, value: str) -> Version:
match = re.match(r"^(\d+).(\d+).(\d+)-?\w*$", value)
if match is None:
raise ValueError(f"Not a valid version number {value}")

View File

@@ -56,6 +56,8 @@ CONF_AP = "ap"
CONF_APPARENT_POWER = "apparent_power"
CONF_ARDUINO_VERSION = "arduino_version"
CONF_AREA = "area"
CONF_AREA_ID = "area_id"
CONF_AREAS = "areas"
CONF_ARGS = "args"
CONF_ASSUMED_STATE = "assumed_state"
CONF_AT = "at"
@@ -217,6 +219,7 @@ CONF_DEST = "dest"
CONF_DEVICE = "device"
CONF_DEVICE_CLASS = "device_class"
CONF_DEVICE_FACTOR = "device_factor"
CONF_DEVICE_ID = "device_id"
CONF_DEVICES = "devices"
CONF_DIELECTRIC_CONSTANT = "dielectric_constant"
CONF_DIMENSIONS = "dimensions"
@@ -1096,7 +1099,7 @@ UNIT_KILOMETER_PER_HOUR = "km/h"
UNIT_KILOVOLT_AMPS = "kVA"
UNIT_KILOVOLT_AMPS_HOURS = "kVAh"
UNIT_KILOVOLT_AMPS_REACTIVE = "kVAR"
UNIT_KILOVOLT_AMPS_REACTIVE_HOURS = "kVARh"
UNIT_KILOVOLT_AMPS_REACTIVE_HOURS = "kvarh"
UNIT_KILOWATT = "kW"
UNIT_KILOWATT_HOURS = "kWh"
UNIT_LITRE = "L"
@@ -1132,7 +1135,7 @@ UNIT_VOLT = "V"
UNIT_VOLT_AMPS = "VA"
UNIT_VOLT_AMPS_HOURS = "VAh"
UNIT_VOLT_AMPS_REACTIVE = "var"
UNIT_VOLT_AMPS_REACTIVE_HOURS = "VARh"
UNIT_VOLT_AMPS_REACTIVE_HOURS = "varh"
UNIT_WATT = "W"
UNIT_WATT_HOURS = "Wh"

View File

@@ -522,6 +522,9 @@ class EsphomeCore:
# Dict to track platform entity counts for pre-allocation
# Key: platform name (e.g. "sensor", "binary_sensor"), Value: count
self.platform_counts: defaultdict[str, int] = defaultdict(int)
# Track entity unique IDs to handle duplicates
# Set of (device_id, platform, sanitized_name) tuples
self.unique_ids: set[tuple[str, str, str]] = set()
# Whether ESPHome was started in verbose mode
self.verbose = False
# Whether ESPHome was started in quiet mode
@@ -553,6 +556,7 @@ class EsphomeCore:
self.loaded_integrations = set()
self.component_ids = set()
self.platform_counts = defaultdict(int)
self.unique_ids = set()
PIN_SCHEMA_REGISTRY.reset()
@property

View File

@@ -9,6 +9,13 @@
#include "esphome/core/preferences.h"
#include "esphome/core/scheduler.h"
#ifdef USE_DEVICES
#include "esphome/core/device.h"
#endif
#ifdef USE_AREAS
#include "esphome/core/area.h"
#endif
#ifdef USE_SOCKET_SELECT_SUPPORT
#include <sys/select.h>
#endif
@@ -87,7 +94,7 @@ static const uint32_t TEARDOWN_TIMEOUT_REBOOT_MS = 1000; // 1 second for quick
class Application {
public:
void pre_setup(const std::string &name, const std::string &friendly_name, const char *area, const char *comment,
void pre_setup(const std::string &name, const std::string &friendly_name, const char *comment,
const char *compilation_time, bool name_add_mac_suffix) {
arch_init();
this->name_add_mac_suffix_ = name_add_mac_suffix;
@@ -102,11 +109,17 @@ class Application {
this->name_ = name;
this->friendly_name_ = friendly_name;
}
this->area_ = area;
this->comment_ = comment;
this->compilation_time_ = compilation_time;
}
#ifdef USE_DEVICES
void register_device(Device *device) { this->devices_.push_back(device); }
#endif
#ifdef USE_AREAS
void register_area(Area *area) { this->areas_.push_back(area); }
#endif
void set_current_component(Component *component) { this->current_component_ = component; }
Component *get_current_component() { return this->current_component_; }
@@ -264,6 +277,12 @@ class Application {
#ifdef USE_UPDATE
void reserve_update(size_t count) { this->updates_.reserve(count); }
#endif
#ifdef USE_AREAS
void reserve_area(size_t count) { this->areas_.reserve(count); }
#endif
#ifdef USE_DEVICES
void reserve_device(size_t count) { this->devices_.reserve(count); }
#endif
/// Register the component in this Application instance.
template<class C> C *register_component(C *c) {
@@ -285,7 +304,15 @@ class Application {
const std::string &get_friendly_name() const { return this->friendly_name_; }
/// Get the area of this Application set by pre_setup().
std::string get_area() const { return this->area_ == nullptr ? "" : this->area_; }
const char *get_area() const {
#ifdef USE_AREAS
// If we have areas registered, return the name of the first one (which is the top-level area)
if (!this->areas_.empty() && this->areas_[0] != nullptr) {
return this->areas_[0]->get_name();
}
#endif
return "";
}
/// Get the comment of this Application set by pre_setup().
std::string get_comment() const { return this->comment_; }
@@ -334,6 +361,12 @@ class Application {
uint8_t get_app_state() const { return this->app_state_; }
#ifdef USE_DEVICES
const std::vector<Device *> &get_devices() { return this->devices_; }
#endif
#ifdef USE_AREAS
const std::vector<Area *> &get_areas() { return this->areas_; }
#endif
#ifdef USE_BINARY_SENSOR
const std::vector<binary_sensor::BinarySensor *> &get_binary_sensors() { return this->binary_sensors_; }
binary_sensor::BinarySensor *get_binary_sensor_by_key(uint32_t key, bool include_internal = false) {
@@ -610,6 +643,12 @@ class Application {
uint16_t current_loop_index_{0};
bool in_loop_{false};
#ifdef USE_DEVICES
std::vector<Device *> devices_{};
#endif
#ifdef USE_AREAS
std::vector<Area *> areas_{};
#endif
#ifdef USE_BINARY_SENSOR
std::vector<binary_sensor::BinarySensor *> binary_sensors_{};
#endif
@@ -676,7 +715,6 @@ class Application {
std::string name_;
std::string friendly_name_;
const char *area_{nullptr};
const char *comment_{nullptr};
const char *compilation_time_{nullptr};
bool name_add_mac_suffix_;

19
esphome/core/area.h Normal file
View File

@@ -0,0 +1,19 @@
#pragma once
#include <cstdint>
namespace esphome {
class Area {
public:
void set_area_id(uint32_t area_id) { this->area_id_ = area_id; }
uint32_t get_area_id() { return this->area_id_; }
void set_name(const char *name) { this->name_ = name; }
const char *get_name() { return this->name_; }
protected:
uint32_t area_id_{};
const char *name_ = "";
};
} // namespace esphome

View File

@@ -1,18 +1,24 @@
from __future__ import annotations
import logging
import os
from pathlib import Path
from esphome import automation
from esphome import automation, core
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import (
CONF_AREA,
CONF_AREA_ID,
CONF_AREAS,
CONF_BUILD_PATH,
CONF_COMMENT,
CONF_COMPILE_PROCESS_LIMIT,
CONF_DEBUG_SCHEDULER,
CONF_DEVICES,
CONF_ESPHOME,
CONF_FRIENDLY_NAME,
CONF_ID,
CONF_INCLUDES,
CONF_LIBRARIES,
CONF_MIN_VERSION,
@@ -32,7 +38,13 @@ from esphome.const import (
__version__ as ESPHOME_VERSION,
)
from esphome.core import CORE, coroutine_with_priority
from esphome.helpers import copy_file_if_changed, get_str_env, walk_files
from esphome.helpers import (
copy_file_if_changed,
fnv1a_32bit_hash,
get_str_env,
walk_files,
)
from esphome.types import ConfigType
_LOGGER = logging.getLogger(__name__)
@@ -48,7 +60,8 @@ LoopTrigger = cg.esphome_ns.class_(
ProjectUpdateTrigger = cg.esphome_ns.class_(
"ProjectUpdateTrigger", cg.Component, automation.Trigger.template(cg.std_string)
)
Device = cg.esphome_ns.class_("Device")
Area = cg.esphome_ns.class_("Area")
VALID_INCLUDE_EXTS = {".h", ".hpp", ".tcc", ".ino", ".cpp", ".c"}
@@ -71,6 +84,56 @@ def validate_hostname(config):
return config
def validate_ids_and_references(config: ConfigType) -> ConfigType:
"""Validate that there are no hash collisions between IDs and that area_id references are valid.
This validation is critical because we use 32-bit hashes for performance on microcontrollers.
By detecting collisions at compile time, we prevent any runtime issues while maintaining
optimal performance on 32-bit platforms. In practice, with typical deployments having only
a handful of areas and devices, hash collisions are virtually impossible.
"""
# Helper to check hash collisions
def check_hash_collision(
id_obj: core.ID,
hash_dict: dict[int, str],
item_type: str,
path: list[str | int],
) -> None:
hash_val: int = fnv1a_32bit_hash(id_obj.id)
if hash_val in hash_dict and hash_dict[hash_val] != id_obj.id:
raise cv.Invalid(
f"{item_type} ID '{id_obj.id}' with hash {hash_val} collides with "
f"existing {item_type.lower()} ID '{hash_dict[hash_val]}'",
path=path,
)
hash_dict[hash_val] = id_obj.id
# Collect all areas
all_areas: list[dict[str, str | core.ID]] = []
if CONF_AREA in config:
all_areas.append(config[CONF_AREA])
all_areas.extend(config[CONF_AREAS])
# Validate area hash collisions and collect IDs
area_hashes: dict[int, str] = {}
area_ids: set[str] = set()
for area in all_areas:
area_id: core.ID = area[CONF_ID]
check_hash_collision(area_id, area_hashes, "Area", [CONF_AREAS, area_id.id])
area_ids.add(area_id.id)
# Validate device hash collisions and area references
device_hashes: dict[int, str] = {}
for device in config[CONF_DEVICES]:
device_id: core.ID = device[CONF_ID]
check_hash_collision(
device_id, device_hashes, "Device", [CONF_DEVICES, device_id.id]
)
return config
def valid_include(value):
# Look for "<...>" includes
if value.startswith("<") and value.endswith(">"):
@@ -111,13 +174,32 @@ if "ESPHOME_DEFAULT_COMPILE_PROCESS_LIMIT" in os.environ:
else:
_compile_process_limit_default = cv.UNDEFINED
AREA_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_ID): cv.declare_id(Area),
cv.Required(CONF_NAME): cv.string,
}
)
DEVICE_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_ID): cv.declare_id(Device),
cv.Required(CONF_NAME): cv.string,
cv.Optional(CONF_AREA_ID): cv.use_id(Area),
}
)
def validate_area_config(config: dict | str) -> dict[str, str | core.ID]:
return cv.maybe_simple_value(AREA_SCHEMA, key=CONF_NAME)(config)
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.Required(CONF_NAME): cv.valid_name,
cv.Optional(CONF_FRIENDLY_NAME, ""): cv.string,
cv.Optional(CONF_AREA, ""): cv.string,
cv.Optional(CONF_AREA): validate_area_config,
cv.Optional(CONF_COMMENT): cv.string,
cv.Required(CONF_BUILD_PATH): cv.string,
cv.Optional(CONF_PLATFORMIO_OPTIONS, default={}): cv.Schema(
@@ -167,11 +249,17 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(
CONF_COMPILE_PROCESS_LIMIT, default=_compile_process_limit_default
): cv.int_range(min=1, max=get_usable_cpu_count()),
cv.Optional(CONF_AREAS, default=[]): cv.ensure_list(AREA_SCHEMA),
cv.Optional(CONF_DEVICES, default=[]): cv.ensure_list(DEVICE_SCHEMA),
}
),
validate_hostname,
)
FINAL_VALIDATE_SCHEMA = cv.All(validate_ids_and_references)
PRELOAD_CONFIG_SCHEMA = cv.Schema(
{
cv.Required(CONF_NAME): cv.valid_name,
@@ -336,7 +424,7 @@ async def _add_platform_reserves() -> None:
@coroutine_with_priority(100.0)
async def to_code(config):
async def to_code(config: ConfigType) -> None:
cg.add_global(cg.global_ns.namespace("esphome").using)
# These can be used by user lambdas, put them to default scope
cg.add_global(cg.RawExpression("using std::isnan"))
@@ -347,7 +435,6 @@ async def to_code(config):
cg.App.pre_setup(
config[CONF_NAME],
config[CONF_FRIENDLY_NAME],
config[CONF_AREA],
config.get(CONF_COMMENT, ""),
cg.RawExpression('__DATE__ ", " __TIME__'),
config[CONF_NAME_ADD_MAC_SUFFIX],
@@ -417,3 +504,50 @@ async def to_code(config):
if config[CONF_PLATFORMIO_OPTIONS]:
CORE.add_job(_add_platformio_options, config[CONF_PLATFORMIO_OPTIONS])
# Process areas
all_areas: list[dict[str, str | core.ID]] = []
if CONF_AREA in config:
all_areas.append(config[CONF_AREA])
all_areas.extend(config[CONF_AREAS])
if all_areas:
cg.add(cg.RawStatement(f"App.reserve_area({len(all_areas)});"))
cg.add_define("USE_AREAS")
for area_conf in all_areas:
area_id: core.ID = area_conf[CONF_ID]
area_id_hash: int = fnv1a_32bit_hash(area_id.id)
area_name: str = area_conf[CONF_NAME]
area_var = cg.new_Pvariable(area_id)
cg.add(area_var.set_area_id(area_id_hash))
cg.add(area_var.set_name(area_name))
cg.add(cg.App.register_area(area_var))
# Process devices
devices: list[dict[str, str | core.ID]] = config[CONF_DEVICES]
if not devices:
return
# Reserve space for devices
cg.add(cg.RawStatement(f"App.reserve_device({len(devices)});"))
cg.add_define("USE_DEVICES")
# Process each device
for dev_conf in devices:
device_id: core.ID = dev_conf[CONF_ID]
device_id_hash = fnv1a_32bit_hash(device_id.id)
device_name: str = dev_conf[CONF_NAME]
dev = cg.new_Pvariable(device_id)
cg.add(dev.set_device_id(device_id_hash))
cg.add(dev.set_name(device_name))
# Set area if specified
if CONF_AREA_ID in dev_conf:
area_id: core.ID = dev_conf[CONF_AREA_ID]
area_id_hash = fnv1a_32bit_hash(area_id.id)
cg.add(dev.set_area_id(area_id_hash))
cg.add(cg.App.register_device(dev))

View File

@@ -20,6 +20,7 @@
// Feature flags
#define USE_ALARM_CONTROL_PANEL
#define USE_AREAS
#define USE_BINARY_SENSOR
#define USE_BUTTON
#define USE_CLIMATE
@@ -29,6 +30,7 @@
#define USE_DATETIME_DATETIME
#define USE_DATETIME_TIME
#define USE_DEEP_SLEEP
#define USE_DEVICES
#define USE_DISPLAY
#define USE_ESP32_IMPROV_STATE_CALLBACK
#define USE_EVENT

20
esphome/core/device.h Normal file
View File

@@ -0,0 +1,20 @@
#pragma once
namespace esphome {
class Device {
public:
void set_device_id(uint32_t device_id) { this->device_id_ = device_id; }
uint32_t get_device_id() { return this->device_id_; }
void set_name(const char *name) { this->name_ = name; }
const char *get_name() { return this->name_; }
void set_area_id(uint32_t area_id) { this->area_id_ = area_id; }
uint32_t get_area_id() { return this->area_id_; }
protected:
uint32_t device_id_{};
uint32_t area_id_{};
const char *name_ = "";
};
} // namespace esphome

View File

@@ -11,7 +11,14 @@ const StringRef &EntityBase::get_name() const { return this->name_; }
void EntityBase::set_name(const char *name) {
this->name_ = StringRef(name);
if (this->name_.empty()) {
#ifdef USE_DEVICES
if (this->device_ != nullptr) {
this->name_ = StringRef(this->device_->get_name());
} else
#endif
{
this->name_ = StringRef(App.get_friendly_name());
}
this->flags_.has_own_name = false;
} else {
this->flags_.has_own_name = true;
@@ -47,19 +54,7 @@ void EntityBase::set_object_id(const char *object_id) {
}
// Calculate Object ID Hash from Entity Name
void EntityBase::calc_object_id_() {
// Check if `App.get_friendly_name()` is constant or dynamic.
if (!this->flags_.has_own_name && App.is_name_add_mac_suffix_enabled()) {
// `App.get_friendly_name()` is dynamic.
const auto object_id = str_sanitize(str_snake_case(App.get_friendly_name()));
// FNV-1 hash
this->object_id_hash_ = fnv1_hash(object_id);
} else {
// `App.get_friendly_name()` is constant.
// FNV-1 hash
this->object_id_hash_ = fnv1_hash(this->object_id_c_str_);
}
}
void EntityBase::calc_object_id_() { this->object_id_hash_ = fnv1_hash(this->get_object_id()); }
uint32_t EntityBase::get_object_id_hash() { return this->object_id_hash_; }

View File

@@ -6,6 +6,10 @@
#include "helpers.h"
#include "log.h"
#ifdef USE_DEVICES
#include "device.h"
#endif
namespace esphome {
enum EntityCategory : uint8_t {
@@ -51,6 +55,17 @@ class EntityBase {
std::string get_icon() const;
void set_icon(const char *icon);
#ifdef USE_DEVICES
// Get/set this entity's device id
uint32_t get_device_id() const {
if (this->device_ == nullptr) {
return 0; // No device set, return 0
}
return this->device_->get_device_id();
}
void set_device(Device *device) { this->device_ = device; }
#endif
// Check if this entity has state
bool has_state() const { return this->flags_.has_state; }
@@ -67,6 +82,9 @@ class EntityBase {
const char *object_id_c_str_{nullptr};
const char *icon_c_str_{nullptr};
uint32_t object_id_hash_{};
#ifdef USE_DEVICES
Device *device_{};
#endif
// Bit-packed flags to save memory (1 byte instead of 5)
struct EntityFlags {

View File

@@ -1,5 +1,116 @@
from esphome.const import CONF_ID
from collections.abc import Callable
import logging
import esphome.config_validation as cv
from esphome.const import (
CONF_DEVICE_ID,
CONF_DISABLED_BY_DEFAULT,
CONF_ENTITY_CATEGORY,
CONF_ICON,
CONF_ID,
CONF_INTERNAL,
CONF_NAME,
)
from esphome.core import CORE, ID
from esphome.cpp_generator import MockObj, add, get_variable
import esphome.final_validate as fv
from esphome.helpers import sanitize, snake_case
from esphome.types import ConfigType
_LOGGER = logging.getLogger(__name__)
def get_base_entity_object_id(
name: str, friendly_name: str | None, device_name: str | None = None
) -> str:
"""Calculate the base object ID for an entity that will be set via set_object_id().
This function calculates what object_id_c_str_ should be set to in C++.
The C++ EntityBase::get_object_id() (entity_base.cpp lines 38-49) works as:
- If !has_own_name && is_name_add_mac_suffix_enabled():
return str_sanitize(str_snake_case(App.get_friendly_name())) // Dynamic
- Else:
return object_id_c_str_ ?? "" // What we set via set_object_id()
Since we're calculating what to pass to set_object_id(), we always need to
generate the object_id the same way, regardless of name_add_mac_suffix setting.
Args:
name: The entity name (empty string if no name)
friendly_name: The friendly name from CORE.friendly_name
device_name: The device name if entity is on a sub-device
Returns:
The base object ID to use for duplicate checking and to pass to set_object_id()
"""
if name:
# Entity has its own name (has_own_name will be true)
base_str = name
elif device_name:
# Entity has empty name and is on a sub-device
# C++ EntityBase::set_name() uses device->get_name() when device is set
base_str = device_name
elif friendly_name:
# Entity has empty name (has_own_name will be false)
# C++ uses App.get_friendly_name() which returns friendly_name or device name
base_str = friendly_name
else:
# Fallback to device name
base_str = CORE.name
return sanitize(snake_case(base_str))
async def setup_entity(var: MockObj, config: ConfigType, platform: str) -> None:
"""Set up generic properties of an Entity.
This function sets up the common entity properties like name, icon,
entity category, etc.
Args:
var: The entity variable to set up
config: Configuration dictionary containing entity settings
platform: The platform name (e.g., "sensor", "binary_sensor")
"""
# Get device info
device_name: str | None = None
if CONF_DEVICE_ID in config:
device_id_obj: ID = config[CONF_DEVICE_ID]
device: MockObj = await get_variable(device_id_obj)
add(var.set_device(device))
# Get device name for object ID calculation
device_name = device_id_obj.id
add(var.set_name(config[CONF_NAME]))
# Calculate base object_id using the same logic as C++
# This must match the C++ behavior in esphome/core/entity_base.cpp
base_object_id = get_base_entity_object_id(
config[CONF_NAME], CORE.friendly_name, device_name
)
if not config[CONF_NAME]:
_LOGGER.debug(
"Entity has empty name, using '%s' as object_id base", base_object_id
)
# Set the object ID
add(var.set_object_id(base_object_id))
_LOGGER.debug(
"Setting object_id '%s' for entity '%s' on platform '%s'",
base_object_id,
config[CONF_NAME],
platform,
)
add(var.set_disabled_by_default(config[CONF_DISABLED_BY_DEFAULT]))
if CONF_INTERNAL in config:
add(var.set_internal(config[CONF_INTERNAL]))
if CONF_ICON in config:
add(var.set_icon(config[CONF_ICON]))
if CONF_ENTITY_CATEGORY in config:
add(var.set_entity_category(config[CONF_ENTITY_CATEGORY]))
def inherit_property_from(property_to_inherit, parent_id_property, transform=None):
@@ -54,3 +165,48 @@ def inherit_property_from(property_to_inherit, parent_id_property, transform=Non
return config
return inherit_property
def entity_duplicate_validator(platform: str) -> Callable[[ConfigType], ConfigType]:
"""Create a validator function to check for duplicate entity names.
This validator is meant to be used with schema.add_extra() for entity base schemas.
Args:
platform: The platform name (e.g., "sensor", "binary_sensor")
Returns:
A validator function that checks for duplicate names
"""
def validator(config: ConfigType) -> ConfigType:
if CONF_NAME not in config:
# No name to validate
return config
# Get the entity name and device info
entity_name = config[CONF_NAME]
device_id = "" # Empty string for main device
if CONF_DEVICE_ID in config:
device_id_obj = config[CONF_DEVICE_ID]
# Use the device ID string directly for uniqueness
device_id = device_id_obj.id
# For duplicate detection, just use the sanitized name
name_key = sanitize(snake_case(entity_name))
# Check for duplicates
unique_key = (device_id, platform, name_key)
if unique_key in CORE.unique_ids:
device_prefix = f" on device '{device_id}'" if device_id else ""
raise cv.Invalid(
f"Duplicate {platform} entity with name '{entity_name}' found{device_prefix}. "
f"Each entity on a device must have a unique name within its platform."
)
# Add to tracking set
CORE.unique_ids.add(unique_key)
return config
return validator

View File

@@ -356,6 +356,10 @@ size_t parse_hex(const char *str, size_t length, uint8_t *data, size_t count) {
return chars;
}
std::string format_mac_address_pretty(const uint8_t *mac) {
return str_snprintf("%02X:%02X:%02X:%02X:%02X:%02X", 17, mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
}
static char format_hex_char(uint8_t v) { return v >= 10 ? 'a' + (v - 10) : '0' + v; }
std::string format_hex(const uint8_t *data, size_t length) {
std::string ret;
@@ -732,7 +736,7 @@ std::string get_mac_address() {
std::string get_mac_address_pretty() {
uint8_t mac[6];
get_mac_address_raw(mac);
return str_snprintf("%02X:%02X:%02X:%02X:%02X:%02X", 17, mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
return format_mac_address_pretty(mac);
}
#ifdef USE_ESP32

View File

@@ -402,6 +402,8 @@ template<typename T, enable_if_t<std::is_unsigned<T>::value, int> = 0> optional<
return parse_hex<T>(str.c_str(), str.length());
}
/// Format the six-byte array \p mac into a MAC address.
std::string format_mac_address_pretty(const uint8_t mac[6]);
/// Format the byte array \p data of length \p len in lowercased hex.
std::string format_hex(const uint8_t *data, size_t length);
/// Format the vector \p data in lowercased hex.

View File

@@ -1,11 +1,6 @@
import logging
from esphome.const import (
CONF_DISABLED_BY_DEFAULT,
CONF_ENTITY_CATEGORY,
CONF_ICON,
CONF_INTERNAL,
CONF_NAME,
CONF_SAFE_MODE,
CONF_SETUP_PRIORITY,
CONF_TYPE_ID,
@@ -16,7 +11,6 @@ from esphome.core import CORE, ID, coroutine
from esphome.coroutine import FakeAwaitable
from esphome.cpp_generator import add, get_variable
from esphome.cpp_types import App
from esphome.helpers import sanitize, snake_case
from esphome.types import ConfigFragmentType, ConfigType
from esphome.util import Registry, RegistryEntry
@@ -96,22 +90,6 @@ async def register_parented(var, value):
add(var.set_parent(paren))
async def setup_entity(var, config):
"""Set up generic properties of an Entity"""
add(var.set_name(config[CONF_NAME]))
if not config[CONF_NAME]:
add(var.set_object_id(sanitize(snake_case(CORE.friendly_name))))
else:
add(var.set_object_id(sanitize(snake_case(config[CONF_NAME]))))
add(var.set_disabled_by_default(config[CONF_DISABLED_BY_DEFAULT]))
if CONF_INTERNAL in config:
add(var.set_internal(config[CONF_INTERNAL]))
if CONF_ICON in config:
add(var.set_icon(config[CONF_ICON]))
if CONF_ENTITY_CATEGORY in config:
add(var.set_entity_category(config[CONF_ENTITY_CATEGORY]))
def extract_registry_entry_config(
registry: Registry,
full_config: ConfigType,

View File

@@ -1,25 +1,9 @@
from __future__ import annotations
import unicodedata
from esphome.const import ALLOWED_NAME_CHARS
from esphome.helpers import slugify
def strip_accents(value):
return "".join(
c
for c in unicodedata.normalize("NFD", str(value))
if unicodedata.category(c) != "Mn"
)
def friendly_name_slugify(value):
value = (
strip_accents(value)
.lower()
.replace(" ", "-")
.replace("_", "-")
.replace("--", "-")
.strip("-")
)
return "".join(c for c in value if c in ALLOWED_NAME_CHARS)
def friendly_name_slugify(value: str) -> str:
"""Convert a friendly name to a slug with dashes instead of underscores."""
# First use the standard slugify, then convert underscores to dashes
return slugify(value).replace("_", "-")

View File

@@ -29,6 +29,53 @@ def ensure_unique_string(preferred_string, current_strings):
return test_string
def fnv1a_32bit_hash(string: str) -> int:
"""FNV-1a 32-bit hash function.
Note: This uses 32-bit hash instead of 64-bit for several reasons:
1. ESPHome targets 32-bit microcontrollers with limited RAM (often <320KB)
2. Using 64-bit hashes would double the RAM usage for storing IDs
3. 64-bit operations are slower on 32-bit processors
While there's a ~50% collision probability at ~77,000 unique IDs,
ESPHome validates for collisions at compile time, preventing any
runtime issues. In practice, most ESPHome installations only have
a handful of area_ids and device_ids (typically <10 areas and <100
devices), making collisions virtually impossible.
"""
hash_value = 2166136261
for char in string:
hash_value ^= ord(char)
hash_value = (hash_value * 16777619) & 0xFFFFFFFF
return hash_value
def strip_accents(value: str) -> str:
"""Remove accents from a string."""
import unicodedata
return "".join(
c
for c in unicodedata.normalize("NFD", str(value))
if unicodedata.category(c) != "Mn"
)
def slugify(value: str) -> str:
"""Convert a string to a valid C++ identifier slug."""
from esphome.const import ALLOWED_NAME_CHARS
value = (
strip_accents(value)
.lower()
.replace(" ", "_")
.replace("-", "_")
.replace("__", "_")
.strip("_")
)
return "".join(c for c in value if c in ALLOWED_NAME_CHARS)
def indent_all_but_first_and_last(text, padding=" "):
lines = text.splitlines(True)
if len(lines) <= 2:

View File

@@ -12,12 +12,12 @@ sensor:
frequency: 60Hz
phase_a:
name: Channel A
voltage: Voltage
current: Current
active_power: Active Power
power_factor: Power Factor
forward_active_energy: Forward Active Energy
reverse_active_energy: Reverse Active Energy
voltage: Channel A Voltage
current: Channel A Current
active_power: Channel A Active Power
power_factor: Channel A Power Factor
forward_active_energy: Channel A Forward Active Energy
reverse_active_energy: Channel A Reverse Active Energy
calibration:
current_gain: 3116628
voltage_gain: -757178
@@ -25,12 +25,12 @@ sensor:
phase_angle: 188
phase_b:
name: Channel B
voltage: Voltage
current: Current
active_power: Active Power
power_factor: Power Factor
forward_active_energy: Forward Active Energy
reverse_active_energy: Reverse Active Energy
voltage: Channel B Voltage
current: Channel B Current
active_power: Channel B Active Power
power_factor: Channel B Power Factor
forward_active_energy: Channel B Forward Active Energy
reverse_active_energy: Channel B Reverse Active Energy
calibration:
current_gain: 3133655
voltage_gain: -755235
@@ -38,12 +38,12 @@ sensor:
phase_angle: 188
phase_c:
name: Channel C
voltage: Voltage
current: Current
active_power: Active Power
power_factor: Power Factor
forward_active_energy: Forward Active Energy
reverse_active_energy: Reverse Active Energy
voltage: Channel C Voltage
current: Channel C Current
active_power: Channel C Active Power
power_factor: Channel C Power Factor
forward_active_energy: Channel C Forward Active Energy
reverse_active_energy: Channel C Reverse Active Energy
calibration:
current_gain: 3111158
voltage_gain: -743813
@@ -51,6 +51,6 @@ sensor:
phase_angle: 180
neutral:
name: Neutral
current: Current
current: Neutral Current
calibration:
current_gain: 3189

View File

@@ -26,7 +26,7 @@ alarm_control_panel:
ESP_LOGD("TEST", "State change %s", LOG_STR_ARG(alarm_control_panel_state_to_string(id(alarmcontrolpanel1)->get_state())));
- platform: template
id: alarmcontrolpanel2
name: Alarm Panel
name: Alarm Panel 2
codes:
- "1234"
requires_code_to_arm: true

View File

@@ -4,6 +4,31 @@ binary_sensor:
id: some_binary_sensor
name: "Random binary"
lambda: return (random_uint32() & 1) == 0;
filters:
- invert:
- delayed_on: 100ms
- delayed_off: 100ms
# Templated, delays for 1s (1000ms) only if a reed switch is active
- delayed_on_off: !lambda "return 1000;"
- delayed_on_off:
time_on: 10s
time_off: !lambda "return 1000;"
- autorepeat:
- delay: 1s
time_off: 100ms
time_on: 900ms
- delay: 5s
time_off: 100ms
time_on: 400ms
- lambda: |-
if (id(some_binary_sensor).state) {
return x;
} else {
return {};
}
- settle: 100ms
- timeout: 10s
on_state_change:
then:
- logger.log:

View File

@@ -26,7 +26,7 @@ binary_sensor:
sensor:
- platform: binary_sensor_map
name: Binary Sensor Map
name: Binary Sensor Map Group
type: group
channels:
- binary_sensor: bin1
@@ -36,7 +36,7 @@ sensor:
- binary_sensor: bin3
value: 100.0
- platform: binary_sensor_map
name: Binary Sensor Map
name: Binary Sensor Map Sum
type: sum
channels:
- binary_sensor: bin1
@@ -46,7 +46,7 @@ sensor:
- binary_sensor: bin3
value: 100.0
- platform: binary_sensor_map
name: Binary Sensor Map
name: Binary Sensor Map Bayesian
type: bayesian
prior: 0.4
observations:

View File

@@ -5,7 +5,7 @@ one_wire:
sensor:
- platform: dallas_temp
address: 0x1C0000031EDD2A28
name: Dallas Temperature
name: Dallas Temperature 1
resolution: 9
- platform: dallas_temp
name: Dallas Temperature
name: Dallas Temperature 2

View File

@@ -2,7 +2,9 @@ esphome:
debug_scheduler: true
platformio_options:
board_build.flash_mode: dio
area: testing
area:
id: testing_area
name: Testing Area
on_boot:
logger.log: on_boot
on_shutdown:
@@ -17,4 +19,20 @@ esphome:
version: "1.1"
on_update:
logger.log: on_update
areas:
- id: another_area
name: Another area
devices:
- id: other_device
name: Another device
area_id: another_area
- id: test_device
name: Test device in main area
area_id: testing_area # Reference the main area (not in areas)
- id: no_area_device
name: Device without area # This device has no area_id
binary_sensor:
- platform: template
name: Other device sensor
device_id: other_device

View File

@@ -7,20 +7,20 @@ climate:
protocol: mitsubishi_heavy_zm
horizontal_default: left
vertical_default: up
name: HeatpumpIR Climate
name: HeatpumpIR Climate Mitsubishi
min_temperature: 18
max_temperature: 30
- platform: heatpumpir
protocol: daikin
horizontal_default: mleft
vertical_default: mup
name: HeatpumpIR Climate
name: HeatpumpIR Climate Daikin
min_temperature: 18
max_temperature: 30
- platform: heatpumpir
protocol: panasonic_altdke
horizontal_default: mright
vertical_default: mdown
name: HeatpumpIR Climate
name: HeatpumpIR Climate Panasonic
min_temperature: 18
max_temperature: 30

View File

@@ -114,7 +114,7 @@ light:
warm_white_color_temperature: 500 mireds
- platform: rgb
id: test_rgb_light_initial_state
name: RGB Light
name: RGB Light Initial State
red: test_ledc_1
green: test_ledc_2
blue: test_ledc_3

View File

@@ -6,13 +6,13 @@ i2c:
sensor:
- platform: ltr390
uv:
name: LTR390 UV
name: LTR390 UV 1
uv_index:
name: LTR390 UVI
name: LTR390 UVI 1
light:
name: LTR390 Light
name: LTR390 Light 1
ambient_light:
name: LTR390 ALS
name: LTR390 ALS 1
gain: X3
resolution: 18
window_correction_factor: 1.0
@@ -20,13 +20,13 @@ sensor:
update_interval: 60s
- platform: ltr390
uv:
name: LTR390 UV
name: LTR390 UV 2
uv_index:
name: LTR390 UVI
name: LTR390 UVI 2
light:
name: LTR390 Light
name: LTR390 Light 2
ambient_light:
name: LTR390 ALS
name: LTR390 ALS 2
gain:
ambient_light: X9
uv: X3

View File

@@ -24,33 +24,33 @@ sensor:
widget: lv_arc
- platform: lvgl
widget: slider_id
name: LVGL Slider
name: LVGL Slider Sensor
- platform: lvgl
widget: bar_id
id: lvgl_bar_sensor
name: LVGL Bar
name: LVGL Bar Sensor
- platform: lvgl
widget: spinbox_id
name: LVGL Spinbox
name: LVGL Spinbox Sensor
number:
- platform: lvgl
widget: slider_id
name: LVGL Slider
name: LVGL Slider Number
update_on_release: true
restore_value: true
- platform: lvgl
widget: lv_arc
id: lvgl_arc_number
name: LVGL Arc
name: LVGL Arc Number
- platform: lvgl
widget: bar_id
id: lvgl_bar_number
name: LVGL Bar
name: LVGL Bar Number
- platform: lvgl
widget: spinbox_id
id: lvgl_spinbox_number
name: LVGL Spinbox
name: LVGL Spinbox Number
light:
- platform: lvgl

View File

@@ -646,7 +646,9 @@ lvgl:
on_click:
lvgl.qrcode.update:
id: lv_qr
text: homeassistant.io
text:
format: "A string with a number %d"
args: ['(int)(random_uint32() % 1000)']
- slider:
min_value: 0

View File

@@ -170,4 +170,4 @@ switch:
otc_active:
name: "Boiler Outside temperature compensation active"
ch2_active:
name: "Boiler Central Heating 2 active"
name: "Boiler Central Heating 2 active status"

View File

@@ -5,7 +5,7 @@ packages:
- !include package.yaml
- github://esphome/esphome/tests/components/template/common.yaml@dev
- url: https://github.com/esphome/esphome
file: tests/components/binary_sensor_map/common.yaml
file: tests/components/absolute_humidity/common.yaml
ref: dev
refresh: 1d

View File

@@ -7,7 +7,7 @@ packages:
shorthand: github://esphome/esphome/tests/components/template/common.yaml@dev
github:
url: https://github.com/esphome/esphome
file: tests/components/binary_sensor_map/common.yaml
file: tests/components/absolute_humidity/common.yaml
ref: dev
refresh: 1d

View File

@@ -115,7 +115,7 @@ button:
address: 0x00
command: 0x0B
- platform: template
name: RC5
name: RC5 Raw
on_press:
remote_transmitter.transmit_raw:
code: [1000, -1000]

View File

@@ -12,7 +12,7 @@
using namespace esphome;
void setup() {
App.pre_setup("livingroom", "LivingRoom", "LivingRoomArea", "comment", __DATE__ ", " __TIME__, false);
App.pre_setup("livingroom", "LivingRoom", "comment", __DATE__ ", " __TIME__, false);
auto *log = new logger::Logger(115200, 512); // NOLINT
log->pre_setup();
log->set_uart_selection(logger::UART_SELECTION_UART0);

View File

@@ -203,6 +203,7 @@ async def compile_esphome(
loop = asyncio.get_running_loop()
def _read_config_and_get_binary():
CORE.reset() # Reset CORE state between test runs
CORE.config_path = str(config_path)
config = esphome.config.read_config(
{"command": "compile", "config": str(config_path)}

View File

@@ -0,0 +1,57 @@
esphome:
name: areas-devices-test
# Define top-level area
area:
id: living_room_area
name: Living Room
# Define additional areas
areas:
- id: bedroom_area
name: Bedroom
- id: kitchen_area
name: Kitchen
# Define devices with area assignments
devices:
- id: light_controller_device
name: Light Controller
area_id: living_room_area # Uses top-level area
- id: temp_sensor_device
name: Temperature Sensor
area_id: bedroom_area
- id: motion_detector_device
name: Motion Detector
area_id: living_room_area # Reuses top-level area
- id: smart_switch_device
name: Smart Switch
area_id: kitchen_area
host:
api:
logger:
# Sensors assigned to different devices
sensor:
- platform: template
name: Light Controller Sensor
device_id: light_controller_device
lambda: return 1.0;
update_interval: 0.1s
- platform: template
name: Temperature Sensor Reading
device_id: temp_sensor_device
lambda: return 2.0;
update_interval: 0.1s
- platform: template
name: Motion Detector Status
device_id: motion_detector_device
lambda: return 3.0;
update_interval: 0.1s
- platform: template
name: Smart Switch Power
device_id: smart_switch_device
lambda: return 4.0;
update_interval: 0.1s

View File

@@ -0,0 +1,154 @@
esphome:
name: duplicate-entities-test
# Define devices to test multi-device duplicate handling
devices:
- id: controller_1
name: Controller 1
- id: controller_2
name: Controller 2
- id: controller_3
name: Controller 3
host:
api: # Port will be automatically injected
logger:
# Test that duplicate entity names are allowed on different devices
# Scenario 1: Same sensor name on different devices (allowed)
sensor:
- platform: template
name: Temperature
device_id: controller_1
lambda: return 21.0;
update_interval: 0.1s
- platform: template
name: Temperature
device_id: controller_2
lambda: return 22.0;
update_interval: 0.1s
- platform: template
name: Temperature
device_id: controller_3
lambda: return 23.0;
update_interval: 0.1s
# Main device sensor (no device_id)
- platform: template
name: Temperature
lambda: return 20.0;
update_interval: 0.1s
# Different sensor with unique name
- platform: template
name: Humidity
lambda: return 60.0;
update_interval: 0.1s
# Scenario 2: Same binary sensor name on different devices (allowed)
binary_sensor:
- platform: template
name: Status
device_id: controller_1
lambda: return true;
- platform: template
name: Status
device_id: controller_2
lambda: return false;
- platform: template
name: Status
lambda: return true; # Main device
# Different platform can have same name as sensor
- platform: template
name: Temperature
lambda: return true;
# Scenario 3: Same text sensor name on different devices
text_sensor:
- platform: template
name: Device Info
device_id: controller_1
lambda: return {"Controller 1 Active"};
update_interval: 0.1s
- platform: template
name: Device Info
device_id: controller_2
lambda: return {"Controller 2 Active"};
update_interval: 0.1s
- platform: template
name: Device Info
lambda: return {"Main Device Active"};
update_interval: 0.1s
# Scenario 4: Same switch name on different devices
switch:
- platform: template
name: Power
device_id: controller_1
lambda: return false;
turn_on_action: []
turn_off_action: []
- platform: template
name: Power
device_id: controller_2
lambda: return true;
turn_on_action: []
turn_off_action: []
- platform: template
name: Power
device_id: controller_3
lambda: return false;
turn_on_action: []
turn_off_action: []
# Unique switch on main device
- platform: template
name: Main Power
lambda: return true;
turn_on_action: []
turn_off_action: []
# Scenario 5: Empty names on different devices (should use device name)
button:
- platform: template
name: ""
device_id: controller_1
on_press: []
- platform: template
name: ""
device_id: controller_2
on_press: []
- platform: template
name: ""
on_press: [] # Main device
# Scenario 6: Special characters in names
number:
- platform: template
name: "Temperature Setpoint!"
device_id: controller_1
min_value: 10.0
max_value: 30.0
step: 0.1
lambda: return 21.0;
set_action: []
- platform: template
name: "Temperature Setpoint!"
device_id: controller_2
min_value: 10.0
max_value: 30.0
step: 0.1
lambda: return 22.0;
set_action: []

View File

@@ -0,0 +1,15 @@
esphome:
name: legacy-area-test
# Using legacy string-based area configuration
area: Master Bedroom
host:
api:
logger:
# Simple sensor to ensure the device compiles and runs
sensor:
- platform: template
name: Test Sensor
lambda: return 42.0;
update_interval: 1s

View File

@@ -0,0 +1,121 @@
"""Integration test for areas and devices feature."""
from __future__ import annotations
import asyncio
from aioesphomeapi import EntityState
import pytest
from .types import APIClientConnectedFactory, RunCompiledFunction
@pytest.mark.asyncio
async def test_areas_and_devices(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""Test areas and devices configuration with entity mapping."""
async with run_compiled(yaml_config), api_client_connected() as client:
# Get device info which includes areas and devices
device_info = await client.device_info()
assert device_info is not None
# Verify areas are reported
areas = device_info.areas
assert len(areas) >= 2, f"Expected at least 2 areas, got {len(areas)}"
# Find our specific areas
main_area = next((a for a in areas if a.name == "Living Room"), None)
bedroom_area = next((a for a in areas if a.name == "Bedroom"), None)
kitchen_area = next((a for a in areas if a.name == "Kitchen"), None)
assert main_area is not None, "Living Room area not found"
assert bedroom_area is not None, "Bedroom area not found"
assert kitchen_area is not None, "Kitchen area not found"
# Verify devices are reported
devices = device_info.devices
assert len(devices) >= 4, f"Expected at least 4 devices, got {len(devices)}"
# Find our specific devices
light_controller = next(
(d for d in devices if d.name == "Light Controller"), None
)
temp_sensor = next((d for d in devices if d.name == "Temperature Sensor"), None)
motion_detector = next(
(d for d in devices if d.name == "Motion Detector"), None
)
smart_switch = next((d for d in devices if d.name == "Smart Switch"), None)
assert light_controller is not None, "Light Controller device not found"
assert temp_sensor is not None, "Temperature Sensor device not found"
assert motion_detector is not None, "Motion Detector device not found"
assert smart_switch is not None, "Smart Switch device not found"
# Verify device area assignments
assert light_controller.area_id == main_area.area_id, (
"Light Controller should be in Living Room"
)
assert temp_sensor.area_id == bedroom_area.area_id, (
"Temperature Sensor should be in Bedroom"
)
assert motion_detector.area_id == main_area.area_id, (
"Motion Detector should be in Living Room"
)
assert smart_switch.area_id == kitchen_area.area_id, (
"Smart Switch should be in Kitchen"
)
# Verify suggested_area is set to the top-level area name
assert device_info.suggested_area == "Living Room", (
f"Expected suggested_area to be 'Living Room', got '{device_info.suggested_area}'"
)
# Get entity list to verify device_id mapping
entities = await client.list_entities_services()
# Collect sensor entities
sensor_entities = [e for e in entities[0] if hasattr(e, "device_id")]
assert len(sensor_entities) >= 4, (
f"Expected at least 4 sensor entities, got {len(sensor_entities)}"
)
# Subscribe to states to get sensor values
loop = asyncio.get_running_loop()
states: dict[int, EntityState] = {}
states_future: asyncio.Future[bool] = loop.create_future()
def on_state(state: EntityState) -> None:
states[state.key] = state
# Check if we have all expected sensor states
if len(states) >= 4 and not states_future.done():
states_future.set_result(True)
client.subscribe_states(on_state)
# Wait for sensor states
try:
await asyncio.wait_for(states_future, timeout=10.0)
except asyncio.TimeoutError:
pytest.fail(
f"Did not receive all sensor states within 10 seconds. "
f"Received {len(states)} states"
)
# Verify we have sensor entities with proper device_id assignments
device_id_mapping = {
"Light Controller Sensor": light_controller.device_id,
"Temperature Sensor Reading": temp_sensor.device_id,
"Motion Detector Status": motion_detector.device_id,
"Smart Switch Power": smart_switch.device_id,
}
for entity in sensor_entities:
if entity.name in device_id_mapping:
expected_device_id = device_id_mapping[entity.name]
assert entity.device_id == expected_device_id, (
f"{entity.name} has device_id {entity.device_id}, "
f"expected {expected_device_id}"
)

View File

@@ -0,0 +1,184 @@
"""Integration test for duplicate entity handling with new validation."""
from __future__ import annotations
import asyncio
from aioesphomeapi import EntityInfo
import pytest
from .types import APIClientConnectedFactory, RunCompiledFunction
@pytest.mark.asyncio
async def test_duplicate_entities_on_different_devices(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""Test that duplicate entity names are allowed on different devices."""
async with run_compiled(yaml_config), api_client_connected() as client:
# Get device info
device_info = await client.device_info()
assert device_info is not None
# Get devices
devices = device_info.devices
assert len(devices) >= 3, f"Expected at least 3 devices, got {len(devices)}"
# Find our test devices
controller_1 = next((d for d in devices if d.name == "Controller 1"), None)
controller_2 = next((d for d in devices if d.name == "Controller 2"), None)
controller_3 = next((d for d in devices if d.name == "Controller 3"), None)
assert controller_1 is not None, "Controller 1 device not found"
assert controller_2 is not None, "Controller 2 device not found"
assert controller_3 is not None, "Controller 3 device not found"
# Get entity list
entities = await client.list_entities_services()
all_entities: list[EntityInfo] = []
for entity_list in entities[0]:
all_entities.append(entity_list)
# Group entities by type for easier testing
sensors = [e for e in all_entities if e.__class__.__name__ == "SensorInfo"]
binary_sensors = [
e for e in all_entities if e.__class__.__name__ == "BinarySensorInfo"
]
text_sensors = [
e for e in all_entities if e.__class__.__name__ == "TextSensorInfo"
]
switches = [e for e in all_entities if e.__class__.__name__ == "SwitchInfo"]
buttons = [e for e in all_entities if e.__class__.__name__ == "ButtonInfo"]
numbers = [e for e in all_entities if e.__class__.__name__ == "NumberInfo"]
# Scenario 1: Check sensors with same "Temperature" name on different devices
temp_sensors = [s for s in sensors if s.name == "Temperature"]
assert len(temp_sensors) == 4, (
f"Expected exactly 4 temperature sensors, got {len(temp_sensors)}"
)
# Verify each sensor is on a different device
temp_device_ids = set()
temp_object_ids = set()
for sensor in temp_sensors:
temp_device_ids.add(sensor.device_id)
temp_object_ids.add(sensor.object_id)
# All should have object_id "temperature" (no suffix)
assert sensor.object_id == "temperature", (
f"Expected object_id 'temperature', got '{sensor.object_id}'"
)
# Should have 4 different device IDs (including None for main device)
assert len(temp_device_ids) == 4, (
f"Temperature sensors should be on different devices, got {temp_device_ids}"
)
# Scenario 2: Check binary sensors "Status" on different devices
status_binary = [b for b in binary_sensors if b.name == "Status"]
assert len(status_binary) == 3, (
f"Expected exactly 3 status binary sensors, got {len(status_binary)}"
)
# All should have object_id "status"
for binary in status_binary:
assert binary.object_id == "status", (
f"Expected object_id 'status', got '{binary.object_id}'"
)
# Scenario 3: Check that sensor and binary_sensor can have same name
temp_binary = [b for b in binary_sensors if b.name == "Temperature"]
assert len(temp_binary) == 1, (
f"Expected exactly 1 temperature binary sensor, got {len(temp_binary)}"
)
assert temp_binary[0].object_id == "temperature"
# Scenario 4: Check text sensors "Device Info" on different devices
info_text = [t for t in text_sensors if t.name == "Device Info"]
assert len(info_text) == 3, (
f"Expected exactly 3 device info text sensors, got {len(info_text)}"
)
# All should have object_id "device_info"
for text in info_text:
assert text.object_id == "device_info", (
f"Expected object_id 'device_info', got '{text.object_id}'"
)
# Scenario 5: Check switches "Power" on different devices
power_switches = [s for s in switches if s.name == "Power"]
assert len(power_switches) == 3, (
f"Expected exactly 3 power switches, got {len(power_switches)}"
)
# All should have object_id "power"
for switch in power_switches:
assert switch.object_id == "power", (
f"Expected object_id 'power', got '{switch.object_id}'"
)
# Scenario 6: Check empty name buttons (should use device name)
empty_buttons = [b for b in buttons if b.name == ""]
assert len(empty_buttons) == 3, (
f"Expected exactly 3 empty name buttons, got {len(empty_buttons)}"
)
# Group by device
c1_buttons = [b for b in empty_buttons if b.device_id == controller_1.device_id]
c2_buttons = [b for b in empty_buttons if b.device_id == controller_2.device_id]
# For main device, device_id is 0
main_buttons = [b for b in empty_buttons if b.device_id == 0]
# Check object IDs for empty name entities
assert len(c1_buttons) == 1 and c1_buttons[0].object_id == "controller_1"
assert len(c2_buttons) == 1 and c2_buttons[0].object_id == "controller_2"
assert (
len(main_buttons) == 1
and main_buttons[0].object_id == "duplicate-entities-test"
)
# Scenario 7: Check special characters in number names
temp_numbers = [n for n in numbers if n.name == "Temperature Setpoint!"]
assert len(temp_numbers) == 2, (
f"Expected exactly 2 temperature setpoint numbers, got {len(temp_numbers)}"
)
# Special characters should be sanitized to _ in object_id
for number in temp_numbers:
assert number.object_id == "temperature_setpoint_", (
f"Expected object_id 'temperature_setpoint_', got '{number.object_id}'"
)
# Verify we can get states for all entities (ensures they're functional)
loop = asyncio.get_running_loop()
states_future: asyncio.Future[None] = loop.create_future()
state_count = 0
expected_count = (
len(sensors)
+ len(binary_sensors)
+ len(text_sensors)
+ len(switches)
+ len(buttons)
+ len(numbers)
)
def on_state(state) -> None:
nonlocal state_count
state_count += 1
if state_count >= expected_count and not states_future.done():
states_future.set_result(None)
client.subscribe_states(on_state)
# Wait for all entity states
try:
await asyncio.wait_for(states_future, timeout=10.0)
except asyncio.TimeoutError:
pytest.fail(
f"Did not receive all entity states within 10 seconds. "
f"Expected {expected_count}, received {state_count}"
)

View File

@@ -0,0 +1,41 @@
"""Integration test for legacy string-based area configuration."""
from __future__ import annotations
import pytest
from .types import APIClientConnectedFactory, RunCompiledFunction
@pytest.mark.asyncio
async def test_legacy_area(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""Test legacy string-based area configuration."""
async with run_compiled(yaml_config), api_client_connected() as client:
# Get device info which includes areas
device_info = await client.device_info()
assert device_info is not None
# Verify the area is reported (should be converted to structured format)
areas = device_info.areas
assert len(areas) == 1, f"Expected exactly 1 area, got {len(areas)}"
# Find the area - should be slugified from "Master Bedroom"
area = areas[0]
assert area.name == "Master Bedroom", (
f"Expected area name 'Master Bedroom', got '{area.name}'"
)
# Verify area.id is set (it should be a hash)
assert area.area_id > 0, "Area ID should be a positive hash value"
# The suggested_area field should be set for backward compatibility
assert device_info.suggested_area == "Master Bedroom", (
f"Expected suggested_area to be 'Master Bedroom', got '{device_info.suggested_area}'"
)
# Verify deprecated warning would have been logged during compilation
# (We can't check logs directly in integration tests, but the code should work)

View File

@@ -14,6 +14,8 @@ import sys
import pytest
from esphome.core import CORE
here = Path(__file__).parent
# Configure location of package root
@@ -21,6 +23,13 @@ package_root = here.parent.parent
sys.path.insert(0, package_root.as_posix())
@pytest.fixture(autouse=True)
def reset_core():
"""Reset CORE after each test."""
yield
CORE.reset()
@pytest.fixture
def fixture_path() -> Path:
"""

View File

View File

@@ -0,0 +1,33 @@
"""Common test utilities for core unit tests."""
from collections.abc import Callable
from pathlib import Path
from unittest.mock import patch
from esphome import config, yaml_util
from esphome.config import Config
from esphome.core import CORE
def load_config_from_yaml(
yaml_file: Callable[[str], str], yaml_content: str
) -> Config | None:
"""Load configuration from YAML content."""
yaml_path = yaml_file(yaml_content)
parsed_yaml = yaml_util.load_yaml(yaml_path)
# Mock yaml_util.load_yaml to return our parsed content
with (
patch.object(yaml_util, "load_yaml", return_value=parsed_yaml),
patch.object(CORE, "config_path", yaml_path),
):
return config.read_config({})
def load_config_from_fixture(
yaml_file: Callable[[str], str], fixture_name: str, fixtures_dir: Path
) -> Config | None:
"""Load configuration from a fixture file."""
fixture_path = fixtures_dir / fixture_name
yaml_content = fixture_path.read_text()
return load_config_from_yaml(yaml_file, yaml_content)

View File

@@ -0,0 +1,18 @@
"""Shared fixtures for core unit tests."""
from collections.abc import Callable
from pathlib import Path
import pytest
@pytest.fixture
def yaml_file(tmp_path: Path) -> Callable[[str], str]:
"""Create a temporary YAML file for testing."""
def _yaml_file(content: str) -> str:
yaml_path = tmp_path / "test.yaml"
yaml_path.write_text(content)
return str(yaml_path)
return _yaml_file

View File

@@ -0,0 +1,225 @@
"""Unit tests for core config functionality including areas and devices."""
from collections.abc import Callable
from pathlib import Path
from typing import Any
import pytest
from esphome import config_validation as cv, core
from esphome.const import CONF_AREA, CONF_AREAS, CONF_DEVICES
from esphome.core.config import Area, validate_area_config
from .common import load_config_from_fixture
FIXTURES_DIR = Path(__file__).parent.parent / "fixtures" / "core" / "config"
def test_validate_area_config_with_string() -> None:
"""Test that string area config is converted to structured format."""
result = validate_area_config("Living Room")
assert isinstance(result, dict)
assert "id" in result
assert "name" in result
assert result["name"] == "Living Room"
assert isinstance(result["id"], core.ID)
assert result["id"].is_declaration
assert not result["id"].is_manual
def test_validate_area_config_with_dict() -> None:
"""Test that structured area config passes through unchanged."""
area_id = cv.declare_id(Area)("test_area")
input_config: dict[str, Any] = {
"id": area_id,
"name": "Test Area",
}
result = validate_area_config(input_config)
assert result == input_config
assert result["id"] == area_id
assert result["name"] == "Test Area"
def test_device_with_valid_area_id(yaml_file: Callable[[str], str]) -> None:
"""Test that device with valid area_id works correctly."""
result = load_config_from_fixture(yaml_file, "valid_area_device.yaml", FIXTURES_DIR)
assert result is not None
esphome_config = result["esphome"]
# Verify areas were parsed correctly
assert CONF_AREAS in esphome_config
areas = esphome_config[CONF_AREAS]
assert len(areas) == 1
assert areas[0]["id"].id == "bedroom_area"
assert areas[0]["name"] == "Bedroom"
# Verify devices were parsed correctly
assert CONF_DEVICES in esphome_config
devices = esphome_config[CONF_DEVICES]
assert len(devices) == 1
assert devices[0]["id"].id == "test_device"
assert devices[0]["name"] == "Test Device"
assert devices[0]["area_id"].id == "bedroom_area"
def test_multiple_areas_and_devices(yaml_file: Callable[[str], str]) -> None:
"""Test multiple areas and devices configuration."""
result = load_config_from_fixture(
yaml_file, "multiple_areas_devices.yaml", FIXTURES_DIR
)
assert result is not None
esphome_config = result["esphome"]
# Verify main area
assert CONF_AREA in esphome_config
main_area = esphome_config[CONF_AREA]
assert main_area["id"].id == "main_area"
assert main_area["name"] == "Main Area"
# Verify additional areas
assert CONF_AREAS in esphome_config
areas = esphome_config[CONF_AREAS]
assert len(areas) == 2
area_ids = {area["id"].id for area in areas}
assert area_ids == {"area1", "area2"}
# Verify devices
assert CONF_DEVICES in esphome_config
devices = esphome_config[CONF_DEVICES]
assert len(devices) == 3
# Check device-area associations
device_area_map = {dev["id"].id: dev["area_id"].id for dev in devices}
assert device_area_map == {
"device1": "main_area",
"device2": "area1",
"device3": "area2",
}
def test_legacy_string_area(
yaml_file: Callable[[str], str], caplog: pytest.LogCaptureFixture
) -> None:
"""Test legacy string area configuration with deprecation warning."""
result = load_config_from_fixture(
yaml_file, "legacy_string_area.yaml", FIXTURES_DIR
)
assert result is not None
esphome_config = result["esphome"]
# Verify the string was converted to structured format
assert CONF_AREA in esphome_config
area = esphome_config[CONF_AREA]
assert isinstance(area, dict)
assert area["name"] == "Living Room"
assert isinstance(area["id"], core.ID)
assert area["id"].is_declaration
assert not area["id"].is_manual
def test_area_id_collision(
yaml_file: Callable[[str], str], capsys: pytest.CaptureFixture[str]
) -> None:
"""Test that duplicate area IDs are detected."""
result = load_config_from_fixture(yaml_file, "area_id_collision.yaml", FIXTURES_DIR)
assert result is None
# Check for the specific error message in stdout
captured = capsys.readouterr()
# Exact duplicates are now caught by IDPassValidationStep
assert "ID duplicate_id redefined! Check esphome->area->id." in captured.out
def test_device_without_area(yaml_file: Callable[[str], str]) -> None:
"""Test that devices without area_id work correctly."""
result = load_config_from_fixture(
yaml_file, "device_without_area.yaml", FIXTURES_DIR
)
assert result is not None
esphome_config = result["esphome"]
# Verify device was parsed
assert CONF_DEVICES in esphome_config
devices = esphome_config[CONF_DEVICES]
assert len(devices) == 1
device = devices[0]
assert device["id"].id == "test_device"
assert device["name"] == "Test Device"
# Verify no area_id is present
assert "area_id" not in device
def test_device_with_invalid_area_id(
yaml_file: Callable[[str], str], capsys: pytest.CaptureFixture[str]
) -> None:
"""Test that device with non-existent area_id fails validation."""
result = load_config_from_fixture(
yaml_file, "device_invalid_area.yaml", FIXTURES_DIR
)
assert result is None
# Check for the specific error message in stdout
captured = capsys.readouterr()
assert (
"Couldn't find ID 'nonexistent_area'. Please check you have defined an ID with that name in your configuration."
in captured.out
)
def test_device_id_hash_collision(
yaml_file: Callable[[str], str], capsys: pytest.CaptureFixture[str]
) -> None:
"""Test that device IDs with hash collisions are detected."""
result = load_config_from_fixture(
yaml_file, "device_id_collision.yaml", FIXTURES_DIR
)
assert result is None
# Check for the specific error message about hash collision
captured = capsys.readouterr()
# The error message shows the ID that collides and includes the hash value
assert (
"Device ID 'd6ka' with hash 3082558663 collides with existing device ID 'test_2258'"
in captured.out
)
def test_area_id_hash_collision(
yaml_file: Callable[[str], str], capsys: pytest.CaptureFixture[str]
) -> None:
"""Test that area IDs with hash collisions are detected."""
result = load_config_from_fixture(
yaml_file, "area_id_hash_collision.yaml", FIXTURES_DIR
)
assert result is None
# Check for the specific error message about hash collision
captured = capsys.readouterr()
# The error message shows the ID that collides and includes the hash value
assert (
"Area ID 'd6ka' with hash 3082558663 collides with existing area ID 'test_2258'"
in captured.out
)
def test_device_duplicate_id(
yaml_file: Callable[[str], str], capsys: pytest.CaptureFixture[str]
) -> None:
"""Test that duplicate device IDs are detected by IDPassValidationStep."""
result = load_config_from_fixture(
yaml_file, "device_duplicate_id.yaml", FIXTURES_DIR
)
assert result is None
# Check for the specific error message from IDPassValidationStep
captured = capsys.readouterr()
assert "ID duplicate_device redefined!" in captured.out

View File

@@ -0,0 +1,595 @@
"""Test get_base_entity_object_id function matches C++ behavior."""
from collections.abc import Callable, Generator
from pathlib import Path
import re
from typing import Any
import pytest
from esphome.config_validation import Invalid
from esphome.const import CONF_DEVICE_ID, CONF_DISABLED_BY_DEFAULT, CONF_ICON, CONF_NAME
from esphome.core import CORE, ID, entity_helpers
from esphome.core.entity_helpers import get_base_entity_object_id, setup_entity
from esphome.cpp_generator import MockObj
from esphome.helpers import sanitize, snake_case
from .common import load_config_from_fixture
# Pre-compiled regex pattern for extracting object IDs from expressions
OBJECT_ID_PATTERN = re.compile(r'\.set_object_id\(["\'](.*?)["\']\)')
FIXTURES_DIR = Path(__file__).parent.parent / "fixtures" / "core" / "entity_helpers"
@pytest.fixture(autouse=True)
def restore_core_state() -> Generator[None, None, None]:
"""Save and restore CORE state for tests."""
original_name = CORE.name
original_friendly_name = CORE.friendly_name
yield
CORE.name = original_name
CORE.friendly_name = original_friendly_name
def test_with_entity_name() -> None:
"""Test when entity has its own name - should use entity name."""
# Simple name
assert get_base_entity_object_id("Temperature Sensor", None) == "temperature_sensor"
assert (
get_base_entity_object_id("Temperature Sensor", "Device Name")
== "temperature_sensor"
)
# Even with device name, entity name takes precedence
assert (
get_base_entity_object_id("Temperature Sensor", "Device Name", "Sub Device")
== "temperature_sensor"
)
# Name with special characters
assert (
get_base_entity_object_id("Temp!@#$%^&*()Sensor", None)
== "temp__________sensor"
)
assert get_base_entity_object_id("Temp-Sensor_123", None) == "temp-sensor_123"
# Already snake_case
assert get_base_entity_object_id("temperature_sensor", None) == "temperature_sensor"
# Mixed case
assert get_base_entity_object_id("TemperatureSensor", None) == "temperaturesensor"
assert get_base_entity_object_id("TEMPERATURE SENSOR", None) == "temperature_sensor"
def test_empty_name_with_device_name() -> None:
"""Test when entity has empty name and is on a sub-device - should use device name."""
# C++ behavior: when has_own_name is false and device is set, uses device->get_name()
assert (
get_base_entity_object_id("", "Friendly Device", "Sub Device 1")
== "sub_device_1"
)
assert (
get_base_entity_object_id("", "Kitchen Controller", "controller_1")
== "controller_1"
)
assert get_base_entity_object_id("", None, "Test-Device_123") == "test-device_123"
def test_empty_name_with_friendly_name() -> None:
"""Test when entity has empty name and no device - should use friendly name."""
# C++ behavior: when has_own_name is false, uses App.get_friendly_name()
assert get_base_entity_object_id("", "Friendly Device") == "friendly_device"
assert get_base_entity_object_id("", "Kitchen Controller") == "kitchen_controller"
assert get_base_entity_object_id("", "Test-Device_123") == "test-device_123"
# Special characters in friendly name
assert get_base_entity_object_id("", "Device!@#$%") == "device_____"
def test_empty_name_no_friendly_name() -> None:
"""Test when entity has empty name and no friendly name - should use device name."""
# Test with CORE.name set
CORE.name = "device-name"
assert get_base_entity_object_id("", None) == "device-name"
CORE.name = "Test Device"
assert get_base_entity_object_id("", None) == "test_device"
def test_edge_cases() -> None:
"""Test edge cases."""
# Only spaces
assert get_base_entity_object_id(" ", None) == "___"
# Unicode characters (should be replaced)
assert get_base_entity_object_id("Température", None) == "temp_rature"
assert get_base_entity_object_id("测试", None) == "__"
# Empty string with empty friendly name (empty friendly name is treated as None)
# Falls back to CORE.name
CORE.name = "device"
assert get_base_entity_object_id("", "") == "device"
# Very long name (should work fine)
long_name = "a" * 100 + " " + "b" * 100
expected = "a" * 100 + "_" + "b" * 100
assert get_base_entity_object_id(long_name, None) == expected
@pytest.mark.parametrize(
("name", "expected"),
[
("Temperature Sensor", "temperature_sensor"),
("Living Room Light", "living_room_light"),
("Test-Device_123", "test-device_123"),
("Special!@#Chars", "special___chars"),
("UPPERCASE NAME", "uppercase_name"),
("lowercase name", "lowercase_name"),
("Mixed Case Name", "mixed_case_name"),
(" Spaces ", "___spaces___"),
],
)
def test_matches_cpp_helpers(name: str, expected: str) -> None:
"""Test that the logic matches using snake_case and sanitize directly."""
# For non-empty names, verify our function produces same result as direct snake_case + sanitize
assert get_base_entity_object_id(name, None) == sanitize(snake_case(name))
assert get_base_entity_object_id(name, None) == expected
def test_empty_name_fallback() -> None:
"""Test empty name handling which falls back to friendly_name or CORE.name."""
# Empty name is handled specially - it doesn't just use sanitize(snake_case(""))
# Instead it falls back to friendly_name or CORE.name
assert sanitize(snake_case("")) == "" # Direct conversion gives empty string
# But our function returns a fallback
CORE.name = "device"
assert get_base_entity_object_id("", None) == "device" # Uses device name
def test_name_add_mac_suffix_behavior() -> None:
"""Test behavior related to name_add_mac_suffix.
In C++, when name_add_mac_suffix is enabled and entity has no name,
get_object_id() returns str_sanitize(str_snake_case(App.get_friendly_name()))
dynamically. Our function always returns the same result since we're
calculating the base for duplicate tracking.
"""
# The function should always return the same result regardless of
# name_add_mac_suffix setting, as we're calculating the base object_id
assert get_base_entity_object_id("", "Test Device") == "test_device"
assert get_base_entity_object_id("Entity Name", "Test Device") == "entity_name"
def test_priority_order() -> None:
"""Test the priority order: entity name > device name > friendly name > CORE.name."""
CORE.name = "core-device"
# 1. Entity name has highest priority
assert (
get_base_entity_object_id("Entity Name", "Friendly Name", "Device Name")
== "entity_name"
)
# 2. Device name is next priority (when entity name is empty)
assert (
get_base_entity_object_id("", "Friendly Name", "Device Name") == "device_name"
)
# 3. Friendly name is next (when entity and device names are empty)
assert get_base_entity_object_id("", "Friendly Name", None) == "friendly_name"
# 4. CORE.name is last resort
assert get_base_entity_object_id("", None, None) == "core-device"
@pytest.mark.parametrize(
("name", "friendly_name", "device_name", "expected"),
[
# name, friendly_name, device_name, expected
("Living Room Light", None, None, "living_room_light"),
("", "Kitchen Controller", None, "kitchen_controller"),
(
"",
"ESP32 Device",
"controller_1",
"controller_1",
), # Device name takes precedence
("GPIO2 Button", None, None, "gpio2_button"),
("WiFi Signal", "My Device", None, "wifi_signal"),
("", None, "esp32_node", "esp32_node"),
("Front Door Sensor", "Home Assistant", "door_controller", "front_door_sensor"),
],
)
def test_real_world_examples(
name: str, friendly_name: str | None, device_name: str | None, expected: str
) -> None:
"""Test real-world entity naming scenarios."""
result = get_base_entity_object_id(name, friendly_name, device_name)
assert result == expected
def test_issue_6953_scenarios() -> None:
"""Test specific scenarios from issue #6953."""
# Scenario 1: Multiple empty names on main device with name_add_mac_suffix
# The Python code calculates the base, C++ might append MAC suffix dynamically
CORE.name = "device-name"
CORE.friendly_name = "Friendly Device"
# All empty names should resolve to same base
assert get_base_entity_object_id("", CORE.friendly_name) == "friendly_device"
assert get_base_entity_object_id("", CORE.friendly_name) == "friendly_device"
assert get_base_entity_object_id("", CORE.friendly_name) == "friendly_device"
# Scenario 2: Empty names on sub-devices
assert (
get_base_entity_object_id("", "Main Device", "controller_1") == "controller_1"
)
assert (
get_base_entity_object_id("", "Main Device", "controller_2") == "controller_2"
)
# Scenario 3: xyz duplicates
assert get_base_entity_object_id("xyz", None) == "xyz"
assert get_base_entity_object_id("xyz", "Device") == "xyz"
# Tests for setup_entity function
@pytest.fixture
def setup_test_environment() -> Generator[list[str], None, None]:
"""Set up test environment for setup_entity tests."""
# Set CORE state for tests
CORE.name = "test-device"
CORE.friendly_name = "Test Device"
# Store original add function
original_add = entity_helpers.add
# Track what gets added
added_expressions: list[str] = []
def mock_add(expression: Any) -> Any:
added_expressions.append(str(expression))
return original_add(expression)
# Patch add function in entity_helpers module
entity_helpers.add = mock_add
yield added_expressions
# Clean up
entity_helpers.add = original_add
def extract_object_id_from_expressions(expressions: list[str]) -> str | None:
"""Extract the object ID that was set from the generated expressions."""
for expr in expressions:
# Look for set_object_id calls with regex to handle various formats
# Matches: var.set_object_id("temperature_2") or var.set_object_id('temperature_2')
if match := OBJECT_ID_PATTERN.search(expr):
return match.group(1)
return None
@pytest.mark.asyncio
async def test_setup_entity_no_duplicates(setup_test_environment: list[str]) -> None:
"""Test setup_entity with unique names."""
added_expressions = setup_test_environment
# Create mock entities
var1 = MockObj("sensor1")
var2 = MockObj("sensor2")
# Set up first entity
config1 = {
CONF_NAME: "Temperature",
CONF_DISABLED_BY_DEFAULT: False,
}
await setup_entity(var1, config1, "sensor")
# Get object ID from first entity
object_id1 = extract_object_id_from_expressions(added_expressions)
assert object_id1 == "temperature"
# Clear for next entity
added_expressions.clear()
# Set up second entity with different name
config2 = {
CONF_NAME: "Humidity",
CONF_DISABLED_BY_DEFAULT: False,
}
await setup_entity(var2, config2, "sensor")
# Get object ID from second entity
object_id2 = extract_object_id_from_expressions(added_expressions)
assert object_id2 == "humidity"
@pytest.mark.asyncio
async def test_setup_entity_different_platforms(
setup_test_environment: list[str],
) -> None:
"""Test that same name on different platforms doesn't conflict."""
added_expressions = setup_test_environment
# Create mock entities
sensor = MockObj("sensor1")
binary_sensor = MockObj("binary_sensor1")
text_sensor = MockObj("text_sensor1")
config = {
CONF_NAME: "Status",
CONF_DISABLED_BY_DEFAULT: False,
}
# Set up entities on different platforms
platforms = [
(sensor, "sensor"),
(binary_sensor, "binary_sensor"),
(text_sensor, "text_sensor"),
]
object_ids: list[str] = []
for var, platform in platforms:
added_expressions.clear()
await setup_entity(var, config, platform)
object_id = extract_object_id_from_expressions(added_expressions)
object_ids.append(object_id)
# All should get base object ID without suffix
assert all(obj_id == "status" for obj_id in object_ids)
@pytest.fixture
def mock_get_variable() -> Generator[dict[ID, MockObj], None, None]:
"""Mock get_variable to return test devices."""
devices = {}
original_get_variable = entity_helpers.get_variable
async def _mock_get_variable(device_id: ID) -> MockObj:
if device_id in devices:
return devices[device_id]
return await original_get_variable(device_id)
entity_helpers.get_variable = _mock_get_variable
yield devices
# Clean up
entity_helpers.get_variable = original_get_variable
@pytest.mark.asyncio
async def test_setup_entity_with_devices(
setup_test_environment: list[str], mock_get_variable: dict[ID, MockObj]
) -> None:
"""Test that same name on different devices doesn't conflict."""
added_expressions = setup_test_environment
# Create mock devices
device1_id = ID("device1", type="Device")
device2_id = ID("device2", type="Device")
device1 = MockObj("device1_obj")
device2 = MockObj("device2_obj")
# Register devices with the mock
mock_get_variable[device1_id] = device1
mock_get_variable[device2_id] = device2
# Create sensors with same name on different devices
sensor1 = MockObj("sensor1")
sensor2 = MockObj("sensor2")
config1 = {
CONF_NAME: "Temperature",
CONF_DEVICE_ID: device1_id,
CONF_DISABLED_BY_DEFAULT: False,
}
config2 = {
CONF_NAME: "Temperature",
CONF_DEVICE_ID: device2_id,
CONF_DISABLED_BY_DEFAULT: False,
}
# Get object IDs
object_ids: list[str] = []
for var, config in [(sensor1, config1), (sensor2, config2)]:
added_expressions.clear()
await setup_entity(var, config, "sensor")
object_id = extract_object_id_from_expressions(added_expressions)
object_ids.append(object_id)
# Both should get base object ID without suffix (different devices)
assert object_ids[0] == "temperature"
assert object_ids[1] == "temperature"
@pytest.mark.asyncio
async def test_setup_entity_empty_name(setup_test_environment: list[str]) -> None:
"""Test setup_entity with empty entity name."""
added_expressions = setup_test_environment
var = MockObj("sensor1")
config = {
CONF_NAME: "",
CONF_DISABLED_BY_DEFAULT: False,
}
await setup_entity(var, config, "sensor")
object_id = extract_object_id_from_expressions(added_expressions)
# Should use friendly name
assert object_id == "test_device"
@pytest.mark.asyncio
async def test_setup_entity_special_characters(
setup_test_environment: list[str],
) -> None:
"""Test setup_entity with names containing special characters."""
added_expressions = setup_test_environment
var = MockObj("sensor1")
config = {
CONF_NAME: "Temperature Sensor!",
CONF_DISABLED_BY_DEFAULT: False,
}
await setup_entity(var, config, "sensor")
object_id = extract_object_id_from_expressions(added_expressions)
# Special characters should be sanitized
assert object_id == "temperature_sensor_"
@pytest.mark.asyncio
async def test_setup_entity_with_icon(setup_test_environment: list[str]) -> None:
"""Test setup_entity sets icon correctly."""
added_expressions = setup_test_environment
var = MockObj("sensor1")
config = {
CONF_NAME: "Temperature",
CONF_DISABLED_BY_DEFAULT: False,
CONF_ICON: "mdi:thermometer",
}
await setup_entity(var, config, "sensor")
# Check icon was set
assert any(
'sensor1.set_icon("mdi:thermometer")' in expr for expr in added_expressions
)
@pytest.mark.asyncio
async def test_setup_entity_disabled_by_default(
setup_test_environment: list[str],
) -> None:
"""Test setup_entity sets disabled_by_default correctly."""
added_expressions = setup_test_environment
var = MockObj("sensor1")
config = {
CONF_NAME: "Temperature",
CONF_DISABLED_BY_DEFAULT: True,
}
await setup_entity(var, config, "sensor")
# Check disabled_by_default was set
assert any(
"sensor1.set_disabled_by_default(true)" in expr for expr in added_expressions
)
def test_entity_duplicate_validator() -> None:
"""Test the entity_duplicate_validator function."""
from esphome.core.entity_helpers import entity_duplicate_validator
# Reset CORE unique_ids for clean test
CORE.unique_ids.clear()
# Create validator for sensor platform
validator = entity_duplicate_validator("sensor")
# First entity should pass
config1 = {CONF_NAME: "Temperature"}
validated1 = validator(config1)
assert validated1 == config1
assert ("", "sensor", "temperature") in CORE.unique_ids
# Second entity with different name should pass
config2 = {CONF_NAME: "Humidity"}
validated2 = validator(config2)
assert validated2 == config2
assert ("", "sensor", "humidity") in CORE.unique_ids
# Duplicate entity should fail
config3 = {CONF_NAME: "Temperature"}
with pytest.raises(
Invalid, match=r"Duplicate sensor entity with name 'Temperature' found"
):
validator(config3)
def test_entity_duplicate_validator_with_devices() -> None:
"""Test entity_duplicate_validator with devices."""
from esphome.core.entity_helpers import entity_duplicate_validator
# Reset CORE unique_ids for clean test
CORE.unique_ids.clear()
# Create validator for sensor platform
validator = entity_duplicate_validator("sensor")
# Create mock device IDs
device1 = ID("device1", type="Device")
device2 = ID("device2", type="Device")
# Same name on different devices should pass
config1 = {CONF_NAME: "Temperature", CONF_DEVICE_ID: device1}
validated1 = validator(config1)
assert validated1 == config1
assert ("device1", "sensor", "temperature") in CORE.unique_ids
config2 = {CONF_NAME: "Temperature", CONF_DEVICE_ID: device2}
validated2 = validator(config2)
assert validated2 == config2
assert ("device2", "sensor", "temperature") in CORE.unique_ids
# Duplicate on same device should fail
config3 = {CONF_NAME: "Temperature", CONF_DEVICE_ID: device1}
with pytest.raises(
Invalid,
match=r"Duplicate sensor entity with name 'Temperature' found on device 'device1'",
):
validator(config3)
def test_duplicate_entity_yaml_validation(
yaml_file: Callable[[str], str], capsys: pytest.CaptureFixture[str]
) -> None:
"""Test that duplicate entity names are caught during YAML config validation."""
result = load_config_from_fixture(yaml_file, "duplicate_entity.yaml", FIXTURES_DIR)
assert result is None
# Check for the duplicate entity error message
captured = capsys.readouterr()
assert "Duplicate sensor entity with name 'Temperature' found" in captured.out
def test_duplicate_entity_with_devices_yaml_validation(
yaml_file: Callable[[str], str], capsys: pytest.CaptureFixture[str]
) -> None:
"""Test duplicate entity validation with devices."""
result = load_config_from_fixture(
yaml_file, "duplicate_entity_with_devices.yaml", FIXTURES_DIR
)
assert result is None
# Check for the duplicate entity error message with device
captured = capsys.readouterr()
assert (
"Duplicate sensor entity with name 'Temperature' found on device 'device1'"
in captured.out
)
def test_entity_different_platforms_yaml_validation(
yaml_file: Callable[[str], str],
) -> None:
"""Test that same entity name on different platforms is allowed."""
result = load_config_from_fixture(
yaml_file, "entity_different_platforms.yaml", FIXTURES_DIR
)
# This should succeed
assert result is not None

View File

@@ -0,0 +1,10 @@
esphome:
name: test-collision
area:
id: duplicate_id
name: Area 1
areas:
- id: duplicate_id
name: Area 2
host:

View File

@@ -0,0 +1,10 @@
esphome:
name: test
areas:
- id: test_2258
name: "Area 1"
- id: d6ka
name: "Area 2"
esp32:
board: esp32dev

View File

@@ -0,0 +1,10 @@
esphome:
name: test
devices:
- id: duplicate_device
name: "Device 1"
- id: duplicate_device
name: "Device 2"
esp32:
board: esp32dev

View File

@@ -0,0 +1,10 @@
esphome:
name: test
devices:
- id: test_2258
name: "Device 1"
- id: d6ka
name: "Device 2"
esp32:
board: esp32dev

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