Esempio n. 1
0
File: light.py Progetto: s2233/core
    async def async_added_to_hass(self) -> None:
        """Register callbacks."""
        async def async_state_changed_listener(event):
            """Handle child updates."""
            self.async_set_context(event.context)
            await self.async_defer_or_update_ha_state()

        self.async_on_remove(
            async_track_state_change_event(self.hass, self._entity_ids,
                                           async_state_changed_listener))

        if self.hass.state == CoreState.running:
            await self.async_update()
            return

        await super().async_added_to_hass()
Esempio n. 2
0
async def wait_for_state_change_or_timeout(hass, entity_id, timeout):
    """Wait for an entity to change state."""
    ev = asyncio.Event()

    @core.callback
    def _async_event_changed(_):
        ev.set()

    unsub = async_track_state_change_event(hass, [entity_id], _async_event_changed)

    try:
        await asyncio.wait_for(ev.wait(), timeout=STATE_CHANGE_WAIT_TIMEOUT)
    except asyncio.TimeoutError:
        pass
    finally:
        unsub()
Esempio n. 3
0
    async def async_added_to_hass(self) -> None:
        """Register callbacks."""

        @callback
        def _async_state_changed_listener(event: Event | None = None) -> None:
            """Handle child updates."""
            self.async_state_changed_listener(event)
            self.async_write_ha_state()

        self.async_on_remove(
            async_track_state_change_event(
                self.hass, [self.tracked_entity_id], _async_state_changed_listener
            )
        )

        # Call once on adding
        _async_state_changed_listener()
Esempio n. 4
0
async def test_track_state_change_event_chain_multple_entity(hass):
    """Test that adding a new state tracker inside a tracker does not fire right away."""
    tracker_called = []
    chained_tracker_called = []

    chained_tracker_unsub = []
    tracker_unsub = []

    @ha.callback
    def chained_single_run_callback(event):
        old_state = event.data.get("old_state")
        new_state = event.data.get("new_state")

        chained_tracker_called.append((old_state, new_state))

    @ha.callback
    def single_run_callback(event):
        old_state = event.data.get("old_state")
        new_state = event.data.get("new_state")

        tracker_called.append((old_state, new_state))

        chained_tracker_unsub.append(
            async_track_state_change_event(hass, ["light.bowl", "light.top"],
                                           chained_single_run_callback))

    tracker_unsub.append(
        async_track_state_change_event(hass, ["light.bowl", "light.top"],
                                       single_run_callback))

    hass.states.async_set("light.bowl", "on")
    hass.states.async_set("light.top", "on")
    await hass.async_block_till_done()

    assert len(tracker_called) == 2
    assert len(chained_tracker_called) == 1
    assert len(tracker_unsub) == 1
    assert len(chained_tracker_unsub) == 2

    hass.states.async_set("light.bowl", "off")
    await hass.async_block_till_done()

    assert len(tracker_called) == 3
    assert len(chained_tracker_called) == 3
    assert len(tracker_unsub) == 1
    assert len(chained_tracker_unsub) == 3
Esempio n. 5
0
    def _change_status(self, tariff):
        if self._tariff == tariff:
            self._collecting = async_track_state_change_event(
                self.hass, [self._sensor_source_id], self.async_reading)
        else:
            if self._collecting:
                self._collecting()
            self._collecting = None

        _LOGGER.debug(
            "%s - %s - source <%s>",
            self._name,
            COLLECTING if self._collecting is not None else PAUSED,
            self._sensor_source_id,
        )

        self.async_write_ha_state()
Esempio n. 6
0
    async def async_added_to_hass(self):
        """Subscribe to children and template state changes."""

        @callback
        def _async_on_dependency_update(event):
            """Update ha state when dependencies update."""
            self.async_set_context(event.context)
            self.async_schedule_update_ha_state(True)

        @callback
        def _async_on_template_update(event, updates):
            """Update ha state when dependencies update."""
            result = updates.pop().result

            if isinstance(result, TemplateError):
                self._state_template_result = None
            else:
                self._state_template_result = result

            if event:
                self.async_set_context(event.context)

            self.async_schedule_update_ha_state(True)

        if self._state_template is not None:
            result = async_track_template_result(
                self.hass,
                [TrackTemplate(self._state_template, None)],
                _async_on_template_update,
            )
            self.hass.bus.async_listen_once(
                EVENT_HOMEASSISTANT_START, callback(lambda _: result.async_refresh())
            )

            self.async_on_remove(result.async_remove)

        depend = copy(self._children)
        for entity in self._attrs.values():
            depend.append(entity[0])

        self.async_on_remove(
            async_track_state_change_event(
                self.hass, list(set(depend)), _async_on_dependency_update
            )
        )
