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/tapo_control/sensor.py
"""Tapo camera sensors."""

import re

from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
    PERCENTAGE,
    SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
    STATE_UNAVAILABLE,
    UnitOfInformation,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from .const import DOMAIN, ENABLE_MEDIA_SYNC, LOGGER
from .tapo.entities import TapoSensorEntity


async def async_setup_entry(
    hass: HomeAssistant,
    config_entry: ConfigEntry,
    async_add_entities: AddEntitiesCallback,
) -> None:
    """Setup Tapo camera sensors using config entry."""
    LOGGER.debug("Setting up sensors")
    entry = hass.data[DOMAIN][config_entry.entry_id]

    async def setupEntities(entry: dict) -> list:
        """Setup the entities."""
        sensors = []

        if "camData" in entry:
            camData: dict = entry["camData"]
            if "basic_info" in camData and "battery_percent" in camData["basic_info"]:
                LOGGER.debug("Adding tapoBatterySensor...")
                sensors.append(TapoBatterySensor(entry, hass, config_entry))

            if (
                camData.get("connectionInformation", False) is not False
                and camData["connectionInformation"] is not None
            ):
                if "ssid" in camData["connectionInformation"]:
                    LOGGER.debug("Adding TapoSSIDSensor...")
                    sensors.append(TapoSSIDSensor(entry, hass, config_entry))
                if "link_type" in camData["connectionInformation"]:
                    LOGGER.debug("Adding TapoLinkTypeSensor...")
                    sensors.append(TapoLinkTypeSensor(entry, hass, config_entry))
                if "rssiValue" in camData["connectionInformation"]:
                    LOGGER.debug("Adding TapoRSSISensor...")
                    sensors.append(TapoRSSISensor(entry, hass, config_entry))

            if "sdCardData" in camData and len(camData["sdCardData"]) > 0:
                for hdd in camData["sdCardData"]:
                    for field in hdd:
                        LOGGER.debug(
                            "Adding TapoHDDSensor for disk %s and property %s...",
                            hdd["disk_name"],
                            field,
                        )
                        sensors.append(
                            TapoHDDSensor(
                                entry, hass, config_entry, hdd["disk_name"], field
                            )
                        )
            if (
                "basic_info" in camData
                and camData["basic_info"] is not None
                and "signal_level" in camData["basic_info"]
                and camData["basic_info"]["signal_level"] is not None
            ):
                sensors.append(TapoChimeSignalLevel(entry, hass, config_entry))

        if entry["controller"].isKLAP is False:
            sensors.append(TapoSyncSensor(entry, hass, config_entry))

        return sensors

    sensors = await setupEntities(entry)
    for childDevice in entry["childDevices"]:
        sensors.extend(await setupEntities(childDevice))

    async_add_entities(sensors)


class TapoRSSISensor(TapoSensorEntity):
    """Tapo RSSI sensor entity."""

    _attr_device_class = SensorDeviceClass.SIGNAL_STRENGTH
    _attr_entity_category = EntityCategory.DIAGNOSTIC
    _attr_state_class = SensorStateClass.MEASUREMENT
    _attr_native_unit_of_measurement = SIGNAL_STRENGTH_DECIBELS_MILLIWATT

    def __init__(
        self, entry: dict, hass: HomeAssistant, config_entry: ConfigEntry
    ) -> None:
        """Initialize the entity."""
        TapoSensorEntity.__init__(
            self,
            "RSSI",
            entry,
            hass,
            config_entry,
            "mdi:signal-variant",
            None,
        )

    async def async_update(self) -> None:
        await self._coordinator.async_request_refresh()

    def updateTapo(self, camData: dict | None) -> None:
        """Update the entity."""
        if (
            not camData
            or camData["connectionInformation"] is False
            or "rssiValue" not in camData["connectionInformation"]
        ):
            self._attr_native_value = STATE_UNAVAILABLE
        else:
            self._attr_native_value = camData["connectionInformation"]["rssiValue"]


