Extract goodwe data update coordinator to a separate module (#88396)
* Extract coordinator to separate module * Make field protected and replace castpull/88493/head
parent
8722f5b42b
commit
60ca3b3223
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
|
@ -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(
|
||||
|
|
Loading…
Reference in New Issue