Esempio n. 7
0
class ThresholdSensor(BinarySensorEntity):
    """Representation of a Threshold sensor."""

    _attr_should_poll = False

    def __init__(self, hass, entity_id, name, lower, upper, hysteresis,
                 device_class, unique_id):
        """Initialize the Threshold sensor."""
        self._attr_unique_id = unique_id
        self._entity_id = entity_id
        self._name = name
        self._threshold_lower = lower
        self._threshold_upper = upper
        self._hysteresis = hysteresis
        self._device_class = device_class

        self._state_position = POSITION_UNKNOWN
        self._state = None
        self.sensor_value = None

        def _update_sensor_state():
            """Handle sensor state changes."""
            if (new_state := hass.states.get(self._entity_id)) is None:
                return

            try:
                self.sensor_value = (None if new_state.state in [
                    STATE_UNKNOWN, STATE_UNAVAILABLE
                ] else float(new_state.state))
            except (ValueError, TypeError):
                self.sensor_value = None
                _LOGGER.warning("State is not numerical")

            self._update_state()

        @callback
        def async_threshold_sensor_state_listener(event):
            """Handle sensor state changes."""
            _update_sensor_state()
            self.async_write_ha_state()

        self.async_on_remove(
            async_track_state_change_event(
                hass, [entity_id], async_threshold_sensor_state_listener))
        _update_sensor_state()
Esempio n. 8
0
    async def async_update_config(self, config):
        """Handle when the config is updated."""
        self._config = config

        if self._unsub_track_device is not None:
            self._unsub_track_device()
            self._unsub_track_device = None

        trackers = self._config[CONF_DEVICE_TRACKERS]

        if trackers:
            _LOGGER.debug("Subscribe to device trackers for %s", self.entity_id)

            self._unsub_track_device = async_track_state_change_event(
                self.hass, trackers, self._async_handle_tracker_update
            )

        self._update_state()
Esempio n. 9
0
    async def async_added_to_hass(self):
        """Handle added to Hass."""
        self.async_on_remove(
            async_track_state_change_event(
                self.hass, self._entity_ids,
                self._async_min_max_sensor_state_listener))

        # Replay current state of source entities
        for entity_id in self._entity_ids:
            state = self.hass.states.get(entity_id)
            state_event = Event("", {
                "entity_id": entity_id,
                "new_state": state
            })
            self._async_min_max_sensor_state_listener(state_event,
                                                      update_state=False)

        self._calc_values()
Esempio n. 10
0
    async def async_added_to_hass(self) -> None:
        """Register listeners."""
        for entity_id in self._entities:
            new_state = self.hass.states.get(entity_id)
            if new_state is None:
                continue
            await self.async_update_supported_features(entity_id,
                                                       new_state,
                                                       update_state=False)
        self.async_on_remove(
            async_track_state_change_event(
                self.hass, self._entities,
                self._update_supported_features_event))

        if self.hass.state == CoreState.running:
            await self.async_update()
            return
        await super().async_added_to_hass()
Esempio n. 11
0
    async def async_added_to_hass(self) -> None:
        """Register callbacks."""
        await super().async_added_to_hass()

        self.async_accept_signal(
            None,
            f"{SIGNAL_GROUP_MEMBERSHIP_CHANGE}_0x{self._group_id:04x}",
            self._handle_group_membership_changed,
            signal_override=True,
        )

        self._async_unsub_state_changed = async_track_state_change_event(
            self.hass, self._entity_ids, self.async_state_changed_listener)

        def send_removed_signal():
            async_dispatcher_send(self.hass, SIGNAL_GROUP_ENTITY_REMOVED,
                                  self._group_id)

        self.async_on_remove(send_removed_signal)
Esempio n. 12
0
    def async_on_state_subscription(
        entity_id: str, attribute: str | None = None
    ) -> None:
        """Subscribe and forward states for requested entities."""

        async def send_home_assistant_state_event(event: Event) -> None:
            """Forward Home Assistant states updates to ESPHome."""

            # Only communicate changes to the state or attribute tracked
            if (
                event.data.get("old_state") is not None
                and "new_state" in event.data
                and (
                    (
                        not attribute
                        and event.data["old_state"].state
                        == event.data["new_state"].state
                    )
                    or (
                        attribute
                        and attribute in event.data["old_state"].attributes
                        and attribute in event.data["new_state"].attributes
                        and event.data["old_state"].attributes[attribute]
                        == event.data["new_state"].attributes[attribute]
                    )
                )
            ):
                return

            await _send_home_assistant_state(
                event.data["entity_id"], attribute, event.data.get("new_state")
            )

        unsub = async_track_state_change_event(
            hass, [entity_id], send_home_assistant_state_event
        )
        entry_data.disconnect_callbacks.append(unsub)

        # Send initial state
        hass.async_create_task(
            _send_home_assistant_state(entity_id, attribute, hass.states.get(entity_id))
        )
