Esempio n. 1
0
def async_track_time_interval(
    opp: OpenPeerPower,
    action: Callable[..., Awaitable[None] | None],
    interval: timedelta,
) -> CALLBACK_TYPE:
    """Add a listener that fires repetitively at every timedelta interval."""
    remove = None
    interval_listener_job = None

    job = OppJob(action)

    def next_interval() -> datetime:
        """Return the next interval."""
        return dt_util.utcnow() + interval

    @callback
    def interval_listener(now: datetime) -> None:
        """Handle elapsed intervals."""
        nonlocal remove
        nonlocal interval_listener_job

        remove = async_track_point_in_utc_time(
            opp, interval_listener_job, next_interval()  # type: ignore
        )
        opp.async_run_opp_job(job, now)

    interval_listener_job = OppJob(interval_listener)
    remove = async_track_point_in_utc_time(opp, interval_listener_job, next_interval())

    def remove_listener() -> None:
        """Remove interval listener."""
        remove()  # type: ignore

    return remove_listener
Esempio n. 2
0
def _attach_trigger(
    opp: OpenPeerPower,
    config: ConfigType,
    action: AutomationActionType,
    event_type,
    automation_info: dict,
):
    trigger_id = automation_info.get("trigger_id") if automation_info else None
    job = OppJob(action)

    @callback
    def _handle_event(event: Event):
        if event.data[ATTR_ENTITY_ID] == config[CONF_ENTITY_ID]:
            opp.async_run_opp_job(
                job,
                {
                    "trigger": {
                        **config, "description": event_type,
                        "id": trigger_id
                    }
                },
                event.context,
            )

    return opp.bus.async_listen(event_type, _handle_event)
Esempio n. 3
0
    def __init__(
        self,
        opp: OpenPeerPower,
        logger: Logger,
        *,
        cooldown: float,
        immediate: bool,
        function: Callable[..., Awaitable[Any]] | None = None,
    ) -> None:
        """Initialize debounce.

        immediate: indicate if the function needs to be called right away and
                   wait <cooldown> until executing next invocation.
        function: optional and can be instantiated later.
        """
        self.opp = opp
        self.logger = logger
        self._function = function
        self.cooldown = cooldown
        self.immediate = immediate
        self._timer_task: asyncio.TimerHandle | None = None
        self._execute_at_end_of_timer: bool = False
        self._execute_lock = asyncio.Lock()
        self._job: OppJob | None = None if function is None else OppJob(
            function)
Esempio n. 4
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
    hours = config.get(CONF_HOURS)
    minutes = config.get(CONF_MINUTES)
    seconds = config.get(CONF_SECONDS)
    job = OppJob(action)

    # If larger units are specified, default the smaller units to zero
    if minutes is None and hours is not None:
        minutes = 0
    if seconds is None and minutes is not None:
        seconds = 0

    @callback
    def time_automation_listener(now):
        """Listen for time changes and calls action."""
        opp.async_run_opp_job(
            job,
            {
                "trigger": {
                    "platform": "time_pattern",
                    "now": now,
                    "description": "time pattern",
                    "id": trigger_id,
                }
            },
        )

    return async_track_time_change(opp,
                                   time_automation_listener,
                                   hour=hours,
                                   minute=minutes,
                                   second=seconds)
Esempio n. 5
0
async def async_attach_trigger(opp, config, action, automation_info):
    """Listen for tag_scanned events based on configuration."""
    trigger_id = automation_info.get("trigger_id") if automation_info else None
    tag_ids = set(config[TAG_ID])
    device_ids = set(config[DEVICE_ID]) if DEVICE_ID in config else None

    job = OppJob(action)

    async def handle_event(event):
        """Listen for tag scan events and calls the action when data matches."""
        if event.data.get(TAG_ID) not in tag_ids or (
            device_ids is not None and event.data.get(DEVICE_ID) not in device_ids
        ):
            return

        task = opp.async_run_opp_job(
            job,
            {
                "trigger": {
                    "platform": DOMAIN,
                    "event": event,
                    "description": "Tag scanned",
                    "id": trigger_id,
                }
            },
            event.context,
        )

        if task:
            await task

    return opp.bus.async_listen(EVENT_TAG_SCANNED, handle_event)
