def __init__(self, opp, entity_id, name, lower, upper, hysteresis, device_class): """Initialize the Threshold sensor.""" self._opp = opp 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 @callback def async_threshold_sensor_state_listener(event): """Handle sensor state changes.""" new_state = event.data.get("new_state") if new_state is None: return try: self.sensor_value = ( None if new_state.state == STATE_UNKNOWN else float(new_state.state) ) except (ValueError, TypeError): self.sensor_value = None _LOGGER.warning("State is not numerical") self._update_state() self.async_write_op_state() async_track_state_change_event( opp, [entity_id], async_threshold_sensor_state_listener )
def mold_indicator_startup(event): """Add listeners and get 1st state.""" _LOGGER.debug("Startup for %s", self.entity_id) async_track_state_change_event( self.opp, list(self._entities), mold_indicator_sensors_state_listener) # Read initial state indoor_temp = self.opp.states.get(self._indoor_temp_sensor) outdoor_temp = self.opp.states.get(self._outdoor_temp_sensor) indoor_hum = self.opp.states.get(self._indoor_humidity_sensor) schedule_update = self._update_sensor(self._indoor_temp_sensor, None, indoor_temp) schedule_update = (False if not self._update_sensor( self._outdoor_temp_sensor, None, outdoor_temp) else schedule_update) schedule_update = (False if not self._update_sensor( self._indoor_humidity_sensor, None, indoor_hum) else schedule_update) if schedule_update: self.async_schedule_update_op_state(True)
async def async_added_to_opp(self): """Handle entity which will be added.""" await super().async_added_to_opp() state = await self.async_get_last_state() if state: try: self._state = Decimal(state.state) except ValueError as err: _LOGGER.warning("Could not restore last state: %s", err) @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 (old_state is None or old_state.state in [STATE_UNKNOWN, STATE_UNAVAILABLE] or new_state.state in [STATE_UNKNOWN, STATE_UNAVAILABLE]): return 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: # 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_op_state() async_track_state_change_event(self.opp, [self._sensor_source_id], calc_integration)
async def run(self): """Handle accessory driver started event. Run inside the Open Peer Power event loop. """ if self.linked_humidity_sensor: async_track_state_change_event( self.opp, [self.linked_humidity_sensor], self.async_update_current_humidity_event, ) await super().run()
async def async_added_to_opp(self): """After being added to opp, load from history.""" if ENABLE_LOAD_HISTORY and "recorder" in self.opp.config.components: # only use the database if it's configured await self.opp.async_add_executor_job(self._load_history_from_db) self.async_write_op_state() async_track_state_change_event(self.opp, list(self._sensormap), self._state_changed_event) for entity_id in self._sensormap: state = self.opp.states.get(entity_id) if state is not None: self.state_changed(entity_id, state)
def __init__( self, opp, entity_id, name, watched_entity_id, state, repeat, skip_first, message_template, done_message_template, notifiers, can_ack, title_template, data, ): """Initialize the alert.""" self.opp = opp self._name = name self._alert_state = state self._skip_first = skip_first self._data = data self._message_template = message_template if self._message_template is not None: self._message_template.opp = opp self._done_message_template = done_message_template if self._done_message_template is not None: self._done_message_template.opp = opp self._title_template = title_template if self._title_template is not None: self._title_template.opp = opp self._notifiers = notifiers self._can_ack = can_ack self._delay = [timedelta(minutes=val) for val in repeat] self._next_delay = 0 self._firing = False self._ack = False self._cancel = None self._send_done_message = False self.entity_id = f"{DOMAIN}.{entity_id}" event.async_track_state_change_event(opp, [watched_entity_id], self.watched_entity_change)
def async_source_tracking(event): """Wait for source to be ready, then start meter.""" if self._tariff_entity is not None: _LOGGER.debug("<%s> tracks utility meter %s", self.name, self._tariff_entity) async_track_state_change_event(self.opp, [self._tariff_entity], self.async_tariff_change) tariff_entity_state = self.opp.states.get(self._tariff_entity) self._change_status(tariff_entity_state.state) return _LOGGER.debug("<%s> collecting from %s", self.name, self._sensor_source_id) self._collecting = async_track_state_change_event( self.opp, [self._sensor_source_id], self.async_reading)
async def async_added_to_opp(self): """Handle added to Opp.""" self.async_on_remove( async_track_state_change_event( self.opp, self._entity_ids, self._async_min_max_sensor_state_listener)) self._calc_values()
async def async_added_to_opp(self): """Handle added to Opp.""" self.async_on_remove( async_track_state_change_event( self.opp, [self._source_entity_id], self._async_compensation_sensor_state_listener, ))
def start_refresh(*args): """Register state tracking.""" @callback def force_refresh(*args): """Force the component to refresh.""" self.async_schedule_update_op_state(True) force_refresh() self.async_on_remove( async_track_state_change_event(self.opp, [self._entity_id], force_refresh))
async def run(self): """Handle accessory driver started event. Run inside the Open Peer Power event loop. """ if self._char_motion_detected: async_track_state_change_event( self.opp, [self.linked_motion_sensor], self._async_update_motion_state_event, ) if self._char_doorbell_detected: async_track_state_change_event( self.opp, [self.linked_doorbell_sensor], self._async_update_doorbell_state_event, ) await super().run()
def _async_start_tracking(self): """Start tracking members. This method must be run in the event loop. """ if self.trackable and self._async_unsub_state_changed is None: self._async_unsub_state_changed = async_track_state_change_event( self.opp, self.trackable, self._async_state_changed_listener ) self._async_update_group_state()
async def async_added_to_opp(self): """Subscribe to MQTT events.""" async_track_state_change_event(self.opp, [self.entity_id], self._async_state_changed_listener) async def message_received(msg): """Run when new MQTT message has been received.""" if msg.payload == self._payload_disarm: await self.async_alarm_disarm(self._code) elif msg.payload == self._payload_arm_home: await self.async_alarm_arm_home(self._code) elif msg.payload == self._payload_arm_away: await self.async_alarm_arm_away(self._code) elif msg.payload == self._payload_arm_night: await self.async_alarm_arm_night(self._code) else: _LOGGER.warning("Received unexpected payload: %s", msg.payload) return await mqtt.async_subscribe(self.opp, self._command_topic, message_received, self._qos)
async def run(self): """Handle accessory driver started event.""" state = self.opp.states.get(self.entity_id) self.async_update_state_callback(state) self._subscriptions.append( async_track_state_change_event( self.opp, [self.entity_id], self.async_update_event_state_callback)) battery_charging_state = None battery_state = None if self.linked_battery_sensor: linked_battery_sensor_state = self.opp.states.get( self.linked_battery_sensor) battery_state = linked_battery_sensor_state.state battery_charging_state = linked_battery_sensor_state.attributes.get( ATTR_BATTERY_CHARGING) self._subscriptions.append( async_track_state_change_event( self.opp, [self.linked_battery_sensor], self.async_update_linked_battery_callback, )) elif state is not None: battery_state = state.attributes.get(ATTR_BATTERY_LEVEL) if self.linked_battery_charging_sensor: state = self.opp.states.get(self.linked_battery_charging_sensor) battery_charging_state = state and state.state == STATE_ON self._subscriptions.append( async_track_state_change_event( self.opp, [self.linked_battery_charging_sensor], self.async_update_linked_battery_charging_callback, )) elif battery_charging_state is None and state is not None: battery_charging_state = state.attributes.get( ATTR_BATTERY_CHARGING) if battery_state is not None or battery_charging_state is not None: self.async_update_battery(battery_state, battery_charging_state)
async def async_added_to_opp(self) -> None: """Register callbacks.""" self._switch_state = self.opp.states.get(self._switch_entity_id) @callback def async_state_changed_listener(*_: Any) -> None: """Handle child updates.""" self._switch_state = self.opp.states.get(self._switch_entity_id) self.async_write_op_state() self.async_on_remove( async_track_state_change_event(self.opp, [self._switch_entity_id], async_state_changed_listener))
def async_stats_sensor_startup(_): """Add listener and get recorded state.""" _LOGGER.debug("Startup for %s", self.entity_id) self.async_on_remove( async_track_state_change_event( self.opp, [self._entity_id], async_stats_sensor_state_listener ) ) if "recorder" in self.opp.config.components: # Only use the database if it's configured self.opp.async_create_task(self._async_initialize_from_database())
async def async_attach_trigger(opp, config, action, automation_info, *, platform_type: str = "zone") -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" trigger_id = automation_info.get("trigger_id") if automation_info else None entity_id = config.get(CONF_ENTITY_ID) zone_entity_id = config.get(CONF_ZONE) event = config.get(CONF_EVENT) job = OppJob(action) @callback def zone_automation_listener(zone_event): """Listen for state changes and calls action.""" entity = zone_event.data.get("entity_id") from_s = zone_event.data.get("old_state") to_s = zone_event.data.get("new_state") if (from_s and not location.has_location(from_s) or not location.has_location(to_s)): return zone_state = opp.states.get(zone_entity_id) from_match = condition.zone(opp, zone_state, from_s) if from_s else False to_match = condition.zone(opp, 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]}" opp.async_run_opp_job( job, { "trigger": { "platform": platform_type, "entity_id": entity, "from_state": from_s, "to_state": to_s, "zone": zone_state, "event": event, "description": description, "id": trigger_id, } }, to_s.context, ) return async_track_state_change_event(opp, entity_id, zone_automation_listener)
def _change_status(self, tariff): if self._tariff == tariff: self._collecting = async_track_state_change_event( self.opp, [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_op_state()
async def wait_for_state_change_or_timeout(opp, 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(opp, [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_opp(self) -> None: """Register callbacks.""" await super().async_added_to_opp() 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.opp, self._entity_ids, self.async_state_changed_listener) def send_removed_signal(): async_dispatcher_send(self.opp, SIGNAL_GROUP_ENTITY_REMOVED, self._group_id) self.async_on_remove(send_removed_signal)
async def async_added_to_opp(self): """Register listeners.""" for entity_id in self._entities: new_state = self.opp.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.opp, self._entities, self._update_supported_features_event ) ) if self.opp.state == CoreState.running: await self.async_update() return await super().async_added_to_opp()
async def async_added_to_opp(self): """Complete device setup after being added to opp.""" @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_op_state(True) except (ValueError, TypeError) as ex: _LOGGER.error(ex) self.async_on_remove( async_track_state_change_event(self.opp, [self._entity_id], trend_sensor_state_listener))
async def async_added_to_opp(self): """ Call when entity about to be added. All relevant update logic for instance attributes occurs within this closure. Other methods in this class are designed to avoid directly modifying instance attributes, by instead focusing on returning relevant data back to this method. The goal of this method is to ensure that `self.current_observations` and `self.probability` are set on a best-effort basis when this entity is register with opp. In addition, this method must register the state listener defined within, which will be called any time a relevant entity changes its state. """ @callback def async_threshold_sensor_state_listener(event): """ Handle sensor state changes. When a state changes, we must update our list of current observations, then calculate the new probability. """ new_state = event.data.get("new_state") if new_state is None or new_state.state == STATE_UNKNOWN: return entity = event.data.get("entity_id") self.current_observations.update( self._record_entity_observations(entity)) self.async_set_context(event.context) self._recalculate_and_write_state() self.async_on_remove( async_track_state_change_event( self.opp, list(self.observations_by_entity), async_threshold_sensor_state_listener, )) @callback def _async_template_result_changed(event, updates): track_template_result = updates.pop() template = track_template_result.template result = track_template_result.result entity = event and event.data.get("entity_id") if isinstance(result, TemplateError): _LOGGER.error( "TemplateError('%s') " "while processing template '%s' " "in entity '%s'", result, template, self.entity_id, ) should_trigger = False else: should_trigger = result_as_boolean(result) for obs in self.observations_by_template[template]: if should_trigger: obs_entry = {"entity_id": entity, **obs} else: obs_entry = None self.current_observations[obs["id"]] = obs_entry if event: self.async_set_context(event.context) self._recalculate_and_write_state() for template in self.observations_by_template: info = async_track_template_result( self.opp, [TrackTemplate(template, None)], _async_template_result_changed, ) self._callbacks.append(info) self.async_on_remove(info.async_remove) info.async_refresh() self.current_observations.update( self._initialize_current_observations()) self.probability = self._calculate_new_probability() self._deviation = bool(self.probability >= self._probability_threshold)
async def async_added_to_opp(self): """Handle entity which will be added.""" await super().async_added_to_opp() 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_op_state() async_track_state_change_event(self.opp, [self._sensor_source_id], calc_derivative)
async def async_attach_trigger( opp, 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(opp, time_delta) value_template = config.get(CONF_VALUE_TEMPLATE) unsub_track_same = {} armed_entities = set() period: dict = {} attribute = config.get(CONF_ATTRIBUTE) job = OppJob(action) trigger_id = automation_info.get("trigger_id") if automation_info else None _variables = {} if automation_info: _variables = automation_info.get("variables") or {} if value_template is not None: value_template.opp = opp def variables(entity_id): """Return a dict with trigger variables.""" trigger_info = { "trigger": { "platform": "numeric_state", "entity_id": entity_id, "below": below, "above": above, "attribute": attribute, } } return {**_variables, **trigger_info} @callback def check_numeric_state(entity_id, from_s, to_s): """Return whether the criteria are met, raise ConditionError if unknown.""" return condition.async_numeric_state( opp, to_s, below, above, value_template, variables(entity_id), attribute ) # Each entity that starts outside the range is already armed (ready to fire). for entity_id in entity_ids: try: if not check_numeric_state(entity_id, None, entity_id): armed_entities.add(entity_id) except exceptions.ConditionError as ex: _LOGGER.warning( "Error initializing '%s' trigger: %s", automation_info["name"], ex, ) @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.""" opp.async_run_opp_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}", "id": trigger_id, } }, to_s.context, ) @callback def check_numeric_state_no_raise(entity_id, from_s, to_s): """Return True if the criteria are now met, False otherwise.""" try: return check_numeric_state(entity_id, from_s, to_s) except exceptions.ConditionError: # This is an internal same-state listener so we just drop the # error. The same error will be reached and logged by the # primary async_track_state_change_event() listener. return False try: matching = check_numeric_state(entity_id, from_s, to_s) except exceptions.ConditionError as ex: _LOGGER.warning("Error in '%s' trigger: %s", automation_info["name"], ex) return if not matching: armed_entities.add(entity_id) elif entity_id in armed_entities: armed_entities.discard(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, ) return unsub_track_same[entity_id] = async_track_same_state( opp, period[entity_id], call_action, entity_ids=entity_id, async_check_same_func=check_numeric_state_no_raise, ) else: call_action() unsub = async_track_state_change_event(opp, 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
async def async_added_to_opp(self): """Register callbacks.""" if "recorder" in self.opp.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.opp.async_add_executor_job( partial( history.get_last_state_changes, self.opp, 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.opp.async_add_executor_job( partial( history.state_changes_during_period, self.opp, 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.opp, [self._entity], self._update_filter_sensor_state_event ) )
async def async_attach_trigger( opp: OpenPeerPower, config, action, automation_info, *, platform_type: str = "state", ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" entity_id = config.get(CONF_ENTITY_ID) from_state = config.get(CONF_FROM, MATCH_ALL) to_state = config.get(CONF_TO, MATCH_ALL) time_delta = config.get(CONF_FOR) template.attach(opp, time_delta) match_all = from_state == MATCH_ALL and to_state == MATCH_ALL unsub_track_same = {} period: dict[str, timedelta] = {} match_from_state = process_state_match(from_state) match_to_state = process_state_match(to_state) attribute = config.get(CONF_ATTRIBUTE) job = OppJob(action) trigger_id = automation_info.get("trigger_id") if automation_info else None _variables = {} if automation_info: _variables = automation_info.get("variables") or {} @callback def state_automation_listener(event: Event): """Listen for state changes and calls action.""" entity: str = event.data["entity_id"] from_s: State | None = event.data.get("old_state") to_s: State | None = event.data.get("new_state") if from_s is None: old_value = None elif attribute is None: old_value = from_s.state else: old_value = from_s.attributes.get(attribute) if to_s is None: new_value = None elif attribute is None: new_value = to_s.state else: new_value = to_s.attributes.get(attribute) # When we listen for state changes with `match_all`, we # will trigger even if just an attribute changes. When # we listen to just an attribute, we should ignore all # other attribute changes. if attribute is not None and old_value == new_value: return if ( not match_from_state(old_value) or not match_to_state(new_value) or (not match_all and old_value == new_value) ): return @callback def call_action(): """Call action with right context.""" opp.async_run_opp_job( job, { "trigger": { "platform": platform_type, "entity_id": entity, "from_state": from_s, "to_state": to_s, "for": time_delta if not time_delta else period[entity], "attribute": attribute, "description": f"state of {entity}", "id": trigger_id, } }, event.context, ) if not time_delta: call_action() return trigger_info = { "trigger": { "platform": "state", "entity_id": entity, "from_state": from_s, "to_state": to_s, } } variables = {**_variables, **trigger_info} try: period[entity] = cv.positive_time_period( template.render_complex(time_delta, variables) ) except (exceptions.TemplateError, vol.Invalid) as ex: _LOGGER.error( "Error rendering '%s' for template: %s", automation_info["name"], ex ) return def _check_same_state(_, _2, new_st: State): if new_st is None: return False if attribute is None: cur_value = new_st.state else: cur_value = new_st.attributes.get(attribute) if CONF_FROM in config and CONF_TO not in config: return cur_value != old_value return cur_value == new_value unsub_track_same[entity] = async_track_same_state( opp, period[entity], call_action, _check_same_state, entity_ids=entity, ) unsub = async_track_state_change_event(opp, entity_id, 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
async def async_attach_trigger(opp, config, action, automation_info): """Listen for state changes based on configuration.""" trigger_id = automation_info.get("trigger_id") if automation_info else None entities = {} removes = [] job = OppJob(action) @callback def time_automation_listener(description, now, *, entity_id=None): """Listen for time changes and calls action.""" opp.async_run_opp_job( job, { "trigger": { "platform": "time", "now": now, "description": description, "entity_id": entity_id, "id": trigger_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( opp, 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( opp, 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( opp, 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=opp.states.get(at_time)) else: # datetime.time removes.append( async_track_time_change( opp, 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(opp, 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_opp(self): """Run when entity about to be added.""" await super().async_added_to_opp() # Add listener self.async_on_remove( async_track_state_change_event(self.opp, [self.sensor_entity_id], self._async_sensor_changed)) self.async_on_remove( async_track_state_change_event(self.opp, [self.heater_entity_id], self._async_switch_changed)) if self._keep_alive: self.async_on_remove( async_track_time_interval(self.opp, self._async_control_heating, self._keep_alive)) @callback def _async_startup(*_): """Init on startup.""" sensor_state = self.opp.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.async_write_op_state() if self.opp.state == CoreState.running: _async_startup() else: self.opp.bus.async_listen_once(EVENT_OPENPEERPOWER_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 # Prevent the device from keep running if HVAC_MODE_OFF if self._hvac_mode == HVAC_MODE_OFF and self._is_device_active: await self._async_heater_turn_off() _LOGGER.warning( "The climate mode is OFF, but the switch device is ON. Turning off device %s", self.heater_entity_id, )