Extract goodwe data update coordinator to a separate module (#88396)

* Extract coordinator to separate module

* Make field protected and replace cast
pull/88493/head
mletenay 2023-02-21 08:14:12 +01:00 committed by GitHub
parent 8722f5b42b
commit 60ca3b3223
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 107 additions and 64 deletions

View File

@ -414,6 +414,7 @@ omit =
homeassistant/components/goalfeed/*
homeassistant/components/goodwe/__init__.py
homeassistant/components/goodwe/button.py
homeassistant/components/goodwe/coordinator.py
homeassistant/components/goodwe/number.py
homeassistant/components/goodwe/select.py
homeassistant/components/goodwe/sensor.py

View File

@ -1,14 +1,12 @@
"""The Goodwe inverter component."""
import logging
from goodwe import InverterError, RequestFailedException, connect
from goodwe import InverterError, connect
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import (
CONF_MODEL_FAMILY,
@ -17,16 +15,13 @@ from .const import (
KEY_DEVICE_INFO,
KEY_INVERTER,
PLATFORMS,
SCAN_INTERVAL,
)
_LOGGER = logging.getLogger(__name__)
from .coordinator import GoodweUpdateCoordinator
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up the Goodwe components from a config entry."""
hass.data.setdefault(DOMAIN, {})
name = entry.title
host = entry.data[CONF_HOST]
model_family = entry.data[CONF_MODEL_FAMILY]
@ -49,39 +44,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
sw_version=f"{inverter.firmware} / {inverter.arm_firmware}",
)
async def async_update_data():
"""Fetch data from the inverter."""
try:
return await inverter.read_runtime_data()
except RequestFailedException as ex:
# UDP communication with inverter is by definition unreliable.
# It is rather normal in many environments to fail to receive
# proper response in usual time, so we intentionally ignore isolated
# failures and report problem with availability only after
# consecutive streak of 3 of failed requests.
if ex.consecutive_failures_count < 3:
_LOGGER.debug(
"No response received (streak of %d)", ex.consecutive_failures_count
)
# return empty dictionary, sensors will keep their previous values
return {}
# Inverter does not respond anymore (e.g. it went to sleep mode)
_LOGGER.debug(
"Inverter not responding (streak of %d)", ex.consecutive_failures_count
)
raise UpdateFailed(ex) from ex
except InverterError as ex:
raise UpdateFailed(ex) from ex
# Create update coordinator
coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
name=name,
update_method=async_update_data,
# Polling interval. Will only be polled if there are subscribers.
update_interval=SCAN_INTERVAL,
)
coordinator = GoodweUpdateCoordinator(hass, entry, inverter)
# Fetch initial data so we have data when entities subscribe
await coordinator.async_config_entry_first_refresh()

View File

@ -0,0 +1,80 @@
"""Update coordinator for Goodwe."""
from __future__ import annotations
import logging
from typing import Any
from goodwe import Inverter, InverterError, RequestFailedException
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import SCAN_INTERVAL
_LOGGER = logging.getLogger(__name__)
class GoodweUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
"""Gather data for the energy device."""
def __init__(
self,
hass: HomeAssistant,
entry: ConfigEntry,
inverter: Inverter,
) -> None:
"""Initialize update coordinator."""
super().__init__(
hass,
_LOGGER,
name=entry.title,
update_interval=SCAN_INTERVAL,
update_method=self._async_update_data,
)
self.inverter: Inverter = inverter
self._last_data: dict[str, Any] = {}
async def _async_update_data(self) -> dict[str, Any]:
"""Fetch data from the inverter."""
try:
self._last_data = self.data if self.data else {}
return await self.inverter.read_runtime_data()
except RequestFailedException as ex:
# UDP communication with inverter is by definition unreliable.
# It is rather normal in many environments to fail to receive
# proper response in usual time, so we intentionally ignore isolated
# failures and report problem with availability only after
# consecutive streak of 3 of failed requests.
if ex.consecutive_failures_count < 3:
_LOGGER.debug(
"No response received (streak of %d)", ex.consecutive_failures_count
)
# return last known data
return self._last_data
# Inverter does not respond anymore (e.g. it went to sleep mode)
_LOGGER.debug(
"Inverter not responding (streak of %d)", ex.consecutive_failures_count
)
raise UpdateFailed(ex) from ex
except InverterError as ex:
raise UpdateFailed(ex) from ex
def sensor_value(self, sensor: str) -> Any:
"""Answer current (or last known) value of the sensor."""
val = self.data.get(sensor)
return val if val is not None else self._last_data.get(sensor)
def total_sensor_value(self, sensor: str) -> Any:
"""Answer current value of the 'total' (never 0) sensor."""
val = self.data.get(sensor)
return val if val else self._last_data.get(sensor)
def reset_sensor(self, sensor: str) -> None:
"""Reset sensor value to 0.
Intended for "daily" cumulative sensors (e.g. PV energy produced today),
which should be explicitly reset to 0 at midnight if inverter is suspended.
"""
self._last_data[sensor] = 0
self.data[sensor] = 0

View File

@ -6,7 +6,7 @@ from dataclasses import dataclass
from datetime import date, datetime, timedelta
from decimal import Decimal
import logging
from typing import Any, cast
from typing import Any
from goodwe import Inverter, Sensor, SensorKind
@ -32,13 +32,11 @@ from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_track_point_in_time
from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
)
from homeassistant.helpers.update_coordinator import CoordinatorEntity
import homeassistant.util.dt as dt_util
from .const import DOMAIN, KEY_COORDINATOR, KEY_DEVICE_INFO, KEY_INVERTER
from .coordinator import GoodweUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
@ -78,10 +76,12 @@ _ICONS: dict[SensorKind, str] = {
class GoodweSensorEntityDescription(SensorEntityDescription):
"""Class describing Goodwe sensor entities."""
value: Callable[[Any, Any], Any] = lambda prev, val: val
value: Callable[
[GoodweUpdateCoordinator, str], Any
] = lambda coordinator, sensor: coordinator.sensor_value(sensor)
available: Callable[
[CoordinatorEntity], bool
] = lambda entity: entity.coordinator.last_update_success
[GoodweUpdateCoordinator], bool
] = lambda coordinator: coordinator.last_update_success
_DESCRIPTIONS: dict[str, GoodweSensorEntityDescription] = {
@ -108,8 +108,8 @@ _DESCRIPTIONS: dict[str, GoodweSensorEntityDescription] = {
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
value=lambda prev, val: val if val else prev,
available=lambda entity: entity.coordinator.data is not None,
value=lambda coordinator, sensor: coordinator.total_sensor_value(sensor),
available=lambda coordinator: coordinator.data is not None,
),
"C": GoodweSensorEntityDescription(
key="C",
@ -159,12 +159,14 @@ async def async_setup_entry(
async_add_entities(entities)
class InverterSensor(CoordinatorEntity, SensorEntity):
class InverterSensor(CoordinatorEntity[GoodweUpdateCoordinator], SensorEntity):
"""Entity representing individual inverter sensor."""
entity_description: GoodweSensorEntityDescription
def __init__(
self,
coordinator: DataUpdateCoordinator,
coordinator: GoodweUpdateCoordinator,
device_info: DeviceInfo,
inverter: Inverter,
sensor: Sensor,
@ -190,18 +192,12 @@ class InverterSensor(CoordinatorEntity, SensorEntity):
if sensor.id_ == BATTERY_SOC:
self._attr_device_class = SensorDeviceClass.BATTERY
self._sensor = sensor
self._previous_value = None
self._stop_reset: Callable[[], None] | None = None
@property
def native_value(self) -> StateType | date | datetime | Decimal:
"""Return the value reported by the sensor."""
value = cast(GoodweSensorEntityDescription, self.entity_description).value(
self._previous_value,
self.coordinator.data.get(self._sensor.id_, self._previous_value),
)
self._previous_value = value
return value
return self.entity_description.value(self.coordinator, self._sensor.id_)
@property
def available(self) -> bool:
@ -212,16 +208,18 @@ class InverterSensor(CoordinatorEntity, SensorEntity):
as available even when the (non-battery) pv inverter is off-line during night
and most of the sensors are actually unavailable.
"""
return cast(GoodweSensorEntityDescription, self.entity_description).available(
self
)
return self.entity_description.available(self.coordinator)
@callback
def async_reset(self, now):
"""Reset the value back to 0 at midnight."""
"""Reset the value back to 0 at midnight.
Some sensors values like daily produced energy are kept available,
even when the inverter is in sleep mode and no longer responds to request.
In contrast to "total" sensors, these "daily" sensors need to be reset to 0 on midnight.
"""
if not self.coordinator.last_update_success:
self._previous_value = 0
self.coordinator.data[self._sensor.id_] = 0
self.coordinator.reset_sensor(self._sensor.id)
self.async_write_ha_state()
_LOGGER.debug("Goodwe reset %s to 0", self.name)
next_midnight = dt_util.start_of_local_day(