Esempio n. 6
0
async def async_attach_trigger(
    opp: OpenPeerPower,
    config: ConfigType,
    action: AutomationActionType,
    automation_info: dict,
) -> CALLBACK_TYPE:
    """Attach a trigger."""
    trigger_id = automation_info.get("trigger_id") if automation_info else None
    job = OppJob(action)

    if config[CONF_TYPE] == "turn_on":
        entity_id = config[CONF_ENTITY_ID]

        @callback
        def _handle_event(event: Event):
            if event.data[ATTR_ENTITY_ID] == entity_id:
                opp.async_run_opp_job(
                    job,
                    {
                        "trigger": {
                            **config,
                            "description": f"{DOMAIN} - {entity_id}",
                            "id": trigger_id,
                        }
                    },
                    event.context,
                )

        return opp.bus.async_listen(EVENT_TURN_ON, _handle_event)

    return lambda: None
Esempio n. 7
0
def async_track_sunset(
    opp: OpenPeerPower, action: Callable[..., None], offset: timedelta | None = None
) -> CALLBACK_TYPE:
    """Add a listener that will fire a specified offset from sunset daily."""
    listener = SunListener(opp, OppJob(action), SUN_EVENT_SUNSET, offset)
    listener.async_attach()
    return listener.async_detach
Esempio n. 8
0
def async_dispatcher_connect(opp: OpenPeerPower, signal: str,
                             target: Callable[..., Any]) -> Callable[[], None]:
    """Connect a callable function to a signal.

    This method must be run in the event loop.
    """
    if DATA_DISPATCHER not in opp.data:
        opp.data[DATA_DISPATCHER] = {}

    job = OppJob(
        catch_log_exception(
            target,
            lambda *args: "Exception in {} when dispatching '{}': {}".format(
                # Functions wrapped in partial do not have a __name__
                getattr(target, "__name__", None) or str(target),
                signal,
                args,
            ),
        ))

    opp.data[DATA_DISPATCHER].setdefault(signal, []).append(job)

    @callback
    def async_remove_dispatcher() -> None:
        """Remove signal listener."""
        try:
            opp.data[DATA_DISPATCHER][signal].remove(job)
        except (KeyError, ValueError):
            # KeyError is key target listener did not exist
            # ValueError if listener did not exist within signal
            _LOGGER.warning("Unable to remove unknown dispatcher %s", target)

    return async_remove_dispatcher
Esempio n. 9
0
def async_subscribe_connection_status(opp, connection_status_callback):
    """Subscribe to MQTT connection changes."""
    connection_status_callback_job = OppJob(connection_status_callback)

    async def connected():
        task = opp.async_run_opp_job(connection_status_callback_job, True)
        if task:
            await task

    async def disconnected():
        task = opp.async_run_opp_job(connection_status_callback_job, False)
        if task:
            await task

    subscriptions = {
        "connect":
        async_dispatcher_connect(opp, MQTT_CONNECTED, connected),
        "disconnect":
        async_dispatcher_connect(opp, MQTT_DISCONNECTED, disconnected),
    }

    @callback
    def unsubscribe():
        subscriptions["connect"]()
        subscriptions["disconnect"]()

    return unsubscribe
Esempio n. 10
0
async def async_attach_trigger(opp, config, action, automation_info):
    """Listen for events based on configuration."""
    trigger_id = automation_info.get("trigger_id") if automation_info else None
    event = config.get(CONF_EVENT)
    offset = config.get(CONF_OFFSET)
    description = event
    if offset:
        description = f"{description} with offset"
    job = OppJob(action)

    @callback
    def call_action():
        """Call action with right context."""
        opp.async_run_opp_job(
            job,
            {
                "trigger": {
                    "platform": "sun",
                    "event": event,
                    "offset": offset,
                    "description": description,
                    "id": trigger_id,
                }
            },
        )

    if event == SUN_EVENT_SUNRISE:
        return async_track_sunrise(opp, call_action, offset)
    return async_track_sunset(opp, call_action, offset)
Esempio n. 11
0
def async_track_utc_time_change(
    opp: OpenPeerPower,
    action: Callable[..., Awaitable[None] | None],
    hour: Any | None = None,
    minute: Any | None = None,
    second: Any | None = None,
    local: bool = False,
) -> CALLBACK_TYPE:
    """Add a listener that will fire if time matches a pattern."""
    job = OppJob(action)
    # We do not have to wrap the function with time pattern matching logic
    # if no pattern given
    if all(val is None for val in (hour, minute, second)):

        @callback
        def time_change_listener(event: Event) -> None:
            """Fire every time event that comes in."""
            opp.async_run_opp_job(job, event.data[ATTR_NOW])

        return opp.bus.async_listen(EVENT_TIME_CHANGED, time_change_listener)

    matching_seconds = dt_util.parse_time_expression(second, 0, 59)
    matching_minutes = dt_util.parse_time_expression(minute, 0, 59)
    matching_hours = dt_util.parse_time_expression(hour, 0, 23)

    def calculate_next(now: datetime) -> datetime:
        """Calculate and set the next time the trigger should fire."""
        localized_now = dt_util.as_local(now) if local else now
        return dt_util.find_next_time_expression_time(
            localized_now, matching_seconds, matching_minutes, matching_hours
        )

    time_listener: CALLBACK_TYPE | None = None

    @callback
    def pattern_time_change_listener(_: datetime) -> None:
        """Listen for matching time_changed events."""
        nonlocal time_listener

        now = time_tracker_utcnow()
        opp.async_run_opp_job(job, dt_util.as_local(now) if local else now)

        time_listener = async_track_point_in_utc_time(
            opp,
            pattern_time_change_listener,
            calculate_next(now + timedelta(seconds=1)),
        )

    time_listener = async_track_point_in_utc_time(
        opp, pattern_time_change_listener, calculate_next(dt_util.utcnow())
    )

    @callback
    def unsub_pattern_time_change_listener() -> None:
        """Cancel the time listener."""
        assert time_listener is not None
        time_listener()

    return unsub_pattern_time_change_listener
