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 __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import logging
|
import logging
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
@ -18,7 +19,11 @@ from solarlog_cli.solarlog_models import SolarlogData
|
||||||
from homeassistant.const import CONF_HOST
|
from homeassistant.const import CONF_HOST
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||||
|
import homeassistant.helpers.device_registry as dr
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
from homeassistant.util import slugify
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -35,6 +40,9 @@ class SolarLogCoordinator(DataUpdateCoordinator[SolarlogData]):
|
||||||
hass, _LOGGER, name="SolarLog", update_interval=timedelta(seconds=60)
|
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]
|
host_entry = entry.data[CONF_HOST]
|
||||||
password = entry.data.get("password", "")
|
password = entry.data.get("password", "")
|
||||||
|
|
||||||
|
@ -84,8 +92,55 @@ class SolarLogCoordinator(DataUpdateCoordinator[SolarlogData]):
|
||||||
|
|
||||||
_LOGGER.debug("Data successfully updated")
|
_LOGGER.debug("Data successfully updated")
|
||||||
|
|
||||||
|
if self.solarlog.extended_data:
|
||||||
|
self._async_add_remove_devices(data)
|
||||||
|
_LOGGER.debug("Add_remove_devices finished")
|
||||||
|
|
||||||
return data
|
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:
|
async def renew_authentication(self) -> bool:
|
||||||
"""Renew access token for SolarLog API."""
|
"""Renew access token for SolarLog API."""
|
||||||
logged_in = False
|
logged_in = False
|
||||||
|
|
|
@ -254,7 +254,9 @@ INVERTER_SENSOR_TYPES: tuple[SolarLogInverterSensorEntityDescription, ...] = (
|
||||||
native_unit_of_measurement=UnitOfPower.WATT,
|
native_unit_of_measurement=UnitOfPower.WATT,
|
||||||
device_class=SensorDeviceClass.POWER,
|
device_class=SensorDeviceClass.POWER,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
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(
|
SolarLogInverterSensorEntityDescription(
|
||||||
key="consumption_year",
|
key="consumption_year",
|
||||||
|
@ -265,9 +267,7 @@ INVERTER_SENSOR_TYPES: tuple[SolarLogInverterSensorEntityDescription, ...] = (
|
||||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||||
suggested_display_precision=3,
|
suggested_display_precision=3,
|
||||||
value_fn=(
|
value_fn=(
|
||||||
lambda inverter: None
|
lambda inverter: None if inverter is None else inverter.consumption_year
|
||||||
if inverter.consumption_year is None
|
|
||||||
else inverter.consumption_year
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -297,6 +297,14 @@ async def async_setup_entry(
|
||||||
|
|
||||||
async_add_entities(entities)
|
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):
|
class SolarLogCoordinatorSensor(SolarLogCoordinatorEntity, SensorEntity):
|
||||||
"""Represents a SolarLog sensor."""
|
"""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_device_list.return_value = DEVICE_LIST
|
||||||
mock_solarlog_api.update_inverter_data.return_value = INVERTER_DATA
|
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_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"
|
mock_solarlog_api.password.return_value = "pwd"
|
||||||
|
|
||||||
with (
|
with (
|
||||||
|
|
|
@ -9,11 +9,13 @@ from solarlog_cli.solarlog_exceptions import (
|
||||||
SolarLogConnectionError,
|
SolarLogConnectionError,
|
||||||
SolarLogUpdateError,
|
SolarLogUpdateError,
|
||||||
)
|
)
|
||||||
|
from solarlog_cli.solarlog_models import InverterData
|
||||||
from syrupy import SnapshotAssertion
|
from syrupy import SnapshotAssertion
|
||||||
|
|
||||||
from homeassistant.const import STATE_UNAVAILABLE, Platform
|
from homeassistant.const import STATE_UNAVAILABLE, Platform
|
||||||
from homeassistant.core import HomeAssistant
|
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
|
from . import setup_platform
|
||||||
|
|
||||||
|
@ -25,7 +27,7 @@ async def test_all_entities(
|
||||||
snapshot: SnapshotAssertion,
|
snapshot: SnapshotAssertion,
|
||||||
mock_solarlog_connector: AsyncMock,
|
mock_solarlog_connector: AsyncMock,
|
||||||
mock_config_entry: MockConfigEntry,
|
mock_config_entry: MockConfigEntry,
|
||||||
entity_registry: er.EntityRegistry,
|
entity_registry: EntityRegistry,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test all entities."""
|
"""Test all entities."""
|
||||||
|
|
||||||
|
@ -33,6 +35,49 @@ async def test_all_entities(
|
||||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
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(
|
@pytest.mark.parametrize(
|
||||||
"exception",
|
"exception",
|
||||||
[
|
[
|
||||||
|
|
Loading…
Add table
Reference in a new issue