HEX
Server: Apache/2.4.63 (Unix)
System: Linux Synopilou92 4.4.302+ #72806 SMP Mon Jul 21 23:16:00 CST 2025 x86_64
User: pilou92 (1026)
PHP: 8.0.30
Disabled: NONE
Upload Files
File: /volume1/docker/homeassistant2025/custom_components/sleep_as_android/sensor.py
"""Sensor for Sleep as android states."""

import json
import logging
from typing import TYPE_CHECKING

from homeassistant.components.sensor import RestoreSensor, SensorDeviceClass
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.entity_registry import async_entries_for_config_entry

from .const import DOMAIN, sleep_tracking_states
from .device_trigger import TRIGGERS

if TYPE_CHECKING:
    from . import SleepAsAndroidInstance

_LOGGER = logging.getLogger(__name__)


async def async_setup_entry(
    hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities
):
    """Set up the sensor entry."""

    async def add_configured_entities():
        """Scan entity registry and add previously created entities to Home Assistant."""

        entities = async_entries_for_config_entry(
            instance.entity_registry, config_entry.entry_id
        )
        sensors: list[SleepAsAndroidSensor] = []
        for entity in entities:

            device_name = instance.device_name_from_entity_id(entity.unique_id)
            _LOGGER.debug(
                f"add_configured_entities: creating sensor with name {device_name}"
            )
            (sensor, _) = instance.get_sensor(device_name)
            sensors.append(sensor)

        async_add_entities(sensors)

    instance: SleepAsAndroidInstance = hass.data[DOMAIN][config_entry.entry_id]
    await add_configured_entities()
    _LOGGER.debug("async_setup_entry: adding configured entities is finished.")
    _LOGGER.debug("Going to subscribe to root topic.")
    await instance.subscribe_root_topic(async_add_entities)
    _LOGGER.debug("async_setup_entry is finished")
    return True


class SleepAsAndroidSensor(RestoreSensor):
    """Sensor for the integration."""

    __additional_attributes: dict[str, str] = {
        "value1": "timestamp",
        "value2": "label",
    }
    """Mapping for value*.

    It is comfortable to have human readable names.
    Keys is field names from SleepAsAndroid event https://docs.sleep.urbandroid.org/services/automation.html#events
    Values is sensor attributes.
    """
    _attr_icon = "mdi:sleep"
    _attr_should_poll = False
    _attr_device_class = SensorDeviceClass.ENUM
    _attr_options = [
        STATE_UNKNOWN,
        *sleep_tracking_states,
    ]
    _attr_translation_key = "sleep_as_android_status"

    def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry, name: str):
        """Initialize entry."""
        self._instance: SleepAsAndroidInstance = hass.data[DOMAIN][
            config_entry.entry_id
        ]

        self.hass: HomeAssistant = hass

        self._name: str = name
        self._attr_native_value: str = STATE_UNKNOWN
        self._device_id: str = "unknown"
        self._attr_extra_state_attributes = {}
        self._set_attributes(
            {}
        )  # initiate _attr_extra_state_attributes with empty values
        _LOGGER.debug(f"Creating sensor with name {name}")

    async def async_added_to_hass(self):
        """When sensor added to Home Assistant.

        Should create device for sensor here
        """
        await super().async_added_to_hass()
        device_registry = dr.async_get(self.hass)
        device = device_registry.async_get_device(
            identifiers=self.device_info["identifiers"], connections=set()
        )
        _LOGGER.debug("My device id is %s", device.id)
        self._device_id = device.id

        if (old_state := await self.async_get_last_sensor_data()) is not None:
            self._attr_native_value = old_state.native_value
            if self._attr_native_value.lower() == STATE_UNKNOWN.lower():
                _LOGGER.debug(
                    f"Got {self._attr_native_value=}. Will use {STATE_UNKNOWN=} instead"
                )
                self._attr_native_value = STATE_UNKNOWN
            _LOGGER.debug(
                f"async_added_to_hass: restored previous state for {self.name}: "
                f"{self.state=}, {self.native_value=}."
            )
        else:
            # No previous state. It is fine, but it would be nice to report
            _LOGGER.debug(
                f"async_added_to_hass: no previously saved state for {self.name}"
            )

        self.async_write_ha_state()

    async def async_will_remove_from_hass(self):
        """When sensor is removed from Home Assistant.

        Should remove device here
        """
        # ToDo: should we remove device?
        pass

    def process_message(self, msg):
        """Process new MQTT messages.

        Set sensor state, attributes and fire events.

        :param msg: MQTT message
        """
        _LOGGER.debug(f"Processing message {msg}")
        try:
            new_state = STATE_UNKNOWN
            payload = json.loads(msg.payload)
            try:
                new_state = payload["event"]
                if new_state.lower() == STATE_UNKNOWN.lower():
                    _LOGGER.debug(
                        f"Got {payload['event']=}. Will use {STATE_UNKNOWN=} instead"
                    )
                    new_state = STATE_UNKNOWN
            except KeyError:
                _LOGGER.warning("Got unexpected payload: '%s'", payload)

            self._set_attributes(payload)
            if self.state != new_state:
                self._attr_native_value = new_state
                self.async_write_ha_state()
            else:
                _LOGGER.debug(
                    f"Will not update state because old {self.state=} == {new_state=}"
                )

            # fire events in any case: we may have same state but changed labels
            self._fire_event(self.state)
            self._fire_trigger(self.state)

        except json.decoder.JSONDecodeError:
            _LOGGER.warning("expected JSON payload. got '%s' instead", msg.payload)

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

    @property
    def unique_id(self) -> str:
        """Return a unique ID."""
        return self._instance.create_entity_id(self._name)

    @property
    def available(self) -> bool:
        """Is sensor available or not."""
        return self.native_value != STATE_UNKNOWN

    @property
    def device_id(self) -> str:
        """Device identification for sensor."""
        return self._device_id

    @property
    def device_info(self):
        """Device info for sensor."""
        _LOGGER.debug("My identifiers is %s", {(DOMAIN, self.unique_id)})
        info = {
            "identifiers": {(DOMAIN, self.unique_id)},
            "name": self.name,
            "manufacturer": "SleepAsAndroid",
            "model": "MQTT",
        }
        return info

    def _fire_event(self, event_payload: str):
        """Fire event with payload {'event': event_payload}.

        :param event_payload: payload for event
        """
        payload = {"event": event_payload}
        _LOGGER.debug("Firing '%s' with payload: '%s'", self.name, payload)
        self.hass.bus.fire(self.name, payload)

    def _fire_trigger(self, new_state: str):
        """Fire trigger based on new state.

        :param new_state: type of trigger to fire
        """
        if new_state in TRIGGERS:
            self.hass.bus.async_fire(
                DOMAIN + "_event", {"device_id": self.device_id, "type": new_state}
            )
        else:
            _LOGGER.warning(
                "Got %s event, but it is not in TRIGGERS list: will not fire this event for "
                "trigger!",
                new_state,
            )

    def _set_attributes(self, payload: dict):
        new_attributes = {}
        for k, v in self.__additional_attributes.items():
            new_attributes[v] = payload.get(k, STATE_UNAVAILABLE)
        _LOGGER.debug(f"New attributes is {new_attributes}")
        return self._attr_extra_state_attributes.update(new_attributes)