Home Assistant Unofficial Reference 2024.12.1
sensor.py
Go to the documentation of this file.
1 """Support for GoodWe inverter via UDP."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Callable
6 from dataclasses import dataclass
7 from datetime import date, datetime, timedelta
8 from decimal import Decimal
9 import logging
10 from typing import Any
11 
12 from goodwe import Inverter, Sensor, SensorKind
13 
15  SensorDeviceClass,
16  SensorEntity,
17  SensorEntityDescription,
18  SensorStateClass,
19 )
20 from homeassistant.config_entries import ConfigEntry
21 from homeassistant.const import (
22  PERCENTAGE,
23  EntityCategory,
24  UnitOfApparentPower,
25  UnitOfElectricCurrent,
26  UnitOfElectricPotential,
27  UnitOfEnergy,
28  UnitOfFrequency,
29  UnitOfPower,
30  UnitOfReactivePower,
31  UnitOfTemperature,
32  UnitOfTime,
33 )
34 from homeassistant.core import HomeAssistant, callback
35 from homeassistant.helpers.device_registry import DeviceInfo
36 from homeassistant.helpers.entity_platform import AddEntitiesCallback
37 from homeassistant.helpers.event import async_track_point_in_time
38 from homeassistant.helpers.typing import StateType
39 from homeassistant.helpers.update_coordinator import CoordinatorEntity
40 import homeassistant.util.dt as dt_util
41 
42 from .const import DOMAIN, KEY_COORDINATOR, KEY_DEVICE_INFO, KEY_INVERTER
43 from .coordinator import GoodweUpdateCoordinator
44 
45 _LOGGER = logging.getLogger(__name__)
46 
47 # Sensor name of battery SoC
48 BATTERY_SOC = "battery_soc"
49 
50 # Sensors that are reset to 0 at midnight.
51 # The inverter is only powered by the solar panels and not mains power, so it goes dead when the sun goes down.
52 # The "_day" sensors are reset to 0 when the inverter wakes up in the morning when the sun comes up and power to the inverter is restored.
53 # This makes sure daily values are reset at midnight instead of at sunrise.
54 # When the inverter has a battery connected, HomeAssistant will not reset the values but let the inverter reset them by looking at the unavailable state of the inverter.
55 DAILY_RESET = ["e_day", "e_load_day"]
56 
57 _MAIN_SENSORS = (
58  "ppv",
59  "house_consumption",
60  "active_power",
61  "battery_soc",
62  "e_day",
63  "e_total",
64  "meter_e_total_exp",
65  "meter_e_total_imp",
66  "e_bat_charge_total",
67  "e_bat_discharge_total",
68 )
69 
70 _ICONS: dict[SensorKind, str] = {
71  SensorKind.PV: "mdi:solar-power",
72  SensorKind.AC: "mdi:power-plug-outline",
73  SensorKind.UPS: "mdi:power-plug-off-outline",
74  SensorKind.BAT: "mdi:battery-high",
75  SensorKind.GRID: "mdi:transmission-tower",
76 }
77 
78 
79 @dataclass(frozen=True)
81  """Class describing Goodwe sensor entities."""
82 
83  value: Callable[[GoodweUpdateCoordinator, str], Any] = (
84  lambda coordinator, sensor: coordinator.sensor_value(sensor)
85  )
86  available: Callable[[GoodweUpdateCoordinator], bool] = (
87  lambda coordinator: coordinator.last_update_success
88  )
89 
90 
91 _DESCRIPTIONS: dict[str, GoodweSensorEntityDescription] = {
93  key="A",
94  device_class=SensorDeviceClass.CURRENT,
95  state_class=SensorStateClass.MEASUREMENT,
96  native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
97  ),
99  key="V",
100  device_class=SensorDeviceClass.VOLTAGE,
101  state_class=SensorStateClass.MEASUREMENT,
102  native_unit_of_measurement=UnitOfElectricPotential.VOLT,
103  ),
105  key="W",
106  device_class=SensorDeviceClass.POWER,
107  state_class=SensorStateClass.MEASUREMENT,
108  native_unit_of_measurement=UnitOfPower.WATT,
109  ),
111  key="kWh",
112  device_class=SensorDeviceClass.ENERGY,
113  state_class=SensorStateClass.TOTAL_INCREASING,
114  native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
115  value=lambda coordinator, sensor: coordinator.total_sensor_value(sensor),
116  available=lambda coordinator: coordinator.data is not None,
117  ),
119  key="VA",
120  device_class=SensorDeviceClass.APPARENT_POWER,
121  state_class=SensorStateClass.MEASUREMENT,
122  native_unit_of_measurement=UnitOfApparentPower.VOLT_AMPERE,
123  entity_registry_enabled_default=False,
124  ),
126  key="var",
127  device_class=SensorDeviceClass.REACTIVE_POWER,
128  state_class=SensorStateClass.MEASUREMENT,
129  native_unit_of_measurement=UnitOfReactivePower.VOLT_AMPERE_REACTIVE,
130  entity_registry_enabled_default=False,
131  ),
133  key="C",
134  device_class=SensorDeviceClass.TEMPERATURE,
135  state_class=SensorStateClass.MEASUREMENT,
136  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
137  ),
139  key="Hz",
140  device_class=SensorDeviceClass.FREQUENCY,
141  state_class=SensorStateClass.MEASUREMENT,
142  native_unit_of_measurement=UnitOfFrequency.HERTZ,
143  ),
145  key="h",
146  device_class=SensorDeviceClass.DURATION,
147  state_class=SensorStateClass.MEASUREMENT,
148  native_unit_of_measurement=UnitOfTime.HOURS,
149  entity_registry_enabled_default=False,
150  ),
152  key="%",
153  state_class=SensorStateClass.MEASUREMENT,
154  native_unit_of_measurement=PERCENTAGE,
155  ),
156 }
158  key="_",
159  state_class=SensorStateClass.MEASUREMENT,
160 )
162  key="text",
163 )
164 
165 
167  hass: HomeAssistant,
168  config_entry: ConfigEntry,
169  async_add_entities: AddEntitiesCallback,
170 ) -> None:
171  """Set up the GoodWe inverter from a config entry."""
172  entities: list[InverterSensor] = []
173  inverter = hass.data[DOMAIN][config_entry.entry_id][KEY_INVERTER]
174  coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR]
175  device_info = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE_INFO]
176 
177  # Individual inverter sensors entities
178  entities.extend(
179  InverterSensor(coordinator, device_info, inverter, sensor)
180  for sensor in inverter.sensors()
181  if not sensor.id_.startswith("xx")
182  )
183 
184  async_add_entities(entities)
185 
186 
187 class InverterSensor(CoordinatorEntity[GoodweUpdateCoordinator], SensorEntity):
188  """Entity representing individual inverter sensor."""
189 
190  entity_description: GoodweSensorEntityDescription
191 
192  def __init__(
193  self,
194  coordinator: GoodweUpdateCoordinator,
195  device_info: DeviceInfo,
196  inverter: Inverter,
197  sensor: Sensor,
198  ) -> None:
199  """Initialize an inverter sensor."""
200  super().__init__(coordinator)
201  self._attr_name_attr_name = sensor.name.strip()
202  self._attr_unique_id_attr_unique_id = f"{DOMAIN}-{sensor.id_}-{inverter.serial_number}"
203  self._attr_device_info_attr_device_info = device_info
204  self._attr_entity_category_attr_entity_category = (
205  EntityCategory.DIAGNOSTIC if sensor.id_ not in _MAIN_SENSORS else None
206  )
207  try:
208  self.entity_descriptionentity_description = _DESCRIPTIONS[sensor.unit]
209  except KeyError:
210  if "Enum" in type(sensor).__name__ or sensor.id_ == "timestamp":
211  self.entity_descriptionentity_description = TEXT_SENSOR
212  else:
213  self.entity_descriptionentity_description = DIAG_SENSOR
214  self._attr_native_unit_of_measurement_attr_native_unit_of_measurement = sensor.unit
215  self._attr_icon_attr_icon = _ICONS.get(sensor.kind)
216  # Set the inverter SoC as main device battery sensor
217  if sensor.id_ == BATTERY_SOC:
218  self._attr_device_class_attr_device_class = SensorDeviceClass.BATTERY
219  self._sensor_sensor = sensor
220  self._stop_reset_stop_reset: Callable[[], None] | None = None
221 
222  @property
223  def native_value(self) -> StateType | date | datetime | Decimal:
224  """Return the value reported by the sensor."""
225  return self.entity_descriptionentity_description.value(self.coordinator, self._sensor_sensor.id_)
226 
227  @property
228  def available(self) -> bool:
229  """Return if entity is available.
230 
231  We delegate the behavior to entity description lambda, since
232  some sensors (like energy produced today) should report themselves
233  as available even when the (non-battery) pv inverter is off-line during night
234  and most of the sensors are actually unavailable.
235  """
236  return self.entity_descriptionentity_description.available(self.coordinator)
237 
238  @callback
239  def async_reset(self, now):
240  """Reset the value back to 0 at midnight.
241 
242  Some sensors values like daily produced energy are kept available,
243  even when the inverter is in sleep mode and no longer responds to request.
244  In contrast to "total" sensors, these "daily" sensors need to be reset to 0 on midnight.
245  """
246  if not self.coordinator.last_update_success:
247  self.coordinator.reset_sensor(self._sensor_sensor.id_)
248  self.async_write_ha_stateasync_write_ha_state()
249  _LOGGER.debug("Goodwe reset %s to 0", self.namenamename)
250  next_midnight = dt_util.start_of_local_day(
251  dt_util.now() + timedelta(days=1, minutes=1)
252  )
254  self.hasshasshass, self.async_resetasync_reset, next_midnight
255  )
256 
257  async def async_added_to_hass(self) -> None:
258  """Schedule reset task at midnight."""
259  if self._sensor_sensor.id_ in DAILY_RESET:
260  next_midnight = dt_util.start_of_local_day(
261  dt_util.now() + timedelta(days=1)
262  )
263  self._stop_reset_stop_reset = async_track_point_in_time(
264  self.hasshasshass, self.async_resetasync_reset, next_midnight
265  )
266  await super().async_added_to_hass()
267 
268  async def async_will_remove_from_hass(self) -> None:
269  """Remove reset task at midnight."""
270  if self._sensor_sensor.id_ in DAILY_RESET and self._stop_reset_stop_reset is not None:
271  self._stop_reset_stop_reset()
272  await super().async_will_remove_from_hass()
None __init__(self, GoodweUpdateCoordinator coordinator, DeviceInfo device_info, Inverter inverter, Sensor sensor)
Definition: sensor.py:198
StateType|date|datetime|Decimal native_value(self)
Definition: sensor.py:223
str|UndefinedType|None name(self)
Definition: entity.py:738
None async_setup_entry(HomeAssistant hass, ConfigEntry config_entry, AddEntitiesCallback async_add_entities)
Definition: sensor.py:170
CALLBACK_TYPE async_track_point_in_time(HomeAssistant hass, HassJob[[datetime], Coroutine[Any, Any, None]|None]|Callable[[datetime], Coroutine[Any, Any, None]|None] action, datetime point_in_time)
Definition: event.py:1462