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/update.py
from typing import Callable

from homeassistant.core import HomeAssistant

from homeassistant.config_entries import ConfigEntry
from homeassistant.components.update import UpdateEntity, UpdateEntityFeature
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers import device_registry as dr
import datetime

from .const import DOMAIN, LOGGER
from .utils import build_device_info
from .tapo.entities import TapoUpdateEntity


async def async_setup_entry(
    hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable
):
    entry = hass.data[DOMAIN][config_entry.entry_id]

    async def setupEntities(entry):
        updates = []
        if entry["controller"].isKLAP is False:
            entry["updateEntity"] = TapoCamUpdate(entry, hass, entry)
            updates.append(entry["updateEntity"])

        return updates

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

    async_add_entities(updates)


class TapoCamUpdate(UpdateEntity):
    def __init__(self, entry: dict, hass: HomeAssistant, config_entry):
        self._in_progress = False
        self._hass = hass
        self._lastDataUpdate = 0
        # This is needed because some cameras still report normal firmware update process
        # just after hitting update, even if they are in progress, we need to give them time.
        self._installRequestedTime = 0
        TapoUpdateEntity.__init__(self, "Update", entry, hass, config_entry)

    @property
    def name(self) -> str:
        return "{} {}".format(self._name, self._name_suffix)

    @property
    def device_info(self) -> DeviceInfo:
        return build_device_info(self._attributes)

    @property
    def unique_id(self) -> str:
        id_suffix = "".join(self._name_suffix.split())
        return "{}-{}-{}".format(self._attributes["mac"], self._name, id_suffix).lower()

    def updateTapo(self, camData):
        # prevent instantinous internal refresh with old data triggering update
        # on this entity and cancelling in progress update
        LOGGER.debug("updateTapo in update entity")
        if camData and camData["updated"] > self._lastDataUpdate:
            LOGGER.debug(f"Processing new data (updated at {camData["updated"]})...")
            self._lastDataUpdate = camData["updated"]
            self._attributes = camData["basic_info"]
            if self._in_progress:
                LOGGER.debug("Status of firmware status:")
                LOGGER.debug(camData["firmwareUpdateStatus"])
                ts = datetime.datetime.utcnow().timestamp()
                if (
                    ts > self._installRequestedTime + 60
                    and "firmwareUpdateStatus" in camData
                    and "upgrade_status" in camData["firmwareUpdateStatus"]
                    and "state" in camData["firmwareUpdateStatus"]["upgrade_status"]
                    and camData["firmwareUpdateStatus"]["upgrade_status"]["state"]
                    == "normal"
                ):
                    LOGGER.debug(
                        "Update has been finished, updating information in integration..."
                    )
                    # Update Device Registry with new information
                    deviceRegistry = dr.async_get(self._hass)
                    newDeviceInfo = build_device_info(camData["basic_info"])
                    device = deviceRegistry.async_get_device(
                        newDeviceInfo["identifiers"]
                    )
                    deviceRegistry.async_update_device(
                        device.id, sw_version=newDeviceInfo["sw_version"]
                    )
                    # Reset check for firmware check
                    self._entry["lastFirmwareCheck"] = 0
                    self._in_progress = False
                    LOGGER.debug("Update has been fully completed.")

    async def async_added_to_hass(self) -> None:
        self._enabled = True

    async def async_will_remove_from_hass(self) -> None:
        self._enabled = False

    @property
    def supported_features(self):
        return (
            UpdateEntityFeature.INSTALL
            | UpdateEntityFeature.PROGRESS
            | UpdateEntityFeature.RELEASE_NOTES
        )

    async def async_release_notes(self) -> str:
        """Return the release notes."""
        if (
            self._entry["latestFirmwareVersion"]
            and "release_log" in self._entry["latestFirmwareVersion"]
        ):
            return self._entry["latestFirmwareVersion"]["release_log"].replace(
                "\\n", "\n"
            )
        else:
            return "No update available."

    @property
    def in_progress(self) -> bool:
        return self._in_progress

    @property
    def installed_version(self) -> str:
        return self._attributes["sw_version"]

    @property
    def latest_version(self) -> str:
        if (
            self._entry["latestFirmwareVersion"]
            and "version" in self._entry["latestFirmwareVersion"]
        ):
            LOGGER.debug("Latest version - coming from cloud")
            LOGGER.debug(self._entry["latestFirmwareVersion"])
            return self._entry["latestFirmwareVersion"]["version"]
        else:
            LOGGER.debug("Latest version - no cloud update")
            LOGGER.debug(self._attributes["sw_version"])
            return self._attributes["sw_version"]

    @property
    def release_summary(self) -> str:
        if (
            self._entry["latestFirmwareVersion"]
            and "release_log" in self._entry["latestFirmwareVersion"]
        ):
            LOGGER.debug("Release_summary - coming from cloud")
            maxLength = 255
            releaseLog = self._entry["latestFirmwareVersion"]["release_log"].replace(
                "\\n", "\n"
            )
            return (
                (releaseLog[: maxLength - 3] + "...")
                if len(releaseLog) > maxLength
                else releaseLog
            )
        else:
            LOGGER.debug("Release_summary - none")
            return None

    async def async_install(
        self,
        version,
        backup,
    ):
        LOGGER.debug("Install new firmware has been triggerred")
        LOGGER.debug(version)
        LOGGER.debug(backup)
        try:
            await self.hass.async_add_executor_job(
                self._controller.startFirmwareUpgrade
            )
            self._installRequestedTime = datetime.datetime.utcnow().timestamp()
            self._in_progress = True
            LOGGER.debug("Install is now in progress...")
        except Exception as e:
            LOGGER.error(e)