Esempio n. 13
0
    async def async_added_to_hass(self) -> None:
        """Register callbacks."""
        @callback
        def _async_state_changed_listener(event: Event | None = None) -> None:
            """Handle child updates."""
            self.async_state_changed_listener(event)
            self.async_write_ha_state()

        self.async_on_remove(
            async_track_state_change_event(self.hass, [self._switch_entity_id],
                                           _async_state_changed_listener))

        # Call once on adding
        _async_state_changed_listener()

        # Add this entity to the wrapped switch's device
        registry = er.async_get(self.hass)
        if registry.async_get(self.entity_id) is not None:
            registry.async_update_entity(self.entity_id,
                                         device_id=self._device_id)
Esempio n. 14
0
    def async_tariff_change(self, event):
        """Handle tariff changes."""
        new_state = event.data.get("new_state")
        if new_state is None:
            return
        if self._tariff == new_state.state:
            self._collecting = async_track_state_change_event(
                self.hass, [self._sensor_source_id], self.async_reading)
        else:
            if self._collecting:
                self._collecting()
            self._collecting = None

        _LOGGER.debug(
            "%s - %s - source <%s>",
            self._name,
            COLLECTING if self._collecting is not None else PAUSED,
            self._sensor_source_id,
        )

        self.async_write_ha_state()
    async def async_added_to_hass(self):
        """Complete device setup after being added to hass."""
        @callback
        def trend_sensor_state_listener(event):
            """Handle state changes on the observed device."""
            new_state = event.data.get("new_state")
            if new_state is None:
                return
            try:
                if self._attribute:
                    state = new_state.attributes.get(self._attribute)
                else:
                    state = new_state.state
                if state not in (STATE_UNKNOWN, STATE_UNAVAILABLE):
                    sample = (new_state.last_updated.timestamp(), float(state))
                    self.samples.append(sample)
                    self.async_schedule_update_ha_state(True)
            except (ValueError, TypeError) as ex:
                _LOGGER.error(ex)

        self.async_on_remove(
            async_track_state_change_event(self.hass, [self._entity_id],
                                           trend_sensor_state_listener))
Esempio n. 16
0
async def async_attach_trigger(hass,
                               config,
                               action,
                               automation_info,
                               *,
                               platform_type="numeric_state") -> CALLBACK_TYPE:
    """Listen for state changes based on configuration."""
    entity_ids = config.get(CONF_ENTITY_ID)
    below = config.get(CONF_BELOW)
    above = config.get(CONF_ABOVE)
    time_delta = config.get(CONF_FOR)
    template.attach(hass, time_delta)
    value_template = config.get(CONF_VALUE_TEMPLATE)
    unsub_track_same = {}
    entities_triggered = set()
    period: dict = {}
    attribute = config.get(CONF_ATTRIBUTE)
    job = HassJob(action)

    if value_template is not None:
        value_template.hass = hass

    def variables(entity_id):
        """Return a dict with trigger variables."""
        return {
            "trigger": {
                "platform": "numeric_state",
                "entity_id": entity_id,
                "below": below,
                "above": above,
                "attribute": attribute,
            }
        }

    @callback
    def check_numeric_state(entity_id, from_s, to_s):
        """Return True if criteria are now met."""
        if to_s is None:
            return False

        return condition.async_numeric_state(hass, to_s, below, above,
                                             value_template,
                                             variables(entity_id), attribute)

    @callback
    def state_automation_listener(event):
        """Listen for state changes and calls action."""
        entity_id = event.data.get("entity_id")
        from_s = event.data.get("old_state")
        to_s = event.data.get("new_state")

        @callback
        def call_action():
            """Call action with right context."""
            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()

    unsub = async_track_state_change_event(hass, entity_ids,
                                           state_automation_listener)

    @callback
    def async_remove():
        """Remove state listeners async."""
        unsub()
        for async_remove in unsub_track_same.values():
            async_remove()
        unsub_track_same.clear()

    return async_remove
Esempio n. 17
0
                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,
            )

    return async_track_state_change_event(hass, entity_id,
                                          zone_automation_listener)
