예제 #1
0
    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
        )
예제 #2
0
        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)
예제 #3
0
    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)
예제 #4
0
    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()
예제 #5
0
    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)
예제 #6
0
    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)
예제 #7
0
        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)
예제 #8
0
    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()
예제 #9
0
 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,
         ))
예제 #10
0
        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))
예제 #11
0
    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()
예제 #12
0
    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()
예제 #13
0
    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)
예제 #14
0
    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)
예제 #15
0
파일: light.py 프로젝트: OpenPeerPower/core
    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))
예제 #16
0
        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())
예제 #17
0
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)
예제 #18
0
    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()
예제 #19
0
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()
예제 #20
0
    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)
예제 #21
0
파일: cover.py 프로젝트: OpenPeerPower/core
    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()
예제 #22
0
    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))
예제 #23
0
    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)
예제 #24
0
    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)
예제 #25
0
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
예제 #26
0
    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
            )
        )
예제 #27
0
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
예제 #28
0
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
예제 #29
0
    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,
            )