Esempio n. 12
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
    source = config.get(CONF_SOURCE).lower()
    zone_entity_id = config.get(CONF_ZONE)
    trigger_event = config.get(CONF_EVENT)
    job = OppJob(action)

    @callback
    def state_change_listener(event):
        """Handle specific state changes."""
        # Skip if the event's source does not match the trigger's source.
        from_state = event.data.get("old_state")
        to_state = event.data.get("new_state")
        if not source_match(from_state, source) and not source_match(
                to_state, source):
            return

        zone_state = opp.states.get(zone_entity_id)
        if zone_state is None:
            _LOGGER.warning(
                "Unable to execute automation %s: Zone %s not found",
                automation_info["name"],
                zone_entity_id,
            )
            return

        from_match = (condition.zone(opp, zone_state, from_state)
                      if from_state else False)
        to_match = condition.zone(opp, zone_state,
                                  to_state) if to_state else False

        if (trigger_event == EVENT_ENTER and not from_match and to_match or
                trigger_event == EVENT_LEAVE and from_match and not to_match):
            opp.async_run_opp_job(
                job,
                {
                    "trigger": {
                        "platform": "geo_location",
                        "source": source,
                        "entity_id": event.data.get("entity_id"),
                        "from_state": from_state,
                        "to_state": to_state,
                        "zone": zone_state,
                        "event": trigger_event,
                        "description": f"geo_location - {source}",
                        "id": trigger_id,
                    }
                },
                event.context,
            )

    return async_track_state_change_filtered(
        opp, TrackStates(False, set(), {DOMAIN}),
        state_change_listener).async_remove
Esempio n. 13
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)
Esempio n. 14
0
    def async_attach(self, action: AutomationActionType, variables: dict[str, Any]):
        """Attach a device trigger for turn on."""

        @callback
        def _remove():
            del self._actions[_remove]
            self._update()

        job = OppJob(action)

        self._actions[_remove] = (job, variables)
        self._update()

        return _remove
Esempio n. 15
0
def async_track_point_in_time(
    opp: OpenPeerPower,
    action: OppJob | Callable[..., Awaitable[None] | None],
    point_in_time: datetime,
) -> CALLBACK_TYPE:
    """Add a listener that fires once after a specific point in time."""
    job = action if isinstance(action, OppJob) else OppJob(action)

    @callback
    def utc_converter(utc_now: datetime) -> None:
        """Convert passed in UTC now to local now."""
        opp.async_run_opp_job(job, dt_util.as_local(utc_now))

    return async_track_point_in_utc_time(opp, utc_converter, point_in_time)
Esempio n. 16
0
def async_track_state_removed_domain(
    opp: OpenPeerPower,
    domains: str | Iterable[str],
    action: Callable[[Event], Any],
) -> Callable[[], None]:
    """Track state change events when an entity is removed from domains."""
    domains = _async_string_to_lower_list(domains)
    if not domains:
        return _remove_empty_listener

    domain_callbacks = opp.data.setdefault(TRACK_STATE_REMOVED_DOMAIN_CALLBACKS, {})

    if TRACK_STATE_REMOVED_DOMAIN_LISTENER not in opp.data:

        @callback
        def _async_state_change_filter(event: Event) -> bool:
            """Filter state changes by entity_id."""
            return event.data.get("new_state") is None

        @callback
        def _async_state_change_dispatcher(event: Event) -> None:
            """Dispatch state changes by entity_id."""
            if event.data.get("new_state") is not None:
                return

            _async_dispatch_domain_event(opp, event, domain_callbacks)

        opp.data[TRACK_STATE_REMOVED_DOMAIN_LISTENER] = opp.bus.async_listen(
            EVENT_STATE_CHANGED,
            _async_state_change_dispatcher,
            event_filter=_async_state_change_filter,
        )

    job = OppJob(action)

    for domain in domains:
        domain_callbacks.setdefault(domain, []).append(job)

    @callback
    def remove_listener() -> None:
        """Remove state change listener."""
        _async_remove_indexed_listeners(
            opp,
            TRACK_STATE_REMOVED_DOMAIN_CALLBACKS,
            TRACK_STATE_REMOVED_DOMAIN_LISTENER,
            domains,
            job,
        )

    return remove_listener
