Skip to content

Commit a099249

Browse files
Improve removal of stale entities/devices in Husqvarna Automower (home-assistant#148428)
Co-authored-by: Abílio Costa <[email protected]>
1 parent d6175fb commit a099249

File tree

1 file changed

+93
-106
lines changed

1 file changed

+93
-106
lines changed

homeassistant/components/husqvarna_automower/coordinator.py

Lines changed: 93 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,6 @@ def __init__(
5858
self.new_devices_callbacks: list[Callable[[set[str]], None]] = []
5959
self.new_zones_callbacks: list[Callable[[str, set[str]], None]] = []
6060
self.new_areas_callbacks: list[Callable[[str, set[int]], None]] = []
61-
self._devices_last_update: set[str] = set()
62-
self._zones_last_update: dict[str, set[str]] = {}
63-
self._areas_last_update: dict[str, set[int]] = {}
6461

6562
@override
6663
@callback
@@ -87,11 +84,15 @@ def _on_data_update(self) -> None:
8784
"""Handle data updates and process dynamic entity management."""
8885
if self.data is not None:
8986
self._async_add_remove_devices()
90-
for mower_id in self.data:
91-
if self.data[mower_id].capabilities.stay_out_zones:
92-
self._async_add_remove_stay_out_zones()
93-
if self.data[mower_id].capabilities.work_areas:
94-
self._async_add_remove_work_areas()
87+
if any(
88+
mower_data.capabilities.stay_out_zones
89+
for mower_data in self.data.values()
90+
):
91+
self._async_add_remove_stay_out_zones()
92+
if any(
93+
mower_data.capabilities.work_areas for mower_data in self.data.values()
94+
):
95+
self._async_add_remove_work_areas()
9596

9697
@callback
9798
def handle_websocket_updates(self, ws_data: MowerDictionary) -> None:
@@ -161,44 +162,36 @@ async def client_listen(
161162
)
162163

163164
def _async_add_remove_devices(self) -> None:
164-
"""Add new device, remove non-existing device."""
165+
"""Add new devices and remove orphaned devices from the registry."""
165166
current_devices = set(self.data)
167+
device_registry = dr.async_get(self.hass)
166168

167-
# Skip update if no changes
168-
if current_devices == self._devices_last_update:
169-
return
169+
registered_devices: set[str] = {
170+
str(mower_id)
171+
for device in device_registry.devices.get_devices_for_config_entry_id(
172+
self.config_entry.entry_id
173+
)
174+
for domain, mower_id in device.identifiers
175+
if domain == DOMAIN
176+
}
170177

171-
# Process removed devices
172-
removed_devices = self._devices_last_update - current_devices
173-
if removed_devices:
174-
_LOGGER.debug("Removed devices: %s", ", ".join(map(str, removed_devices)))
175-
self._remove_device(removed_devices)
178+
orphaned_devices = registered_devices - current_devices
179+
if orphaned_devices:
180+
_LOGGER.debug("Removing orphaned devices: %s", orphaned_devices)
181+
device_registry = dr.async_get(self.hass)
182+
for mower_id in orphaned_devices:
183+
dev = device_registry.async_get_device(identifiers={(DOMAIN, mower_id)})
184+
if dev is not None:
185+
device_registry.async_update_device(
186+
device_id=dev.id,
187+
remove_config_entry_id=self.config_entry.entry_id,
188+
)
176189

177-
# Process new device
178-
new_devices = current_devices - self._devices_last_update
190+
new_devices = current_devices - registered_devices
179191
if new_devices:
180-
_LOGGER.debug("New devices found: %s", ", ".join(map(str, new_devices)))
181-
self._add_new_devices(new_devices)
182-
183-
# Update device state
184-
self._devices_last_update = current_devices
185-
186-
def _remove_device(self, removed_devices: set[str]) -> None:
187-
"""Remove device from the registry."""
188-
device_registry = dr.async_get(self.hass)
189-
for mower_id in removed_devices:
190-
if device := device_registry.async_get_device(
191-
identifiers={(DOMAIN, str(mower_id))}
192-
):
193-
device_registry.async_update_device(
194-
device_id=device.id,
195-
remove_config_entry_id=self.config_entry.entry_id,
196-
)
197-
198-
def _add_new_devices(self, new_devices: set[str]) -> None:
199-
"""Add new device and trigger callbacks."""
200-
for mower_callback in self.new_devices_callbacks:
201-
mower_callback(new_devices)
192+
_LOGGER.debug("New devices found: %s", new_devices)
193+
for mower_callback in self.new_devices_callbacks:
194+
mower_callback(new_devices)
202195

203196
def _async_add_remove_stay_out_zones(self) -> None:
204197
"""Add new stay-out zones, remove non-existing stay-out zones."""
@@ -209,42 +202,39 @@ def _async_add_remove_stay_out_zones(self) -> None:
209202
and mower_data.stay_out_zones is not None
210203
}
211204

212-
if not self._zones_last_update:
213-
self._zones_last_update = current_zones
214-
return
215-
216-
if current_zones == self._zones_last_update:
217-
return
205+
entity_registry = er.async_get(self.hass)
206+
entries = er.async_entries_for_config_entry(
207+
entity_registry, self.config_entry.entry_id
208+
)
218209

219-
self._zones_last_update = self._update_stay_out_zones(current_zones)
210+
registered_zones: dict[str, set[str]] = {}
211+
for mower_id in self.data:
212+
registered_zones[mower_id] = set()
213+
for entry in entries:
214+
uid = entry.unique_id
215+
if uid.startswith(f"{mower_id}_") and uid.endswith("_stay_out_zones"):
216+
zone_id = uid.removeprefix(f"{mower_id}_").removesuffix(
217+
"_stay_out_zones"
218+
)
219+
registered_zones[mower_id].add(zone_id)
220220

221-
def _update_stay_out_zones(
222-
self, current_zones: dict[str, set[str]]
223-
) -> dict[str, set[str]]:
224-
"""Update stay-out zones by adding and removing as needed."""
225-
new_zones = {
226-
mower_id: zones - self._zones_last_update.get(mower_id, set())
227-
for mower_id, zones in current_zones.items()
228-
}
229-
removed_zones = {
230-
mower_id: self._zones_last_update.get(mower_id, set()) - zones
231-
for mower_id, zones in current_zones.items()
232-
}
221+
for mower_id, current_ids in current_zones.items():
222+
known_ids = registered_zones.get(mower_id, set())
233223

234-
for mower_id, zones in new_zones.items():
235-
for zone_callback in self.new_zones_callbacks:
236-
zone_callback(mower_id, set(zones))
224+
new_zones = current_ids - known_ids
225+
removed_zones = known_ids - current_ids
237226

238-
entity_registry = er.async_get(self.hass)
239-
for mower_id, zones in removed_zones.items():
240-
for entity_entry in er.async_entries_for_config_entry(
241-
entity_registry, self.config_entry.entry_id
242-
):
243-
for zone in zones:
244-
if entity_entry.unique_id.startswith(f"{mower_id}_{zone}"):
245-
entity_registry.async_remove(entity_entry.entity_id)
227+
if new_zones:
228+
_LOGGER.debug("New stay-out zones: %s", new_zones)
229+
for zone_callback in self.new_zones_callbacks:
230+
zone_callback(mower_id, new_zones)
246231

247-
return current_zones
232+
if removed_zones:
233+
_LOGGER.debug("Removing stay-out zones: %s", removed_zones)
234+
for entry in entries:
235+
for zone_id in removed_zones:
236+
if entry.unique_id == f"{mower_id}_{zone_id}_stay_out_zones":
237+
entity_registry.async_remove(entry.entity_id)
248238

249239
def _async_add_remove_work_areas(self) -> None:
250240
"""Add new work areas, remove non-existing work areas."""
@@ -254,39 +244,36 @@ def _async_add_remove_work_areas(self) -> None:
254244
if mower_data.capabilities.work_areas and mower_data.work_areas is not None
255245
}
256246

