2022-01-04 21:19:31 +00:00
""" Support for GoodWe inverter via UDP. """
from __future__ import annotations
from collections . abc import Callable
from dataclasses import dataclass
2022-09-27 12:09:55 +00:00
from datetime import timedelta
2022-10-21 20:50:00 +00:00
import logging
2022-06-29 04:09:24 +00:00
from typing import Any , cast
2022-01-04 21:19:31 +00:00
from goodwe import Inverter , Sensor , SensorKind
from homeassistant . components . sensor import (
2022-04-04 15:45:44 +00:00
SensorDeviceClass ,
2022-01-04 21:19:31 +00:00
SensorEntity ,
SensorEntityDescription ,
2022-04-04 09:45:53 +00:00
SensorStateClass ,
2022-01-04 21:19:31 +00:00
)
2022-01-05 12:49:14 +00:00
from homeassistant . config_entries import ConfigEntry
2022-01-04 21:19:31 +00:00
from homeassistant . const import (
ELECTRIC_CURRENT_AMPERE ,
ELECTRIC_POTENTIAL_VOLT ,
ENERGY_KILO_WATT_HOUR ,
FREQUENCY_HERTZ ,
PERCENTAGE ,
POWER_WATT ,
TEMP_CELSIUS ,
)
2022-09-27 12:09:55 +00:00
from homeassistant . core import HomeAssistant , callback
2022-01-24 11:51:11 +00:00
from homeassistant . helpers . entity import DeviceInfo , EntityCategory
2022-01-05 12:49:14 +00:00
from homeassistant . helpers . entity_platform import AddEntitiesCallback
2022-09-27 12:09:55 +00:00
from homeassistant . helpers . event import async_track_point_in_time
2022-01-04 21:19:31 +00:00
from homeassistant . helpers . update_coordinator import (
CoordinatorEntity ,
DataUpdateCoordinator ,
)
2022-09-27 12:09:55 +00:00
import homeassistant . util . dt as dt_util
2022-01-04 21:19:31 +00:00
from . const import DOMAIN , KEY_COORDINATOR , KEY_DEVICE_INFO , KEY_INVERTER
2022-10-21 20:50:00 +00:00
_LOGGER = logging . getLogger ( __name__ )
2022-01-04 21:19:31 +00:00
# Sensor name of battery SoC
BATTERY_SOC = " battery_soc "
2022-09-27 12:09:55 +00:00
# Sensors that are reset to 0 at midnight.
# The inverter is only powered by the solar panels and not mains power, so it goes dead when the sun goes down.
# 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.
# This makes sure daily values are reset at midnight instead of at sunrise.
# 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.
DAILY_RESET = [ " e_day " , " e_load_day " ]
2022-01-04 21:19:31 +00:00
_MAIN_SENSORS = (
" ppv " ,
" house_consumption " ,
" active_power " ,
" battery_soc " ,
" e_day " ,
" e_total " ,
" meter_e_total_exp " ,
" meter_e_total_imp " ,
" e_bat_charge_total " ,
" e_bat_discharge_total " ,
)
2022-06-29 04:09:24 +00:00
_ICONS : dict [ SensorKind , str ] = {
2022-01-04 21:19:31 +00:00
SensorKind . PV : " mdi:solar-power " ,
SensorKind . AC : " mdi:power-plug-outline " ,
SensorKind . UPS : " mdi:power-plug-off-outline " ,
SensorKind . BAT : " mdi:battery-high " ,
SensorKind . GRID : " mdi:transmission-tower " ,
}
@dataclass
class GoodweSensorEntityDescription ( SensorEntityDescription ) :
""" Class describing Goodwe sensor entities. """
2022-06-29 04:09:24 +00:00
value : Callable [ [ Any , Any ] , Any ] = lambda prev , val : val
available : Callable [
[ CoordinatorEntity ] , bool
] = lambda entity : entity . coordinator . last_update_success
2022-01-04 21:19:31 +00:00
2022-06-29 04:09:24 +00:00
_DESCRIPTIONS : dict [ str , GoodweSensorEntityDescription ] = {
2022-01-04 21:19:31 +00:00
" A " : GoodweSensorEntityDescription (
key = " A " ,
2022-04-04 15:45:44 +00:00
device_class = SensorDeviceClass . CURRENT ,
2022-04-04 09:45:53 +00:00
state_class = SensorStateClass . MEASUREMENT ,
2022-01-04 21:19:31 +00:00
native_unit_of_measurement = ELECTRIC_CURRENT_AMPERE ,
) ,
" V " : GoodweSensorEntityDescription (
key = " V " ,
2022-04-04 15:45:44 +00:00
device_class = SensorDeviceClass . VOLTAGE ,
2022-04-04 09:45:53 +00:00
state_class = SensorStateClass . MEASUREMENT ,
2022-01-04 21:19:31 +00:00
native_unit_of_measurement = ELECTRIC_POTENTIAL_VOLT ,
) ,
" W " : GoodweSensorEntityDescription (
key = " W " ,
2022-04-04 15:45:44 +00:00
device_class = SensorDeviceClass . POWER ,
2022-04-04 09:45:53 +00:00
state_class = SensorStateClass . MEASUREMENT ,
2022-01-04 21:19:31 +00:00
native_unit_of_measurement = POWER_WATT ,
) ,
" kWh " : GoodweSensorEntityDescription (
key = " kWh " ,
2022-04-04 15:45:44 +00:00
device_class = SensorDeviceClass . ENERGY ,
2022-04-04 09:45:53 +00:00
state_class = SensorStateClass . TOTAL_INCREASING ,
2022-01-04 21:19:31 +00:00
native_unit_of_measurement = ENERGY_KILO_WATT_HOUR ,
2022-06-29 04:09:24 +00:00
value = lambda prev , val : prev if not val else val ,
available = lambda entity : entity . coordinator . data is not None ,
2022-01-04 21:19:31 +00:00
) ,
" C " : GoodweSensorEntityDescription (
key = " C " ,
2022-04-04 15:45:44 +00:00
device_class = SensorDeviceClass . TEMPERATURE ,
2022-04-04 09:45:53 +00:00
state_class = SensorStateClass . MEASUREMENT ,
2022-01-04 21:19:31 +00:00
native_unit_of_measurement = TEMP_CELSIUS ,
) ,
" Hz " : GoodweSensorEntityDescription (
key = " Hz " ,
2022-04-04 15:45:44 +00:00
device_class = SensorDeviceClass . VOLTAGE ,
2022-04-04 09:45:53 +00:00
state_class = SensorStateClass . MEASUREMENT ,
2022-01-04 21:19:31 +00:00
native_unit_of_measurement = FREQUENCY_HERTZ ,
) ,
" % " : GoodweSensorEntityDescription (
key = " % " ,
2022-04-04 09:45:53 +00:00
state_class = SensorStateClass . MEASUREMENT ,
2022-01-04 21:19:31 +00:00
native_unit_of_measurement = PERCENTAGE ,
) ,
}
DIAG_SENSOR = GoodweSensorEntityDescription (
key = " _ " ,
2022-04-04 09:45:53 +00:00
state_class = SensorStateClass . MEASUREMENT ,
2022-01-04 21:19:31 +00:00
)
2022-01-05 12:49:14 +00:00
async def async_setup_entry (
hass : HomeAssistant ,
config_entry : ConfigEntry ,
async_add_entities : AddEntitiesCallback ,
) - > None :
2022-01-04 21:19:31 +00:00
""" Set up the GoodWe inverter from a config entry. """
2022-01-05 12:49:14 +00:00
entities : list [ InverterSensor ] = [ ]
2022-01-04 21:19:31 +00:00
inverter = hass . data [ DOMAIN ] [ config_entry . entry_id ] [ KEY_INVERTER ]
coordinator = hass . data [ DOMAIN ] [ config_entry . entry_id ] [ KEY_COORDINATOR ]
device_info = hass . data [ DOMAIN ] [ config_entry . entry_id ] [ KEY_DEVICE_INFO ]
# Individual inverter sensors entities
entities . extend (
InverterSensor ( coordinator , device_info , inverter , sensor )
for sensor in inverter . sensors ( )
if not sensor . id_ . startswith ( " xx " )
)
async_add_entities ( entities )
class InverterSensor ( CoordinatorEntity , SensorEntity ) :
""" Entity representing individual inverter sensor. """
def __init__ (
self ,
coordinator : DataUpdateCoordinator ,
device_info : DeviceInfo ,
inverter : Inverter ,
sensor : Sensor ,
) - > None :
""" Initialize an inverter sensor. """
super ( ) . __init__ ( coordinator )
self . _attr_name = sensor . name . strip ( )
self . _attr_unique_id = f " { DOMAIN } - { sensor . id_ } - { inverter . serial_number } "
self . _attr_device_info = device_info
self . _attr_entity_category = (
2022-01-24 11:51:11 +00:00
EntityCategory . DIAGNOSTIC if sensor . id_ not in _MAIN_SENSORS else None
2022-01-04 21:19:31 +00:00
)
self . entity_description = _DESCRIPTIONS . get ( sensor . unit , DIAG_SENSOR )
if not self . entity_description . native_unit_of_measurement :
self . _attr_native_unit_of_measurement = sensor . unit
self . _attr_icon = _ICONS . get ( sensor . kind )
# Set the inverter SoC as main device battery sensor
if sensor . id_ == BATTERY_SOC :
2022-04-04 15:45:44 +00:00
self . _attr_device_class = SensorDeviceClass . BATTERY
2022-01-04 21:19:31 +00:00
self . _sensor = sensor
self . _previous_value = None
2022-09-27 12:09:55 +00:00
self . _stop_reset = None
2022-01-04 21:19:31 +00:00
@property
def native_value ( self ) :
""" Return the value reported by the sensor. """
2022-06-29 04:09:24 +00:00
value = cast ( GoodweSensorEntityDescription , self . entity_description ) . value (
2022-01-04 21:19:31 +00:00
self . _previous_value ,
self . coordinator . data . get ( self . _sensor . id_ , self . _previous_value ) ,
)
self . _previous_value = value
return value
2022-06-29 04:09:24 +00:00
@property
def available ( self ) - > bool :
""" Return if entity is available.
We delegate the behavior to entity description lambda , since
some sensors ( like energy produced today ) should report themselves
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
)
2022-09-27 12:09:55 +00:00
@callback
def async_reset ( self , now ) :
""" Reset the value back to 0 at midnight. """
if not self . coordinator . last_update_success :
self . _previous_value = 0
self . coordinator . data [ self . _sensor . id_ ] = 0
self . async_write_ha_state ( )
2022-10-21 20:50:00 +00:00
_LOGGER . debug ( " Goodwe reset %s to 0 " , self . name )
next_midnight = dt_util . start_of_local_day (
dt_util . now ( ) + timedelta ( days = 1 , minutes = 1 )
)
2022-09-27 12:09:55 +00:00
self . _stop_reset = async_track_point_in_time (
self . hass , self . async_reset , next_midnight
)
async def async_added_to_hass ( self ) :
""" Schedule reset task at midnight. """
if self . _sensor . id_ in DAILY_RESET :
next_midnight = dt_util . start_of_local_day (
2022-10-21 20:50:00 +00:00
dt_util . now ( ) + timedelta ( days = 1 )
2022-09-27 12:09:55 +00:00
)
self . _stop_reset = async_track_point_in_time (
self . hass , self . async_reset , next_midnight
)
await super ( ) . async_added_to_hass ( )
async def async_will_remove_from_hass ( self ) :
""" Remove reset task at midnight. """
if self . _sensor . id_ in DAILY_RESET and self . _stop_reset is not None :
self . _stop_reset ( )
await super ( ) . async_will_remove_from_hass ( )