Example #1
0
async def test_track_same_state_simple_no_trigger(hass):
    """Test track_same_change with no trigger."""
    callback_runs = []
    period = timedelta(minutes=1)

    @ha.callback
    def callback_run_callback():
        callback_runs.append(1)

    async_track_same_state(
        hass,
        period,
        callback_run_callback,
        lambda _, _2, to_s: to_s.state == "on",
        entity_ids="light.Bowl",
    )

    # Adding state to state machine
    hass.states.async_set("light.Bowl", "on")
    await hass.async_block_till_done()
    assert len(callback_runs) == 0

    # Change state on state machine
    hass.states.async_set("light.Bowl", "off")
    await hass.async_block_till_done()
    assert len(callback_runs) == 0

    # change time to track and see if they trigger
    future = dt_util.utcnow() + period
    async_fire_time_changed(hass, future)
    await hass.async_block_till_done()
    assert len(callback_runs) == 0
Example #2
0
async def test_track_same_state_simple_trigger_check_funct(hass):
    """Test track_same_change with trigger and check funct."""
    callback_runs = []
    check_func = []
    period = timedelta(minutes=1)

    @ha.callback
    def callback_run_callback():
        callback_runs.append(1)

    @ha.callback
    def async_check_func(entity, from_s, to_s):
        check_func.append((entity, from_s, to_s))
        return True

    async_track_same_state(
        hass,
        period,
        callback_run_callback,
        entity_ids="light.Bowl",
        async_check_same_func=async_check_func,
    )

    # Adding state to state machine
    hass.states.async_set("light.Bowl", "on")
    await hass.async_block_till_done()
    assert len(callback_runs) == 0
    assert check_func[-1][2].state == "on"
    assert check_func[-1][0] == "light.bowl"

    # change time to track and see if they trigger
    future = dt_util.utcnow() + period
    async_fire_time_changed(hass, future)
    await hass.async_block_till_done()
    assert len(callback_runs) == 1
Example #3
0
    def async_check_state(self):
        """Update the state from the template."""
        state = self._async_render()

        # return if the state don't change or is invalid
        if state is None or state == self.state:
            return

        @callback
        def set_state():
            """Set state of template binary sensor."""
            self._state = state
            self.async_schedule_update_ha_state()

        # state without delay
        if (state and not self._delay_on) or (not state
                                              and not self._delay_off):
            set_state()
            return

        period = self._delay_on if state else self._delay_off
        async_track_same_state(
            self.hass,
            period,
            set_state,
            entity_ids=self._entities,
            async_check_same_func=lambda *args: self._async_render() == state,
        )
Example #4
0
    def state_automation_listener(entity, from_s, to_s):
        """Listen for state changes and calls action."""
        @callback
        def call_action():
            """Call action with right context."""
            hass.async_run_job(action({
                'trigger': {
                    'platform': 'state',
                    'entity_id': entity,
                    'from_state': from_s,
                    'to_state': to_s,
                    'for': time_delta,
                }
            }, context=to_s.context))

        # Ignore changes to state attributes if from/to is in use
        if (not match_all and from_s is not None and to_s is not None and
                from_s.state == to_s.state):
            return

        if not time_delta:
            call_action()
            return

        unsub_track_same[entity] = async_track_same_state(
            hass, time_delta, call_action,
            lambda _, _2, to_state: to_state.state == to_s.state,
            entity_ids=entity_id)