257-
if not self._areas_last_update:
258-
self._areas_last_update = current_areas
259-
return
260-
261-
if current_areas == self._areas_last_update:
262-
return
263-
264-
self._areas_last_update = self._update_work_areas(current_areas)
265-
266-
def _update_work_areas(
267-
self, current_areas: dict[str, set[int]]
268-
) -> dict[str, set[int]]:
269-
"""Update work areas by adding and removing as needed."""
270-
new_areas = {
271-
mower_id: areas - self._areas_last_update.get(mower_id, set())
272-
for mower_id, areas in current_areas.items()
273-
}
274-
removed_areas = {
275-
mower_id: self._areas_last_update.get(mower_id, set()) - areas
276-
for mower_id, areas in current_areas.items()
277-
}
278-
279-
for mower_id, areas in new_areas.items():
280-
for area_callback in self.new_areas_callbacks:
281-
area_callback(mower_id, set(areas))
282-
283247
entity_registry = er.async_get(self.hass)
284-
for mower_id, areas in removed_areas.items():
285-
for entity_entry in er.async_entries_for_config_entry(
286-
entity_registry, self.config_entry.entry_id
287-
):
288-
for area in areas:
289-
if entity_entry.unique_id.startswith(f"{mower_id}_{area}_"):
290-
entity_registry.async_remove(entity_entry.entity_id)
248+
entries = er.async_entries_for_config_entry(
249+
entity_registry, self.config_entry.entry_id
250+
)
291251

292-
return current_areas
252+
registered_areas: dict[str, set[int]] = {}
253+
for mower_id in self.data:
254+
registered_areas[mower_id] = set()
255+
for entry in entries:
256+
uid = entry.unique_id
257+
if uid.startswith(f"{mower_id}_") and uid.endswith("_work_area"):
258+
parts = uid.removeprefix(f"{mower_id}_").split("_")
259+
area_id_str = parts[0] if parts else None
260+
if area_id_str and area_id_str.isdigit():
261+
registered_areas[mower_id].add(int(area_id_str))
262+
263+
for mower_id, current_ids in current_areas.items():
264+
known_ids = registered_areas.get(mower_id, set())
265+
266+
new_areas = current_ids - known_ids
267+
removed_areas = known_ids - current_ids
268+
269+
if new_areas:
270+
_LOGGER.debug("New work areas: %s", new_areas)
271+
for area_callback in self.new_areas_callbacks:
272+
area_callback(mower_id, new_areas)
273+
274+
if removed_areas:
275+
_LOGGER.debug("Removing work areas: %s", removed_areas)
276+
for entry in entries:
277+
for area_id in removed_areas:
278+
if entry.unique_id.startswith(f"{mower_id}_{area_id}_"):
279+
entity_registry.async_remove(entry.entity_id)

0 commit comments

Comments
 (0)