Esempio n. 17
0
def async_track_point_in_utc_time(
    opp: OpenPeerPower,
    action: OppJob | Callable[..., Awaitable[None] | None],
    point_in_time: datetime,
) -> CALLBACK_TYPE:
    """Add a listener that fires once after a specific point in UTC time."""
    # Ensure point_in_time is UTC
    utc_point_in_time = dt_util.as_utc(point_in_time)

    # Since this is called once, we accept a OppJob so we can avoid
    # having to figure out how to call the action every time its called.
    cancel_callback: asyncio.TimerHandle | None = None

    @callback
    def run_action(job: OppJob) -> None:
        """Call the action."""
        nonlocal cancel_callback

        now = time_tracker_utcnow()

        # Depending on the available clock support (including timer hardware
        # and the OS kernel) it can happen that we fire a little bit too early
        # as measured by utcnow(). That is bad when callbacks have assumptions
        # about the current time. Thus, we rearm the timer for the remaining
        # time.
        delta = (utc_point_in_time - now).total_seconds()
        if delta > 0:
            _LOGGER.debug("Called %f seconds too early, rearming", delta)

            cancel_callback = opp.loop.call_later(delta, run_action, job)
            return

        opp.async_run_opp_job(job, utc_point_in_time)

    job = action if isinstance(action, OppJob) else OppJob(action)
    delta = utc_point_in_time.timestamp() - time.time()
    cancel_callback = opp.loop.call_later(delta, run_action, job)

    @callback
    def unsub_point_in_time_listener() -> None:
        """Cancel the call_later."""
        assert cancel_callback is not None
        cancel_callback.cancel()

    return unsub_point_in_time_listener
Esempio n. 18
0
async def async_attach_trigger(opp, config, action, automation_info):
    """Trigger based on incoming webhooks."""
    trigger_id = automation_info.get("trigger_id") if automation_info else None
    webhook_id = config.get(CONF_WEBHOOK_ID)
    job = OppJob(action)
    opp.components.webhook.async_register(
        automation_info["domain"],
        automation_info["name"],
        webhook_id,
        partial(_handle_webhook, job, trigger_id),
    )

    @callback
    def unregister():
        """Unregister webhook."""
        opp.components.webhook.async_unregister(webhook_id)

    return unregister
Esempio n. 19
0
async def async_attach_trigger(opp, config, action, automation_info):
    """Listen for events based on configuration."""
    trigger_id = automation_info.get("trigger_id") if automation_info else None
    event = config.get(CONF_EVENT)
    job = OppJob(action)

    if event == EVENT_SHUTDOWN:

        @callback
        def opp_shutdown(event):
            """Execute when Open Peer Power is shutting down."""
            opp.async_run_opp_job(
                job,
                {
                    "trigger": {
                        "platform": "openpeerpower",
                        "event": event,
                        "description": "Open Peer Power stopping",
                        "id": trigger_id,
                    }
                },
                event.context,
            )

        return opp.bus.async_listen_once(EVENT_OPENPEERPOWER_STOP,
                                         opp_shutdown)

    # Automation are enabled while opp is starting up, fire right away
    # Check state because a config reload shouldn't trigger it.
    if automation_info["open_peer_power_start"]:
        opp.async_run_opp_job(
            job,
            {
                "trigger": {
                    "platform": "openpeerpower",
                    "event": event,
                    "description": "Open Peer Power starting",
                    "id": trigger_id,
                }
            },
        )

    return lambda: None
Esempio n. 20
0
    def __init__(
        self,
        opp: OpenPeerPower,
        track_templates: Iterable[TrackTemplate],
        action: Callable,
    ) -> None:
        """Handle removal / refresh of tracker init."""
        self.opp = opp
        self._job = OppJob(action)

        for track_template_ in track_templates:
            track_template_.template.opp = opp
        self._track_templates = track_templates

        self._last_result: dict[Template, str | TemplateError] = {}

        self._rate_limit = KeyedRateLimit(opp)
        self._info: dict[Template, RenderInfo] = {}
        self._track_state_changes: _TrackStateChangeFiltered | None = None
        self._time_listeners: dict[Template, Callable] = {}