Example #5
0
    def template_listener(entity_id, from_s, to_s):
        """Listen for state changes and calls action."""
        nonlocal unsub_track_same

        @callback
        def call_action():
            """Call action with right context."""
            hass.async_run_job(
                action(
                    {
                        'trigger': {
                            'platform': 'template',
                            'entity_id': entity_id,
                            'from_state': from_s,
                            'to_state': to_s,
                        },
                    },
                    context=(to_s.context if to_s else None)))

        if not time_delta:
            call_action()
            return

        unsub_track_same = async_track_same_state(
            hass, time_delta, call_action,
            lambda _, _2, _3: condition.async_template(hass, value_template),
            value_template.extract_entities())
    def state_automation_listener(entity, from_s, to_s):
        """Listen for state changes and calls action."""
        nonlocal already_triggered, async_remove_track_same

        @callback
        def call_action():
            """Call action with right context."""
            hass.async_run_job(action, {
                'trigger': {
                    'platform': 'numeric_state',
                    'entity_id': entity,
                    'below': below,
                    'above': above,
                    'from_state': from_s,
                    'to_state': to_s,
                }
            })

        matching = check_numeric_state(entity, from_s, to_s)

        if matching and not already_triggered:
            if time_delta:
                async_remove_track_same = async_track_same_state(
                    hass, time_delta, call_action, entity_ids=entity_id,
                    async_check_same_func=check_numeric_state)
            else:
                call_action()

        already_triggered = matching
    def state_automation_listener(entity, from_s, to_s):
        """Listen for state changes and calls action."""
        nonlocal async_remove_track_same

        @callback
        def call_action():
            """Call action with right context."""
            hass.async_run_job(
                action, {
                    'trigger': {
                        'platform': 'state',
                        'entity_id': entity,
                        'from_state': from_s,
                        'to_state': to_s,
                        'for': time_delta,
                    }
                })

        # Ignore changes to state attributes if from/to is in use
        if (not match_all and from_s is not None and to_s is not None
                and from_s.last_changed == to_s.last_changed):
            return

        if not time_delta:
            call_action()
            return

        async_remove_track_same = async_track_same_state(hass,
                                                         to_s.state,
                                                         time_delta,
                                                         call_action,
                                                         entity_ids=entity_id)
    def state_automation_listener(entity, from_s, to_s):
        """Listen for state changes and calls action."""
        nonlocal already_triggered, async_remove_track_same

        @callback
        def call_action():
            """Call action with right context."""
            hass.async_run_job(
                action, {
                    'trigger': {
                        'platform': 'numeric_state',
                        'entity_id': entity,
                        'below': below,
                        'above': above,
                        'from_state': from_s,
                        'to_state': to_s,
                    }
                })

        matching = check_numeric_state(entity, from_s, to_s)

        if matching and not already_triggered:
            if time_delta:
                async_remove_track_same = async_track_same_state(
                    hass,
                    time_delta,
                    call_action,
                    entity_ids=entity_id,
                    async_check_same_func=check_numeric_state)
            else:
                call_action()

        already_triggered = matching
Example #9
0
    def state_automation_listener(entity, from_s, to_s):
        """Listen for state changes and calls action."""
        nonlocal async_remove_track_same

        @callback
        def call_action():
            """Call action with right context."""
            hass.async_run_job(action, {
                'trigger': {
                    'platform': 'state',
                    'entity_id': entity,
                    'from_state': from_s,
                    'to_state': to_s,
                    'for': time_delta,
                }
            })

        # Ignore changes to state attributes if from/to is in use
        if (not match_all and from_s is not None and to_s is not None and
                from_s.last_changed == to_s.last_changed):
            return

        if not time_delta:
            call_action()
            return

        async_remove_track_same = async_track_same_state(
            hass, to_s.state, time_delta, call_action, entity_ids=entity_id)
Example #10
0
    def state_automation_listener(entity, from_s, to_s):
        """Listen for state changes and calls action."""
        @callback
        def call_action():
            """Call action with right context."""
            hass.async_run_job(
                action(
                    {
                        'trigger': {
                            'platform': 'state',
                            'entity_id': entity,
                            'from_state': from_s,
                            'to_state': to_s,
                            'for': time_delta,
                        }
                    },
                    context=to_s.context))

        # Ignore changes to state attributes if from/to is in use
        if (not match_all and from_s is not None and to_s is not None
                and from_s.state == to_s.state):
            return

        if not time_delta:
            call_action()
            return

        unsub_track_same[entity] = async_track_same_state(
            hass,
            time_delta,
            call_action,
            lambda _, _2, to_state: to_state.state == to_s.state,
            entity_ids=entity)
Example #11
0
    def state_automation_listener(entity, from_s, to_s):
        """Listen for state changes and calls action."""
        @callback
        def call_action():
            """Call action with right context."""
            hass.async_run_job(action({
                'trigger': {
                    'platform': 'numeric_state',
                    'entity_id': entity,
                    'below': below,
                    'above': above,
                    'from_state': from_s,
                    'to_state': to_s,
                }
            }, context=to_s.context))

        matching = check_numeric_state(entity, from_s, to_s)

        if not matching:
            entities_triggered.discard(entity)
        elif entity not in entities_triggered:
            entities_triggered.add(entity)

            if time_delta:
                unsub_track_same[entity] = async_track_same_state(
                    hass, time_delta, call_action, entity_ids=entity_id,
                    async_check_same_func=check_numeric_state)
            else:
                call_action()
