Dynamic add/remove devices for solarlog (#128668)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
parent
c71c8d56ce
commit
4b680ffa5f
4 changed files with 115 additions and 7 deletions
|
@ -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
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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",
|
||||
[
|
||||
|
|
Loading…
Add table
Reference in a new issue