1 """Numeric derivative of data coming from a source sensor over time."""
3 from __future__
import annotations
5 from datetime
import datetime, timedelta
6 from decimal
import Decimal, DecimalException
9 import voluptuous
as vol
13 PLATFORM_SCHEMA
as SENSOR_PLATFORM_SCHEMA,
20 ATTR_UNIT_OF_MEASUREMENT,
43 _LOGGER = logging.getLogger(__name__)
45 ATTR_SOURCE_ID =
"source"
61 UnitOfTime.SECONDS: 1,
62 UnitOfTime.MINUTES: 60,
63 UnitOfTime.HOURS: 60 * 60,
64 UnitOfTime.DAYS: 24 * 60 * 60,
68 DEFAULT_TIME_WINDOW = 0
70 PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
72 vol.Optional(CONF_NAME): cv.string,
73 vol.Required(CONF_SOURCE): cv.entity_id,
74 vol.Optional(CONF_ROUND_DIGITS, default=DEFAULT_ROUND): vol.Coerce(int),
75 vol.Optional(CONF_UNIT_PREFIX, default=
None): vol.In(UNIT_PREFIXES),
76 vol.Optional(CONF_UNIT_TIME, default=UnitOfTime.HOURS): vol.In(UNIT_TIME),
77 vol.Optional(CONF_UNIT): cv.string,
78 vol.Optional(CONF_TIME_WINDOW, default=DEFAULT_TIME_WINDOW): cv.time_period,
85 config_entry: ConfigEntry,
86 async_add_entities: AddEntitiesCallback,
88 """Initialize Derivative config entry."""
89 registry = er.async_get(hass)
91 source_entity_id = er.async_validate_entity_id(
92 registry, config_entry.options[CONF_SOURCE]
100 if (unit_prefix := config_entry.options.get(CONF_UNIT_PREFIX)) ==
"none":
105 name=config_entry.title,
106 round_digits=
int(config_entry.options[CONF_ROUND_DIGITS]),
107 source_entity=source_entity_id,
108 time_window=cv.time_period_dict(config_entry.options[CONF_TIME_WINDOW]),
109 unique_id=config_entry.entry_id,
110 unit_of_measurement=
None,
111 unit_prefix=unit_prefix,
112 unit_time=config_entry.options[CONF_UNIT_TIME],
113 device_info=device_info,
122 async_add_entities: AddEntitiesCallback,
123 discovery_info: DiscoveryInfoType |
None =
None,
125 """Set up the derivative sensor."""
127 name=config.get(CONF_NAME),
128 round_digits=config[CONF_ROUND_DIGITS],
129 source_entity=config[CONF_SOURCE],
130 time_window=config[CONF_TIME_WINDOW],
131 unit_of_measurement=config.get(CONF_UNIT),
132 unit_prefix=config[CONF_UNIT_PREFIX],
133 unit_time=config[CONF_UNIT_TIME],
141 """Representation of a derivative sensor."""
143 _attr_translation_key =
"derivative"
144 _attr_should_poll =
False
152 time_window: timedelta,
153 unit_of_measurement: str |
None,
154 unit_prefix: str |
None,
155 unit_time: UnitOfTime,
156 unique_id: str |
None,
157 device_info: DeviceInfo |
None =
None,
159 """Initialize the derivative sensor."""
166 self.
_state_list_state_list: list[tuple[datetime, datetime, Decimal]] = []
168 self.
_attr_name_attr_name = name
if name
is not None else f
"{source_entity} derivative"
171 if unit_of_measurement
is None:
172 final_unit_prefix =
"" if unit_prefix
is None else unit_prefix
184 """Handle entity which will be added."""
189 restored_data.native_unit_of_measurement
193 Decimal(restored_data.native_value),
196 except SyntaxError
as err:
197 _LOGGER.warning(
"Could not restore last state: %s", err)
200 def calc_derivative(event: Event[EventStateChangedData]) ->
None:
201 """Handle the sensor state changes."""
203 (old_state := event.data[
"old_state"])
is None
204 or old_state.state
in (STATE_UNKNOWN, STATE_UNAVAILABLE)
205 or (new_state := event.data[
"new_state"])
is None
206 or new_state.state
in (STATE_UNKNOWN, STATE_UNAVAILABLE)
211 unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
213 "" if unit
is None else unit
218 (time_start, time_end, state)
219 for time_start, time_end, state
in self.
_state_list_state_list
220 if (new_state.last_updated - time_end).total_seconds()
226 new_state.last_updated - old_state.last_updated
236 except ValueError
as err:
237 _LOGGER.warning(
"While calculating derivative: %s", err)
238 except DecimalException
as err:
240 "Invalid state (%s > %s): %s", old_state.state, new_state.state, err
242 except AssertionError
as err:
243 _LOGGER.error(
"Could not calculate derivative: %s", err)
249 new_state.attributes.get(ATTR_STATE_CLASS)
250 == SensorStateClass.TOTAL_INCREASING
251 and new_derivative < 0
257 (old_state.last_updated, new_state.last_updated, new_derivative)
260 def calculate_weight(
261 start: datetime, end: datetime, now: datetime
264 if start < window_start:
265 weight = (end - window_start).total_seconds() / self.
_time_window_time_window
267 weight = (end - start).total_seconds() / self.
_time_window_time_window
273 derivative = new_derivative
276 for start, end, value
in self.
_state_list_state_list:
277 weight = calculate_weight(start, end, new_state.last_updated)
278 derivative = derivative + (value *
Decimal(weight))
_attr_extra_state_attributes
None async_added_to_hass(self)
_attr_native_unit_of_measurement
None __init__(self, *str|None name, int round_digits, str source_entity, timedelta time_window, str|None unit_of_measurement, str|None unit_prefix, UnitOfTime unit_time, str|None unique_id, DeviceInfo|None device_info=None)
SensorExtraStoredData|None async_get_last_sensor_data(self)
str|None native_unit_of_measurement(self)
None async_write_ha_state(self)
None async_on_remove(self, CALLBACK_TYPE func)
None async_setup_platform(HomeAssistant hass, ConfigType config, AddEntitiesCallback async_add_entities, DiscoveryInfoType|None discovery_info=None)
None async_setup_entry(HomeAssistant hass, ConfigEntry config_entry, AddEntitiesCallback async_add_entities)
dr.DeviceInfo|None async_device_info_to_link_from_entity(HomeAssistant hass, str entity_id_or_uuid)
CALLBACK_TYPE async_track_state_change_event(HomeAssistant hass, str|Iterable[str] entity_ids, Callable[[Event[EventStateChangedData]], Any] action, HassJobType|None job_type=None)