Example #12
0
    def state_automation_listener(entity, from_s, to_s):
        """Listen for state changes and calls action."""
        @callback
        def call_action():
            """Call action with right context."""
            hass.async_run_job(action({
                'trigger': {
                    'platform': 'state',
                    'entity_id': entity,
                    'from_state': from_s,
                    'to_state': to_s,
                    'for': time_delta if not time_delta else period[entity]
                }
            }, context=to_s.context))

        # Ignore changes to state attributes if from/to is in use
        if (not match_all and from_s is not None and to_s is not None and
                from_s.state == to_s.state):
            return

        if not time_delta:
            call_action()
            return

        variables = {
            'trigger': {
                'platform': 'state',
                'entity_id': entity,
                'from_state': from_s,
                'to_state': to_s,
            }
        }

        try:
            if isinstance(time_delta, template.Template):
                period[entity] = vol.All(
                    cv.time_period,
                    cv.positive_timedelta)(
                        time_delta.async_render(variables))
            elif isinstance(time_delta, dict):
                time_delta_data = {}
                time_delta_data.update(
                    template.render_complex(time_delta, variables))
                period[entity] = vol.All(
                    cv.time_period,
                    cv.positive_timedelta)(
                        time_delta_data)
            else:
                period[entity] = time_delta
        except (exceptions.TemplateError, vol.Invalid) as ex:
            _LOGGER.error("Error rendering '%s' for template: %s",
                          automation_info['name'], ex)
            return

        unsub_track_same[entity] = async_track_same_state(
            hass, period[entity], call_action,
            lambda _, _2, to_state: to_state.state == to_s.state,
            entity_ids=entity)
Example #13
0
    def template_listener(entity_id, from_s, to_s):
        """Listen for state changes and calls action."""
        nonlocal unsub_track_same

        @callback
        def call_action():
            """Call action with right context."""
            hass.async_run_job(
                action(
                    {
                        "trigger": {
                            "platform": "template",
                            "entity_id": entity_id,
                            "from_state": from_s,
                            "to_state": to_s,
                            "for": time_delta if not time_delta else period,
                        }
                    },
                    context=(to_s.context if to_s else None),
                ))

        if not time_delta:
            call_action()
            return

        variables = {
            "trigger": {
                "platform": platform_type,
                "entity_id": entity_id,
                "from_state": from_s,
                "to_state": to_s,
            }
        }

        try:
            if isinstance(time_delta, template.Template):
                period = vol.All(cv.time_period, cv.positive_timedelta)(
                    time_delta.async_render(variables))
            elif isinstance(time_delta, dict):
                time_delta_data = {}
                time_delta_data.update(
                    template.render_complex(time_delta, variables))
                period = vol.All(cv.time_period,
                                 cv.positive_timedelta)(time_delta_data)
            else:
                period = time_delta
        except (exceptions.TemplateError, vol.Invalid) as ex:
            _LOGGER.error("Error rendering '%s' for template: %s",
                          automation_info["name"], ex)
            return

        unsub_track_same = async_track_same_state(
            hass,
            period,
            call_action,
            lambda _, _2, _3: condition.async_template(hass, value_template),
            value_template.extract_entities(),
        )
Example #14
0
    def state_automation_listener(event):
        """Listen for state changes and calls action."""
        entity_id = event.data.get("entity_id")
        from_s = event.data.get("old_state")
        to_s = event.data.get("new_state")

        @callback
        def call_action():
            """Call action with right context."""
            hass.async_run_hass_job(
                job,
                {
                    "trigger": {
                        "platform": platform_type,
                        "entity_id": entity_id,
                        "below": below,
                        "above": above,
                        "from_state": from_s,
                        "to_state": to_s,
                        "for":
                        time_delta if not time_delta else period[entity_id],
                        "description": f"numeric state of {entity_id}",
                    }
                },
                to_s.context,
            )

        matching = check_numeric_state(entity_id, from_s, to_s)

        if not matching:
            entities_triggered.discard(entity_id)
        elif entity_id not in entities_triggered:
            entities_triggered.add(entity_id)

            if time_delta:
                try:
                    period[entity_id] = cv.positive_time_period(
                        template.render_complex(time_delta,
                                                variables(entity_id)))
                except (exceptions.TemplateError, vol.Invalid) as ex:
                    _LOGGER.error(
                        "Error rendering '%s' for template: %s",
                        automation_info["name"],
                        ex,
                    )
                    entities_triggered.discard(entity_id)
                    return

                unsub_track_same[entity_id] = async_track_same_state(
                    hass,
                    period[entity_id],
                    call_action,
                    entity_ids=entity_id,
                    async_check_same_func=check_numeric_state,
                )
            else:
                call_action()
