mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-29 22:24:26 +00:00 
			
		
		
		
	Merge branch 'dev' into message_creator_ram
This commit is contained in:
		| @@ -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) | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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; | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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)) | ||||
|   | ||||
| @@ -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); }); | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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: | ||||
|   | ||||
| @@ -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)) | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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, | ||||
|   | ||||
| @@ -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"); | ||||
|   } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -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(): | ||||
|   | ||||
							
								
								
									
										0
									
								
								esphome/components/esp32_hall/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								esphome/components/esp32_hall/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										5
									
								
								esphome/components/esp32_hall/sensor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								esphome/components/esp32_hall/sensor.py
									
									
									
									
									
										Normal 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." | ||||
| ) | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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])) | ||||
|  | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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); | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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: | ||||
|   | ||||
| @@ -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])) | ||||
|  | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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() | ||||
|   | ||||
| @@ -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( | ||||
|         { | ||||
|   | ||||
| @@ -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; } | ||||
|  | ||||
|   | ||||
| @@ -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]: | ||||
|             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( | ||||
|                 var.add_server_register( | ||||
|                     cg.new_Pvariable( | ||||
|                         server_register[CONF_ID], | ||||
|                         server_register[CONF_ADDRESS], | ||||
|                         server_register[CONF_VALUE_TYPE], | ||||
|                         TYPE_REGISTER_MAP[server_register[CONF_VALUE_TYPE]], | ||||
|                         await cg.process_lambda( | ||||
|                             server_register[CONF_READ_LAMBDA], | ||||
|                             [], | ||||
|                             return_type=cg.float_, | ||||
|                         ), | ||||
|                     ) | ||||
|                 server_register_var.set_read_lambda( | ||||
|                     cg.TemplateArguments(cpp_type), | ||||
|                     await cg.process_lambda( | ||||
|                         server_register[CONF_READ_LAMBDA], | ||||
|                         [(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) | ||||
|   | ||||
| @@ -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; | ||||
|     } | ||||
|   } | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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( | ||||
|     { | ||||
|   | ||||
| @@ -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. | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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)) | ||||
|   | ||||
| @@ -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)) | ||||
|  | ||||
|   | ||||
| @@ -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)) | ||||
|   | ||||
| @@ -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)) | ||||
|   | ||||
| @@ -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)) | ||||
|   | ||||
| @@ -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)) | ||||
|   | ||||
| @@ -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)) | ||||
|   | ||||
| @@ -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)) | ||||
|   | ||||
| @@ -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(); | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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: | ||||
|   | ||||
| @@ -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; | ||||
|     } | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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: | ||||
|   | ||||
| @@ -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()); | ||||
|   | ||||
| @@ -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}") | ||||
|   | ||||
| @@ -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" | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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
									
								
							
							
						
						
									
										19
									
								
								esphome/core/area.h
									
									
									
									
									
										Normal 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 | ||||
| @@ -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)) | ||||
|   | ||||
| @@ -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
									
								
							
							
						
						
									
										20
									
								
								esphome/core/device.h
									
									
									
									
									
										Normal 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 | ||||
| @@ -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()) { | ||||
|     this->name_ = StringRef(App.get_friendly_name()); | ||||
| #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_; } | ||||
|  | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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. | ||||
|   | ||||
| @@ -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, | ||||
|   | ||||
| @@ -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("_", "-") | ||||
|   | ||||
| @@ -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: | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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: | ||||
|   | ||||
| @@ -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: | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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" | ||||
|   | ||||
| @@ -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 | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
|  | ||||
|   | ||||
| @@ -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] | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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)} | ||||
|   | ||||
							
								
								
									
										57
									
								
								tests/integration/fixtures/areas_and_devices.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								tests/integration/fixtures/areas_and_devices.yaml
									
									
									
									
									
										Normal 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 | ||||
|  | ||||
| @@ -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: [] | ||||
							
								
								
									
										15
									
								
								tests/integration/fixtures/legacy_area.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								tests/integration/fixtures/legacy_area.yaml
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										121
									
								
								tests/integration/test_areas_and_devices.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								tests/integration/test_areas_and_devices.py
									
									
									
									
									
										Normal 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}" | ||||
|                 ) | ||||
							
								
								
									
										184
									
								
								tests/integration/test_duplicate_entities.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										184
									
								
								tests/integration/test_duplicate_entities.py
									
									
									
									
									
										Normal 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}" | ||||
|             ) | ||||
							
								
								
									
										41
									
								
								tests/integration/test_legacy_area.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								tests/integration/test_legacy_area.py
									
									
									
									
									
										Normal 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) | ||||
| @@ -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: | ||||
|     """ | ||||
|   | ||||
							
								
								
									
										0
									
								
								tests/unit_tests/core/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								tests/unit_tests/core/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										33
									
								
								tests/unit_tests/core/common.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								tests/unit_tests/core/common.py
									
									
									
									
									
										Normal 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) | ||||
							
								
								
									
										18
									
								
								tests/unit_tests/core/conftest.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								tests/unit_tests/core/conftest.py
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										225
									
								
								tests/unit_tests/core/test_config.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										225
									
								
								tests/unit_tests/core/test_config.py
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										595
									
								
								tests/unit_tests/core/test_entity_helpers.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										595
									
								
								tests/unit_tests/core/test_entity_helpers.py
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										10
									
								
								tests/unit_tests/fixtures/core/config/area_id_collision.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								tests/unit_tests/fixtures/core/config/area_id_collision.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| esphome: | ||||
|   name: test-collision | ||||
|   area: | ||||
|     id: duplicate_id | ||||
|     name: Area 1 | ||||
|   areas: | ||||
|     - id: duplicate_id | ||||
|       name: Area 2 | ||||
|  | ||||
| host: | ||||
| @@ -0,0 +1,10 @@ | ||||
| esphome: | ||||
|   name: test | ||||
|   areas: | ||||
|     - id: test_2258 | ||||
|       name: "Area 1" | ||||
|     - id: d6ka | ||||
|       name: "Area 2" | ||||
|  | ||||
| esp32: | ||||
|   board: esp32dev | ||||
| @@ -0,0 +1,10 @@ | ||||
| esphome: | ||||
|   name: test | ||||
|   devices: | ||||
|     - id: duplicate_device | ||||
|       name: "Device 1" | ||||
|     - id: duplicate_device | ||||
|       name: "Device 2" | ||||
|  | ||||
| esp32: | ||||
|   board: esp32dev | ||||
| @@ -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
		Reference in New Issue
	
	Block a user