class TapoLinkTypeSensor(TapoSensorEntity):
    """Tapo link type sensor entity."""

    _attr_entity_category = EntityCategory.DIAGNOSTIC

    def __init__(
        self, entry: dict, hass: HomeAssistant, config_entry: ConfigEntry
    ) -> None:
        """Initialize the entity."""
        TapoSensorEntity.__init__(
            self,
            "Link Type",
            entry,
            hass,
            config_entry,
            "mdi:connection",
            None,
        )

    async def async_update(self) -> None:
        """Update the entity."""
        await self._coordinator.async_request_refresh()

    def updateTapo(self, camData: dict | None) -> None:
        """Update the entity."""
        if (
            not camData
            or camData["connectionInformation"] is False
            or "link_type" not in camData["connectionInformation"]
        ):
            self._attr_native_value = STATE_UNAVAILABLE
        else:
            self._attr_native_value = camData["connectionInformation"]["link_type"]


class TapoChimeSignalLevel(TapoSensorEntity):
    """Tapo Chime Signal Level sensor entity."""

    _attr_entity_category = EntityCategory.DIAGNOSTIC

    def __init__(
        self, entry: dict, hass: HomeAssistant, config_entry: ConfigEntry
    ) -> None:
        """Initialize the entity."""
        TapoSensorEntity.__init__(
            self,
            "Signal Level",
            entry,
            hass,
            config_entry,
            "mdi:signal",
            None,
        )

    async def async_update(self) -> None:
        """Update the entity."""
        await self._coordinator.async_request_refresh()

    def updateTapo(self, camData: dict | None) -> None:
        """Update the entity."""
        if (
            not camData
            or camData["basic_info"] is False
            or camData["basic_info"] is None
            or "signal_level" not in camData["basic_info"]
        ):
            self._attr_native_value = STATE_UNAVAILABLE
        else:
            self._attr_native_value = camData["basic_info"]["signal_level"]


class TapoSSIDSensor(TapoSensorEntity):
    """Tapo SSID sensor entity."""

    _attr_entity_category = EntityCategory.DIAGNOSTIC

    def __init__(
        self, entry: dict, hass: HomeAssistant, config_entry: ConfigEntry
    ) -> None:
        """Initialize the entity."""
        TapoSensorEntity.__init__(
            self,
            "Network SSID",
            entry,
            hass,
            config_entry,
            "mdi:wifi",
            None,
        )

    async def async_update(self) -> None:
        """Update the entity."""
        await self._coordinator.async_request_refresh()

    def updateTapo(self, camData: dict | None) -> None:
        """Update the entity."""
        if (
            not camData
            or camData["connectionInformation"] is False
            or "ssid" not in camData["connectionInformation"]
        ):
            self._attr_native_value = STATE_UNAVAILABLE
        else:
            self._attr_native_value = camData["connectionInformation"]["ssid"]


class TapoBatterySensor(TapoSensorEntity):
    """Tapo battery sensor entity."""

    _attr_device_class = SensorDeviceClass.BATTERY
    _attr_entity_category = EntityCategory.DIAGNOSTIC
    _attr_state_class = SensorStateClass.MEASUREMENT
    _attr_native_unit_of_measurement = PERCENTAGE

    def __init__(
        self, entry: dict, hass: HomeAssistant, config_entry: ConfigEntry
    ) -> None:
        """Initialize the entity."""
        TapoSensorEntity.__init__(
            self,
            "Battery",
            entry,
            hass,
            config_entry,
            "mdi:battery",
            SensorDeviceClass.BATTERY,
        )

    async def async_update(self) -> None:
        """Update the entity."""
        await self._coordinator.async_request_refresh()

    def updateTapo(self, camData: dict | None) -> None:
        """Update the entity."""
        if not camData:
            self._attr_native_value = STATE_UNAVAILABLE
        else:
            self._attr_native_value = camData["basic_info"]["battery_percent"]


