From e3497e79c2126ad5469e3f85ca5b8a244ee1a0fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Nyi=CC=81ri?= Date: Wed, 6 Apr 2022 22:55:02 +0200 Subject: [PATCH 1/5] Add: WIP update entity --- custom_components/tapo_control/__init__.py | 19 ++++ custom_components/tapo_control/const.py | 1 + custom_components/tapo_control/manifest.json | 2 +- custom_components/tapo_control/update.py | 112 +++++++++++++++++++ custom_components/tapo_control/utils.py | 16 +++ 5 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 custom_components/tapo_control/update.py diff --git a/custom_components/tapo_control/__init__.py b/custom_components/tapo_control/__init__.py index d0106ca..0ba391c 100644 --- a/custom_components/tapo_control/__init__.py +++ b/custom_components/tapo_control/__init__.py @@ -23,6 +23,7 @@ SOUND_DETECTION_PEAK, SOUND_DETECTION_RESET, TIME_SYNC_PERIOD, + UPDATE_CHECK_PERIOD, ) from .utils import ( registerController, @@ -32,6 +33,7 @@ update_listener, initOnvifEvents, syncTime, + getLatestFirmwareVersion, ) @@ -194,6 +196,16 @@ async def async_update_data(): > TIME_SYNC_PERIOD ): await syncTime(hass, entry) + ts = datetime.datetime.utcnow().timestamp() + if ( + ts - hass.data[DOMAIN][entry.entry_id]["lastFirmwareCheck"] + > UPDATE_CHECK_PERIOD + ): + hass.data[DOMAIN][entry.entry_id][ + "latestFirmwareVersion" + ] = await getLatestFirmwareVersion( + hass, entry, hass.data[DOMAIN][entry.entry_id]["controller"] + ) # cameras state someCameraEnabled = False @@ -217,6 +229,8 @@ async def async_update_data(): and entity._enable_sound_detection ): await entity.startNoiseDetection() + if hass.data[DOMAIN][entry.entry_id]["updateEntity"]._enabled: + hass.data[DOMAIN][entry.entry_id]["updateEntity"].updateCam(camData) tapoCoordinator = DataUpdateCoordinator( hass, LOGGER, name="Tapo resource status", update_method=async_update_data, @@ -230,6 +244,8 @@ async def async_update_data(): "coordinator": tapoCoordinator, "camData": camData, "lastTimeSync": 0, + "lastFirmwareCheck": 0, + "latestFirmwareVersion": False, "motionSensorCreated": False, "eventsDevice": False, "onvifManagement": False, @@ -252,6 +268,9 @@ async def async_update_data(): hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, "camera") ) + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, "update") + ) async def unsubscribe(event): if hass.data[DOMAIN][entry.entry_id]["events"]: diff --git a/custom_components/tapo_control/const.py b/custom_components/tapo_control/const.py index 2f856eb..60ddd62 100644 --- a/custom_components/tapo_control/const.py +++ b/custom_components/tapo_control/const.py @@ -93,3 +93,4 @@ LOGGER = logging.getLogger("custom_components." + DOMAIN) TIME_SYNC_PERIOD = 3600 +UPDATE_CHECK_PERIOD = 10 diff --git a/custom_components/tapo_control/manifest.json b/custom_components/tapo_control/manifest.json index 0193156..749ee3d 100644 --- a/custom_components/tapo_control/manifest.json +++ b/custom_components/tapo_control/manifest.json @@ -5,7 +5,7 @@ "issue_tracker": "https://github.com/JurajNyiri/HomeAssistant-Tapo-Control/issues", "codeowners": ["@JurajNyiri"], "version": "3.4.1", - "requirements": ["pytapo==1.2.1", "onvif-zeep-async==1.2.0"], + "requirements": ["pytapo==2.1", "onvif-zeep-async==1.2.0"], "dependencies": ["ffmpeg"], "config_flow": true, "homeassistant": "2021.2.0", diff --git a/custom_components/tapo_control/update.py b/custom_components/tapo_control/update.py new file mode 100644 index 0000000..49ad237 --- /dev/null +++ b/custom_components/tapo_control/update.py @@ -0,0 +1,112 @@ +from homeassistant.core import HomeAssistant +from homeassistant.config_entries import ConfigEntry +from typing import Callable +from homeassistant.components.update import UpdateEntity, UpdateEntityFeature +from .const import DOMAIN, LOGGER +from homeassistant.util import slugify +from pytapo import Tapo + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable +): + hass.data[DOMAIN][entry.entry_id]["updateEntity"] = TapoCamUpdate( + hass, entry, hass.data[DOMAIN][entry.entry_id] + ) + async_add_entities([hass.data[DOMAIN][entry.entry_id]["updateEntity"]]) + + +class TapoCamUpdate(UpdateEntity): + def __init__( + self, hass: HomeAssistant, entry: dict, tapoData: Tapo, + ): + super().__init__() + self._controller = tapoData["controller"] + self._coordinator = tapoData["coordinator"] + self._entry = entry + self._hass = hass + self._enabled = False + self._attributes = tapoData["camData"]["basic_info"] + self._in_progress = False + + def updateCam(self, camData): + if not camData: + self._state = "unavailable" + else: + self._attributes = camData["basic_info"] + self._in_progress = False + + 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.RELEASE_NOTES + + async def async_release_notes(self) -> str: + """Return the release notes.""" + return "todo" + + @property + def name(self) -> str: + return "Camera - " + self._attributes["device_alias"] + + @property + def device_info(self): + return { + "identifiers": { + (DOMAIN, slugify(f"{self._attributes['mac']}_tapo_control")) + }, + "name": self._attributes["device_alias"], + "manufacturer": "TP-Link", + "model": self._attributes["device_model"], + "sw_version": self._attributes["sw_version"], + } + + @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._hass.data[DOMAIN][self._entry.entry_id]["latestFirmwareVersion"]: + return self._hass.data[DOMAIN][self._entry.entry_id][ + "latestFirmwareVersion" + ] + else: + return self._attributes["sw_version"] + + @property + def release_summary(self) -> str: + if self.latest_version == self._attributes["sw_version"]: + return None + return "todo" + + @property + def title(self) -> str: + return "Tapo Camera: {0}".format(self._attributes["device_alias"]) + + async def async_install( + self, version, backup, + ): + LOGGER.warn("Install async") + self._in_progress = True + await self.hass.async_add_executor_job(self._controller.reboot) # temp + + """Install an update. + + Version can be specified to install a specific version. When `None`, the + latest version needs to be installed. + + The backup parameter indicates a backup should be taken before + installing the update. + """ + print("async install") + diff --git a/custom_components/tapo_control/utils.py b/custom_components/tapo_control/utils.py index 6a1efcd..4a987ba 100644 --- a/custom_components/tapo_control/utils.py +++ b/custom_components/tapo_control/utils.py @@ -207,6 +207,22 @@ async def update_listener(hass, entry): await setupOnvif(hass, entry) +async def getLatestFirmwareVersion(hass, entry, controller): + hass.data[DOMAIN][entry.entry_id][ + "lastFirmwareCheck" + ] = datetime.datetime.utcnow().timestamp() + try: + updateInfo = await hass.async_add_executor_job(controller.isUpdateAvailable) + if updateInfo["result"]["responses"][0]["result"]: + LOGGER.warn("TODO process this output") + LOGGER.warn(updateInfo) + else: + updateInfo = False + except Exception: + updateInfo = False + return updateInfo + + async def syncTime(hass, entry): device_mgmt = hass.data[DOMAIN][entry.entry_id]["onvifManagement"] if device_mgmt: From 9f33c1bdf548058a7083804e28ff0bb4717b9c9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Nyi=CC=81ri?= Date: Thu, 7 Apr 2022 00:10:10 +0200 Subject: [PATCH 2/5] Add: Showing new available update with all information --- custom_components/tapo_control/__init__.py | 3 ++ custom_components/tapo_control/update.py | 36 +++++++++++++++++++--- custom_components/tapo_control/utils.py | 12 ++++++-- 3 files changed, 43 insertions(+), 8 deletions(-) diff --git a/custom_components/tapo_control/__init__.py b/custom_components/tapo_control/__init__.py index 0ba391c..d841538 100644 --- a/custom_components/tapo_control/__init__.py +++ b/custom_components/tapo_control/__init__.py @@ -231,6 +231,9 @@ async def async_update_data(): await entity.startNoiseDetection() if hass.data[DOMAIN][entry.entry_id]["updateEntity"]._enabled: hass.data[DOMAIN][entry.entry_id]["updateEntity"].updateCam(camData) + hass.data[DOMAIN][entry.entry_id][ + "updateEntity" + ].async_schedule_update_ha_state(True) tapoCoordinator = DataUpdateCoordinator( hass, LOGGER, name="Tapo resource status", update_method=async_update_data, diff --git a/custom_components/tapo_control/update.py b/custom_components/tapo_control/update.py index 49ad237..cc9ca8d 100644 --- a/custom_components/tapo_control/update.py +++ b/custom_components/tapo_control/update.py @@ -48,7 +48,16 @@ def supported_features(self): async def async_release_notes(self) -> str: """Return the release notes.""" - return "todo" + if ( + self._hass.data[DOMAIN][self._entry.entry_id]["latestFirmwareVersion"] + and "release_log" + in self._hass.data[DOMAIN][self._entry.entry_id]["latestFirmwareVersion"] + ): + return self._hass.data[DOMAIN][self._entry.entry_id][ + "latestFirmwareVersion" + ]["release_log"].replace("\\n", "\n") + else: + return None @property def name(self) -> str: @@ -76,18 +85,35 @@ def installed_version(self) -> str: @property def latest_version(self) -> str: - if self._hass.data[DOMAIN][self._entry.entry_id]["latestFirmwareVersion"]: + if ( + self._hass.data[DOMAIN][self._entry.entry_id]["latestFirmwareVersion"] + and "version" + in self._hass.data[DOMAIN][self._entry.entry_id]["latestFirmwareVersion"] + ): return self._hass.data[DOMAIN][self._entry.entry_id][ "latestFirmwareVersion" - ] + ]["version"] else: return self._attributes["sw_version"] @property def release_summary(self) -> str: - if self.latest_version == self._attributes["sw_version"]: + if ( + self._hass.data[DOMAIN][self._entry.entry_id]["latestFirmwareVersion"] + and "release_log" + in self._hass.data[DOMAIN][self._entry.entry_id]["latestFirmwareVersion"] + ): + maxLength = 255 + releaseLog = self._hass.data[DOMAIN][self._entry.entry_id][ + "latestFirmwareVersion" + ]["release_log"].replace("\\n", "\n") + return ( + (releaseLog[: maxLength - 3] + "...") + if len(releaseLog) > maxLength + else releaseLog + ) + else: return None - return "todo" @property def title(self) -> str: diff --git a/custom_components/tapo_control/utils.py b/custom_components/tapo_control/utils.py index 4a987ba..4193f7a 100644 --- a/custom_components/tapo_control/utils.py +++ b/custom_components/tapo_control/utils.py @@ -213,9 +213,15 @@ async def getLatestFirmwareVersion(hass, entry, controller): ] = datetime.datetime.utcnow().timestamp() try: updateInfo = await hass.async_add_executor_job(controller.isUpdateAvailable) - if updateInfo["result"]["responses"][0]["result"]: - LOGGER.warn("TODO process this output") - LOGGER.warn(updateInfo) + if ( + "version" + in updateInfo["result"]["responses"][1]["result"]["cloud_config"][ + "upgrade_info" + ] + ): + updateInfo = updateInfo["result"]["responses"][1]["result"]["cloud_config"][ + "upgrade_info" + ] else: updateInfo = False except Exception: From 8accfd534a4581d228e774ef1f0e0224fb39d160 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Nyi=CC=81ri?= Date: Thu, 7 Apr 2022 00:21:27 +0200 Subject: [PATCH 3/5] Update: Check for new firmware only once per day --- custom_components/tapo_control/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/tapo_control/const.py b/custom_components/tapo_control/const.py index 60ddd62..aceccd8 100644 --- a/custom_components/tapo_control/const.py +++ b/custom_components/tapo_control/const.py @@ -93,4 +93,4 @@ LOGGER = logging.getLogger("custom_components." + DOMAIN) TIME_SYNC_PERIOD = 3600 -UPDATE_CHECK_PERIOD = 10 +UPDATE_CHECK_PERIOD = 86400 From 1cb2fbc8bad2ab7b33a53f423818466b4efd6108 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Nyi=CC=81ri?= Date: Thu, 7 Apr 2022 16:12:00 +0200 Subject: [PATCH 4/5] Add: Ability to update camera via Home Assistant --- custom_components/tapo_control/update.py | 31 +++++++++++++----------- custom_components/tapo_control/utils.py | 9 +++++++ 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/custom_components/tapo_control/update.py b/custom_components/tapo_control/update.py index cc9ca8d..cf0ec8b 100644 --- a/custom_components/tapo_control/update.py +++ b/custom_components/tapo_control/update.py @@ -34,7 +34,15 @@ def updateCam(self, camData): self._state = "unavailable" else: self._attributes = camData["basic_info"] - self._in_progress = False + if ( + self._in_progress + and "firmwareUpdateStatus" in camData + and "upgrade_status" in camData["firmwareUpdateStatus"] + and "state" in camData["firmwareUpdateStatus"]["upgrade_status"] + and camData["firmwareUpdateStatus"]["upgrade_status"]["state"] + == "normal" + ): + self._in_progress = False async def async_added_to_hass(self) -> None: self._enabled = True @@ -122,17 +130,12 @@ def title(self) -> str: async def async_install( self, version, backup, ): - LOGGER.warn("Install async") - self._in_progress = True - await self.hass.async_add_executor_job(self._controller.reboot) # temp - - """Install an update. - - Version can be specified to install a specific version. When `None`, the - latest version needs to be installed. - - The backup parameter indicates a backup should be taken before - installing the update. - """ - print("async install") + try: + await self.hass.async_add_executor_job( + self._controller.startFirmwareUpgrade + ) + self._in_progress = True + await self._coordinator.async_request_refresh() + except Exception as e: + LOGGER.error(e) diff --git a/custom_components/tapo_control/utils.py b/custom_components/tapo_control/utils.py index 4193f7a..1c260fe 100644 --- a/custom_components/tapo_control/utils.py +++ b/custom_components/tapo_control/utils.py @@ -161,6 +161,15 @@ async def getCamData(hass, controller): else: camData["presets"] = {} + try: + firmwareUpdateStatus = await hass.async_add_executor_job( + controller.getFirmwareUpdateStatus + ) + firmwareUpdateStatus = firmwareUpdateStatus["cloud_config"] + except Exception: + firmwareUpdateStatus = None + camData["firmwareUpdateStatus"] = firmwareUpdateStatus + return camData From 5ef8800223c4c285c3bd23033d9feb9e4bd35f4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20Nyi=CC=81ri?= Date: Thu, 7 Apr 2022 16:20:57 +0200 Subject: [PATCH 5/5] Update: Minimum required Home Assistant version --- custom_components/tapo_control/manifest.json | 4 ++-- hacs.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/custom_components/tapo_control/manifest.json b/custom_components/tapo_control/manifest.json index 749ee3d..5ba5896 100644 --- a/custom_components/tapo_control/manifest.json +++ b/custom_components/tapo_control/manifest.json @@ -4,11 +4,11 @@ "documentation": "https://github.com/JurajNyiri/HomeAssistant-Tapo-Control", "issue_tracker": "https://github.com/JurajNyiri/HomeAssistant-Tapo-Control/issues", "codeowners": ["@JurajNyiri"], - "version": "3.4.1", + "version": "3.5.0", "requirements": ["pytapo==2.1", "onvif-zeep-async==1.2.0"], "dependencies": ["ffmpeg"], "config_flow": true, - "homeassistant": "2021.2.0", + "homeassistant": "2022.4.0", "dhcp": [ { "hostname": "c200_*", diff --git a/hacs.json b/hacs.json index 5e373be..31d525c 100644 --- a/hacs.json +++ b/hacs.json @@ -1 +1 @@ -{ "name": "Tapo: Cameras Control", "homeassistant": "2021.3.0" } +{ "name": "Tapo: Cameras Control", "homeassistant": "2022.4.0" }