Esempio n. 18
0
    async def async_added_to_hass(self):
        """Handle entity which will be added."""
        await super().async_added_to_hass()
        state = await self.async_get_last_state()
        if state:
            try:
                self._state = Decimal(state.state)
            except (DecimalException, ValueError) as err:
                _LOGGER.warning("Could not restore last state: %s", err)
            else:
                self._attr_device_class = state.attributes.get(ATTR_DEVICE_CLASS)

                self._unit_of_measurement = state.attributes.get(
                    ATTR_UNIT_OF_MEASUREMENT
                )

        @callback
        def calc_integration(event):
            """Handle the sensor state changes."""
            old_state = event.data.get("old_state")
            new_state = event.data.get("new_state")

            if self._unit_of_measurement is None:
                unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
                self._unit_of_measurement = self._unit_template.format(
                    "" if unit is None else unit
                )
            if (
                self.device_class is None
                and new_state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_POWER
            ):
                self._attr_device_class = DEVICE_CLASS_ENERGY

            if (
                old_state is None
                or old_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE)
                or new_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE)
            ):
                return

            try:
                # integration as the Riemann integral of previous measures.
                area = 0
                elapsed_time = (
                    new_state.last_updated - old_state.last_updated
                ).total_seconds()

                if self._method == TRAPEZOIDAL_METHOD:
                    area = (
                        (Decimal(new_state.state) + Decimal(old_state.state))
                        * Decimal(elapsed_time)
                        / 2
                    )
                elif self._method == LEFT_METHOD:
                    area = Decimal(old_state.state) * Decimal(elapsed_time)
                elif self._method == RIGHT_METHOD:
                    area = Decimal(new_state.state) * Decimal(elapsed_time)

                integral = area / (self._unit_prefix * self._unit_time)
                assert isinstance(integral, Decimal)
            except ValueError as err:
                _LOGGER.warning("While calculating integration: %s", err)
            except DecimalException as err:
                _LOGGER.warning(
                    "Invalid state (%s > %s): %s", old_state.state, new_state.state, err
                )
            except AssertionError as err:
                _LOGGER.error("Could not calculate integral: %s", err)
            else:
                self._state += integral
                self.async_write_ha_state()

        async_track_state_change_event(
            self.hass, [self._sensor_source_id], calc_integration
        )
Esempio n. 19
0
class SensorTrend(BinarySensorEntity):
    """Representation of a trend Sensor."""
    def __init__(
        self,
        hass,
        device_id,
        friendly_name,
        entity_id,
        attribute,
        device_class,
        invert,
        max_samples,
        min_gradient,
        sample_duration,
    ):
        """Initialize the sensor."""
        self._hass = hass
        self.entity_id = generate_entity_id(ENTITY_ID_FORMAT,
                                            device_id,
                                            hass=hass)
        self._name = friendly_name
        self._entity_id = entity_id
        self._attribute = attribute
        self._device_class = device_class
        self._invert = invert
        self._sample_duration = sample_duration
        self._min_gradient = min_gradient
        self._gradient = None
        self._state = None
        self.samples = deque(maxlen=max_samples)

    @property
    def name(self):
        """Return the name of the sensor."""
        return self._name

    @property
    def is_on(self):
        """Return true if sensor is on."""
        return self._state

    @property
    def device_class(self):
        """Return the sensor class of the sensor."""
        return self._device_class

    @property
    def extra_state_attributes(self):
        """Return the state attributes of the sensor."""
        return {
            ATTR_ENTITY_ID: self._entity_id,
            ATTR_FRIENDLY_NAME: self._name,
            ATTR_GRADIENT: self._gradient,
            ATTR_INVERT: self._invert,
            ATTR_MIN_GRADIENT: self._min_gradient,
            ATTR_SAMPLE_COUNT: len(self.samples),
            ATTR_SAMPLE_DURATION: self._sample_duration,
        }

    @property
    def should_poll(self):
        """No polling needed."""
        return False

    async def async_added_to_hass(self):
        """Complete device setup after being added to hass."""
        @callback
        def trend_sensor_state_listener(event):
            """Handle state changes on the observed device."""
            if (new_state := event.data.get("new_state")) is None:
                return
            try:
                if self._attribute:
                    state = new_state.attributes.get(self._attribute)
                else:
                    state = new_state.state
                if state not in (STATE_UNKNOWN, STATE_UNAVAILABLE):
                    sample = (new_state.last_updated.timestamp(), float(state))
                    self.samples.append(sample)
                    self.async_schedule_update_ha_state(True)
            except (ValueError, TypeError) as ex:
                _LOGGER.error(ex)

        self.async_on_remove(
            async_track_state_change_event(self.hass, [self._entity_id],
                                           trend_sensor_state_listener))