Example #15
0
    def async_check_state(self):
        """Update the state from the template."""
        state = self._async_render()

        # return if the state don't change or is invalid
        if state is None or state == self.state:
            return

        @callback
        def set_state():
            """Set state of template binary sensor."""
            self._state = state
            self.async_schedule_update_ha_state()

        # state without delay
        if (state and not self._delay_on) or \
                (not state and not self._delay_off):
            set_state()
            return

        period = self._delay_on if state else self._delay_off
        async_track_same_state(
            self.hass, period, set_state, entity_ids=self._entities,
            async_check_same_func=lambda *args: self._async_render() == state)
Example #16
0
async def test_track_same_state_simple_trigger(hass):
    """Test track_same_change with trigger simple."""
    thread_runs = []
    callback_runs = []
    coroutine_runs = []
    period = timedelta(minutes=1)

    def thread_run_callback():
        thread_runs.append(1)

    async_track_same_state(hass,
                           period,
                           thread_run_callback,
                           lambda _, _2, to_s: to_s.state == 'on',
                           entity_ids='light.Bowl')

    @ha.callback
    def callback_run_callback():
        callback_runs.append(1)

    async_track_same_state(hass,
                           period,
                           callback_run_callback,
                           lambda _, _2, to_s: to_s.state == 'on',
                           entity_ids='light.Bowl')

    @asyncio.coroutine
    def coroutine_run_callback():
        coroutine_runs.append(1)

    async_track_same_state(hass, period, coroutine_run_callback,
                           lambda _, _2, to_s: to_s.state == 'on')

    # Adding state to state machine
    hass.states.async_set("light.Bowl", "on")
    await hass.async_block_till_done()
    assert len(thread_runs) == 0
    assert len(callback_runs) == 0
    assert len(coroutine_runs) == 0

    # change time to track and see if they trigger
    future = dt_util.utcnow() + period
    async_fire_time_changed(hass, future)
    await hass.async_block_till_done()
    assert len(thread_runs) == 1
    assert len(callback_runs) == 1
    assert len(coroutine_runs) == 1
    def state_automation_listener(entity, from_s, to_s):
        """Listen for state changes and calls action."""
        nonlocal async_remove_track_same

        if not check_numeric_state(entity, from_s, to_s):
            return

        variables = {
            'trigger': {
                'platform': 'numeric_state',
                'entity_id': entity,
                'below': below,
                'above': above,
                'from_state': from_s,
                'to_state': to_s,
            }
        }

        # Only match if old didn't exist or existed but didn't match
        # Written as: skip if old one did exist and matched
        if from_s is not None and condition.async_numeric_state(
                hass, from_s, below, above, value_template, variables):
            return

        @callback
        def call_action():
            """Call action with right context."""
            hass.async_run_job(action, variables)

        if not time_delta:
            call_action()
            return

        async_remove_track_same = async_track_same_state(
            hass, True, time_delta, call_action, entity_ids=entity_id,
            async_check_func=check_numeric_state)