Esempio n. 21
0
    async def async_subscribe(
        self,
        topic: str,
        msg_callback: MessageCallbackType,
        qos: int,
        encoding: str | None = None,
    ) -> Callable[[], None]:
        """Set up a subscription to a topic with the provided qos.

        This method is a coroutine.
        """
        if not isinstance(topic, str):
            raise OpenPeerPowerError("Topic needs to be a string!")

        subscription = Subscription(topic, _matcher_for_topic(topic),
                                    OppJob(msg_callback), qos, encoding)
        self.subscriptions.append(subscription)
        self._matching_subscriptions.cache_clear()

        # Only subscribe if currently connected.
        if self.connected:
            self._last_subscribe = time.time()
            await self._async_perform_subscription(topic, qos)

        @callback
        def async_remove() -> None:
            """Remove subscription."""
            if subscription not in self.subscriptions:
                raise OpenPeerPowerError("Can't remove subscription twice")
            self.subscriptions.remove(subscription)
            self._matching_subscriptions.cache_clear()

            if any(other.topic == topic for other in self.subscriptions):
                # Other subscriptions on topic remaining - don't unsubscribe.
                return

            # Only unsubscribe if currently connected.
            if self.connected:
                self.opp.async_create_task(self._async_unsubscribe(topic))

        return async_remove
Esempio n. 22
0
def async_track_entity_registry_updated_event(
    opp: OpenPeerPower,
    entity_ids: str | Iterable[str],
    action: Callable[[Event], Any],
) -> Callable[[], None]:
    """Track specific entity registry updated events indexed by entity_id.

    Similar to async_track_state_change_event.
    """
    entity_ids = _async_string_to_lower_list(entity_ids)
    if not entity_ids:
        return _remove_empty_listener

    entity_callbacks = opp.data.setdefault(TRACK_ENTITY_REGISTRY_UPDATED_CALLBACKS, {})

    if TRACK_ENTITY_REGISTRY_UPDATED_LISTENER not in opp.data:

        @callback
        def _async_entity_registry_updated_filter(event: Event) -> bool:
            """Filter entity registry updates by entity_id."""
            entity_id = event.data.get("old_entity_id", event.data["entity_id"])
            return entity_id in entity_callbacks

        @callback
        def _async_entity_registry_updated_dispatcher(event: Event) -> None:
            """Dispatch entity registry updates by entity_id."""
            entity_id = event.data.get("old_entity_id", event.data["entity_id"])

            if entity_id not in entity_callbacks:
                return

            for job in entity_callbacks[entity_id][:]:
                try:
                    opp.async_run_opp_job(job, event)
                except Exception:  # pylint: disable=broad-except
                    _LOGGER.exception(
                        "Error while processing entity registry update for %s",
                        entity_id,
                    )

        opp.data[TRACK_ENTITY_REGISTRY_UPDATED_LISTENER] = opp.bus.async_listen(
            EVENT_ENTITY_REGISTRY_UPDATED,
            _async_entity_registry_updated_dispatcher,
            event_filter=_async_entity_registry_updated_filter,
        )

    job = OppJob(action)

    for entity_id in entity_ids:
        entity_callbacks.setdefault(entity_id, []).append(job)

    @callback
    def remove_listener() -> None:
        """Remove state change listener."""
        _async_remove_indexed_listeners(
            opp,
            TRACK_ENTITY_REGISTRY_UPDATED_CALLBACKS,
            TRACK_ENTITY_REGISTRY_UPDATED_LISTENER,
            entity_ids,
            job,
        )

    return remove_listener