Esempio n. 20
0
            self._tilts[KEY_POSITION].discard(entity_id)

        if update_state:
            self.async_defer_or_update_ha_state()

    async def async_added_to_hass(self) -> None:
        """Register listeners."""
        for entity_id in self._entities:
            if (new_state := self.hass.states.get(entity_id)) is None:
                continue
            self.async_update_supported_features(
                entity_id, new_state, update_state=False
            )
        self.async_on_remove(
            async_track_state_change_event(
                self.hass, self._entities, self._update_supported_features_event
            )
        )

        await super().async_added_to_hass()

    async def async_open_cover(self, **kwargs: Any) -> None:
        """Move the covers up."""
        data = {ATTR_ENTITY_ID: self._covers[KEY_OPEN_CLOSE]}
        await self.hass.services.async_call(
            DOMAIN, SERVICE_OPEN_COVER, data, blocking=True, context=self._context
        )

    async def async_close_cover(self, **kwargs: Any) -> None:
        """Move the covers down."""
        data = {ATTR_ENTITY_ID: self._covers[KEY_OPEN_CLOSE]}
    async def async_added_to_hass(self):
        """Run when entity about to be added."""
        await super().async_added_to_hass()

        # Add listener
        self.async_on_remove(
            async_track_state_change_event(
                self.hass, self.sensor_entity_id, self._async_sensor_changed))
        if self._hvac_mode == HVAC_MODE_HEAT:
            self.async_on_remove(
                async_track_state_change_event(
                    self.hass, self.heaters_entity_ids, self._async_switch_changed))
        elif self._hvac_mode == HVAC_MODE_COOL:
            self.async_on_remove(
                async_track_state_change_event(
                    self.hass, self.coolers_entity_ids, self._async_switch_changed))
        self.async_on_remove(
            async_track_state_change_event(
                self.hass, self.target_entity_id, self._async_target_changed))
        if self._related_climate is not None:
            for _related_entity in self._related_climate:
                self.async_on_remove(
                    async_track_state_change_event(
                        self.hass, _related_entity, self._async_switch_changed))

        @callback
        def _async_startup(event):
            """Init on startup."""
            sensor_state = self._getStateSafe(self.sensor_entity_id)
            if sensor_state and sensor_state != STATE_UNKNOWN:
                self._async_update_temp(sensor_state)
            target_state = self._getStateSafe(self.target_entity_id)
            if target_state and \
               target_state != STATE_UNKNOWN and \
               self._hvac_mode != HVAC_MODE_HEAT_COOL:
                self._async_update_program_temp(target_state)

        self.hass.bus.async_listen_once(
            EVENT_HOMEASSISTANT_START, _async_startup)

        # Check If we have an old state
        old_state = await self.async_get_last_state()
        _LOGGER.info("climate.%s old state: %s", self._name, old_state)
        if old_state is not None:
            # If we have no initial temperature, restore
            if self._target_temp is None:
                # If we have a previously saved temperature
                if old_state.attributes.get(ATTR_TEMPERATURE) is None:
                    target_entity_state = self._getStateSafe(self.target_entity_id)
                    if target_entity_state is not None:
                        self._target_temp = float(target_entity_state)
                    else:
                        self._target_temp = float((self._min_temp + self._max_temp)/2)
                    _LOGGER.warning("climate.%s - Undefined target temperature,"
                                    "falling back to %s", self._name , self._target_temp)
                else:
                    self._target_temp = float(
                        old_state.attributes[ATTR_TEMPERATURE])
            if (self._initial_hvac_mode is None and
                    old_state.state is not None):
                self._hvac_mode = \
                    old_state.state
                self._enabled = self._hvac_mode != HVAC_MODE_OFF

        else:
            # No previous state, try and restore defaults
            if self._target_temp is None:
                self._target_temp = float((self._min_temp + self._max_temp)/2)
            _LOGGER.warning("climate.%s - No previously saved temperature, setting to %s", self._name,
                            self._target_temp)

        # Set default state to off
        if not self._hvac_mode:
            self._hvac_mode = HVAC_MODE_OFF
Esempio n. 22
0
    async def async_added_to_hass(self):
        """Run when entity about to be added."""
        await super().async_added_to_hass()

        # Add listener
        self.async_on_remove(
            async_track_state_change_event(self.hass, [self.sensor_entity_id],
                                           self._async_sensor_changed))
        self.async_on_remove(
            async_track_state_change_event(self.hass, [self.heater_entity_id],
                                           self._async_switch_changed))

        if self._keep_alive:
            self.async_on_remove(
                async_track_time_interval(self.hass,
                                          self._async_control_heating,
                                          self._keep_alive))

        @callback
        def _async_startup(event):
            """Init on startup."""
            sensor_state = self.hass.states.get(self.sensor_entity_id)
            if sensor_state and sensor_state.state not in (
                    STATE_UNAVAILABLE,
                    STATE_UNKNOWN,
            ):
                self._async_update_temp(sensor_state)

        self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START,
                                        _async_startup)

        # Check If we have an old state
        old_state = await self.async_get_last_state()
        if old_state is not None:
            # If we have no initial temperature, restore
            if self._target_temp is None:
                # If we have a previously saved temperature
                if old_state.attributes.get(ATTR_TEMPERATURE) is None:
                    if self.ac_mode:
                        self._target_temp = self.max_temp
                    else:
                        self._target_temp = self.min_temp
                    _LOGGER.warning(
                        "Undefined target temperature, falling back to %s",
                        self._target_temp,
                    )
                else:
                    self._target_temp = float(
                        old_state.attributes[ATTR_TEMPERATURE])
            if old_state.attributes.get(ATTR_PRESET_MODE) == PRESET_AWAY:
                self._is_away = True
            if not self._hvac_mode and old_state.state:
                self._hvac_mode = old_state.state

        else:
            # No previous state, try and restore defaults
            if self._target_temp is None:
                if self.ac_mode:
                    self._target_temp = self.max_temp
                else:
                    self._target_temp = self.min_temp
            _LOGGER.warning("No previously saved temperature, setting to %s",
                            self._target_temp)

        # Set default state to off
        if not self._hvac_mode:
            self._hvac_mode = HVAC_MODE_OFF