class TapoHDDSensor(TapoSensorEntity):
    """Tapo HDD sensor entities."""

    _attr_entity_category = EntityCategory.DIAGNOSTIC
    _attr_entity_registry_enabled_default = False

    def __init__(
        self,
        entry: dict,
        hass: HomeAssistant,
        config_entry: ConfigEntry,
        sensorName: str,
        sensorProperty: str,
    ) -> None:
        """Initialize the entity."""
        self._sensor_name = sensorName
        self._sensor_property = sensorProperty
        TapoSensorEntity.__init__(
            self,
            f"Disk {sensorName} {sensorProperty}",
            entry,
            hass,
            config_entry,
            "mdi:sd",
            None,
        )

    async def async_update(self) -> None:
        """Update the entity."""
        await self._coordinator.async_request_refresh()

    def updateTapo(self, camData: dict | None) -> None:
        """Update the entity."""
        state = STATE_UNAVAILABLE
        if camData and "sdCardData" in camData and len(camData["sdCardData"]) > 0:
            for hdd in camData["sdCardData"]:
                if hdd["disk_name"] == self._sensor_name:
                    state = hdd[self._sensor_property]
        if "space" in self._sensor_property and (
            match := re.search(r"[-+]?\d*\.?\d+", state)
        ):
            value = match.group()
            if (unit := state.replace(value, "")) in UnitOfInformation:
                self._attr_device_class = SensorDeviceClass.DATA_SIZE
                self._attr_native_unit_of_measurement = UnitOfInformation(unit)
                self._attr_suggested_unit_of_measurement = UnitOfInformation.GIGABYTES
                state = value
        self._attr_native_value = state


class TapoSyncSensor(TapoSensorEntity):
    """Tapo sync sensor entities."""

    _attr_entity_category = EntityCategory.DIAGNOSTIC

    def __init__(
        self, entry: dict, hass: HomeAssistant, config_entry: ConfigEntry
    ) -> None:
        """Initialize the entity."""
        TapoSensorEntity.__init__(
            self,
            "Recordings Synchronization",
            entry,
            hass,
            config_entry,
            None,
            None,
        )

    async def async_update(self) -> None:
        """Update the entity."""
        await self._coordinator.async_request_refresh()

    def updateTapo(self, camData: dict | None) -> None:
        """Update the entity."""
        enable_media_sync = self._hass.data[DOMAIN][self._config_entry.entry_id][
            ENABLE_MEDIA_SYNC
        ]
        runningMediaSync = self._hass.data[DOMAIN][self._config_entry.entry_id][
            "runningMediaSync"
        ]
        LOGGER.debug("Enable Media Sync: %s", enable_media_sync)
        if enable_media_sync or runningMediaSync is True:
            data = self._hass.data[DOMAIN][self._config_entry.entry_id]
            LOGGER.debug("Initial Media Scan: %s", data["initialMediaScanDone"])
            LOGGER.debug("Media Sync Available: %s", data["mediaSyncAvailable"])
            LOGGER.debug("Download Progress: %s", data["downloadProgress"])
            LOGGER.debug("Running media sync: %s", data["runningMediaSync"])
            LOGGER.debug("Media Sync Schedueled: %s", data["mediaSyncScheduled"])
            LOGGER.debug("Media Sync Ran Once: %s", data["mediaSyncRanOnce"])

            if not data["initialMediaScanDone"] or (
                data["initialMediaScanDone"] and not data["mediaSyncRanOnce"]
            ):
                self._attr_native_value = "Starting"
            elif not data["mediaSyncAvailable"]:
                self._attr_native_value = "No Recordings Found"
            elif data["downloadProgress"]:
                if data["downloadProgress"] == "Finished download":
                    self._attr_native_value = "Idle"
                else:
                    self._attr_native_value = data["downloadProgress"]
            else:
                self._attr_native_value = "Idle"
        else:
            self._attr_native_value = "Idle"