Esempio n. 23
0
def async_enable_report_state(opp: OpenPeerPower,
                              google_config: AbstractConfig):
    """Enable state reporting."""
    checker = None
    unsub_pending: CALLBACK_TYPE | None = None
    pending = deque([{}])

    async def report_states(now=None):
        """Report the states."""
        nonlocal pending
        nonlocal unsub_pending

        pending.append({})

        # We will report all batches except last one because those are finalized.
        while len(pending) > 1:
            await google_config.async_report_state_all(
                {"devices": {
                    "states": pending.popleft()
                }})

        # If things got queued up in last batch while we were reporting, schedule ourselves again
        if pending[0]:
            unsub_pending = async_call_later(opp, REPORT_STATE_WINDOW,
                                             report_states_job)
        else:
            unsub_pending = None

    report_states_job = OppJob(report_states)

    async def async_entity_state_listener(changed_entity, old_state,
                                          new_state):
        nonlocal unsub_pending

        if not opp.is_running:
            return

        if not new_state:
            return

        if not google_config.should_expose(new_state):
            return

        entity = GoogleEntity(opp, google_config, new_state)

        if not entity.is_supported():
            return

        try:
            entity_data = entity.query_serialize()
        except SmartHomeError as err:
            _LOGGER.debug("Not reporting state for %s: %s", changed_entity,
                          err.code)
            return

        if not checker.async_is_significant_change(new_state,
                                                   extra_arg=entity_data):
            return

        _LOGGER.debug("Scheduling report state for %s: %s", changed_entity,
                      entity_data)

        # If a significant change is already scheduled and we have another significant one,
        # let's create a new batch of changes
        if changed_entity in pending[-1]:
            pending.append({})

        pending[-1][changed_entity] = entity_data

        if unsub_pending is None:
            unsub_pending = async_call_later(opp, REPORT_STATE_WINDOW,
                                             report_states_job)

    @callback
    def extra_significant_check(
        opp: OpenPeerPower,
        old_state: str,
        old_attrs: dict,
        old_extra_arg: dict,
        new_state: str,
        new_attrs: dict,
        new_extra_arg: dict,
    ):
        """Check if the serialized data has changed."""
        return old_extra_arg != new_extra_arg

    async def inital_report(_now):
        """Report initially all states."""
        nonlocal unsub, checker
        entities = {}

        checker = await create_checker(opp, DOMAIN, extra_significant_check)

        for entity in async_get_entities(opp, google_config):
            if not entity.should_expose():
                continue

            try:
                entity_data = entity.query_serialize()
            except SmartHomeError:
                continue

            # Tell our significant change checker that we're reporting
            # So it knows with subsequent changes what was already reported.
            if not checker.async_is_significant_change(entity.state,
                                                       extra_arg=entity_data):
                continue

            entities[entity.entity_id] = entity_data

        if not entities:
            return

        await google_config.async_report_state_all(
            {"devices": {
                "states": entities
            }})

        unsub = opp.helpers.event.async_track_state_change(
            MATCH_ALL, async_entity_state_listener)

    unsub = async_call_later(opp, INITIAL_REPORT_DELAY, inital_report)

    @callback
    def unsub_all():
        unsub()
        if unsub_pending:
            unsub_pending()  # pylint: disable=not-callable

    return unsub_all
Esempio n. 24
0
 def change_listener(self, change_listener: Callable[..., Any]) -> None:
     """Update the change_listener."""
     self._change_listener = change_listener
     if (self._change_listener_job is None
             or change_listener != self._change_listener_job.target):
         self._change_listener_job = OppJob(change_listener)
Esempio n. 25
0
    def __init__(
        self,
        opp: OpenPeerPower,
        sequence: Sequence[dict[str, Any]],
        name: str,
        domain: str,
        *,
        # Used in "Running <running_description>" log message
        running_description: str | None = None,
        change_listener: Callable[..., Any] | None = None,
        script_mode: str = DEFAULT_SCRIPT_MODE,
        max_runs: int = DEFAULT_MAX,
        max_exceeded: str = DEFAULT_MAX_EXCEEDED,
        logger: logging.Logger | None = None,
        log_exceptions: bool = True,
        top_level: bool = True,
        variables: ScriptVariables | None = None,
    ) -> None:
        """Initialize the script."""
        all_scripts = opp.data.get(DATA_SCRIPTS)
        if not all_scripts:
            all_scripts = opp.data[DATA_SCRIPTS] = []
            opp.bus.async_listen_once(
                EVENT_OPENPEERPOWER_STOP,
                partial(_async_stop_scripts_at_shutdown, opp))
        self._top_level = top_level
        if top_level:
            all_scripts.append({
                "instance": self,
                "started_before_shutdown": not opp.is_stopping
            })
        if DATA_SCRIPT_BREAKPOINTS not in opp.data:
            opp.data[DATA_SCRIPT_BREAKPOINTS] = {}

        self._opp = opp
        self.sequence = sequence
        template.attach(opp, self.sequence)
        self.name = name
        self.domain = domain
        self.running_description = running_description or f"{domain} script"
        self._change_listener = change_listener
        self._change_listener_job = (None if change_listener is None else
                                     OppJob(change_listener))

        self.script_mode = script_mode
        self._set_logger(logger)
        self._log_exceptions = log_exceptions

        self.last_action = None
        self.last_triggered: datetime | None = None

        self._runs: list[_ScriptRun] = []
        self.max_runs = max_runs
        self._max_exceeded = max_exceeded
        if script_mode == SCRIPT_MODE_QUEUED:
            self._queue_lck = asyncio.Lock()
        self._config_cache: dict[set[tuple], Callable[..., bool]] = {}
        self._repeat_script: dict[int, Script] = {}
        self._choose_data: dict[int, _ChooseData] = {}
        self._referenced_entities: set[str] | None = None
        self._referenced_devices: set[str] | None = None
        self._referenced_areas: set[str] | None = None
        self.variables = variables
        self._variables_dynamic = template.is_complex(variables)
        if self._variables_dynamic:
            template.attach(opp, variables)
