2019-02-13 21:21:14 +01:00
""" Utility meter from sensors providing raw data. """
2022-01-17 14:23:12 +01:00
from __future__ import annotations
2022-04-19 08:01:52 +01:00
from dataclasses import dataclass
2022-03-29 14:46:17 +02:00
from datetime import datetime , timedelta
2021-10-27 12:41:44 +01:00
from decimal import Decimal , DecimalException , InvalidOperation
2019-12-09 11:54:56 +01:00
import logging
2023-07-23 00:03:44 +02:00
from typing import Any , Self
2019-01-26 15:33:11 +00:00
2021-08-25 20:52:39 +01:00
from croniter import croniter
2020-03-11 08:42:22 +00:00
import voluptuous as vol
2021-08-25 14:03:30 +02:00
from homeassistant . components . sensor import (
2021-08-25 15:24:51 +02:00
ATTR_LAST_RESET ,
2022-04-19 08:01:52 +01:00
RestoreSensor ,
2021-12-16 06:50:41 -05:00
SensorDeviceClass ,
2022-04-19 08:01:52 +01:00
SensorExtraStoredData ,
2021-12-16 06:50:41 -05:00
SensorStateClass ,
2021-08-25 14:03:30 +02:00
)
2023-09-10 14:29:38 +01:00
from homeassistant . components . sensor . recorder import _suggest_report_issue
2022-03-29 14:46:17 +02:00
from homeassistant . config_entries import ConfigEntry
2019-01-26 15:33:11 +00:00
from homeassistant . const import (
2019-07-31 12:25:30 -07:00
ATTR_UNIT_OF_MEASUREMENT ,
2019-12-09 11:54:56 +01:00
CONF_NAME ,
2022-04-13 22:58:15 +01:00
CONF_UNIQUE_ID ,
2019-07-31 12:25:30 -07:00
STATE_UNAVAILABLE ,
2019-12-09 11:54:56 +01:00
STATE_UNKNOWN ,
2022-12-19 10:58:37 +01:00
UnitOfEnergy ,
2019-07-31 12:25:30 -07:00
)
2023-07-24 08:07:07 +02:00
from homeassistant . core import HomeAssistant , State , callback
2023-06-26 13:08:13 -03:00
from homeassistant . helpers import (
device_registry as dr ,
entity_platform ,
entity_registry as er ,
)
2023-08-11 04:04:26 +02:00
from homeassistant . helpers . device_registry import DeviceInfo
2019-12-09 11:54:56 +01:00
from homeassistant . helpers . dispatcher import async_dispatcher_connect
2022-01-17 14:23:12 +01:00
from homeassistant . helpers . entity_platform import AddEntitiesCallback
2019-01-26 15:33:11 +00:00
from homeassistant . helpers . event import (
2023-07-24 08:07:07 +02:00
EventStateChangedData ,
2021-08-25 20:52:39 +01:00
async_track_point_in_time ,
2020-07-14 19:25:12 -10:00
async_track_state_change_event ,
2019-07-31 12:25:30 -07:00
)
2023-04-08 15:36:34 +01:00
from homeassistant . helpers . start import async_at_started
2022-02-15 01:16:30 +00:00
from homeassistant . helpers . template import is_number
2023-07-24 08:07:07 +02:00
from homeassistant . helpers . typing import ConfigType , DiscoveryInfoType , EventType
2022-04-19 08:00:36 +01:00
from homeassistant . util import slugify
2019-12-09 11:54:56 +01:00
import homeassistant . util . dt as dt_util
2019-01-26 15:33:11 +00:00
from . const import (
2021-08-25 20:52:39 +01:00
ATTR_CRON_PATTERN ,
2020-03-11 08:42:22 +00:00
ATTR_VALUE ,
2020-09-15 21:22:19 +05:30
BIMONTHLY ,
2021-08-25 20:52:39 +01:00
CONF_CRON_PATTERN ,
2019-12-09 11:54:56 +01:00
CONF_METER ,
2021-10-28 17:00:31 -07:00
CONF_METER_DELTA_VALUES ,
2019-12-09 11:54:56 +01:00
CONF_METER_NET_CONSUMPTION ,
CONF_METER_OFFSET ,
2023-03-28 17:09:20 +02:00
CONF_METER_PERIODICALLY_RESETTING ,
2019-12-09 11:54:56 +01:00
CONF_METER_TYPE ,
CONF_SOURCE_SENSOR ,
CONF_TARIFF ,
CONF_TARIFF_ENTITY ,
2022-03-29 14:46:17 +02:00
CONF_TARIFFS ,
2019-12-09 11:54:56 +01:00
DAILY ,
2021-09-27 23:42:27 +01:00
DATA_TARIFF_SENSORS ,
2019-07-31 12:25:30 -07:00
DATA_UTILITY ,
HOURLY ,
MONTHLY ,
2020-11-14 11:53:59 +01:00
QUARTER_HOURLY ,
2019-12-08 19:49:18 +11:00
QUARTERLY ,
2020-03-11 08:42:22 +00:00
SERVICE_CALIBRATE_METER ,
2019-12-09 11:54:56 +01:00
SIGNAL_RESET_METER ,
WEEKLY ,
2019-07-31 12:25:30 -07:00
YEARLY ,
)
2019-01-26 15:33:11 +00:00
2021-10-27 12:41:44 +01:00
PERIOD2CRON = {
QUARTER_HOURLY : " {minute} /15 * * * * " ,
HOURLY : " {minute} * * * * " ,
DAILY : " {minute} {hour} * * * " ,
WEEKLY : " {minute} {hour} * * {day} " ,
MONTHLY : " {minute} {hour} {day} * * " ,
BIMONTHLY : " {minute} {hour} {day} */2 * " ,
QUARTERLY : " {minute} {hour} {day} */3 * " ,
YEARLY : " {minute} {hour} {day} 1/12 * " ,
}
2019-01-26 15:33:11 +00:00
_LOGGER = logging . getLogger ( __name__ )
2019-07-31 12:25:30 -07:00
ATTR_SOURCE_ID = " source "
ATTR_STATUS = " status "
ATTR_PERIOD = " meter_period "
ATTR_LAST_PERIOD = " last_period "
2023-03-28 17:09:20 +02:00
ATTR_LAST_VALID_STATE = " last_valid_state "
2019-07-31 12:25:30 -07:00
ATTR_TARIFF = " tariff "
2019-01-26 15:33:11 +00:00
2021-05-21 13:23:20 +02:00
DEVICE_CLASS_MAP = {
2022-12-19 10:58:37 +01:00
UnitOfEnergy . WATT_HOUR : SensorDeviceClass . ENERGY ,
UnitOfEnergy . KILO_WATT_HOUR : SensorDeviceClass . ENERGY ,
2021-05-21 13:23:20 +02:00
}
2019-01-26 15:33:11 +00:00
PRECISION = 3
2019-07-31 12:25:30 -07:00
PAUSED = " paused "
COLLECTING = " collecting "
2019-01-26 15:33:11 +00:00
2022-06-01 04:40:42 +01:00
def validate_is_number ( value ) :
""" Validate value is a number. """
if is_number ( value ) :
return value
raise vol . Invalid ( " Value is not a number " )
2022-03-29 14:46:17 +02:00
async def async_setup_entry (
hass : HomeAssistant ,
config_entry : ConfigEntry ,
async_add_entities : AddEntitiesCallback ,
) - > None :
""" Initialize Utility Meter config entry. """
entry_id = config_entry . entry_id
registry = er . async_get ( hass )
# Validate + resolve entity registry id to entity_id
source_entity_id = er . async_validate_entity_id (
registry , config_entry . options [ CONF_SOURCE_SENSOR ]
)
2023-06-26 13:08:13 -03:00
source_entity = registry . async_get ( source_entity_id )
dev_reg = dr . async_get ( hass )
# Resolve source entity device
if (
( source_entity is not None )
and ( source_entity . device_id is not None )
and (
(
device := dev_reg . async_get (
device_id = source_entity . device_id ,
)
)
is not None
)
) :
device_info = DeviceInfo (
identifiers = device . identifiers ,
2023-06-29 22:52:48 -03:00
connections = device . connections ,
2023-06-26 13:08:13 -03:00
)
else :
device_info = None
2022-03-29 14:46:17 +02:00
cron_pattern = None
delta_values = config_entry . options [ CONF_METER_DELTA_VALUES ]
meter_offset = timedelta ( days = config_entry . options [ CONF_METER_OFFSET ] )
meter_type = config_entry . options [ CONF_METER_TYPE ]
if meter_type == " none " :
meter_type = None
name = config_entry . title
net_consumption = config_entry . options [ CONF_METER_NET_CONSUMPTION ]
2023-03-28 17:09:20 +02:00
periodically_resetting = config_entry . options [ CONF_METER_PERIODICALLY_RESETTING ]
2022-03-29 14:46:17 +02:00
tariff_entity = hass . data [ DATA_UTILITY ] [ entry_id ] [ CONF_TARIFF_ENTITY ]
meters = [ ]
2022-03-31 13:57:26 +02:00
tariffs = config_entry . options [ CONF_TARIFFS ]
2022-03-29 14:46:17 +02:00
if not tariffs :
# Add single sensor, not gated by a tariff selector
meter_sensor = UtilityMeterSensor (
cron_pattern = cron_pattern ,
delta_values = delta_values ,
meter_offset = meter_offset ,
meter_type = meter_type ,
name = name ,
net_consumption = net_consumption ,
parent_meter = entry_id ,
2023-03-28 17:09:20 +02:00
periodically_resetting = periodically_resetting ,
2022-03-29 14:46:17 +02:00
source_entity = source_entity_id ,
tariff_entity = tariff_entity ,
tariff = None ,
unique_id = entry_id ,
2023-06-26 13:08:13 -03:00
device_info = device_info ,
2022-03-29 14:46:17 +02:00
)
meters . append ( meter_sensor )
hass . data [ DATA_UTILITY ] [ entry_id ] [ DATA_TARIFF_SENSORS ] . append ( meter_sensor )
else :
# Add sensors for each tariff
for tariff in tariffs :
meter_sensor = UtilityMeterSensor (
cron_pattern = cron_pattern ,
delta_values = delta_values ,
meter_offset = meter_offset ,
meter_type = meter_type ,
name = f " { name } { tariff } " ,
net_consumption = net_consumption ,
parent_meter = entry_id ,
2023-03-28 17:09:20 +02:00
periodically_resetting = periodically_resetting ,
2022-03-29 14:46:17 +02:00
source_entity = source_entity_id ,
tariff_entity = tariff_entity ,
tariff = tariff ,
unique_id = f " { entry_id } _ { tariff } " ,
2023-06-26 13:08:13 -03:00
device_info = device_info ,
2022-03-29 14:46:17 +02:00
)
meters . append ( meter_sensor )
hass . data [ DATA_UTILITY ] [ entry_id ] [ DATA_TARIFF_SENSORS ] . append ( meter_sensor )
async_add_entities ( meters )
platform = entity_platform . async_get_current_platform ( )
platform . async_register_entity_service (
SERVICE_CALIBRATE_METER ,
2022-06-01 04:40:42 +01:00
{ vol . Required ( ATTR_VALUE ) : validate_is_number } ,
2022-03-29 14:46:17 +02:00
" async_calibrate " ,
)
2022-01-17 14:23:12 +01:00
async def async_setup_platform (
hass : HomeAssistant ,
config : ConfigType ,
async_add_entities : AddEntitiesCallback ,
discovery_info : DiscoveryInfoType | None = None ,
) - > None :
2019-01-26 15:33:11 +00:00
""" Set up the utility meter sensor. """
if discovery_info is None :
2022-04-05 15:43:10 +02:00
_LOGGER . error (
" This platform is not available to configure "
" from ' sensor: ' in configuration.yaml "
)
2019-01-26 15:33:11 +00:00
return
meters = [ ]
2022-01-17 14:23:12 +01:00
for conf in discovery_info . values ( ) :
2019-01-26 15:33:11 +00:00
meter = conf [ CONF_METER ]
conf_meter_source = hass . data [ DATA_UTILITY ] [ meter ] [ CONF_SOURCE_SENSOR ]
2022-04-13 22:58:15 +01:00
conf_meter_unique_id = hass . data [ DATA_UTILITY ] [ meter ] . get ( CONF_UNIQUE_ID )
conf_sensor_tariff = conf . get ( CONF_TARIFF , " single_tariff " )
conf_sensor_unique_id = (
f " { conf_meter_unique_id } _ { conf_sensor_tariff } "
if conf_meter_unique_id
else None
)
2022-04-19 08:00:36 +01:00
conf_meter_name = hass . data [ DATA_UTILITY ] [ meter ] . get ( CONF_NAME , meter )
conf_sensor_tariff = conf . get ( CONF_TARIFF )
suggested_entity_id = None
if conf_sensor_tariff :
conf_sensor_name = f " { conf_meter_name } { conf_sensor_tariff } "
slug = slugify ( f " { meter } { conf_sensor_tariff } " )
suggested_entity_id = f " sensor. { slug } "
else :
conf_sensor_name = conf_meter_name
2019-01-26 15:33:11 +00:00
conf_meter_type = hass . data [ DATA_UTILITY ] [ meter ] . get ( CONF_METER_TYPE )
conf_meter_offset = hass . data [ DATA_UTILITY ] [ meter ] [ CONF_METER_OFFSET ]
2021-10-28 17:00:31 -07:00
conf_meter_delta_values = hass . data [ DATA_UTILITY ] [ meter ] [
CONF_METER_DELTA_VALUES
]
2019-07-31 12:25:30 -07:00
conf_meter_net_consumption = hass . data [ DATA_UTILITY ] [ meter ] [
CONF_METER_NET_CONSUMPTION
]
2023-03-28 17:09:20 +02:00
conf_meter_periodically_resetting = hass . data [ DATA_UTILITY ] [ meter ] [
CONF_METER_PERIODICALLY_RESETTING
]
2019-01-26 15:33:11 +00:00
conf_meter_tariff_entity = hass . data [ DATA_UTILITY ] [ meter ] . get (
2019-07-31 12:25:30 -07:00
CONF_TARIFF_ENTITY
)
2021-08-25 20:52:39 +01:00
conf_cron_pattern = hass . data [ DATA_UTILITY ] [ meter ] . get ( CONF_CRON_PATTERN )
2021-09-27 23:42:27 +01:00
meter_sensor = UtilityMeterSensor (
2022-03-29 14:46:17 +02:00
cron_pattern = conf_cron_pattern ,
delta_values = conf_meter_delta_values ,
meter_offset = conf_meter_offset ,
meter_type = conf_meter_type ,
2022-04-19 08:00:36 +01:00
name = conf_sensor_name ,
2022-03-29 14:46:17 +02:00
net_consumption = conf_meter_net_consumption ,
parent_meter = meter ,
2023-03-28 17:09:20 +02:00
periodically_resetting = conf_meter_periodically_resetting ,
2022-03-29 14:46:17 +02:00
source_entity = conf_meter_source ,
tariff_entity = conf_meter_tariff_entity ,
2022-04-13 22:58:15 +01:00
tariff = conf_sensor_tariff ,
unique_id = conf_sensor_unique_id ,
2022-04-19 08:00:36 +01:00
suggested_entity_id = suggested_entity_id ,
2019-07-31 12:25:30 -07:00
)
2021-09-27 23:42:27 +01:00
meters . append ( meter_sensor )
hass . data [ DATA_UTILITY ] [ meter ] [ DATA_TARIFF_SENSORS ] . append ( meter_sensor )
2019-01-26 15:33:11 +00:00
async_add_entities ( meters )
2021-05-03 18:34:28 +02:00
platform = entity_platform . async_get_current_platform ( )
2020-03-11 08:42:22 +00:00
platform . async_register_entity_service (
SERVICE_CALIBRATE_METER ,
2022-06-01 04:40:42 +01:00
{ vol . Required ( ATTR_VALUE ) : validate_is_number } ,
2020-03-11 08:42:22 +00:00
" async_calibrate " ,
)
2019-01-26 15:33:11 +00:00
2022-04-19 08:01:52 +01:00
@dataclass
class UtilitySensorExtraStoredData ( SensorExtraStoredData ) :
""" Object to hold extra stored data. """
last_period : Decimal
last_reset : datetime | None
2023-03-28 17:09:20 +02:00
last_valid_state : Decimal | None
2022-04-19 08:01:52 +01:00
status : str
def as_dict ( self ) - > dict [ str , Any ] :
""" Return a dict representation of the utility sensor data. """
data = super ( ) . as_dict ( )
data [ " last_period " ] = str ( self . last_period )
if isinstance ( self . last_reset , ( datetime ) ) :
data [ " last_reset " ] = self . last_reset . isoformat ( )
2023-03-28 17:09:20 +02:00
data [ " last_valid_state " ] = (
str ( self . last_valid_state ) if self . last_valid_state else None
)
2022-04-19 08:01:52 +01:00
data [ " status " ] = self . status
return data
@classmethod
2023-02-07 05:30:22 +01:00
def from_dict ( cls , restored : dict [ str , Any ] ) - > Self | None :
2022-04-19 08:01:52 +01:00
""" Initialize a stored sensor state from a dict. """
extra = SensorExtraStoredData . from_dict ( restored )
if extra is None :
return None
try :
last_period : Decimal = Decimal ( restored [ " last_period " ] )
last_reset : datetime | None = dt_util . parse_datetime ( restored [ " last_reset " ] )
2023-03-28 17:09:20 +02:00
last_valid_state : Decimal | None = (
Decimal ( restored [ " last_valid_state " ] )
if restored . get ( " last_valid_state " )
else None
)
2022-04-19 08:01:52 +01:00
status : str = restored [ " status " ]
except KeyError :
# restored is a dict, but does not have all values
return None
except InvalidOperation :
# last_period is corrupted
return None
return cls (
extra . native_value ,
extra . native_unit_of_measurement ,
last_period ,
last_reset ,
2023-03-28 17:09:20 +02:00
last_valid_state ,
2022-04-19 08:01:52 +01:00
status ,
)
class UtilityMeterSensor ( RestoreSensor ) :
2019-01-26 15:33:11 +00:00
""" Representation of an utility meter sensor. """
2023-03-31 09:34:17 +02:00
_attr_icon = " mdi:counter "
2022-08-26 21:22:27 +02:00
_attr_should_poll = False
2019-07-31 12:25:30 -07:00
def __init__ (
self ,
2022-03-29 14:46:17 +02:00
* ,
cron_pattern ,
2021-10-28 17:00:31 -07:00
delta_values ,
2022-03-29 14:46:17 +02:00
meter_offset ,
meter_type ,
name ,
2019-07-31 12:25:30 -07:00
net_consumption ,
2022-03-29 14:46:17 +02:00
parent_meter ,
2023-03-28 17:09:20 +02:00
periodically_resetting ,
2022-03-29 14:46:17 +02:00
source_entity ,
tariff_entity ,
tariff ,
unique_id ,
2022-04-19 08:00:36 +01:00
suggested_entity_id = None ,
2023-06-26 13:08:13 -03:00
device_info = None ,
2019-07-31 12:25:30 -07:00
) :
2019-01-26 15:33:11 +00:00
""" Initialize the Utility Meter sensor. """
2022-03-29 14:46:17 +02:00
self . _attr_unique_id = unique_id
2023-06-26 13:08:13 -03:00
self . _attr_device_info = device_info
2022-04-19 08:00:36 +01:00
self . entity_id = suggested_entity_id
2021-09-27 23:42:27 +01:00
self . _parent_meter = parent_meter
2019-01-26 15:33:11 +00:00
self . _sensor_source_id = source_entity
2021-09-27 23:42:27 +01:00
self . _state = None
2022-02-15 01:16:30 +00:00
self . _last_period = Decimal ( 0 )
2021-05-25 15:46:54 +02:00
self . _last_reset = dt_util . utcnow ( )
2023-03-28 17:09:20 +02:00
self . _last_valid_state = None
2019-01-26 15:33:11 +00:00
self . _collecting = None
2022-02-15 01:16:30 +00:00
self . _name = name
2019-01-26 15:33:11 +00:00
self . _unit_of_measurement = None
self . _period = meter_type
2021-10-27 12:41:44 +01:00
if meter_type is not None :
# For backwards compatibility reasons we convert the period and offset into a cron pattern
self . _cron_pattern = PERIOD2CRON [ meter_type ] . format (
minute = meter_offset . seconds % 3600 / / 60 ,
hour = meter_offset . seconds / / 3600 ,
day = meter_offset . days + 1 ,
)
_LOGGER . debug ( " CRON pattern: %s " , self . _cron_pattern )
else :
self . _cron_pattern = cron_pattern
2021-10-28 17:00:31 -07:00
self . _sensor_delta_values = delta_values
2019-02-23 09:02:39 -05:00
self . _sensor_net_consumption = net_consumption
2023-03-28 17:09:20 +02:00
self . _sensor_periodically_resetting = periodically_resetting
2019-01-26 15:33:11 +00:00
self . _tariff = tariff
self . _tariff_entity = tariff_entity
2021-09-27 23:42:27 +01:00
def start ( self , unit ) :
""" Initialize unit and state upon source initial update. """
self . _unit_of_measurement = unit
self . _state = 0
self . async_write_ha_state ( )
2023-03-28 17:09:20 +02:00
@staticmethod
def _validate_state ( state : State | None ) - > Decimal | None :
""" Parse the state as a Decimal if available. Throws DecimalException if the state is not a number. """
try :
return (
None
if state is None or state . state in [ STATE_UNAVAILABLE , STATE_UNKNOWN ]
else Decimal ( state . state )
)
except DecimalException :
return None
def calculate_adjustment (
self , old_state : State | None , new_state : State
) - > Decimal | None :
""" Calculate the adjustment based on the old and new state. """
# First check if the new_state is valid (see discussion in PR #88446)
if ( new_state_val := self . _validate_state ( new_state ) ) is None :
_LOGGER . warning ( " Invalid state %s " , new_state . state )
return None
if self . _sensor_delta_values :
return new_state_val
if (
not self . _sensor_periodically_resetting
and self . _last_valid_state is not None
) : # Fallback to old_state if sensor is periodically resetting but last_valid_state is None
return new_state_val - self . _last_valid_state
if ( old_state_val := self . _validate_state ( old_state ) ) is not None :
return new_state_val - old_state_val
2023-04-08 15:36:34 +01:00
2023-05-24 09:10:51 +02:00
_LOGGER . debug (
2023-04-08 15:36:34 +01:00
" %s received an invalid state change coming from %s ( %s > %s ) " ,
self . name ,
self . _sensor_source_id ,
2023-03-28 17:09:20 +02:00
old_state . state if old_state else None ,
new_state_val ,
)
return None
2019-01-26 15:33:11 +00:00
@callback
2023-07-24 08:07:07 +02:00
def async_reading ( self , event : EventType [ EventStateChangedData ] ) - > None :
2019-01-26 15:33:11 +00:00
""" Handle the sensor state changes. """
2023-04-10 17:37:45 +01:00
if (
source_state := self . hass . states . get ( self . _sensor_source_id )
) is None or source_state . state == STATE_UNAVAILABLE :
self . _attr_available = False
self . async_write_ha_state ( )
return
self . _attr_available = True
2023-07-24 08:07:07 +02:00
old_state = event . data [ " old_state " ]
new_state = event . data [ " new_state " ]
if new_state is None :
return
2021-09-27 23:42:27 +01:00
2023-04-08 15:36:34 +01:00
# First check if the new_state is valid (see discussion in PR #88446)
2023-03-28 17:09:20 +02:00
if ( new_state_val := self . _validate_state ( new_state ) ) is None :
2023-04-08 15:36:34 +01:00
_LOGGER . warning (
" %s received an invalid new state from %s : %s " ,
self . name ,
self . _sensor_source_id ,
new_state . state ,
)
2023-03-28 17:09:20 +02:00
return
if self . _state is None :
2021-09-27 23:42:27 +01:00
# First state update initializes the utility_meter sensors
for sensor in self . hass . data [ DATA_UTILITY ] [ self . _parent_meter ] [
DATA_TARIFF_SENSORS
] :
2023-03-28 17:09:20 +02:00
sensor . start ( new_state . attributes . get ( ATTR_UNIT_OF_MEASUREMENT ) )
2023-09-10 14:29:38 +01:00
if self . _unit_of_measurement is None :
_LOGGER . warning (
" Source sensor %s has no unit of measurement. Please %s " ,
self . _sensor_source_id ,
_suggest_report_issue ( self . hass , self . _sensor_source_id ) ,
)
2021-09-27 23:42:27 +01:00
2019-07-31 12:25:30 -07:00
if (
2023-03-28 17:09:20 +02:00
adjustment := self . calculate_adjustment ( old_state , new_state )
) is not None and ( self . _sensor_net_consumption or adjustment > = 0 ) :
# If net_consumption is off, the adjustment must be non-negative
self . _state + = adjustment # type: ignore[operator] # self._state will be set to by the start function if it is None, therefore it always has a valid Decimal value at this line
2019-01-26 15:33:11 +00:00
2023-09-10 14:29:38 +01:00
self . _unit_of_measurement = new_state . attributes . get ( ATTR_UNIT_OF_MEASUREMENT )
2023-03-28 17:09:20 +02:00
self . _last_valid_state = new_state_val
2020-04-01 14:19:51 -07:00
self . async_write_ha_state ( )
2019-01-26 15:33:11 +00:00
@callback
2023-07-24 08:07:07 +02:00
def async_tariff_change ( self , event : EventType [ EventStateChangedData ] ) - > None :
2019-01-26 15:33:11 +00:00
""" Handle tariff changes. """
2023-07-24 08:07:07 +02:00
if ( new_state := event . data [ " new_state " ] ) is None :
2020-07-14 19:25:12 -10:00
return
2021-01-13 15:42:28 +00:00
self . _change_status ( new_state . state )
2023-07-24 08:07:07 +02:00
def _change_status ( self , tariff : str ) - > None :
2021-01-13 15:42:28 +00:00
if self . _tariff == tariff :
2020-07-14 19:25:12 -10:00
self . _collecting = async_track_state_change_event (
self . hass , [ self . _sensor_source_id ] , self . async_reading
2019-07-31 12:25:30 -07:00
)
2019-01-26 15:33:11 +00:00
else :
2019-04-03 07:49:53 +01:00
if self . _collecting :
self . _collecting ( )
2019-01-26 15:33:11 +00:00
self . _collecting = None
2023-03-28 17:09:20 +02:00
# Reset the last_valid_state during state change because if the last state before the tariff change was invalid,
# there is no way to know how much "adjustment" counts for which tariff. Therefore, we set the last_valid_state
# to None and let the fallback mechanism handle the case that the old state was valid
self . _last_valid_state = None
2019-07-31 12:25:30 -07:00
_LOGGER . debug (
" %s - %s - source < %s > " ,
self . _name ,
COLLECTING if self . _collecting is not None else PAUSED ,
self . _sensor_source_id ,
)
2019-01-26 15:33:11 +00:00
2020-04-01 14:19:51 -07:00
self . async_write_ha_state ( )
2019-01-26 15:33:11 +00:00
2023-10-30 08:46:20 +00:00
async def _program_reset ( self ) :
""" Program the reset of the utility meter. """
2021-08-25 20:52:39 +01:00
if self . _cron_pattern is not None :
2023-10-30 08:46:20 +00:00
tz = dt_util . get_time_zone ( self . hass . config . time_zone )
2022-04-03 13:15:22 +02:00
self . async_on_remove (
async_track_point_in_time (
self . hass ,
self . _async_reset_meter ,
2023-10-30 08:46:20 +00:00
croniter ( self . _cron_pattern , dt_util . now ( tz ) ) . get_next (
datetime
) , # we need timezone for DST purposes (see issue #102984)
2022-04-03 13:15:22 +02:00
)
2021-08-25 20:52:39 +01:00
)
2023-10-30 08:46:20 +00:00
async def _async_reset_meter ( self , event ) :
""" Reset the utility meter status. """
await self . _program_reset ( )
2019-01-26 15:33:11 +00:00
await self . async_reset_meter ( self . _tariff_entity )
async def async_reset_meter ( self , entity_id ) :
""" Reset meter. """
if self . _tariff_entity != entity_id :
return
_LOGGER . debug ( " Reset utility meter < %s > " , self . entity_id )
2021-05-25 15:46:54 +02:00
self . _last_reset = dt_util . utcnow ( )
2022-02-15 01:16:30 +00:00
self . _last_period = Decimal ( self . _state ) if self . _state else Decimal ( 0 )
2019-01-26 15:33:11 +00:00
self . _state = 0
2020-04-03 00:34:50 -07:00
self . async_write_ha_state ( )
2019-01-26 15:33:11 +00:00
2020-03-11 08:42:22 +00:00
async def async_calibrate ( self , value ) :
""" Calibrate the Utility Meter with a given value. """
2022-06-01 04:40:42 +01:00
_LOGGER . debug ( " Calibrate %s = %s type( %s ) " , self . _name , value , type ( value ) )
self . _state = Decimal ( str ( value ) )
2020-03-11 08:42:22 +00:00
self . async_write_ha_state ( )
2019-01-26 15:33:11 +00:00
async def async_added_to_hass ( self ) :
""" Handle entity which will be added. """
await super ( ) . async_added_to_hass ( )
2023-10-30 08:46:20 +00:00
await self . _program_reset ( )
2019-01-26 15:33:11 +00:00
2022-04-03 13:15:22 +02:00
self . async_on_remove (
async_dispatcher_connect (
self . hass , SIGNAL_RESET_METER , self . async_reset_meter
)
)
2019-01-26 15:33:11 +00:00
2022-04-19 08:01:52 +01:00
if ( last_sensor_data := await self . async_get_last_sensor_data ( ) ) is not None :
# new introduced in 2022.04
self . _state = last_sensor_data . native_value
self . _unit_of_measurement = last_sensor_data . native_unit_of_measurement
self . _last_period = last_sensor_data . last_period
self . _last_reset = last_sensor_data . last_reset
2023-03-28 17:09:20 +02:00
self . _last_valid_state = last_sensor_data . last_valid_state
2022-04-19 08:01:52 +01:00
if last_sensor_data . status == COLLECTING :
# Null lambda to allow cancelling the collection on tariff change
self . _collecting = lambda : None
elif state := await self . async_get_last_state ( ) :
# legacy to be removed on 2022.10 (we are keeping this to avoid utility_meter counter losses)
2021-10-04 05:59:36 +01:00
try :
self . _state = Decimal ( state . state )
2021-10-27 12:41:44 +01:00
except InvalidOperation :
2021-10-04 05:59:36 +01:00
_LOGGER . error (
" Could not restore state < %s >. Resetting utility_meter. %s " ,
state . state ,
self . name ,
)
else :
self . _unit_of_measurement = state . attributes . get (
ATTR_UNIT_OF_MEASUREMENT
)
self . _last_period = (
2022-02-15 01:16:30 +00:00
Decimal ( state . attributes [ ATTR_LAST_PERIOD ] )
2021-10-04 05:59:36 +01:00
if state . attributes . get ( ATTR_LAST_PERIOD )
2022-02-15 01:16:30 +00:00
and is_number ( state . attributes [ ATTR_LAST_PERIOD ] )
else Decimal ( 0 )
2021-10-04 05:59:36 +01:00
)
2023-03-28 17:09:20 +02:00
self . _last_valid_state = (
Decimal ( state . attributes [ ATTR_LAST_VALID_STATE ] )
if state . attributes . get ( ATTR_LAST_VALID_STATE )
and is_number ( state . attributes [ ATTR_LAST_VALID_STATE ] )
else None
)
2021-10-04 05:59:36 +01:00
self . _last_reset = dt_util . as_utc (
dt_util . parse_datetime ( state . attributes . get ( ATTR_LAST_RESET ) )
)
if state . attributes . get ( ATTR_STATUS ) == COLLECTING :
2022-04-19 08:01:52 +01:00
# Null lambda to allow cancelling the collection on tariff change
2021-10-04 05:59:36 +01:00
self . _collecting = lambda : None
2019-01-26 15:33:11 +00:00
@callback
def async_source_tracking ( event ) :
""" Wait for source to be ready, then start meter. """
if self . _tariff_entity is not None :
2021-01-13 15:42:28 +00:00
_LOGGER . debug (
" < %s > tracks utility meter %s " , self . name , self . _tariff_entity
)
2022-04-03 13:15:22 +02:00
self . async_on_remove (
async_track_state_change_event (
self . hass , [ self . _tariff_entity ] , self . async_tariff_change
)
2019-07-31 12:25:30 -07:00
)
2019-01-26 15:33:11 +00:00
tariff_entity_state = self . hass . states . get ( self . _tariff_entity )
2022-04-01 17:28:50 +02:00
if not tariff_entity_state :
# The utility meter is not yet added
return
2021-01-13 15:42:28 +00:00
self . _change_status ( tariff_entity_state . state )
return
2019-01-26 15:33:11 +00:00
2021-09-27 23:42:27 +01:00
_LOGGER . debug (
" < %s > collecting %s from %s " ,
self . name ,
self . _unit_of_measurement ,
self . _sensor_source_id ,
)
2020-07-14 19:25:12 -10:00
self . _collecting = async_track_state_change_event (
self . hass , [ self . _sensor_source_id ] , self . async_reading
2019-07-31 12:25:30 -07:00
)
2019-01-26 15:33:11 +00:00
2023-04-08 15:36:34 +01:00
self . async_on_remove ( async_at_started ( self . hass , async_source_tracking ) )
2022-04-03 13:15:22 +02:00
async def async_will_remove_from_hass ( self ) - > None :
""" Run when entity will be removed from hass. """
if self . _collecting :
self . _collecting ( )
self . _collecting = None
2019-01-26 15:33:11 +00:00
@property
def name ( self ) :
""" Return the name of the sensor. """
return self . _name
@property
2021-08-11 18:57:50 +02:00
def native_value ( self ) :
2019-01-26 15:33:11 +00:00
""" Return the state of the sensor. """
return self . _state
2021-05-21 13:23:20 +02:00
@property
def device_class ( self ) :
""" Return the device class of the sensor. """
2022-04-04 20:02:40 +02:00
return DEVICE_CLASS_MAP . get ( self . _unit_of_measurement )
2021-05-21 13:23:20 +02:00
@property
def state_class ( self ) :
""" Return the device class of the sensor. """
2021-08-25 14:03:30 +02:00
return (
2021-12-16 06:50:41 -05:00
SensorStateClass . TOTAL
2021-08-25 14:03:30 +02:00
if self . _sensor_net_consumption
2021-12-16 06:50:41 -05:00
else SensorStateClass . TOTAL_INCREASING
2021-08-25 14:03:30 +02:00
)
2021-05-21 13:23:20 +02:00
2019-01-26 15:33:11 +00:00
@property
2021-08-11 18:57:50 +02:00
def native_unit_of_measurement ( self ) :
2019-01-26 15:33:11 +00:00
""" Return the unit the value is expressed in. """
return self . _unit_of_measurement
@property
2021-03-11 20:16:26 +01:00
def extra_state_attributes ( self ) :
2019-01-26 15:33:11 +00:00
""" Return the state attributes of the sensor. """
state_attr = {
ATTR_SOURCE_ID : self . _sensor_source_id ,
ATTR_STATUS : PAUSED if self . _collecting is None else COLLECTING ,
2022-02-15 01:16:30 +00:00
ATTR_LAST_PERIOD : str ( self . _last_period ) ,
2023-03-28 17:09:20 +02:00
ATTR_LAST_VALID_STATE : str ( self . _last_valid_state ) ,
2019-01-26 15:33:11 +00:00
}
if self . _period is not None :
state_attr [ ATTR_PERIOD ] = self . _period
2021-08-25 20:52:39 +01:00
if self . _cron_pattern is not None :
state_attr [ ATTR_CRON_PATTERN ] = self . _cron_pattern
2019-01-26 15:33:11 +00:00
if self . _tariff is not None :
state_attr [ ATTR_TARIFF ] = self . _tariff
2022-01-11 13:58:35 +01:00
# last_reset in utility meter was used before last_reset was added for long term
# statistics in base sensor. base sensor only supports last reset
# sensors with state_class set to total.
# To avoid a breaking change we set last_reset directly
# in extra state attributes.
if last_reset := self . _last_reset :
state_attr [ ATTR_LAST_RESET ] = last_reset . isoformat ( )
2019-01-26 15:33:11 +00:00
return state_attr
2022-04-19 08:01:52 +01:00
@property
def extra_restore_state_data ( self ) - > UtilitySensorExtraStoredData :
""" Return sensor specific state data to be restored. """
return UtilitySensorExtraStoredData (
self . native_value ,
self . native_unit_of_measurement ,
self . _last_period ,
self . _last_reset ,
2023-03-28 17:09:20 +02:00
self . _last_valid_state ,
2022-04-19 08:01:52 +01:00
PAUSED if self . _collecting is None else COLLECTING ,
)
async def async_get_last_sensor_data ( self ) - > UtilitySensorExtraStoredData | None :
""" Restore Utility Meter Sensor Extra Stored Data. """
if ( restored_last_extra_data := await self . async_get_last_extra_data ( ) ) is None :
return None
return UtilitySensorExtraStoredData . from_dict (
restored_last_extra_data . as_dict ( )
)