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

    job = HassJob(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(
            hass, interval_listener_job, next_interval()  # type: ignore
        )
        hass.async_run_hass_job(job, now)

    interval_listener_job = HassJob(interval_listener)
    remove = async_track_point_in_utc_time(hass, interval_listener_job, next_interval())

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

    return remove_listener
Example #2
0
async def async_attach_trigger(hass, config, action, automation_info):
    """Listen for tag_scanned events based on configuration."""
    tag_ids = set(config[TAG_ID])
    device_ids = set(config[DEVICE_ID]) if DEVICE_ID in config else None

    job = HassJob(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 = hass.async_run_hass_job(
            job,
            {
                "trigger": {
                    "platform": DOMAIN,
                    "event": event,
                    "description": "Tag scanned",
                }
            },
            event.context,
        )

        if task:
            await task

    return hass.bus.async_listen(EVENT_TAG_SCANNED, handle_event)
Example #3
0
def _attach_trigger(
    hass: HomeAssistant,
    config: ConfigType,
    action: TriggerActionType,
    event_type,
    trigger_info: TriggerInfo,
):
    trigger_data = trigger_info["trigger_data"]
    job = HassJob(action)

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

    return hass.bus.async_listen(event_type, _handle_event)
Example #4
0
def async_track_point_in_utc_time(
    hass: HomeAssistant,
    action: Union[HassJob, Callable[..., 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 HassJob so we can avoid
    # having to figure out how to call the action every time its called.
    job = action if isinstance(action, HassJob) else HassJob(action)

    cancel_callback = hass.loop.call_at(
        hass.loop.time() + point_in_time.timestamp() - time.time(),
        hass.async_run_hass_job,
        job,
        utc_point_in_time,
    )

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

    return unsub_point_in_time_listener
    def __init__(
        self,
        hass: HomeAssistant,
        logger: Logger,
        *,
        cooldown: float,
        immediate: bool,
        function: Callable[..., Awaitable[Any]] | 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.hass = hass
        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: HassJob | None = None if function is None else HassJob(
            function)
Example #6
0
async def async_attach_trigger(
    hass: HomeAssistant,
    config: ConfigType,
    action: AutomationActionType,
    automation_info: dict,
) -> CALLBACK_TYPE:
    """Attach a trigger."""
    trigger_data = automation_info.get("trigger_data", {}) if automation_info else {}
    job = HassJob(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:
                hass.async_run_hass_job(
                    job,
                    {
                        "trigger": {
                            **trigger_data,
                            **config,
                            "description": f"{DOMAIN} - {entity_id}",
                        }
                    },
                    event.context,
                )

        return hass.bus.async_listen(EVENT_TURN_ON, _handle_event)

    return lambda: None
Example #7
0
def _attach_trigger(
    hass: HomeAssistant,
    config: ConfigType,
    action: AutomationActionType,
    event_type,
    automation_info: dict,
):
    trigger_data = automation_info.get("trigger_data",
                                       {}) if automation_info else {}
    job = HassJob(action)

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

    return hass.bus.async_listen(event_type, _handle_event)
Example #8
0
File: start.py Project: jbouwh/core
def async_at_start(
    hass: HomeAssistant,
    at_start_cb: Callable[[HomeAssistant], Coroutine[Any, Any, None] | None],
) -> CALLBACK_TYPE:
    """Execute something when Home Assistant is started.

    Will execute it now if Home Assistant is already started.
    """
    at_start_job = HassJob(at_start_cb)
    if hass.is_running:
        hass.async_run_hass_job(at_start_job, hass)
        return lambda: None

    unsub: None | CALLBACK_TYPE = None

    @callback
    def _matched_event(event: Event) -> None:
        """Call the callback when Home Assistant started."""
        hass.async_run_hass_job(at_start_job, hass)
        nonlocal unsub
        unsub = None

    @callback
    def cancel() -> None:
        if unsub:
            unsub()

    unsub = hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START,
                                       _matched_event)
    return cancel
async def async_attach_trigger(hass, config, action, automation_info):
    """Listen for state changes based on configuration."""
    trigger_data = automation_info["trigger_data"]
    hours = config.get(CONF_HOURS)
    minutes = config.get(CONF_MINUTES)
    seconds = config.get(CONF_SECONDS)
    job = HassJob(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."""
        hass.async_run_hass_job(
            job,
            {
                "trigger": {
                    **trigger_data,
                    "platform": "time_pattern",
                    "now": now,
                    "description": "time pattern",
                }
            },
        )

    return async_track_time_change(hass,
                                   time_automation_listener,
                                   hour=hours,
                                   minute=minutes,
                                   second=seconds)
Example #10
0
def async_track_state_change_event(
    hass: HomeAssistant,
    entity_ids: Union[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 = hass.data.setdefault(TRACK_STATE_CHANGE_CALLBACKS, {})

    if TRACK_STATE_CHANGE_LISTENER not in hass.data:

        @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:
                    hass.async_run_hass_job(job, event)
                except Exception:  # pylint: disable=broad-except
                    _LOGGER.exception(
                        "Error while processing state changed for %s",
                        entity_id)

        hass.data[TRACK_STATE_CHANGE_LISTENER] = hass.bus.async_listen(
            EVENT_STATE_CHANGED, _async_state_change_dispatcher)

    job = HassJob(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(
            hass,
            TRACK_STATE_CHANGE_CALLBACKS,
            TRACK_STATE_CHANGE_LISTENER,
            entity_ids,
            job,
        )

    return remove_listener
Example #11
0
 def __init__(self, device: 'MerossDevice', id: object):
     super().__init__(device, id, DEVICE_CLASS_SHUTTER)
     self._position_native = None  # as reported by the device
     self._signalOpen: int = 30000  # msec to fully open (config'd on device)
     self._signalClose: int = 30000  # msec to fully close (config'd on device)
     self._position_timed: int = 50  # estimated based on timings
     self._position_start = None  # set when when we're controlling a timed position
     self._position_starttime = None  # epoch of transition start
     self._position_endtime = None  # epoch of 'target position reached'
     self._transition_unsub = None
     self._transition_job = HassJob(
         self._transition_callback)  # job to follow transition
     self._stop_unsub = None
     self._stop_job = HassJob(
         self._stop_callback)  # job to terminate transition
     self._attr_extra_state_attributes = dict()
Example #12
0
async def async_attach_trigger(
    hass, config, action, automation_info, *, platform_type: str = "zone"
) -> CALLBACK_TYPE:
    """Listen for state changes based on configuration."""
    trigger_data = automation_info["trigger_data"]
    entity_id = config.get(CONF_ENTITY_ID)
    zone_entity_id = config.get(CONF_ZONE)
    event = config.get(CONF_EVENT)
    job = HassJob(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

        if not (zone_state := hass.states.get(zone_entity_id)):
            _LOGGER.warning(
                "Automation '%s' is referencing non-existing zone '%s' in a zone trigger",
                automation_info["name"],
                zone_entity_id,
            )
            return

        from_match = condition.zone(hass, zone_state, from_s) if from_s else False
        to_match = condition.zone(hass, zone_state, to_s) if to_s else False

        if (
            event == EVENT_ENTER
            and not from_match
            and to_match
            or event == EVENT_LEAVE
            and from_match
            and not to_match
        ):
            description = f"{entity} {_EVENT_DESCRIPTION[event]} {zone_state.attributes[ATTR_FRIENDLY_NAME]}"
            hass.async_run_hass_job(
                job,
                {
                    "trigger": {
                        **trigger_data,
                        "platform": platform_type,
                        "entity_id": entity,
                        "from_state": from_s,
                        "to_state": to_s,
                        "zone": zone_state,
                        "event": event,
                        "description": description,
                    }
                },
                to_s.context,
            )
Example #13
0
async def async_attach_trigger(hass, config, action, automation_info):
    """Listen for events based on configuration."""
    event = config.get(CONF_EVENT)
    offset = config.get(CONF_OFFSET)
    description = event
    if offset:
        description = f"{description} with offset"
    job = HassJob(action)

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

    if event == SUN_EVENT_SUNRISE:
        return async_track_sunrise(hass, call_action, offset)
    return async_track_sunset(hass, call_action, offset)
Example #14
0
def async_subscribe_connection_status(
    hass: HomeAssistant, connection_status_callback: ConnectionStatusCallback
) -> Callable[[], None]:
    """Subscribe to MQTT connection changes."""
    connection_status_callback_job = HassJob(connection_status_callback)

    async def connected():
        task = hass.async_run_hass_job(connection_status_callback_job, True)
        if task:
            await task

    async def disconnected():
        task = hass.async_run_hass_job(connection_status_callback_job, False)
        if task:
            await task

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

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

    return unsubscribe
Example #15
0
async def async_attach_trigger(
    hass: HomeAssistant,
    config: ConfigType,
    action: TriggerActionType,
    trigger_info: TriggerInfo,
) -> CALLBACK_TYPE:
    """Attach a trigger."""
    trigger_data = trigger_info["trigger_data"]
    job = HassJob(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:
                hass.async_run_hass_job(
                    job,
                    {
                        "trigger": {
                            **trigger_data,  # type: ignore[arg-type]  # https://github.com/python/mypy/issues/9117
                            **config,
                            "description": f"{DOMAIN} - {entity_id}",
                        }
                    },
                    event.context,
                )

        return hass.bus.async_listen(EVENT_TURN_ON, _handle_event)

    return lambda: None
Example #16
0
async def async_attach_trigger(hass, config, action, automation_info):
    """Listen for state changes based on configuration."""
    topic = config[CONF_TOPIC]
    payload = config.get(CONF_PAYLOAD)
    encoding = config[CONF_ENCODING] or None
    qos = config[CONF_QOS]
    job = HassJob(action)

    @callback
    def mqtt_automation_listener(mqttmsg):
        """Listen for MQTT messages."""
        if payload is None or payload == mqttmsg.payload:
            data = {
                "platform": "mqtt",
                "topic": mqttmsg.topic,
                "payload": mqttmsg.payload,
                "qos": mqttmsg.qos,
                "description": f"mqtt topic {mqttmsg.topic}",
            }

            try:
                data["payload_json"] = json.loads(mqttmsg.payload)
            except ValueError:
                pass

            hass.async_run_hass_job(job, {"trigger": data})

    remove = await mqtt.async_subscribe(
        hass, topic, mqtt_automation_listener, encoding=encoding, qos=qos
    )
    return remove
Example #17
0
    async def async_attach_trigger(
        self,
        config: ConfigType,
        action: TriggerActionType,
        trigger_info: TriggerInfo,
    ) -> CALLBACK_TYPE:
        """Attach a trigger."""
        trigger_data = trigger_info["trigger_data"]
        job = HassJob(action)

        @callback
        def event_handler(char):
            if config[CONF_SUBTYPE] != HK_TO_HA_INPUT_EVENT_VALUES[
                    char["value"]]:
                return
            self._hass.async_run_hass_job(
                job, {"trigger": {
                    **trigger_data,
                    **config
                }})

        trigger = self._triggers[config[CONF_TYPE], config[CONF_SUBTYPE]]
        iid = trigger["characteristic"]

        await self._connection.add_watchable_characteristics([(self._aid, iid)]
                                                             )
        self._callbacks.setdefault(iid, []).append(event_handler)

        def async_remove_handler():
            if iid in self._callbacks:
                self._callbacks[iid].remove(event_handler)

        return async_remove_handler
def async_dispatcher_connect(hass: HomeAssistantType, 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 hass.data:
        hass.data[DATA_DISPATCHER] = {}

    job = HassJob(
        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,
            ),
        ))

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

    @callback
    def async_remove_dispatcher() -> None:
        """Remove signal listener."""
        try:
            hass.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
Example #19
0
def async_track_sunset(
    hass: HomeAssistant, action: Callable[..., None], offset: timedelta | None = None
) -> CALLBACK_TYPE:
    """Add a listener that will fire a specified offset from sunset daily."""
    listener = SunListener(hass, HassJob(action), SUN_EVENT_SUNSET, offset)
    listener.async_attach()
    return listener.async_detach
Example #20
0
async def async_attach_trigger(
    hass: HomeAssistant,
    config: ConfigType,
    action: TriggerActionType,
    trigger_info: TriggerInfo,
) -> CALLBACK_TYPE:
    """Listen for events based on configuration."""
    trigger_data = trigger_info["trigger_data"]
    event = config.get(CONF_EVENT)
    offset = config.get(CONF_OFFSET)
    description = event
    if offset:
        description = f"{description} with offset"
    job = HassJob(action)

    @callback
    def call_action():
        """Call action with right context."""
        hass.async_run_hass_job(
            job,
            {
                "trigger": {
                    **trigger_data,
                    "platform": "sun",
                    "event": event,
                    "offset": offset,
                    "description": description,
                }
            },
        )

    if event == SUN_EVENT_SUNRISE:
        return async_track_sunrise(hass, call_action, offset)
    return async_track_sunset(hass, call_action, offset)
Example #21
0
def async_track_utc_time_change(
    hass: HomeAssistant,
    action: None,
    hour: Optional[Any] = None,
    minute: Optional[Any] = None,
    second: Optional[Any] = None,
    tz: Optional[Any] = None,
) -> CALLBACK_TYPE:
    """Add a listener that will fire if time matches a pattern."""
    # This is function is modifies to support timezones.

    # 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)):
        # Previously this relied on EVENT_TIME_FIRED
        # which meant it would not fire right away because
        # the caller would always be misaligned with the call
        # time vs the fire time by < 1s. To preserve this
        # misalignment we use async_track_time_interval here
        return async_track_time_interval(hass, action, timedelta(seconds=1))

    job = HassJob(action)
    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."""
        ts_now = now.astimezone(tz) if tz else now
        return dt_util.find_next_time_expression_time(
            ts_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()
        hass.async_run_hass_job(job, now.astimezone(tz) if tz else now)

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

    time_listener = async_track_point_in_utc_time(
        hass, 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
Example #22
0
def async_track_utc_time_change(
    hass: HomeAssistant,
    action: Callable[..., None],
    hour: Optional[Any] = None,
    minute: Optional[Any] = None,
    second: Optional[Any] = None,
    local: bool = False,
) -> CALLBACK_TYPE:
    """Add a listener that will fire if time matches a pattern."""

    job = HassJob(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."""
            hass.async_run_hass_job(job, event.data[ATTR_NOW])

        return hass.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: Optional[CALLBACK_TYPE] = None

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

        now = time_tracker_utcnow()
        hass.async_run_hass_job(job, dt_util.as_local(now) if local else now)

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

    time_listener = async_track_point_in_utc_time(
        hass, 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
Example #23
0
async def async_attach_trigger(
    hass: HomeAssistant,
    config: ConfigType,
    action: TriggerActionType,
    trigger_info: TriggerInfo,
) -> CALLBACK_TYPE:
    """Trigger based on incoming webhooks."""
    webhook_id: str = config[CONF_WEBHOOK_ID]
    job = HassJob(action)

    triggers: dict[str, list[TriggerInstance]] = hass.data.setdefault(
        WEBHOOK_TRIGGERS, {})

    if webhook_id not in triggers:
        async_register(
            hass,
            trigger_info["domain"],
            trigger_info["name"],
            webhook_id,
            _handle_webhook,
        )
        triggers[webhook_id] = []

    trigger_instance = TriggerInstance(trigger_info, job)
    triggers[webhook_id].append(trigger_instance)

    @callback
    def unregister():
        """Unregister webhook."""
        triggers[webhook_id].remove(trigger_instance)
        if not triggers[webhook_id]:
            async_unregister(hass, webhook_id)
            triggers.pop(webhook_id)

    return unregister
Example #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 = HassJob(change_listener)
Example #25
0
async def async_attach_trigger(hass,
                               config,
                               action,
                               automation_info,
                               *,
                               platform_type="event"):
    """Listen for events based on configuration."""
    event_type = config.get(CONF_EVENT_TYPE)

    event_data_schema = None
    if config.get(CONF_EVENT_DATA):
        event_data_schema = vol.Schema(
            {
                vol.Required(key): value
                for key, value in config.get(CONF_EVENT_DATA).items()
            },
            extra=vol.ALLOW_EXTRA,
        )

    event_context_schema = None
    if config.get(CONF_EVENT_CONTEXT):
        event_context_schema = vol.Schema(
            {
                vol.Required(key): _schema_value(value)
                for key, value in config.get(CONF_EVENT_CONTEXT).items()
            },
            extra=vol.ALLOW_EXTRA,
        )

    job = HassJob(action)

    @callback
    def handle_event(event):
        """Listen for events and calls the action when data matches."""
        try:
            # Check that the event data and context match the configured
            # schema if one was provided
            if event_data_schema:
                event_data_schema(event.data)
            if event_context_schema:
                event_context_schema(event.context.as_dict())
        except vol.Invalid:
            # If event doesn't match, skip event
            return

        hass.async_run_hass_job(
            job,
            {
                "trigger": {
                    "platform": platform_type,
                    "event": event,
                    "description": f"event '{event.event_type}'",
                }
            },
            event.context,
        )

    return hass.bus.async_listen(event_type, handle_event)
Example #26
0
def async_track_entity_registry_updated_event(
    hass: HomeAssistant,
    entity_ids: Union[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 = hass.data.setdefault(
        TRACK_ENTITY_REGISTRY_UPDATED_CALLBACKS, {})

    if TRACK_ENTITY_REGISTRY_UPDATED_LISTENER not in hass.data:

        @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:
                    hass.async_run_hass_job(job, event)
                except Exception:  # pylint: disable=broad-except
                    _LOGGER.exception(
                        "Error while processing entity registry update for %s",
                        entity_id,
                    )

        hass.data[
            TRACK_ENTITY_REGISTRY_UPDATED_LISTENER] = hass.bus.async_listen(
                EVENT_ENTITY_REGISTRY_UPDATED,
                _async_entity_registry_updated_dispatcher)

    job = HassJob(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(
            hass,
            TRACK_ENTITY_REGISTRY_UPDATED_CALLBACKS,
            TRACK_ENTITY_REGISTRY_UPDATED_LISTENER,
            entity_ids,
            job,
        )

    return remove_listener
Example #27
0
async def async_attach_trigger(
    hass: HomeAssistant,
    config: ConfigType,
    action: AutomationActionType,
    automation_info: AutomationTriggerInfo,
) -> CALLBACK_TYPE:
    """Listen for state changes based on configuration."""
    trigger_data = automation_info["trigger_data"]
    source: str = config[CONF_SOURCE].lower()
    zone_entity_id = config.get(CONF_ZONE)
    trigger_event = config.get(CONF_EVENT)
    job = HassJob(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

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

        from_match = (condition.zone(hass, zone_state, from_state)
                      if from_state else False)
        to_match = condition.zone(hass, 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):
            hass.async_run_hass_job(
                job,
                {
                    "trigger": {
                        **trigger_data,
                        "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}",
                    }
                },
                event.context,
            )
Example #28
0
async def async_attach_trigger(hass, config, action, automation_info):
    """Listen for state changes based on configuration."""
    trigger_data = automation_info["trigger_data"]
    source = config.get(CONF_SOURCE).lower()
    zone_entity_id = config.get(CONF_ZONE)
    trigger_event = config.get(CONF_EVENT)
    job = HassJob(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 = hass.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(hass, zone_state, from_state)
                      if from_state else False)
        to_match = condition.zone(hass, 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):
            hass.async_run_hass_job(
                job,
                {
                    "trigger": {
                        **trigger_data,
                        "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}",
                    }
                },
                event.context,
            )

    return async_track_state_change_filtered(
        hass, TrackStates(False, set(), {DOMAIN}),
        state_change_listener).async_remove
Example #29
0
async def async_attach_trigger(
    hass: HomeAssistant,
    config: ConfigType,
    action: AutomationActionType,
    automation_info: AutomationTriggerInfo,
) -> CALLBACK_TYPE:
    """Listen for state changes based on configuration."""
    trigger_data = automation_info["trigger_data"]
    entities: dict[str, CALLBACK_TYPE] = {}
    removes = []
    job = HassJob(action)

    @callback
    def time_automation_listener(description, now, *, entity_id=None):
        """Listen for time changes and calls action."""
        hass.async_run_hass_job(
            job,
            {
                "trigger": {
                    **trigger_data,
                    "platform": "time",
                    "now": now,
                    "description": description,
                    "entity_id": entity_id,
                }
            },
        )

    @callback
    def update_entity_trigger_event(event):
        """update_entity_trigger from the event."""
        return update_entity_trigger(event.data["entity_id"],
                                     event.data["new_state"])

    @callback
    def update_entity_trigger(entity_id, new_state=None):
        """Update the entity trigger for the entity_id."""
        # If a listener was already set up for entity, remove it.
        if remove := entities.pop(entity_id, None):
            remove()
            remove = None

        if not new_state:
            return

        # Check state of entity. If valid, set up a listener.
        if new_state.domain == "input_datetime":
            if has_date := new_state.attributes["has_date"]:
                year = new_state.attributes["year"]
                month = new_state.attributes["month"]
                day = new_state.attributes["day"]
            if has_time := new_state.attributes["has_time"]:
                hour = new_state.attributes["hour"]
                minute = new_state.attributes["minute"]
                second = new_state.attributes["second"]
Example #30
0
    def __init__(self, hass: HomeAssistant, user_id: str, token: str):
        self._hass = hass
        self._user_id = user_id
        self._token = token

        self._property_entities = self._get_property_entities()
        self._session = async_create_clientsession(hass)

        self._unsub_pending: CALLBACK_TYPE | None = None
        self._pending = deque()
        self._report_states_job = HassJob(self._report_states)