Esempio n. 26
0
def async_track_same_state(
    opp: OpenPeerPower,
    period: timedelta,
    action: Callable[..., Awaitable[None] | None],
    async_check_same_func: Callable[[str, State | None, State | None], bool],
    entity_ids: str | Iterable[str] = MATCH_ALL,
) -> CALLBACK_TYPE:
    """Track the state of entities for a period and run an action.

    If async_check_func is None it use the state of orig_value.
    Without entity_ids we track all state changes.
    """
    async_remove_state_for_cancel: CALLBACK_TYPE | None = None
    async_remove_state_for_listener: CALLBACK_TYPE | None = None

    job = OppJob(action)

    @callback
    def clear_listener() -> None:
        """Clear all unsub listener."""
        nonlocal async_remove_state_for_cancel, async_remove_state_for_listener

        if async_remove_state_for_listener is not None:
            async_remove_state_for_listener()
            async_remove_state_for_listener = None
        if async_remove_state_for_cancel is not None:
            async_remove_state_for_cancel()
            async_remove_state_for_cancel = None

    @callback
    def state_for_listener(now: Any) -> None:
        """Fire on state changes after a delay and calls action."""
        nonlocal async_remove_state_for_listener
        async_remove_state_for_listener = None
        clear_listener()
        opp.async_run_opp_job(job)

    @callback
    def state_for_cancel_listener(event: Event) -> None:
        """Fire on changes and cancel for listener if changed."""
        entity: str = event.data["entity_id"]
        from_state: State | None = event.data.get("old_state")
        to_state: State | None = event.data.get("new_state")

        if not async_check_same_func(entity, from_state, to_state):
            clear_listener()

    async_remove_state_for_listener = async_track_point_in_utc_time(
        opp, state_for_listener, dt_util.utcnow() + period
    )

    if entity_ids == MATCH_ALL:
        async_remove_state_for_cancel = opp.bus.async_listen(
            EVENT_STATE_CHANGED, state_for_cancel_listener
        )
    else:
        async_remove_state_for_cancel = async_track_state_change_event(
            opp,
            [entity_ids] if isinstance(entity_ids, str) else entity_ids,
            state_for_cancel_listener,
        )

    return clear_listener
Esempio n. 27
0
def async_track_template(
    opp: OpenPeerPower,
    template: Template,
    action: Callable[[str, State | None, State | None], Awaitable[None] | None],
    variables: TemplateVarsType | None = None,
) -> Callable[[], None]:
    """Add a listener that fires when a a template evaluates to 'true'.

    Listen for the result of the template becoming true, or a true-like
    string result, such as 'On', 'Open', or 'Yes'. If the template results
    in an error state when the value changes, this will be logged and not
    passed through.

    If the initial check of the template is invalid and results in an
    exception, the listener will still be registered but will only
    fire if the template result becomes true without an exception.

    Action arguments
    ----------------
    entity_id
        ID of the entity that triggered the state change.
    old_state
        The old state of the entity that changed.
    new_state
        New state of the entity that changed.

    Parameters
    ----------
    opp
        Open Peer Power object.
    template
        The template to calculate.
    action
        Callable to call with results. See above for arguments.
    variables
        Variables to pass to the template.

    Returns
    -------
    Callable to unregister the listener.

    """
    job = OppJob(action)

    @callback
    def _template_changed_listener(
        event: Event, updates: list[TrackTemplateResult]
    ) -> None:
        """Check if condition is correct and run action."""
        track_result = updates.pop()

        template = track_result.template
        last_result = track_result.last_result
        result = track_result.result

        if isinstance(result, TemplateError):
            _LOGGER.error(
                "Error while processing template: %s",
                template.template,
                exc_info=result,
            )
            return

        if (
            not isinstance(last_result, TemplateError)
            and result_as_boolean(last_result)
            or not result_as_boolean(result)
        ):
            return

        opp.async_run_opp_job(
            job,
            event and event.data.get("entity_id"),
            event and event.data.get("old_state"),
            event and event.data.get("new_state"),
        )

    info = async_track_template_result(
        opp, [TrackTemplate(template, variables)], _template_changed_listener
    )

    return info.async_remove