Esempio n. 23
0
async def async_attach_trigger(hass, config, action, automation_info):
    """Listen for state changes based on configuration."""
    entities = {}
    removes = []

    @callback
    def time_automation_listener(description, now):
        """Listen for time changes and calls action."""
        hass.async_run_job(
            action,
            {
                "trigger": {
                    "platform": "time",
                    "now": now,
                    "description": description
                }
            },
        )

    @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.get(entity_id)
        if remove:
            remove()
            removes.remove(remove)
            remove = None

        # Check state of entity. If valid, set up a listener.
        if new_state:
            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 = dt_util.DEFAULT_TIME_ZONE.localize(
                    datetime(year, month, day, hour, minute, second))
                # Only set up listener if time is now or in the future.
                if trigger_dt >= dt_util.now():
                    remove = async_track_point_in_time(
                        hass,
                        partial(time_automation_listener,
                                f"time set in {entity_id}"),
                        trigger_dt,
                    )
            elif has_time:
                # Else if it has time, then track time change.
                remove = async_track_time_change(
                    hass,
                    partial(time_automation_listener,
                            f"time set in {entity_id}"),
                    hour=hour,
                    minute=minute,
                    second=second,
                )

        # Was a listener set up?
        if remove:
            removes.append(remove)

        entities[entity_id] = remove

    for at_time in config[CONF_AT]:
        if isinstance(at_time, str):
            # input_datetime entity
            update_entity_trigger(at_time, new_state=hass.states.get(at_time))
        else:
            # datetime.time
            removes.append(
                async_track_time_change(
                    hass,
                    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(hass, list(entities),
                                       update_entity_trigger_event))

    @callback
    def remove_track_time_changes():
        """Remove tracked time changes."""
        for remove in removes:
            remove()

    return remove_track_time_changes
Esempio n. 24
0
class IntegrationSensor(RestoreEntity, SensorEntity):
    """Representation of an integration sensor."""
    def __init__(
        self,
        source_entity,
        name,
        round_digits,
        unit_prefix,
        unit_time,
        unit_of_measurement,
        integration_method,
    ):
        """Initialize the integration sensor."""
        self._sensor_source_id = source_entity
        self._round_digits = round_digits
        self._state = None
        self._method = integration_method

        self._name = name if name is not None else f"{source_entity} integral"
        self._unit_template = (
            f"{'' if unit_prefix is None else unit_prefix}{{}}{unit_time}")
        self._unit_of_measurement = unit_of_measurement
        self._unit_prefix = UNIT_PREFIXES[unit_prefix]
        self._unit_time = UNIT_TIME[unit_time]
        self._attr_state_class = SensorStateClass.TOTAL

    async def async_added_to_hass(self):
        """Handle entity which will be added."""
        await super().async_added_to_hass()
        if state := await self.async_get_last_state():
            try:
                self._state = Decimal(state.state)
            except (DecimalException, ValueError) as err:
                _LOGGER.warning("Could not restore last state: %s", err)
            else:
                self._attr_device_class = state.attributes.get(
                    ATTR_DEVICE_CLASS)
                if self._unit_of_measurement is None:
                    self._unit_of_measurement = state.attributes.get(
                        ATTR_UNIT_OF_MEASUREMENT)

        @callback
        def calc_integration(event):
            """Handle the sensor state changes."""
            old_state = event.data.get("old_state")
            new_state = event.data.get("new_state")

            if self._unit_of_measurement is None:
                unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
                self._unit_of_measurement = self._unit_template.format(
                    "" if unit is None else unit)
            if (self.device_class is None
                    and new_state.attributes.get(ATTR_DEVICE_CLASS)
                    == SensorDeviceClass.POWER):
                self._attr_device_class = SensorDeviceClass.ENERGY

            if (old_state is None or new_state is None
                    or old_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE)
                    or new_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE)):
                return

            try:
                # integration as the Riemann integral of previous measures.
                area = 0
                elapsed_time = (new_state.last_updated -
                                old_state.last_updated).total_seconds()

                if self._method == TRAPEZOIDAL_METHOD:
                    area = (
                        (Decimal(new_state.state) + Decimal(old_state.state)) *
                        Decimal(elapsed_time) / 2)
                elif self._method == LEFT_METHOD:
                    area = Decimal(old_state.state) * Decimal(elapsed_time)
                elif self._method == RIGHT_METHOD:
                    area = Decimal(new_state.state) * Decimal(elapsed_time)

                integral = area / (self._unit_prefix * self._unit_time)
                assert isinstance(integral, Decimal)
            except ValueError as err:
                _LOGGER.warning("While calculating integration: %s", err)
            except DecimalException as err:
                _LOGGER.warning("Invalid state (%s > %s): %s", old_state.state,
                                new_state.state, err)
            except AssertionError as err:
                _LOGGER.error("Could not calculate integral: %s", err)
            else:
                if isinstance(self._state, Decimal):
                    self._state += integral
                else:
                    self._state = integral
                self.async_write_ha_state()

        async_track_state_change_event(self.hass, [self._sensor_source_id],
                                       calc_integration)
