Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions custom_components/andersen_ev/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed

from .const import (
Expand All @@ -30,6 +31,27 @@
_LOGGER = logging.getLogger(__name__)


def _make_stale_device_listener(hass: HomeAssistant, coordinator: "AndersenEvCoordinator"):
"""Build a coordinator listener that removes devices no longer returned by the API.

Compares the device_id set on each coordinator refresh against the previous one; any
device_id that drops out gets its device-registry entry removed. Runs once (not per
platform) since it acts on the registry, not entities.
"""
device_registry = dr.async_get(hass)
known_device_ids = {device.device_id for device in coordinator.data}

def _handle_stale_devices() -> None:
current_device_ids = {device.device_id for device in coordinator.data}
for device_id in known_device_ids - current_device_ids:
if device_entry := device_registry.async_get_device(identifiers={(DOMAIN, device_id)}):
device_registry.async_remove_device(device_entry.id)
known_device_ids.clear()
known_device_ids.update(current_device_ids)

return _handle_stale_devices


async def async_setup_entry(hass: HomeAssistant, entry: AndersenEvConfigEntry) -> bool:
"""Set up Andersen EV from a config entry."""
email = entry.data["email"]
Expand All @@ -44,6 +66,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: AndersenEvConfigEntry) -

entry.runtime_data = coordinator

entry.async_on_unload(coordinator.async_add_listener(_make_stale_device_listener(hass, coordinator)))

# Register services
async def disable_all_schedules(call: ServiceCall) -> None:
"""Disable all schedules for a device."""
Expand Down
27 changes: 27 additions & 0 deletions custom_components/andersen_ev/entity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""Shared entity helpers for Andersen EV."""

from __future__ import annotations


class AndersenEvDeviceInfoMixin:
"""Mixin providing shared device-info update logic for Andersen EV entities.

Entities using this mixin must set ``self._device`` (a ``KonnectDevice``) and
``self._attr_device_info`` (a ``DeviceInfo``) before calling
``_update_model_from_device_status``.
"""

def _update_model_from_device_status(self) -> None:
"""Update model information from device status if available."""
# First try to use the model name from the API if available
if hasattr(self._device, "model_name") and self._device.model_name:
self._attr_device_info["model"] = self._device.model_name
# Fall back to the information from device status
elif self._device.last_status:
status = self._device.last_status
if "sysProductName" in status:
self._attr_device_info["model"] = status["sysProductName"]
elif "sysProductId" in status:
self._attr_device_info["model"] = status["sysProductId"]
elif "sysHwVersion" in status:
self._attr_device_info["model"] = f"A2 (HW: {status['sysHwVersion']})"
37 changes: 17 additions & 20 deletions custom_components/andersen_ev/lock.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

from . import AndersenEvConfigEntry, AndersenEvCoordinator
from .const import DOMAIN
from .entity import AndersenEvDeviceInfoMixin

PARALLEL_UPDATES = 1

Expand All @@ -25,15 +26,26 @@ async def async_setup_entry(
) -> None:
"""Set up the Andersen EV lock platform."""
coordinator = entry.runtime_data
known_device_ids: set[str] = set()

entities = []
for device in coordinator.data:
entities.append(AndersenEvLock(coordinator, device))
def _entities_for_new_devices() -> list[AndersenEvLock]:
"""Build lock entities for any device not seen before."""
new_devices = [device for device in coordinator.data if device.device_id not in known_device_ids]
entities = []
Comment on lines +31 to +34
for device in new_devices:
known_device_ids.add(device.device_id)
entities.append(AndersenEvLock(coordinator, device))
return entities

async_add_entities(entities)
def _handle_coordinator_update() -> None:
if new_entities := _entities_for_new_devices():
async_add_entities(new_entities)

async_add_entities(_entities_for_new_devices())
entry.async_on_unload(coordinator.async_add_listener(_handle_coordinator_update))

class AndersenEvLock(CoordinatorEntity, LockEntity): # pylint: disable=abstract-method

class AndersenEvLock(AndersenEvDeviceInfoMixin, CoordinatorEntity, LockEntity): # pylint: disable=abstract-method
"""Representation of an Andersen EV charging lock."""

_attr_has_entity_name = True
Expand All @@ -54,21 +66,6 @@ def __init__(self, coordinator: AndersenEvCoordinator, device) -> None:
# Update model if device status is already available
self._update_model_from_device_status()

def _update_model_from_device_status(self):
"""Update model information from device status if available."""
# First try to use the model name from the API if available
if hasattr(self._device, "model_name") and self._device.model_name:
self._attr_device_info["model"] = self._device.model_name
# Fall back to the information from device status
elif self._device.last_status:
status = self._device.last_status
if "sysProductName" in status:
self._attr_device_info["model"] = status["sysProductName"]
elif "sysProductId" in status:
self._attr_device_info["model"] = status["sysProductId"]
elif "sysHwVersion" in status:
self._attr_device_info["model"] = f"A2 (HW: {status['sysHwVersion']})"

@property
def available(self) -> bool:
"""Return if entity is available."""
Expand Down
4 changes: 2 additions & 2 deletions custom_components/andersen_ev/quality_scale.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ rules:
docs-supported-functions: todo
docs-troubleshooting: todo
docs-use-cases: todo
dynamic-devices: todo
dynamic-devices: done
entity-category: todo
entity-device-class: todo
entity-disabled-by-default: todo
Expand All @@ -62,7 +62,7 @@ rules:
icon-translations: todo
reconfiguration-flow: todo
repair-issues: todo
stale-devices: todo
stale-devices: done

# Platinum
async-dependency: todo
Expand Down
Loading
Loading