Esempio n. 28
0
def async_track_state_change(
    opp: OpenPeerPower,
    entity_ids: str | Iterable[str],
    action: Callable[[str, State, State], Awaitable[None] | None],
    from_state: None | str | Iterable[str] = None,
    to_state: None | str | Iterable[str] = None,
) -> CALLBACK_TYPE:
    """Track specific state changes.

    entity_ids, from_state and to_state can be string or list.
    Use list to match multiple.

    Returns a function that can be called to remove the listener.

    If entity_ids are not MATCH_ALL along with from_state and to_state
    being None, async_track_state_change_event should be used instead
    as it is slightly faster.

    Must be run within the event loop.
    """
    if from_state is not None:
        match_from_state = process_state_match(from_state)
    if to_state is not None:
        match_to_state = process_state_match(to_state)

    # Ensure it is a lowercase list with entity ids we want to match on
    if entity_ids == MATCH_ALL:
        pass
    elif isinstance(entity_ids, str):
        entity_ids = (entity_ids.lower(),)
    else:
        entity_ids = tuple(entity_id.lower() for entity_id in entity_ids)

    job = OppJob(action)

    @callback
    def state_change_filter(event: Event) -> bool:
        """Handle specific state changes."""
        if from_state is not None:
            old_state = event.data.get("old_state")
            if old_state is not None:
                old_state = old_state.state

            if not match_from_state(old_state):
                return False

        if to_state is not None:
            new_state = event.data.get("new_state")
            if new_state is not None:
                new_state = new_state.state

            if not match_to_state(new_state):
                return False

        return True

    @callback
    def state_change_dispatcher(event: Event) -> None:
        """Handle specific state changes."""
        opp.async_run_opp_job(
            job,
            event.data.get("entity_id"),
            event.data.get("old_state"),
            event.data.get("new_state"),
        )

    @callback
    def state_change_listener(event: Event) -> None:
        """Handle specific state changes."""
        if not state_change_filter(event):
            return

        state_change_dispatcher(event)

    if entity_ids != MATCH_ALL:
        # If we have a list of entity ids we use
        # async_track_state_change_event to route
        # by entity_id to avoid iterating though state change
        # events and creating a jobs where the most
        # common outcome is to return right away because
        # the entity_id does not match since usually
        # only one or two listeners want that specific
        # entity_id.
        return async_track_state_change_event(opp, entity_ids, state_change_listener)

    return opp.bus.async_listen(
        EVENT_STATE_CHANGED, state_change_dispatcher, event_filter=state_change_filter
    )
Esempio n. 29
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
Esempio n. 30
0
def async_track_state_change_event(
    opp: OpenPeerPower,
    entity_ids: str | Iterable[str],
    action: Callable[[Event], Any],
) -> Callable[[], None]:
    """Track specific state change events indexed by entity_id.

    Unlike async_track_state_change, async_track_state_change_event
    passes the full event to the callback.

    In order to avoid having to iterate a long list
    of EVENT_STATE_CHANGED and fire and create a job
    for each one, we keep a dict of entity ids that
    care about the state change events so we can
    do a fast dict lookup to route events.
    """
    entity_ids = _async_string_to_lower_list(entity_ids)
    if not entity_ids:
        return _remove_empty_listener

    entity_callbacks = opp.data.setdefault(TRACK_STATE_CHANGE_CALLBACKS, {})

    if TRACK_STATE_CHANGE_LISTENER not in opp.data:

        @callback
        def _async_state_change_filter(event: Event) -> bool:
            """Filter state changes by entity_id."""
            return event.data.get("entity_id") in entity_callbacks

        @callback
        def _async_state_change_dispatcher(event: Event) -> None:
            """Dispatch state changes by entity_id."""
            entity_id = event.data.get("entity_id")

            if entity_id not in entity_callbacks:
                return

            for job in entity_callbacks[entity_id][:]:
                try:
                    opp.async_run_opp_job(job, event)
                except Exception:  # pylint: disable=broad-except
                    _LOGGER.exception(
                        "Error while processing state change for %s", entity_id
                    )

        opp.data[TRACK_STATE_CHANGE_LISTENER] = opp.bus.async_listen(
            EVENT_STATE_CHANGED,
            _async_state_change_dispatcher,
            event_filter=_async_state_change_filter,
        )

    job = OppJob(action)

    for entity_id in entity_ids:
        entity_callbacks.setdefault(entity_id, []).append(job)

    @callback
    def remove_listener() -> None:
        """Remove state change listener."""
        _async_remove_indexed_listeners(
            opp,
            TRACK_STATE_CHANGE_CALLBACKS,
            TRACK_STATE_CHANGE_LISTENER,
            entity_ids,
            job,
        )

    return remove_listener