diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index b23652a982..58a0b52555 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -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; diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index e40318c34a..e9b46853f4 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1621,6 +1621,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; } diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 66b7ce38a7..da12a3e449 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -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(entity->get_entity_category()); +#ifdef USE_DEVICES + response.device_id = entity->get_device_id(); +#endif } // Helper function to fill common entity state fields diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 517b4d41b4..9793565ee5 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -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()); + return true; + } + case 21: { + this->areas.push_back(value.as_message()); + return true; + } + case 22: { + this->area = value.as_message(); + 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(20, it, true); + } + for (auto &it : this->areas) { + buffer.encode_message(21, it, true); + } + buffer.encode_message(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(); 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(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(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(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(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(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(); 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(); 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(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(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(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(); 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(11, this->legacy_last_reset_type); buffer.encode_bool(12, this->disabled_by_default); buffer.encode_enum(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(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(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(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(); 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(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(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(); 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(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(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(); 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(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(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(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(); 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(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(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(); 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(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(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(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(); 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(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(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(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(); 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(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(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(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(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(); 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(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(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(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(); 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(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(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(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(); 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(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(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(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(); 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(); 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(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(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(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(); 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(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(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 diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 7d92125290..6a5b51d3a1 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -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 devices{}; + std::vector 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 diff --git a/esphome/config_validation.py b/esphome/config_validation.py index bf69b81bb5..09b132a458 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -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}") diff --git a/esphome/const.py b/esphome/const.py index e61af6c5b5..6d7d9c0c1b 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -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" diff --git a/esphome/core/application.h b/esphome/core/application.h index db0e2171e3..6be97c8c33 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -10,6 +10,13 @@ #include "esphome/core/runtime_stats.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 #endif @@ -88,7 +95,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; @@ -103,11 +110,18 @@ class Application { this->name_ = name; this->friendly_name_ = friendly_name; } - this->area_ = area; + // area is now handled through the areas system 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_; } @@ -265,6 +279,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 C *register_component(C *c) { @@ -286,7 +306,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_; } @@ -347,6 +375,12 @@ class Application { uint8_t get_app_state() const { return this->app_state_; } +#ifdef USE_DEVICES + const std::vector &get_devices() { return this->devices_; } +#endif +#ifdef USE_AREAS + const std::vector &get_areas() { return this->areas_; } +#endif #ifdef USE_BINARY_SENSOR const std::vector &get_binary_sensors() { return this->binary_sensors_; } binary_sensor::BinarySensor *get_binary_sensor_by_key(uint32_t key, bool include_internal = false) { @@ -623,6 +657,12 @@ class Application { uint16_t current_loop_index_{0}; bool in_loop_{false}; +#ifdef USE_DEVICES + std::vector devices_{}; +#endif +#ifdef USE_AREAS + std::vector areas_{}; +#endif #ifdef USE_BINARY_SENSOR std::vector binary_sensors_{}; #endif @@ -689,7 +729,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_; diff --git a/esphome/core/area.h b/esphome/core/area.h new file mode 100644 index 0000000000..f6d88fe703 --- /dev/null +++ b/esphome/core/area.h @@ -0,0 +1,19 @@ +#pragma once + +#include + +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 diff --git a/esphome/core/config.py b/esphome/core/config.py index c407e1c11a..641c73a292 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -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)) diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 62aac4382c..8abd6598f7 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -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 diff --git a/esphome/core/device.h b/esphome/core/device.h new file mode 100644 index 0000000000..3d0d1e7c23 --- /dev/null +++ b/esphome/core/device.h @@ -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 diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index 0f0d635962..4bd04a9b1c 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -51,6 +51,12 @@ 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 { return this->device_id_; } + void set_device_id(const uint32_t device_id) { this->device_id_ = device_id; } +#endif + // Check if this entity has state bool has_state() const { return this->flags_.has_state; } @@ -67,6 +73,9 @@ class EntityBase { const char *object_id_c_str_{nullptr}; const char *icon_c_str_{nullptr}; uint32_t object_id_hash_{}; +#ifdef USE_DEVICES + uint32_t device_id_{}; +#endif // Bit-packed flags to save memory (1 byte instead of 5) struct EntityFlags { diff --git a/esphome/cpp_helpers.py b/esphome/cpp_helpers.py index 9a775bad33..8d5440f591 100644 --- a/esphome/cpp_helpers.py +++ b/esphome/cpp_helpers.py @@ -1,6 +1,7 @@ import logging from esphome.const import ( + CONF_DEVICE_ID, CONF_DISABLED_BY_DEFAULT, CONF_ENTITY_CATEGORY, CONF_ICON, @@ -16,7 +17,7 @@ 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.helpers import fnv1a_32bit_hash, sanitize, snake_case from esphome.types import ConfigFragmentType, ConfigType from esphome.util import Registry, RegistryEntry @@ -110,6 +111,9 @@ async def setup_entity(var, config): add(var.set_icon(config[CONF_ICON])) if CONF_ENTITY_CATEGORY in config: add(var.set_entity_category(config[CONF_ENTITY_CATEGORY])) + if CONF_DEVICE_ID in config: + device_id: ID = config[CONF_DEVICE_ID] + add(var.set_device_id(fnv1a_32bit_hash(device_id.id))) def extract_registry_entry_config( diff --git a/esphome/dashboard/util/text.py b/esphome/dashboard/util/text.py index 08d2df6abf..2a3b9042e6 100644 --- a/esphome/dashboard/util/text.py +++ b/esphome/dashboard/util/text.py @@ -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("_", "-") diff --git a/esphome/helpers.py b/esphome/helpers.py index d95546ac94..bf0e3b5cf7 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -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: diff --git a/tests/components/esphome/common.yaml b/tests/components/esphome/common.yaml index 05954e37d7..a4b309b69d 100644 --- a/tests/components/esphome/common.yaml +++ b/tests/components/esphome/common.yaml @@ -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 diff --git a/tests/dummy_main.cpp b/tests/dummy_main.cpp index 3ba4c8bd07..afd393c095 100644 --- a/tests/dummy_main.cpp +++ b/tests/dummy_main.cpp @@ -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); diff --git a/tests/integration/fixtures/areas_and_devices.yaml b/tests/integration/fixtures/areas_and_devices.yaml new file mode 100644 index 0000000000..4a327b73a1 --- /dev/null +++ b/tests/integration/fixtures/areas_and_devices.yaml @@ -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 + diff --git a/tests/integration/fixtures/legacy_area.yaml b/tests/integration/fixtures/legacy_area.yaml new file mode 100644 index 0000000000..4d1617c395 --- /dev/null +++ b/tests/integration/fixtures/legacy_area.yaml @@ -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 diff --git a/tests/integration/test_areas_and_devices.py b/tests/integration/test_areas_and_devices.py new file mode 100644 index 0000000000..4ce55a30a7 --- /dev/null +++ b/tests/integration/test_areas_and_devices.py @@ -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}" + ) diff --git a/tests/integration/test_legacy_area.py b/tests/integration/test_legacy_area.py new file mode 100644 index 0000000000..d10a01ec6a --- /dev/null +++ b/tests/integration/test_legacy_area.py @@ -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) diff --git a/tests/unit_tests/core/test_config.py b/tests/unit_tests/core/test_config.py new file mode 100644 index 0000000000..372c1df7ee --- /dev/null +++ b/tests/unit_tests/core/test_config.py @@ -0,0 +1,256 @@ +"""Unit tests for core config functionality including areas and devices.""" + +from collections.abc import Callable +from pathlib import Path +from typing import Any +from unittest.mock import patch + +import pytest + +from esphome import config, config_validation as cv, core, yaml_util +from esphome.config import Config +from esphome.const import CONF_AREA, CONF_AREAS, CONF_DEVICES +from esphome.core import CORE +from esphome.core.config import Area, validate_area_config + +FIXTURES_DIR = Path(__file__).parent.parent / "fixtures" / "core" / "config" + + +@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 + + +@pytest.fixture(autouse=True) +def reset_core(): + """Reset CORE after each test.""" + yield + CORE.reset() + + +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 +) -> 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) + + +def test_validate_area_config_with_string() -> None: + """Test that string area config is converted to structured format.""" + result: dict[str, Any] = 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: dict[str, Any] = 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") + 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") + 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") + 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") + 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") + 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") + assert result is None + + # Check for the specific error message in stdout + captured = capsys.readouterr() + print(captured.out) + 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") + 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") + 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") + assert result is None + + # Check for the specific error message from IDPassValidationStep + captured = capsys.readouterr() + assert "ID duplicate_device redefined!" in captured.out diff --git a/tests/unit_tests/fixtures/core/config/area_id_collision.yaml b/tests/unit_tests/fixtures/core/config/area_id_collision.yaml new file mode 100644 index 0000000000..fb2e930e61 --- /dev/null +++ b/tests/unit_tests/fixtures/core/config/area_id_collision.yaml @@ -0,0 +1,10 @@ +esphome: + name: test-collision + area: + id: duplicate_id + name: Area 1 + areas: + - id: duplicate_id + name: Area 2 + +host: diff --git a/tests/unit_tests/fixtures/core/config/area_id_hash_collision.yaml b/tests/unit_tests/fixtures/core/config/area_id_hash_collision.yaml new file mode 100644 index 0000000000..3a2e8ab8a9 --- /dev/null +++ b/tests/unit_tests/fixtures/core/config/area_id_hash_collision.yaml @@ -0,0 +1,10 @@ +esphome: + name: test + areas: + - id: test_2258 + name: "Area 1" + - id: d6ka + name: "Area 2" + +esp32: + board: esp32dev diff --git a/tests/unit_tests/fixtures/core/config/device_duplicate_id.yaml b/tests/unit_tests/fixtures/core/config/device_duplicate_id.yaml new file mode 100644 index 0000000000..2aa3055686 --- /dev/null +++ b/tests/unit_tests/fixtures/core/config/device_duplicate_id.yaml @@ -0,0 +1,10 @@ +esphome: + name: test + devices: + - id: duplicate_device + name: "Device 1" + - id: duplicate_device + name: "Device 2" + +esp32: + board: esp32dev diff --git a/tests/unit_tests/fixtures/core/config/device_id_collision.yaml b/tests/unit_tests/fixtures/core/config/device_id_collision.yaml new file mode 100644 index 0000000000..9cf04e0595 --- /dev/null +++ b/tests/unit_tests/fixtures/core/config/device_id_collision.yaml @@ -0,0 +1,10 @@ +esphome: + name: test + devices: + - id: test_2258 + name: "Device 1" + - id: d6ka + name: "Device 2" + +esp32: + board: esp32dev diff --git a/tests/unit_tests/fixtures/core/config/device_invalid_area.yaml b/tests/unit_tests/fixtures/core/config/device_invalid_area.yaml new file mode 100644 index 0000000000..9a8ec0a1eb --- /dev/null +++ b/tests/unit_tests/fixtures/core/config/device_invalid_area.yaml @@ -0,0 +1,12 @@ +esphome: + name: test + areas: + - id: valid_area + name: "Valid Area" + devices: + - id: test_device + name: "Test Device" + area_id: nonexistent_area + +esp32: + board: esp32dev diff --git a/tests/unit_tests/fixtures/core/config/device_without_area.yaml b/tests/unit_tests/fixtures/core/config/device_without_area.yaml new file mode 100644 index 0000000000..8464cf37df --- /dev/null +++ b/tests/unit_tests/fixtures/core/config/device_without_area.yaml @@ -0,0 +1,7 @@ +esphome: + name: test-device-no-area + devices: + - id: test_device + name: Test Device + +host: diff --git a/tests/unit_tests/fixtures/core/config/legacy_string_area.yaml b/tests/unit_tests/fixtures/core/config/legacy_string_area.yaml new file mode 100644 index 0000000000..fe2dc3db17 --- /dev/null +++ b/tests/unit_tests/fixtures/core/config/legacy_string_area.yaml @@ -0,0 +1,5 @@ +esphome: + name: test-legacy-area + area: Living Room + +host: diff --git a/tests/unit_tests/fixtures/core/config/multiple_areas_devices.yaml b/tests/unit_tests/fixtures/core/config/multiple_areas_devices.yaml new file mode 100644 index 0000000000..ef3b4f6e67 --- /dev/null +++ b/tests/unit_tests/fixtures/core/config/multiple_areas_devices.yaml @@ -0,0 +1,22 @@ +esphome: + name: test-multiple + area: + id: main_area + name: Main Area + areas: + - id: area1 + name: Area 1 + - id: area2 + name: Area 2 + devices: + - id: device1 + name: Device 1 + area_id: main_area + - id: device2 + name: Device 2 + area_id: area1 + - id: device3 + name: Device 3 + area_id: area2 + +host: diff --git a/tests/unit_tests/fixtures/core/config/valid_area_device.yaml b/tests/unit_tests/fixtures/core/config/valid_area_device.yaml new file mode 100644 index 0000000000..fc97894586 --- /dev/null +++ b/tests/unit_tests/fixtures/core/config/valid_area_device.yaml @@ -0,0 +1,11 @@ +esphome: + name: test-valid-area + areas: + - id: bedroom_area + name: Bedroom + devices: + - id: test_device + name: Test Device + area_id: bedroom_area + +host: