5
5
import base64
6
6
import logging
7
7
import time
8
- from collections .abc import Mapping , Sequence
8
+ from collections .abc import Sequence
9
9
from datetime import UTC , datetime , timedelta , tzinfo
10
10
from typing import TYPE_CHECKING , Any , TypeAlias , cast
11
11
@@ -68,10 +68,11 @@ def __init__(
68
68
self ._state_information : dict [str , Any ] = {}
69
69
self ._modules : dict [str | ModuleName [Module ], SmartModule ] = {}
70
70
self ._parent : SmartDevice | None = None
71
- self ._children : Mapping [str , SmartDevice ] = {}
71
+ self ._children : dict [str , SmartDevice ] = {}
72
72
self ._last_update_time : float | None = None
73
73
self ._on_since : datetime | None = None
74
74
self ._info : dict [str , Any ] = {}
75
+ self ._logged_missing_child_ids : set [str ] = set ()
75
76
76
77
async def _initialize_children (self ) -> None :
77
78
"""Initialize children for power strips."""
@@ -82,23 +83,86 @@ async def _initialize_children(self) -> None:
82
83
resp = await self .protocol .query (child_info_query )
83
84
self .internal_state .update (resp )
84
85
85
- children = self .internal_state ["get_child_device_list" ]["child_device_list" ]
86
- children_components_raw = {
87
- child ["device_id" ]: child
88
- for child in self .internal_state ["get_child_device_component_list" ][
89
- "child_component_list"
90
- ]
91
- }
86
+ async def _try_create_child (
87
+ self , info : dict , child_components : dict
88
+ ) -> SmartDevice | None :
92
89
from .smartchilddevice import SmartChildDevice
93
90
94
- self ._children = {
95
- child_info ["device_id" ]: await SmartChildDevice .create (
96
- parent = self ,
97
- child_info = child_info ,
98
- child_components_raw = children_components_raw [child_info ["device_id" ]],
99
- )
100
- for child_info in children
91
+ return await SmartChildDevice .create (
92
+ parent = self ,
93
+ child_info = info ,
94
+ child_components_raw = child_components ,
95
+ )
96
+
97
+ async def _create_delete_children (
98
+ self ,
99
+ child_device_resp : dict [str , list ],
100
+ child_device_components_resp : dict [str , list ],
101
+ ) -> bool :
102
+ """Create and delete children. Return True if children changed.
103
+
104
+ Adds newly found children and deletes children that are no longer
105
+ reported by the device. It will only log once per child_id that
106
+ can't be created to avoid spamming the logs on every update.
107
+ """
108
+ changed = False
109
+ smart_children_components = {
110
+ child ["device_id" ]: child
111
+ for child in child_device_components_resp ["child_component_list" ]
101
112
}
113
+ children = self ._children
114
+ child_ids : set [str ] = set ()
115
+ existing_child_ids = set (self ._children .keys ())
116
+
117
+ for info in child_device_resp ["child_device_list" ]:
118
+ if (child_id := info .get ("device_id" )) and (
119
+ child_components := smart_children_components .get (child_id )
120
+ ):
121
+ child_ids .add (child_id )
122
+
123
+ if child_id in existing_child_ids :
124
+ continue
125
+
126
+ child = await self ._try_create_child (info , child_components )
127
+ if child :
128
+ _LOGGER .debug ("Created child device %s for %s" , child , self .host )
129
+ changed = True
130
+ children [child_id ] = child
131
+ continue
132
+
133
+ if child_id not in self ._logged_missing_child_ids :
134
+ self ._logged_missing_child_ids .add (child_id )
135
+ _LOGGER .debug ("Child device type not supported: %s" , info )
136
+ continue
137
+
138
+ if child_id :
139
+ if child_id not in self ._logged_missing_child_ids :
140
+ self ._logged_missing_child_ids .add (child_id )
141
+ _LOGGER .debug (
142
+ "Could not find child components for device %s, "
143
+ "child_id %s, components: %s: " ,
144
+ self .host ,
145
+ child_id ,
146
+ smart_children_components ,
147
+ )
148
+ continue
149
+
150
+ # If we couldn't get a child device id we still only want to
151
+ # log once to avoid spamming the logs on every update cycle
152
+ # so store it under an empty string
153
+ if "" not in self ._logged_missing_child_ids :
154
+ self ._logged_missing_child_ids .add ("" )
155
+ _LOGGER .debug (
156
+ "Could not find child id for device %s, info: %s" , self .host , info
157
+ )
158
+
159
+ removed_ids = existing_child_ids - child_ids
160
+ for removed_id in removed_ids :
161
+ changed = True
162
+ removed = children .pop (removed_id )
163
+ _LOGGER .debug ("Removed child device %s from %s" , removed , self .host )
164
+
165
+ return changed
102
166
103
167
@property
104
168
def children (self ) -> Sequence [SmartDevice ]:
@@ -164,21 +228,29 @@ async def _negotiate(self) -> None:
164
228
if "child_device" in self ._components and not self .children :
165
229
await self ._initialize_children ()
166
230
167
- def _update_children_info (self ) -> None :
168
- """Update the internal child device info from the parent info."""
231
+ async def _update_children_info (self ) -> bool :
232
+ """Update the internal child device info from the parent info.
233
+
234
+ Return true if children added or deleted.
235
+ """
236
+ changed = False
169
237
if child_info := self ._try_get_response (
170
238
self ._last_update , "get_child_device_list" , {}
171
239
):
240
+ changed = await self ._create_delete_children (
241
+ child_info , self ._last_update ["get_child_device_component_list" ]
242
+ )
243
+
172
244
for info in child_info ["child_device_list" ]:
173
- child_id = info [ "device_id" ]
245
+ child_id = info . get ( "device_id" )
174
246
if child_id not in self ._children :
175
- _LOGGER .debug (
176
- "Skipping child update for %s, probably unsupported device" ,
177
- child_id ,
178
- )
247
+ # _create_delete_children has already logged a message
179
248
continue
249
+
180
250
self ._children [child_id ]._update_internal_state (info )
181
251
252
+ return changed
253
+
182
254
def _update_internal_info (self , info_resp : dict ) -> None :
183
255
"""Update the internal device info."""
184
256
self ._info = self ._try_get_response (info_resp , "get_device_info" )
@@ -201,13 +273,13 @@ async def update(self, update_children: bool = True) -> None:
201
273
202
274
resp = await self ._modular_update (first_update , now )
203
275
204
- self ._update_children_info ()
276
+ children_changed = await self ._update_children_info ()
205
277
# Call child update which will only update module calls, info is updated
206
278
# from get_child_device_list. update_children only affects hub devices, other
207
279
# devices will always update children to prevent errors on module access.
208
280
# This needs to go after updating the internal state of the children so that
209
281
# child modules have access to their sysinfo.
210
- if first_update or update_children or self .device_type != DeviceType .Hub :
282
+ if children_changed or update_children or self .device_type != DeviceType .Hub :
211
283
for child in self ._children .values ():
212
284
if TYPE_CHECKING :
213
285
assert isinstance (child , SmartChildDevice )
@@ -469,8 +541,6 @@ async def _initialize_features(self) -> None:
469
541
module ._initialize_features ()
470
542
for feat in module ._module_features .values ():
471
543
self ._add_feature (feat )
472
- for child in self ._children .values ():
473
- await child ._initialize_features ()
474
544
475
545
@property
476
546
def _is_hub_child (self ) -> bool :
0 commit comments