Example #18
0
    def state_automation_listener(entity, from_s, to_s):
        """Listen for state changes and calls action."""
        @callback
        def call_action():
            """Call action with right context."""
            hass.async_run_job(
                action(
                    {
                        "trigger": {
                            "platform":
                            platform_type,
                            "entity_id":
                            entity,
                            "below":
                            below,
                            "above":
                            above,
                            "from_state":
                            from_s,
                            "to_state":
                            to_s,
                            "for":
                            time_delta if not time_delta else period[entity],
                        }
                    },
                    context=to_s.context,
                ))

        matching = check_numeric_state(entity, from_s, to_s)

        if not matching:
            entities_triggered.discard(entity)
        elif entity not in entities_triggered:
            entities_triggered.add(entity)

            if time_delta:
                variables = {
                    "trigger": {
                        "platform": "numeric_state",
                        "entity_id": entity,
                        "below": below,
                        "above": above,
                    }
                }

                try:
                    if isinstance(time_delta, template.Template):
                        period[entity] = vol.All(
                            cv.time_period, cv.positive_timedelta)(
                                time_delta.async_render(variables))
                    elif isinstance(time_delta, dict):
                        time_delta_data = {}
                        time_delta_data.update(
                            template.render_complex(time_delta, variables))
                        period[entity] = vol.All(
                            cv.time_period,
                            cv.positive_timedelta)(time_delta_data)
                    else:
                        period[entity] = time_delta
                except (exceptions.TemplateError, vol.Invalid) as ex:
                    _LOGGER.error(
                        "Error rendering '%s' for template: %s",
                        automation_info["name"],
                        ex,
                    )
                    entities_triggered.discard(entity)
                    return

                unsub_track_same[entity] = async_track_same_state(
                    hass,
                    period[entity],
                    call_action,
                    entity_ids=entity,
                    async_check_same_func=check_numeric_state,
                )
            else:
                call_action()
Example #19
0
    def state_automation_listener(event: Event):
        """Listen for state changes and calls action."""
        entity: str = event.data["entity_id"]
        if entity not in entity_id:
            return

        from_s = event.data.get("old_state")
        to_s = event.data.get("new_state")
        old_state = getattr(from_s, "state", None)
        new_state = getattr(to_s, "state", None)

        if (not match_from_state(old_state) or not match_to_state(new_state)
                or (not match_all and old_state == new_state)):
            return

        @callback
        def call_action():
            """Call action with right context."""
            hass.async_run_job(
                action(
                    {
                        "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],
                        }
                    },
                    context=event.context,
                ))

        if not time_delta:
            call_action()
            return

        variables = {
            "trigger": {
                "platform": "state",
                "entity_id": entity,
                "from_state": from_s,
                "to_state": to_s,
            }
        }

        try:
            if isinstance(time_delta, template.Template):
                period[entity] = vol.All(
                    cv.time_period,
                    cv.positive_timedelta)(time_delta.async_render(variables))
            elif isinstance(time_delta, dict):
                time_delta_data = {}
                time_delta_data.update(
                    template.render_complex(time_delta, variables))
                period[entity] = vol.All(
                    cv.time_period, cv.positive_timedelta)(time_delta_data)
            else:
                period[entity] = time_delta
        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):
            if new_st is None:
                return False
            return new_st.state == to_s.state

        unsub_track_same[entity] = async_track_same_state(
            hass,
            period[entity],
            call_action,
            _check_same_state,
            entity_ids=entity,
        )
Example #20
0
    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."""
            hass.async_run_hass_job(
                job,
                {
                    "trigger": {
                        **trigger_data,
                        "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}",
                    }
                },
                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 | None) -> bool:
            if new_st is None:
                return False

            cur_value: str | None
            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(
            hass,
            period[entity],
            call_action,
            _check_same_state,
            entity_ids=entity,
        )
Example #21
0
    def state_automation_listener(event):
        """Listen for state changes and calls action."""
        entity_id = event.data.get("entity_id")
        from_s = event.data.get("old_state")
        to_s = event.data.get("new_state")

        @callback
        def call_action():
            """Call action with right context."""
            hass.async_run_hass_job(
                job,
                {
                    "trigger": {
                        **trigger_data,
                        "platform": platform_type,
                        "entity_id": entity_id,
                        "below": below,
                        "above": above,
                        "from_state": from_s,
                        "to_state": to_s,
                        "for": time_delta if not time_delta else period[entity_id],
                        "description": f"numeric state of {entity_id}",
                    }
                },
                to_s.context,
            )

        @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", trigger_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",
                        trigger_info["name"],
                        ex,
                    )
                    return

                unsub_track_same[entity_id] = async_track_same_state(
                    hass,
                    period[entity_id],
                    call_action,
                    entity_ids=entity_id,
                    async_check_same_func=check_numeric_state_no_raise,
                )
            else:
                call_action()