Dynamic add/remove devices for solarlog (#128668)

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
dontinelli 2024-10-25 18:02:14 +02:00 committed by GitHub
parent c71c8d56ce
commit 4b680ffa5f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 115 additions and 7 deletions

View file

@ -2,6 +2,7 @@
from __future__ import annotations
from collections.abc import Callable
from datetime import timedelta
import logging
from typing import TYPE_CHECKING
@ -18,7 +19,11 @@ from solarlog_cli.solarlog_models import SolarlogData
from homeassistant.const import CONF_HOST
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
import homeassistant.helpers.device_registry as dr
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util import slugify
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
@ -35,6 +40,9 @@ class SolarLogCoordinator(DataUpdateCoordinator[SolarlogData]):
hass, _LOGGER, name="SolarLog", update_interval=timedelta(seconds=60)
)
self.new_device_callbacks: list[Callable[[int], None]] = []
self._devices_last_update: set[tuple[int, str]] = set()
host_entry = entry.data[CONF_HOST]
password = entry.data.get("password", "")
@ -84,8 +92,55 @@ class SolarLogCoordinator(DataUpdateCoordinator[SolarlogData]):
_LOGGER.debug("Data successfully updated")
if self.solarlog.extended_data:
self._async_add_remove_devices(data)
_LOGGER.debug("Add_remove_devices finished")
return data
def _async_add_remove_devices(self, data: SolarlogData) -> None:
"""Add new devices, remove non-existing devices."""
if (
current_devices := {
(k, self.solarlog.device_name(k)) for k in data.inverter_data
}
) == self._devices_last_update:
return
# remove old devices
if removed_devices := self._devices_last_update - current_devices:
_LOGGER.debug("Removed device(s): %s", ", ".join(map(str, removed_devices)))
device_registry = dr.async_get(self.hass)
for removed_device in removed_devices:
device_name = ""
for did, dn in self._devices_last_update:
if did == removed_device[0]:
device_name = dn
break
if device := device_registry.async_get_device(
identifiers={
(
DOMAIN,
f"{self.unique_id}_{slugify(device_name)}",
)
}
):
device_registry.async_update_device(
device_id=device.id,
remove_config_entry_id=self.unique_id,
)
_LOGGER.debug("Device removed from device registry: %s", device.id)
# add new devices
if new_devices := current_devices - self._devices_last_update:
_LOGGER.debug("New device(s) found: %s", ", ".join(map(str, new_devices)))
for device_id in new_devices:
for callback in self.new_device_callbacks:
callback(device_id[0])
self._devices_last_update = current_devices
async def renew_authentication(self) -> bool:
"""Renew access token for SolarLog API."""
logged_in = False

View file

@ -254,7 +254,9 @@ INVERTER_SENSOR_TYPES: tuple[SolarLogInverterSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda inverter: inverter.current_power,
value_fn=(
lambda inverter: None if inverter is None else inverter.current_power
),
),
SolarLogInverterSensorEntityDescription(
key="consumption_year",
@ -265,9 +267,7 @@ INVERTER_SENSOR_TYPES: tuple[SolarLogInverterSensorEntityDescription, ...] = (
state_class=SensorStateClass.TOTAL_INCREASING,
suggested_display_precision=3,
value_fn=(
lambda inverter: None
if inverter.consumption_year is None
else inverter.consumption_year
lambda inverter: None if inverter is None else inverter.consumption_year
),
),
)
@ -297,6 +297,14 @@ async def async_setup_entry(
async_add_entities(entities)
def _async_add_new_device(device_id: int) -> None:
async_add_entities(
SolarLogInverterSensor(coordinator, sensor, device_id)
for sensor in INVERTER_SENSOR_TYPES
)
coordinator.new_device_callbacks.append(_async_add_new_device)
class SolarLogCoordinatorSensor(SolarLogCoordinatorEntity, SensorEntity):
"""Represents a SolarLog sensor."""

View file

@ -65,7 +65,7 @@ def mock_solarlog_connector():
mock_solarlog_api.update_device_list.return_value = DEVICE_LIST
mock_solarlog_api.update_inverter_data.return_value = INVERTER_DATA
mock_solarlog_api.device_name = {0: "Inverter 1", 1: "Inverter 2"}.get
mock_solarlog_api.device_enabled = {0: True, 1: False}.get
mock_solarlog_api.device_enabled = {0: True, 1: True}.get
mock_solarlog_api.password.return_value = "pwd"
with (

View file

@ -9,11 +9,13 @@ from solarlog_cli.solarlog_exceptions import (
SolarLogConnectionError,
SolarLogUpdateError,
)
from solarlog_cli.solarlog_models import InverterData
from syrupy import SnapshotAssertion
from homeassistant.const import STATE_UNAVAILABLE, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.device_registry import DeviceRegistry
from homeassistant.helpers.entity_registry import EntityRegistry
from . import setup_platform
@ -25,7 +27,7 @@ async def test_all_entities(
snapshot: SnapshotAssertion,
mock_solarlog_connector: AsyncMock,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
entity_registry: EntityRegistry,
) -> None:
"""Test all entities."""
@ -33,6 +35,49 @@ async def test_all_entities(
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
async def test_add_remove_entities(
hass: HomeAssistant,
mock_solarlog_connector: AsyncMock,
mock_config_entry: MockConfigEntry,
device_registry: DeviceRegistry,
entity_registry: EntityRegistry,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test if entities are added and old are removed."""
await setup_platform(hass, mock_config_entry, [Platform.SENSOR])
assert hass.states.get("sensor.inverter_1_consumption_year").state == "354.687"
# test no changes (coordinator.py line 114)
freezer.tick(delta=timedelta(minutes=1))
async_fire_time_changed(hass)
await hass.async_block_till_done()
mock_solarlog_connector.update_device_list.return_value = {
0: InverterData(name="Inv 1", enabled=True),
2: InverterData(name="Inverter 3", enabled=True),
}
mock_solarlog_connector.update_inverter_data.return_value = {
0: InverterData(
name="Inv 1", enabled=True, consumption_year=354687, current_power=5
),
2: InverterData(
name="Inverter 3", enabled=True, consumption_year=454, current_power=7
),
}
mock_solarlog_connector.device_name = {0: "Inv 1", 2: "Inverter 3"}.get
mock_solarlog_connector.device_enabled = {0: True, 2: True}.get
freezer.tick(delta=timedelta(minutes=1))
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert hass.states.get("sensor.inverter_1_consumption_year") is None
assert hass.states.get("sensor.inv_1_consumption_year").state == "354.687"
assert hass.states.get("sensor.inverter_2_consumption_year") is None
assert hass.states.get("sensor.inverter_3_consumption_year").state == "0.454"
@pytest.mark.parametrize(
"exception",
[