44
55from collections .abc import Awaitable , Callable , Coroutine
66import functools
7+ import logging
78import math
89from typing import TYPE_CHECKING , Any , Concatenate , Generic , TypeVar , cast
910
1314 EntityCategory as EsphomeEntityCategory ,
1415 EntityInfo ,
1516 EntityState ,
16- build_unique_id ,
1717)
1818import voluptuous as vol
1919
2424 config_validation as cv ,
2525 device_registry as dr ,
2626 entity_platform ,
27+ entity_registry as er ,
2728)
2829from homeassistant .helpers .device_registry import DeviceInfo
2930from homeassistant .helpers .entity import Entity
3233from .const import DOMAIN
3334
3435# Import config flow so that it's added to the registry
35- from .entry_data import ESPHomeConfigEntry , RuntimeEntryData
36+ from .entry_data import ESPHomeConfigEntry , RuntimeEntryData , build_device_unique_id
3637from .enum_mapper import EsphomeEnumMapper
3738
39+ _LOGGER = logging .getLogger (__name__ )
40+
3841_InfoT = TypeVar ("_InfoT" , bound = EntityInfo )
3942_EntityT = TypeVar ("_EntityT" , bound = "EsphomeEntity[Any,Any]" )
4043_StateT = TypeVar ("_StateT" , bound = EntityState )
@@ -53,21 +56,74 @@ def async_static_info_updated(
5356) -> None :
5457 """Update entities of this platform when entities are listed."""
5558 current_infos = entry_data .info [info_type ]
59+ device_info = entry_data .device_info
60+ if TYPE_CHECKING :
61+ assert device_info is not None
5662 new_infos : dict [int , EntityInfo ] = {}
5763 add_entities : list [_EntityT ] = []
5864
65+ ent_reg = er .async_get (hass )
66+ dev_reg = dr .async_get (hass )
67+
5968 for info in infos :
60- if not current_infos .pop (info .key , None ):
61- # Create new entity
69+ new_infos [info .key ] = info
70+
71+ # Create new entity if it doesn't exist
72+ if not (old_info := current_infos .pop (info .key , None )):
6273 entity = entity_type (entry_data , platform .domain , info , state_type )
6374 add_entities .append (entity )
64- new_infos [info .key ] = info
75+ continue
76+
77+ # Entity exists - check if device_id has changed
78+ if old_info .device_id == info .device_id :
79+ continue
80+
81+ # Entity has switched devices, need to migrate unique_id
82+ old_unique_id = build_device_unique_id (device_info .mac_address , old_info )
83+ entity_id = ent_reg .async_get_entity_id (platform .domain , DOMAIN , old_unique_id )
84+
85+ # If entity not found in registry, re-add it
86+ # This happens when the device_id changed and the old device was deleted
87+ if entity_id is None :
88+ _LOGGER .info (
89+ "Entity with old unique_id %s not found in registry after device_id "
90+ "changed from %s to %s, re-adding entity" ,
91+ old_unique_id ,
92+ old_info .device_id ,
93+ info .device_id ,
94+ )
95+ entity = entity_type (entry_data , platform .domain , info , state_type )
96+ add_entities .append (entity )
97+ continue
98+
99+ updates : dict [str , Any ] = {}
100+ new_unique_id = build_device_unique_id (device_info .mac_address , info )
101+
102+ # Update unique_id if it changed
103+ if old_unique_id != new_unique_id :
104+ updates ["new_unique_id" ] = new_unique_id
105+
106+ # Update device assignment
107+ if info .device_id :
108+ # Entity now belongs to a sub device
109+ new_device = dev_reg .async_get_device (
110+ identifiers = {(DOMAIN , f"{ device_info .mac_address } _{ info .device_id } " )}
111+ )
112+ else :
113+ # Entity now belongs to the main device
114+ new_device = dev_reg .async_get_device (
115+ connections = {(dr .CONNECTION_NETWORK_MAC , device_info .mac_address )}
116+ )
117+
118+ if new_device :
119+ updates ["device_id" ] = new_device .id
120+
121+ # Apply all updates at once
122+ if updates :
123+ ent_reg .async_update_entity (entity_id , ** updates )
65124
66125 # Anything still in current_infos is now gone
67126 if current_infos :
68- device_info = entry_data .device_info
69- if TYPE_CHECKING :
70- assert device_info is not None
71127 entry_data .async_remove_entities (
72128 hass , current_infos .values (), device_info .mac_address
73129 )
@@ -244,19 +300,36 @@ def __init__(
244300 self ._key = entity_info .key
245301 self ._state_type = state_type
246302 self ._on_static_info_update (entity_info )
247- self ._attr_device_info = DeviceInfo (
248- connections = {(dr .CONNECTION_NETWORK_MAC , device_info .mac_address )}
249- )
303+
304+ device_name = device_info .name
305+ # Determine the device connection based on whether this entity belongs to a sub device
306+ if entity_info .device_id :
307+ # Entity belongs to a sub device
308+ self ._attr_device_info = DeviceInfo (
309+ identifiers = {
310+ (DOMAIN , f"{ device_info .mac_address } _{ entity_info .device_id } " )
311+ }
312+ )
313+ # Use the pre-computed device_id_to_name mapping for O(1) lookup
314+ device_name = entry_data .device_id_to_name .get (
315+ entity_info .device_id , device_info .name
316+ )
317+ else :
318+ # Entity belongs to the main device
319+ self ._attr_device_info = DeviceInfo (
320+ connections = {(dr .CONNECTION_NETWORK_MAC , device_info .mac_address )}
321+ )
322+
250323 if entity_info .name :
251- self .entity_id = f"{ domain } .{ device_info . name } _{ entity_info .object_id } "
324+ self .entity_id = f"{ domain } .{ device_name } _{ entity_info .object_id } "
252325 else :
253326 # https://github.com/home-assistant/core/issues/132532
254327 # If name is not set, ESPHome will use the sanitized friendly name
255328 # as the name, however we want to use the original object_id
256329 # as the entity_id before it is sanitized since the sanitizer
257330 # is not utf-8 aware. In this case, its always going to be
258331 # an empty string so we drop the object_id.
259- self .entity_id = f"{ domain } .{ device_info . name } "
332+ self .entity_id = f"{ domain } .{ device_name } "
260333
261334 async def async_added_to_hass (self ) -> None :
262335 """Register callbacks."""
@@ -290,7 +363,9 @@ def _on_static_info_update(self, static_info: EntityInfo) -> None:
290363 static_info = cast (_InfoT , static_info )
291364 assert device_info
292365 self ._static_info = static_info
293- self ._attr_unique_id = build_unique_id (device_info .mac_address , static_info )
366+ self ._attr_unique_id = build_device_unique_id (
367+ device_info .mac_address , static_info
368+ )
294369 self ._attr_entity_registry_enabled_default = not static_info .disabled_by_default
295370 # https://github.com/home-assistant/core/issues/132532
296371 # If the name is "", we need to set it to None since otherwise
0 commit comments