async def async_added_to_hass(self) -> None: """Register callbacks.""" async def async_state_changed_listener(event): """Handle child updates.""" self.async_set_context(event.context) await self.async_defer_or_update_ha_state() self.async_on_remove( async_track_state_change_event(self.hass, self._entity_ids, async_state_changed_listener)) if self.hass.state == CoreState.running: await self.async_update() return await super().async_added_to_hass()
async def wait_for_state_change_or_timeout(hass, entity_id, timeout): """Wait for an entity to change state.""" ev = asyncio.Event() @core.callback def _async_event_changed(_): ev.set() unsub = async_track_state_change_event(hass, [entity_id], _async_event_changed) try: await asyncio.wait_for(ev.wait(), timeout=STATE_CHANGE_WAIT_TIMEOUT) except asyncio.TimeoutError: pass finally: unsub()
async def async_added_to_hass(self) -> None: """Register callbacks.""" @callback def _async_state_changed_listener(event: Event | None = None) -> None: """Handle child updates.""" self.async_state_changed_listener(event) self.async_write_ha_state() self.async_on_remove( async_track_state_change_event( self.hass, [self.tracked_entity_id], _async_state_changed_listener ) ) # Call once on adding _async_state_changed_listener()
async def test_track_state_change_event_chain_multple_entity(hass): """Test that adding a new state tracker inside a tracker does not fire right away.""" tracker_called = [] chained_tracker_called = [] chained_tracker_unsub = [] tracker_unsub = [] @ha.callback def chained_single_run_callback(event): old_state = event.data.get("old_state") new_state = event.data.get("new_state") chained_tracker_called.append((old_state, new_state)) @ha.callback def single_run_callback(event): old_state = event.data.get("old_state") new_state = event.data.get("new_state") tracker_called.append((old_state, new_state)) chained_tracker_unsub.append( async_track_state_change_event(hass, ["light.bowl", "light.top"], chained_single_run_callback)) tracker_unsub.append( async_track_state_change_event(hass, ["light.bowl", "light.top"], single_run_callback)) hass.states.async_set("light.bowl", "on") hass.states.async_set("light.top", "on") await hass.async_block_till_done() assert len(tracker_called) == 2 assert len(chained_tracker_called) == 1 assert len(tracker_unsub) == 1 assert len(chained_tracker_unsub) == 2 hass.states.async_set("light.bowl", "off") await hass.async_block_till_done() assert len(tracker_called) == 3 assert len(chained_tracker_called) == 3 assert len(tracker_unsub) == 1 assert len(chained_tracker_unsub) == 3
def _change_status(self, tariff): if self._tariff == tariff: self._collecting = async_track_state_change_event( self.hass, [self._sensor_source_id], self.async_reading) else: if self._collecting: self._collecting() self._collecting = None _LOGGER.debug( "%s - %s - source <%s>", self._name, COLLECTING if self._collecting is not None else PAUSED, self._sensor_source_id, ) self.async_write_ha_state()
async def async_added_to_hass(self): """Subscribe to children and template state changes.""" @callback def _async_on_dependency_update(event): """Update ha state when dependencies update.""" self.async_set_context(event.context) self.async_schedule_update_ha_state(True) @callback def _async_on_template_update(event, updates): """Update ha state when dependencies update.""" result = updates.pop().result if isinstance(result, TemplateError): self._state_template_result = None else: self._state_template_result = result if event: self.async_set_context(event.context) self.async_schedule_update_ha_state(True) if self._state_template is not None: result = async_track_template_result( self.hass, [TrackTemplate(self._state_template, None)], _async_on_template_update, ) self.hass.bus.async_listen_once( EVENT_HOMEASSISTANT_START, callback(lambda _: result.async_refresh()) ) self.async_on_remove(result.async_remove) depend = copy(self._children) for entity in self._attrs.values(): depend.append(entity[0]) self.async_on_remove( async_track_state_change_event( self.hass, list(set(depend)), _async_on_dependency_update ) )
class ThresholdSensor(BinarySensorEntity): """Representation of a Threshold sensor.""" _attr_should_poll = False def __init__(self, hass, entity_id, name, lower, upper, hysteresis, device_class, unique_id): """Initialize the Threshold sensor.""" self._attr_unique_id = unique_id self._entity_id = entity_id self._name = name self._threshold_lower = lower self._threshold_upper = upper self._hysteresis = hysteresis self._device_class = device_class self._state_position = POSITION_UNKNOWN self._state = None self.sensor_value = None def _update_sensor_state(): """Handle sensor state changes.""" if (new_state := hass.states.get(self._entity_id)) is None: return try: self.sensor_value = (None if new_state.state in [ STATE_UNKNOWN, STATE_UNAVAILABLE ] else float(new_state.state)) except (ValueError, TypeError): self.sensor_value = None _LOGGER.warning("State is not numerical") self._update_state() @callback def async_threshold_sensor_state_listener(event): """Handle sensor state changes.""" _update_sensor_state() self.async_write_ha_state() self.async_on_remove( async_track_state_change_event( hass, [entity_id], async_threshold_sensor_state_listener)) _update_sensor_state()
async def async_update_config(self, config): """Handle when the config is updated.""" self._config = config if self._unsub_track_device is not None: self._unsub_track_device() self._unsub_track_device = None trackers = self._config[CONF_DEVICE_TRACKERS] if trackers: _LOGGER.debug("Subscribe to device trackers for %s", self.entity_id) self._unsub_track_device = async_track_state_change_event( self.hass, trackers, self._async_handle_tracker_update ) self._update_state()
async def async_added_to_hass(self): """Handle added to Hass.""" self.async_on_remove( async_track_state_change_event( self.hass, self._entity_ids, self._async_min_max_sensor_state_listener)) # Replay current state of source entities for entity_id in self._entity_ids: state = self.hass.states.get(entity_id) state_event = Event("", { "entity_id": entity_id, "new_state": state }) self._async_min_max_sensor_state_listener(state_event, update_state=False) self._calc_values()
async def async_added_to_hass(self) -> None: """Register listeners.""" for entity_id in self._entities: new_state = self.hass.states.get(entity_id) if new_state is None: continue await self.async_update_supported_features(entity_id, new_state, update_state=False) self.async_on_remove( async_track_state_change_event( self.hass, self._entities, self._update_supported_features_event)) if self.hass.state == CoreState.running: await self.async_update() return await super().async_added_to_hass()
async def async_added_to_hass(self) -> None: """Register callbacks.""" await super().async_added_to_hass() self.async_accept_signal( None, f"{SIGNAL_GROUP_MEMBERSHIP_CHANGE}_0x{self._group_id:04x}", self._handle_group_membership_changed, signal_override=True, ) self._async_unsub_state_changed = async_track_state_change_event( self.hass, self._entity_ids, self.async_state_changed_listener) def send_removed_signal(): async_dispatcher_send(self.hass, SIGNAL_GROUP_ENTITY_REMOVED, self._group_id) self.async_on_remove(send_removed_signal)
def async_on_state_subscription( entity_id: str, attribute: str | None = None ) -> None: """Subscribe and forward states for requested entities.""" async def send_home_assistant_state_event(event: Event) -> None: """Forward Home Assistant states updates to ESPHome.""" # Only communicate changes to the state or attribute tracked if ( event.data.get("old_state") is not None and "new_state" in event.data and ( ( not attribute and event.data["old_state"].state == event.data["new_state"].state ) or ( attribute and attribute in event.data["old_state"].attributes and attribute in event.data["new_state"].attributes and event.data["old_state"].attributes[attribute] == event.data["new_state"].attributes[attribute] ) ) ): return await _send_home_assistant_state( event.data["entity_id"], attribute, event.data.get("new_state") ) unsub = async_track_state_change_event( hass, [entity_id], send_home_assistant_state_event ) entry_data.disconnect_callbacks.append(unsub) # Send initial state hass.async_create_task( _send_home_assistant_state(entity_id, attribute, hass.states.get(entity_id)) )
async def async_added_to_hass(self) -> None: """Register callbacks.""" @callback def _async_state_changed_listener(event: Event | None = None) -> None: """Handle child updates.""" self.async_state_changed_listener(event) self.async_write_ha_state() self.async_on_remove( async_track_state_change_event(self.hass, [self._switch_entity_id], _async_state_changed_listener)) # Call once on adding _async_state_changed_listener() # Add this entity to the wrapped switch's device registry = er.async_get(self.hass) if registry.async_get(self.entity_id) is not None: registry.async_update_entity(self.entity_id, device_id=self._device_id)
def async_tariff_change(self, event): """Handle tariff changes.""" new_state = event.data.get("new_state") if new_state is None: return if self._tariff == new_state.state: self._collecting = async_track_state_change_event( self.hass, [self._sensor_source_id], self.async_reading) else: if self._collecting: self._collecting() self._collecting = None _LOGGER.debug( "%s - %s - source <%s>", self._name, COLLECTING if self._collecting is not None else PAUSED, self._sensor_source_id, ) self.async_write_ha_state()
async def async_added_to_hass(self): """Complete device setup after being added to hass.""" @callback def trend_sensor_state_listener(event): """Handle state changes on the observed device.""" new_state = event.data.get("new_state") if new_state is None: return try: if self._attribute: state = new_state.attributes.get(self._attribute) else: state = new_state.state if state not in (STATE_UNKNOWN, STATE_UNAVAILABLE): sample = (new_state.last_updated.timestamp(), float(state)) self.samples.append(sample) self.async_schedule_update_ha_state(True) except (ValueError, TypeError) as ex: _LOGGER.error(ex) self.async_on_remove( async_track_state_change_event(self.hass, [self._entity_id], trend_sensor_state_listener))
async def async_attach_trigger(hass, config, action, automation_info, *, platform_type="numeric_state") -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" entity_ids = config.get(CONF_ENTITY_ID) below = config.get(CONF_BELOW) above = config.get(CONF_ABOVE) time_delta = config.get(CONF_FOR) template.attach(hass, time_delta) value_template = config.get(CONF_VALUE_TEMPLATE) unsub_track_same = {} entities_triggered = set() period: dict = {} attribute = config.get(CONF_ATTRIBUTE) job = HassJob(action) if value_template is not None: value_template.hass = hass def variables(entity_id): """Return a dict with trigger variables.""" return { "trigger": { "platform": "numeric_state", "entity_id": entity_id, "below": below, "above": above, "attribute": attribute, } } @callback def check_numeric_state(entity_id, from_s, to_s): """Return True if criteria are now met.""" if to_s is None: return False return condition.async_numeric_state(hass, to_s, below, above, value_template, variables(entity_id), attribute) @callback def state_automation_listener(event): """Listen for state changes and calls action.""" entity_id = event.data.get("entity_id") from_s = event.data.get("old_state") to_s = event.data.get("new_state") @callback def call_action(): """Call action with right context.""" hass.async_run_hass_job( job, { "trigger": { "platform": platform_type, "entity_id": entity_id, "below": below, "above": above, "from_state": from_s, "to_state": to_s, "for": time_delta if not time_delta else period[entity_id], "description": f"numeric state of {entity_id}", } }, to_s.context, ) matching = check_numeric_state(entity_id, from_s, to_s) if not matching: entities_triggered.discard(entity_id) elif entity_id not in entities_triggered: entities_triggered.add(entity_id) if time_delta: try: period[entity_id] = cv.positive_time_period( template.render_complex(time_delta, variables(entity_id))) except (exceptions.TemplateError, vol.Invalid) as ex: _LOGGER.error( "Error rendering '%s' for template: %s", automation_info["name"], ex, ) entities_triggered.discard(entity_id) return unsub_track_same[entity_id] = async_track_same_state( hass, period[entity_id], call_action, entity_ids=entity_id, async_check_same_func=check_numeric_state, ) else: call_action() unsub = async_track_state_change_event(hass, entity_ids, state_automation_listener) @callback def async_remove(): """Remove state listeners async.""" unsub() for async_remove in unsub_track_same.values(): async_remove() unsub_track_same.clear() return async_remove
zone_entity_id, ) return from_match = condition.zone(hass, zone_state, from_s) if from_s else False to_match = condition.zone(hass, zone_state, to_s) if to_s else False if (event == EVENT_ENTER and not from_match and to_match or event == EVENT_LEAVE and from_match and not to_match): description = f"{entity} {_EVENT_DESCRIPTION[event]} {zone_state.attributes[ATTR_FRIENDLY_NAME]}" hass.async_run_hass_job( job, { "trigger": { **trigger_data, "platform": platform_type, "entity_id": entity, "from_state": from_s, "to_state": to_s, "zone": zone_state, "event": event, "description": description, } }, to_s.context, ) return async_track_state_change_event(hass, entity_id, zone_automation_listener)
async def async_added_to_hass(self): """Handle entity which will be added.""" await super().async_added_to_hass() state = await self.async_get_last_state() if state: try: self._state = Decimal(state.state) except (DecimalException, ValueError) as err: _LOGGER.warning("Could not restore last state: %s", err) else: self._attr_device_class = state.attributes.get(ATTR_DEVICE_CLASS) self._unit_of_measurement = state.attributes.get( ATTR_UNIT_OF_MEASUREMENT ) @callback def calc_integration(event): """Handle the sensor state changes.""" old_state = event.data.get("old_state") new_state = event.data.get("new_state") if self._unit_of_measurement is None: unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) self._unit_of_measurement = self._unit_template.format( "" if unit is None else unit ) if ( self.device_class is None and new_state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER ): self._attr_device_class = DEVICE_CLASS_ENERGY if ( old_state is None or old_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE) or new_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE) ): return try: # integration as the Riemann integral of previous measures. area = 0 elapsed_time = ( new_state.last_updated - old_state.last_updated ).total_seconds() if self._method == TRAPEZOIDAL_METHOD: area = ( (Decimal(new_state.state) + Decimal(old_state.state)) * Decimal(elapsed_time) / 2 ) elif self._method == LEFT_METHOD: area = Decimal(old_state.state) * Decimal(elapsed_time) elif self._method == RIGHT_METHOD: area = Decimal(new_state.state) * Decimal(elapsed_time) integral = area / (self._unit_prefix * self._unit_time) assert isinstance(integral, Decimal) except ValueError as err: _LOGGER.warning("While calculating integration: %s", err) except DecimalException as err: _LOGGER.warning( "Invalid state (%s > %s): %s", old_state.state, new_state.state, err ) except AssertionError as err: _LOGGER.error("Could not calculate integral: %s", err) else: self._state += integral self.async_write_ha_state() async_track_state_change_event( self.hass, [self._sensor_source_id], calc_integration )
class SensorTrend(BinarySensorEntity): """Representation of a trend Sensor.""" def __init__( self, hass, device_id, friendly_name, entity_id, attribute, device_class, invert, max_samples, min_gradient, sample_duration, ): """Initialize the sensor.""" self._hass = hass self.entity_id = generate_entity_id(ENTITY_ID_FORMAT, device_id, hass=hass) self._name = friendly_name self._entity_id = entity_id self._attribute = attribute self._device_class = device_class self._invert = invert self._sample_duration = sample_duration self._min_gradient = min_gradient self._gradient = None self._state = None self.samples = deque(maxlen=max_samples) @property def name(self): """Return the name of the sensor.""" return self._name @property def is_on(self): """Return true if sensor is on.""" return self._state @property def device_class(self): """Return the sensor class of the sensor.""" return self._device_class @property def extra_state_attributes(self): """Return the state attributes of the sensor.""" return { ATTR_ENTITY_ID: self._entity_id, ATTR_FRIENDLY_NAME: self._name, ATTR_GRADIENT: self._gradient, ATTR_INVERT: self._invert, ATTR_MIN_GRADIENT: self._min_gradient, ATTR_SAMPLE_COUNT: len(self.samples), ATTR_SAMPLE_DURATION: self._sample_duration, } @property def should_poll(self): """No polling needed.""" return False async def async_added_to_hass(self): """Complete device setup after being added to hass.""" @callback def trend_sensor_state_listener(event): """Handle state changes on the observed device.""" if (new_state := event.data.get("new_state")) is None: return try: if self._attribute: state = new_state.attributes.get(self._attribute) else: state = new_state.state if state not in (STATE_UNKNOWN, STATE_UNAVAILABLE): sample = (new_state.last_updated.timestamp(), float(state)) self.samples.append(sample) self.async_schedule_update_ha_state(True) except (ValueError, TypeError) as ex: _LOGGER.error(ex) self.async_on_remove( async_track_state_change_event(self.hass, [self._entity_id], trend_sensor_state_listener))
self._tilts[KEY_POSITION].discard(entity_id) if update_state: self.async_defer_or_update_ha_state() async def async_added_to_hass(self) -> None: """Register listeners.""" for entity_id in self._entities: if (new_state := self.hass.states.get(entity_id)) is None: continue self.async_update_supported_features( entity_id, new_state, update_state=False ) self.async_on_remove( async_track_state_change_event( self.hass, self._entities, self._update_supported_features_event ) ) await super().async_added_to_hass() async def async_open_cover(self, **kwargs: Any) -> None: """Move the covers up.""" data = {ATTR_ENTITY_ID: self._covers[KEY_OPEN_CLOSE]} await self.hass.services.async_call( DOMAIN, SERVICE_OPEN_COVER, data, blocking=True, context=self._context ) async def async_close_cover(self, **kwargs: Any) -> None: """Move the covers down.""" data = {ATTR_ENTITY_ID: self._covers[KEY_OPEN_CLOSE]}
async def async_added_to_hass(self): """Run when entity about to be added.""" await super().async_added_to_hass() # Add listener self.async_on_remove( async_track_state_change_event( self.hass, self.sensor_entity_id, self._async_sensor_changed)) if self._hvac_mode == HVAC_MODE_HEAT: self.async_on_remove( async_track_state_change_event( self.hass, self.heaters_entity_ids, self._async_switch_changed)) elif self._hvac_mode == HVAC_MODE_COOL: self.async_on_remove( async_track_state_change_event( self.hass, self.coolers_entity_ids, self._async_switch_changed)) self.async_on_remove( async_track_state_change_event( self.hass, self.target_entity_id, self._async_target_changed)) if self._related_climate is not None: for _related_entity in self._related_climate: self.async_on_remove( async_track_state_change_event( self.hass, _related_entity, self._async_switch_changed)) @callback def _async_startup(event): """Init on startup.""" sensor_state = self._getStateSafe(self.sensor_entity_id) if sensor_state and sensor_state != STATE_UNKNOWN: self._async_update_temp(sensor_state) target_state = self._getStateSafe(self.target_entity_id) if target_state and \ target_state != STATE_UNKNOWN and \ self._hvac_mode != HVAC_MODE_HEAT_COOL: self._async_update_program_temp(target_state) self.hass.bus.async_listen_once( EVENT_HOMEASSISTANT_START, _async_startup) # Check If we have an old state old_state = await self.async_get_last_state() _LOGGER.info("climate.%s old state: %s", self._name, old_state) if old_state is not None: # If we have no initial temperature, restore if self._target_temp is None: # If we have a previously saved temperature if old_state.attributes.get(ATTR_TEMPERATURE) is None: target_entity_state = self._getStateSafe(self.target_entity_id) if target_entity_state is not None: self._target_temp = float(target_entity_state) else: self._target_temp = float((self._min_temp + self._max_temp)/2) _LOGGER.warning("climate.%s - Undefined target temperature," "falling back to %s", self._name , self._target_temp) else: self._target_temp = float( old_state.attributes[ATTR_TEMPERATURE]) if (self._initial_hvac_mode is None and old_state.state is not None): self._hvac_mode = \ old_state.state self._enabled = self._hvac_mode != HVAC_MODE_OFF else: # No previous state, try and restore defaults if self._target_temp is None: self._target_temp = float((self._min_temp + self._max_temp)/2) _LOGGER.warning("climate.%s - No previously saved temperature, setting to %s", self._name, self._target_temp) # Set default state to off if not self._hvac_mode: self._hvac_mode = HVAC_MODE_OFF
async def async_added_to_hass(self): """Run when entity about to be added.""" await super().async_added_to_hass() # Add listener self.async_on_remove( async_track_state_change_event(self.hass, [self.sensor_entity_id], self._async_sensor_changed)) self.async_on_remove( async_track_state_change_event(self.hass, [self.heater_entity_id], self._async_switch_changed)) if self._keep_alive: self.async_on_remove( async_track_time_interval(self.hass, self._async_control_heating, self._keep_alive)) @callback def _async_startup(event): """Init on startup.""" sensor_state = self.hass.states.get(self.sensor_entity_id) if sensor_state and sensor_state.state not in ( STATE_UNAVAILABLE, STATE_UNKNOWN, ): self._async_update_temp(sensor_state) self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _async_startup) # Check If we have an old state old_state = await self.async_get_last_state() if old_state is not None: # If we have no initial temperature, restore if self._target_temp is None: # If we have a previously saved temperature if old_state.attributes.get(ATTR_TEMPERATURE) is None: if self.ac_mode: self._target_temp = self.max_temp else: self._target_temp = self.min_temp _LOGGER.warning( "Undefined target temperature, falling back to %s", self._target_temp, ) else: self._target_temp = float( old_state.attributes[ATTR_TEMPERATURE]) if old_state.attributes.get(ATTR_PRESET_MODE) == PRESET_AWAY: self._is_away = True if not self._hvac_mode and old_state.state: self._hvac_mode = old_state.state else: # No previous state, try and restore defaults if self._target_temp is None: if self.ac_mode: self._target_temp = self.max_temp else: self._target_temp = self.min_temp _LOGGER.warning("No previously saved temperature, setting to %s", self._target_temp) # Set default state to off if not self._hvac_mode: self._hvac_mode = HVAC_MODE_OFF
async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" entities = {} removes = [] @callback def time_automation_listener(description, now): """Listen for time changes and calls action.""" hass.async_run_job( action, { "trigger": { "platform": "time", "now": now, "description": description } }, ) @callback def update_entity_trigger_event(event): """update_entity_trigger from the event.""" return update_entity_trigger(event.data["entity_id"], event.data["new_state"]) @callback def update_entity_trigger(entity_id, new_state=None): """Update the entity trigger for the entity_id.""" # If a listener was already set up for entity, remove it. remove = entities.get(entity_id) if remove: remove() removes.remove(remove) remove = None # Check state of entity. If valid, set up a listener. if new_state: has_date = new_state.attributes["has_date"] if has_date: year = new_state.attributes["year"] month = new_state.attributes["month"] day = new_state.attributes["day"] has_time = new_state.attributes["has_time"] if has_time: hour = new_state.attributes["hour"] minute = new_state.attributes["minute"] second = new_state.attributes["second"] else: # If no time then use midnight. hour = minute = second = 0 if has_date: # If input_datetime has date, then track point in time. trigger_dt = dt_util.DEFAULT_TIME_ZONE.localize( datetime(year, month, day, hour, minute, second)) # Only set up listener if time is now or in the future. if trigger_dt >= dt_util.now(): remove = async_track_point_in_time( hass, partial(time_automation_listener, f"time set in {entity_id}"), trigger_dt, ) elif has_time: # Else if it has time, then track time change. remove = async_track_time_change( hass, partial(time_automation_listener, f"time set in {entity_id}"), hour=hour, minute=minute, second=second, ) # Was a listener set up? if remove: removes.append(remove) entities[entity_id] = remove for at_time in config[CONF_AT]: if isinstance(at_time, str): # input_datetime entity update_entity_trigger(at_time, new_state=hass.states.get(at_time)) else: # datetime.time removes.append( async_track_time_change( hass, partial(time_automation_listener, "time"), hour=at_time.hour, minute=at_time.minute, second=at_time.second, )) # Track state changes of any entities. removes.append( async_track_state_change_event(hass, list(entities), update_entity_trigger_event)) @callback def remove_track_time_changes(): """Remove tracked time changes.""" for remove in removes: remove() return remove_track_time_changes
class IntegrationSensor(RestoreEntity, SensorEntity): """Representation of an integration sensor.""" def __init__( self, source_entity, name, round_digits, unit_prefix, unit_time, unit_of_measurement, integration_method, ): """Initialize the integration sensor.""" self._sensor_source_id = source_entity self._round_digits = round_digits self._state = None self._method = integration_method self._name = name if name is not None else f"{source_entity} integral" self._unit_template = ( f"{'' if unit_prefix is None else unit_prefix}{{}}{unit_time}") self._unit_of_measurement = unit_of_measurement self._unit_prefix = UNIT_PREFIXES[unit_prefix] self._unit_time = UNIT_TIME[unit_time] self._attr_state_class = SensorStateClass.TOTAL async def async_added_to_hass(self): """Handle entity which will be added.""" await super().async_added_to_hass() if state := await self.async_get_last_state(): try: self._state = Decimal(state.state) except (DecimalException, ValueError) as err: _LOGGER.warning("Could not restore last state: %s", err) else: self._attr_device_class = state.attributes.get( ATTR_DEVICE_CLASS) if self._unit_of_measurement is None: self._unit_of_measurement = state.attributes.get( ATTR_UNIT_OF_MEASUREMENT) @callback def calc_integration(event): """Handle the sensor state changes.""" old_state = event.data.get("old_state") new_state = event.data.get("new_state") if self._unit_of_measurement is None: unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) self._unit_of_measurement = self._unit_template.format( "" if unit is None else unit) if (self.device_class is None and new_state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER): self._attr_device_class = SensorDeviceClass.ENERGY if (old_state is None or new_state is None or old_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE) or new_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE)): return try: # integration as the Riemann integral of previous measures. area = 0 elapsed_time = (new_state.last_updated - old_state.last_updated).total_seconds() if self._method == TRAPEZOIDAL_METHOD: area = ( (Decimal(new_state.state) + Decimal(old_state.state)) * Decimal(elapsed_time) / 2) elif self._method == LEFT_METHOD: area = Decimal(old_state.state) * Decimal(elapsed_time) elif self._method == RIGHT_METHOD: area = Decimal(new_state.state) * Decimal(elapsed_time) integral = area / (self._unit_prefix * self._unit_time) assert isinstance(integral, Decimal) except ValueError as err: _LOGGER.warning("While calculating integration: %s", err) except DecimalException as err: _LOGGER.warning("Invalid state (%s > %s): %s", old_state.state, new_state.state, err) except AssertionError as err: _LOGGER.error("Could not calculate integral: %s", err) else: if isinstance(self._state, Decimal): self._state += integral else: self._state = integral self.async_write_ha_state() async_track_state_change_event(self.hass, [self._sensor_source_id], calc_integration)
async def set_hass(self, hass: HomeAssistant): self.hass = hass async_track_state_change_event(hass, self.switches, self.external_switch_changed)
async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" trigger_data = automation_info.get("trigger_data", {}) if automation_info else {} entities = {} removes = [] job = HassJob(action) @callback def time_automation_listener(description, now, *, entity_id=None): """Listen for time changes and calls action.""" hass.async_run_hass_job( job, { "trigger": { **trigger_data, "platform": "time", "now": now, "description": description, "entity_id": entity_id, } }, ) @callback def update_entity_trigger_event(event): """update_entity_trigger from the event.""" return update_entity_trigger(event.data["entity_id"], event.data["new_state"]) @callback def update_entity_trigger(entity_id, new_state=None): """Update the entity trigger for the entity_id.""" # If a listener was already set up for entity, remove it. remove = entities.pop(entity_id, None) if remove: remove() remove = None if not new_state: return # Check state of entity. If valid, set up a listener. if new_state.domain == "input_datetime": has_date = new_state.attributes["has_date"] if has_date: year = new_state.attributes["year"] month = new_state.attributes["month"] day = new_state.attributes["day"] has_time = new_state.attributes["has_time"] if has_time: hour = new_state.attributes["hour"] minute = new_state.attributes["minute"] second = new_state.attributes["second"] else: # If no time then use midnight. hour = minute = second = 0 if has_date: # If input_datetime has date, then track point in time. trigger_dt = datetime( year, month, day, hour, minute, second, tzinfo=dt_util.DEFAULT_TIME_ZONE, ) # Only set up listener if time is now or in the future. if trigger_dt >= dt_util.now(): remove = async_track_point_in_time( hass, partial( time_automation_listener, f"time set in {entity_id}", entity_id=entity_id, ), trigger_dt, ) elif has_time: # Else if it has time, then track time change. remove = async_track_time_change( hass, partial( time_automation_listener, f"time set in {entity_id}", entity_id=entity_id, ), hour=hour, minute=minute, second=second, ) elif (new_state.domain == "sensor" and new_state.attributes.get(ATTR_DEVICE_CLASS) == sensor.DEVICE_CLASS_TIMESTAMP and new_state.state not in (STATE_UNAVAILABLE, STATE_UNKNOWN)): trigger_dt = dt_util.parse_datetime(new_state.state) if trigger_dt is not None and trigger_dt > dt_util.utcnow(): remove = async_track_point_in_time( hass, partial( time_automation_listener, f"time set in {entity_id}", entity_id=entity_id, ), trigger_dt, ) # Was a listener set up? if remove: entities[entity_id] = remove to_track = [] for at_time in config[CONF_AT]: if isinstance(at_time, str): # entity to_track.append(at_time) update_entity_trigger(at_time, new_state=hass.states.get(at_time)) else: # datetime.time removes.append( async_track_time_change( hass, partial(time_automation_listener, "time"), hour=at_time.hour, minute=at_time.minute, second=at_time.second, )) # Track state changes of any entities. removes.append( async_track_state_change_event(hass, to_track, update_entity_trigger_event)) @callback def remove_track_time_changes(): """Remove tracked time changes.""" for remove in entities.values(): remove() for remove in removes: remove() return remove_track_time_changes
async def async_added_to_hass(self) -> None: """Register state listeners.""" async_track_state_change_event(self.hass, self._entities, self.on_state_change)
async def async_added_to_hass(self): """Handle entity which will be added.""" await super().async_added_to_hass() state = await self.async_get_last_state() if state is not None: try: self._state = Decimal(state.state) except SyntaxError as err: _LOGGER.warning("Could not restore last state: %s", err) @callback def calc_derivative(event): """Handle the sensor state changes.""" old_state = event.data.get("old_state") new_state = event.data.get("new_state") if (old_state is None or old_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE) or new_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE)): return now = new_state.last_updated # Filter out the tuples that are older than (and outside of the) `time_window` self._state_list = [ (timestamp, state) for timestamp, state in self._state_list if (now - timestamp).total_seconds() < self._time_window ] # It can happen that the list is now empty, in that case # we use the old_state, because we cannot do anything better. if len(self._state_list) == 0: self._state_list.append( (old_state.last_updated, old_state.state)) self._state_list.append((new_state.last_updated, new_state.state)) if self._unit_of_measurement is None: unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) self._unit_of_measurement = self._unit_template.format( "" if unit is None else unit) try: # derivative of previous measures. last_time, last_value = self._state_list[-1] first_time, first_value = self._state_list[0] elapsed_time = (last_time - first_time).total_seconds() delta_value = Decimal(last_value) - Decimal(first_value) derivative = (delta_value / Decimal(elapsed_time) / Decimal(self._unit_prefix) * Decimal(self._unit_time)) assert isinstance(derivative, Decimal) except ValueError as err: _LOGGER.warning("While calculating derivative: %s", err) except DecimalException as err: _LOGGER.warning("Invalid state (%s > %s): %s", old_state.state, new_state.state, err) except AssertionError as err: _LOGGER.error("Could not calculate derivative: %s", err) else: self._state = derivative self.async_write_ha_state() async_track_state_change_event(self.hass, [self._sensor_source_id], calc_derivative)
async def async_added_to_hass(self): """Register callbacks.""" if "recorder" in self.hass.config.components: history_list = [] largest_window_items = 0 largest_window_time = timedelta(0) # Determine the largest window_size by type for filt in self._filters: if ( filt.window_unit == WINDOW_SIZE_UNIT_NUMBER_EVENTS and largest_window_items < filt.window_size ): largest_window_items = filt.window_size elif ( filt.window_unit == WINDOW_SIZE_UNIT_TIME and largest_window_time < filt.window_size ): largest_window_time = filt.window_size # Retrieve the largest window_size of each type if largest_window_items > 0: filter_history = await self.hass.async_add_executor_job( partial( history.get_last_state_changes, self.hass, largest_window_items, entity_id=self._entity, ) ) if self._entity in filter_history: history_list.extend(filter_history[self._entity]) if largest_window_time > timedelta(seconds=0): start = dt_util.utcnow() - largest_window_time filter_history = await self.hass.async_add_executor_job( partial( history.state_changes_during_period, self.hass, start, entity_id=self._entity, ) ) if self._entity in filter_history: history_list.extend( [ state for state in filter_history[self._entity] if state not in history_list ] ) # Sort the window states history_list = sorted(history_list, key=lambda s: s.last_updated) _LOGGER.debug( "Loading from history: %s", [(s.state, s.last_updated) for s in history_list], ) # Replay history through the filter chain for state in history_list: if state.state not in [STATE_UNKNOWN, STATE_UNAVAILABLE, None]: self._update_filter_sensor_state(state, False) self.async_on_remove( async_track_state_change_event( self.hass, [self._entity], self._update_filter_sensor_state_event ) )
class IntegrationSensor(RestoreEntity, SensorEntity): """Representation of an integration sensor.""" def __init__( self, *, integration_method: str, name: str | None, round_digits: int, source_entity: str, unique_id: str | None, unit_prefix: str | None, unit_time: str, ) -> None: """Initialize the integration sensor.""" self._attr_unique_id = unique_id self._sensor_source_id = source_entity self._round_digits = round_digits self._state = None self._method = integration_method self._attr_name = name if name is not None else f"{source_entity} integral" self._unit_template = f"{'' if unit_prefix is None else unit_prefix}{{}}" self._unit_of_measurement = None self._unit_prefix = UNIT_PREFIXES[unit_prefix] self._unit_time = UNIT_TIME[unit_time] self._unit_time_str = unit_time self._attr_state_class = SensorStateClass.TOTAL self._attr_icon = "mdi:chart-histogram" self._attr_should_poll = False self._attr_extra_state_attributes = {ATTR_SOURCE_ID: source_entity} def _unit(self, source_unit: str) -> str: """Derive unit from the source sensor, SI prefix and time unit.""" unit_time = self._unit_time_str if source_unit.endswith(f"/{unit_time}"): integral_unit = source_unit[0:(-(1 + len(unit_time)))] else: integral_unit = f"{source_unit}{unit_time}" return self._unit_template.format(integral_unit) async def async_added_to_hass(self): """Handle entity which will be added.""" await super().async_added_to_hass() if state := await self.async_get_last_state(): try: self._state = Decimal(state.state) except (DecimalException, ValueError) as err: _LOGGER.warning( "%s could not restore last state %s: %s", self.entity_id, state.state, err, ) else: self._attr_device_class = state.attributes.get( ATTR_DEVICE_CLASS) if self._unit_of_measurement is None: self._unit_of_measurement = state.attributes.get( ATTR_UNIT_OF_MEASUREMENT) @callback def calc_integration(event): """Handle the sensor state changes.""" old_state = event.data.get("old_state") new_state = event.data.get("new_state") if new_state is None or new_state.state in ( STATE_UNKNOWN, STATE_UNAVAILABLE, ): return # We may want to update our state before an early return, # based on the source sensor's unit_of_measurement # or device_class. update_state = False unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) if unit is not None: new_unit_of_measurement = self._unit(unit) if self._unit_of_measurement != new_unit_of_measurement: self._unit_of_measurement = new_unit_of_measurement update_state = True if (self.device_class is None and new_state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER): self._attr_device_class = SensorDeviceClass.ENERGY update_state = True if update_state: self.async_write_ha_state() if old_state is None or old_state.state in ( STATE_UNKNOWN, STATE_UNAVAILABLE, ): return try: # integration as the Riemann integral of previous measures. area = 0 elapsed_time = (new_state.last_updated - old_state.last_updated).total_seconds() if self._method == METHOD_TRAPEZOIDAL: area = ( (Decimal(new_state.state) + Decimal(old_state.state)) * Decimal(elapsed_time) / 2) elif self._method == METHOD_LEFT: area = Decimal(old_state.state) * Decimal(elapsed_time) elif self._method == METHOD_RIGHT: area = Decimal(new_state.state) * Decimal(elapsed_time) integral = area / (self._unit_prefix * self._unit_time) assert isinstance(integral, Decimal) except ValueError as err: _LOGGER.warning("While calculating integration: %s", err) except DecimalException as err: _LOGGER.warning("Invalid state (%s > %s): %s", old_state.state, new_state.state, err) except AssertionError as err: _LOGGER.error("Could not calculate integral: %s", err) else: if isinstance(self._state, Decimal): self._state += integral else: self._state = integral self.async_write_ha_state() self.async_on_remove( async_track_state_change_event(self.hass, [self._sensor_source_id], calc_integration))