Esempio n. 25
0
 async def set_hass(self, hass: HomeAssistant):
     self.hass = hass
     async_track_state_change_event(hass, self.switches,
                                    self.external_switch_changed)
Esempio n. 26
0
async def async_attach_trigger(hass, config, action, automation_info):
    """Listen for state changes based on configuration."""
    trigger_data = automation_info.get("trigger_data",
                                       {}) if automation_info else {}
    entities = {}
    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.
        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(
                        hass,
                        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(
                    hass,
                    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(
                    hass,
                    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=hass.states.get(at_time))
        else:
            # datetime.time
            removes.append(
                async_track_time_change(
                    hass,
                    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(hass, 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. 27
0
 async def async_added_to_hass(self) -> None:
     """Register state listeners."""
     async_track_state_change_event(self.hass, self._entities,
                                    self.on_state_change)
Esempio n. 28
0
    async def async_added_to_hass(self):
        """Handle entity which will be added."""
        await super().async_added_to_hass()
        state = await self.async_get_last_state()
        if state is not None:
            try:
                self._state = Decimal(state.state)
            except SyntaxError as err:
                _LOGGER.warning("Could not restore last state: %s", err)

        @callback
        def calc_derivative(event):
            """Handle the sensor state changes."""
            old_state = event.data.get("old_state")
            new_state = event.data.get("new_state")
            if (old_state is None
                    or old_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE)
                    or new_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE)):
                return

            now = new_state.last_updated
            # Filter out the tuples that are older than (and outside of the) `time_window`
            self._state_list = [
                (timestamp, state) for timestamp, state in self._state_list
                if (now - timestamp).total_seconds() < self._time_window
            ]
            # It can happen that the list is now empty, in that case
            # we use the old_state, because we cannot do anything better.
            if len(self._state_list) == 0:
                self._state_list.append(
                    (old_state.last_updated, old_state.state))
            self._state_list.append((new_state.last_updated, new_state.state))

            if self._unit_of_measurement is None:
                unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
                self._unit_of_measurement = self._unit_template.format(
                    "" if unit is None else unit)

            try:
                # derivative of previous measures.
                last_time, last_value = self._state_list[-1]
                first_time, first_value = self._state_list[0]

                elapsed_time = (last_time - first_time).total_seconds()
                delta_value = Decimal(last_value) - Decimal(first_value)
                derivative = (delta_value / Decimal(elapsed_time) /
                              Decimal(self._unit_prefix) *
                              Decimal(self._unit_time))
                assert isinstance(derivative, Decimal)
            except ValueError as err:
                _LOGGER.warning("While calculating derivative: %s", err)
            except DecimalException as err:
                _LOGGER.warning("Invalid state (%s > %s): %s", old_state.state,
                                new_state.state, err)
            except AssertionError as err:
                _LOGGER.error("Could not calculate derivative: %s", err)
            else:
                self._state = derivative
                self.async_write_ha_state()

        async_track_state_change_event(self.hass, [self._sensor_source_id],
                                       calc_derivative)
Esempio n. 29
0
    async def async_added_to_hass(self):
        """Register callbacks."""

        if "recorder" in self.hass.config.components:
            history_list = []
            largest_window_items = 0
            largest_window_time = timedelta(0)

            # Determine the largest window_size by type
            for filt in self._filters:
                if (
                    filt.window_unit == WINDOW_SIZE_UNIT_NUMBER_EVENTS
                    and largest_window_items < filt.window_size
                ):
                    largest_window_items = filt.window_size
                elif (
                    filt.window_unit == WINDOW_SIZE_UNIT_TIME
                    and largest_window_time < filt.window_size
                ):
                    largest_window_time = filt.window_size

            # Retrieve the largest window_size of each type
            if largest_window_items > 0:
                filter_history = await self.hass.async_add_executor_job(
                    partial(
                        history.get_last_state_changes,
                        self.hass,
                        largest_window_items,
                        entity_id=self._entity,
                    )
                )
                if self._entity in filter_history:
                    history_list.extend(filter_history[self._entity])
            if largest_window_time > timedelta(seconds=0):
                start = dt_util.utcnow() - largest_window_time
                filter_history = await self.hass.async_add_executor_job(
                    partial(
                        history.state_changes_during_period,
                        self.hass,
                        start,
                        entity_id=self._entity,
                    )
                )
                if self._entity in filter_history:
                    history_list.extend(
                        [
                            state
                            for state in filter_history[self._entity]
                            if state not in history_list
                        ]
                    )

            # Sort the window states
            history_list = sorted(history_list, key=lambda s: s.last_updated)
            _LOGGER.debug(
                "Loading from history: %s",
                [(s.state, s.last_updated) for s in history_list],
            )

            # Replay history through the filter chain
            for state in history_list:
                if state.state not in [STATE_UNKNOWN, STATE_UNAVAILABLE, None]:
                    self._update_filter_sensor_state(state, False)

        self.async_on_remove(
            async_track_state_change_event(
                self.hass, [self._entity], self._update_filter_sensor_state_event
            )
        )
Esempio n. 30
0
class IntegrationSensor(RestoreEntity, SensorEntity):
    """Representation of an integration sensor."""
    def __init__(
        self,
        *,
        integration_method: str,
        name: str | None,
        round_digits: int,
        source_entity: str,
        unique_id: str | None,
        unit_prefix: str | None,
        unit_time: str,
    ) -> None:
        """Initialize the integration sensor."""
        self._attr_unique_id = unique_id
        self._sensor_source_id = source_entity
        self._round_digits = round_digits
        self._state = None
        self._method = integration_method

        self._attr_name = name if name is not None else f"{source_entity} integral"
        self._unit_template = f"{'' if unit_prefix is None else unit_prefix}{{}}"
        self._unit_of_measurement = None
        self._unit_prefix = UNIT_PREFIXES[unit_prefix]
        self._unit_time = UNIT_TIME[unit_time]
        self._unit_time_str = unit_time
        self._attr_state_class = SensorStateClass.TOTAL
        self._attr_icon = "mdi:chart-histogram"
        self._attr_should_poll = False
        self._attr_extra_state_attributes = {ATTR_SOURCE_ID: source_entity}

    def _unit(self, source_unit: str) -> str:
        """Derive unit from the source sensor, SI prefix and time unit."""
        unit_time = self._unit_time_str
        if source_unit.endswith(f"/{unit_time}"):
            integral_unit = source_unit[0:(-(1 + len(unit_time)))]
        else:
            integral_unit = f"{source_unit}{unit_time}"

        return self._unit_template.format(integral_unit)

    async def async_added_to_hass(self):
        """Handle entity which will be added."""
        await super().async_added_to_hass()
        if state := await self.async_get_last_state():
            try:
                self._state = Decimal(state.state)
            except (DecimalException, ValueError) as err:
                _LOGGER.warning(
                    "%s could not restore last state %s: %s",
                    self.entity_id,
                    state.state,
                    err,
                )
            else:
                self._attr_device_class = state.attributes.get(
                    ATTR_DEVICE_CLASS)
                if self._unit_of_measurement is None:
                    self._unit_of_measurement = state.attributes.get(
                        ATTR_UNIT_OF_MEASUREMENT)

        @callback
        def calc_integration(event):
            """Handle the sensor state changes."""
            old_state = event.data.get("old_state")
            new_state = event.data.get("new_state")

            if new_state is None or new_state.state in (
                    STATE_UNKNOWN,
                    STATE_UNAVAILABLE,
            ):
                return

            # We may want to update our state before an early return,
            # based on the source sensor's unit_of_measurement
            # or device_class.
            update_state = False
            unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
            if unit is not None:
                new_unit_of_measurement = self._unit(unit)
                if self._unit_of_measurement != new_unit_of_measurement:
                    self._unit_of_measurement = new_unit_of_measurement
                    update_state = True

            if (self.device_class is None
                    and new_state.attributes.get(ATTR_DEVICE_CLASS)
                    == SensorDeviceClass.POWER):
                self._attr_device_class = SensorDeviceClass.ENERGY
                update_state = True

            if update_state:
                self.async_write_ha_state()

            if old_state is None or old_state.state in (
                    STATE_UNKNOWN,
                    STATE_UNAVAILABLE,
            ):
                return

            try:
                # integration as the Riemann integral of previous measures.
                area = 0
                elapsed_time = (new_state.last_updated -
                                old_state.last_updated).total_seconds()

                if self._method == METHOD_TRAPEZOIDAL:
                    area = (
                        (Decimal(new_state.state) + Decimal(old_state.state)) *
                        Decimal(elapsed_time) / 2)
                elif self._method == METHOD_LEFT:
                    area = Decimal(old_state.state) * Decimal(elapsed_time)
                elif self._method == METHOD_RIGHT:
                    area = Decimal(new_state.state) * Decimal(elapsed_time)

                integral = area / (self._unit_prefix * self._unit_time)
                assert isinstance(integral, Decimal)
            except ValueError as err:
                _LOGGER.warning("While calculating integration: %s", err)
            except DecimalException as err:
                _LOGGER.warning("Invalid state (%s > %s): %s", old_state.state,
                                new_state.state, err)
            except AssertionError as err:
                _LOGGER.error("Could not calculate integral: %s", err)
            else:
                if isinstance(self._state, Decimal):
                    self._state += integral
                else:
                    self._state = integral
                self.async_write_ha_state()

        self.async_on_remove(
            async_track_state_change_event(self.hass, [self._sensor_source_id],
                                           calc_integration))