From e9dfdf6cf7edf31653fc7d0d8aa80acea6413824 Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Fri, 23 Feb 2024 02:32:38 +0100 Subject: [PATCH 1/8] Improve smartdevice update module * Expose current and latest firmware as features * Implement update loop that blocks until the update is complete --- kasa/smart/modules/firmware.py | 47 ++++++++++++++++++++++++++++++---- 1 file changed, 42 insertions(+), 5 deletions(-) diff --git a/kasa/smart/modules/firmware.py b/kasa/smart/modules/firmware.py index 626add0f6..d223b0e70 100644 --- a/kasa/smart/modules/firmware.py +++ b/kasa/smart/modules/firmware.py @@ -1,6 +1,7 @@ """Implementation of firmware module.""" from __future__ import annotations +import asyncio from datetime import date from typing import TYPE_CHECKING, Any, Optional @@ -11,6 +12,11 @@ from ...feature import Feature from ..smartmodule import SmartModule +# When support for cpython older than 3.11 is dropped +# async_timeout can be replaced with asyncio.timeout +from async_timeout import timeout as asyncio_timeout + + if TYPE_CHECKING: from ..smartdevice import SmartDevice @@ -19,7 +25,7 @@ class UpdateInfo(BaseModel): """Update info status object.""" status: int = Field(alias="type") - fw_ver: Optional[str] = None # noqa: UP007 + version: Optional[str] = Field(alias="fw_ver", default=None) # noqa: UP007 release_date: Optional[date] = None # noqa: UP007 release_notes: Optional[str] = Field(alias="release_note", default=None) # noqa: UP007 fw_size: Optional[int] = None # noqa: UP007 @@ -71,6 +77,12 @@ def __init__(self, device: SmartDevice, module: str): category=Feature.Category.Info, ) ) + self._add_feature( + Feature(device, "Current firmware version", container=self, attribute_getter="current_firmware") + ) + self._add_feature( + Feature(device, "Available firmware version", container=self, attribute_getter="latest_firmware") + ) def query(self) -> dict: """Query to execute during the update cycle.""" @@ -80,7 +92,18 @@ def query(self) -> dict: return req @property - def latest_firmware(self): + def current_firmware(self) -> str: + """Return the current firmware version.""" + return self._device.hw_info["sw_ver"] + + + @property + def latest_firmware(self) -> str: + """Return the latest firmware version.""" + return self.firmware_update_info.version + + @property + def firmware_update_info(self): """Return latest firmware information.""" fw = self.data.get("get_latest_fw") or self.data if not self._device.is_cloud_connected or isinstance(fw, SmartErrorCode): @@ -94,7 +117,7 @@ def update_available(self) -> bool | None: """Return True if update is available.""" if not self._device.is_cloud_connected: return None - return self.latest_firmware.update_available + return self.firmware_update_info.update_available async def get_update_state(self): """Return update state.""" @@ -102,7 +125,21 @@ async def get_update_state(self): async def update(self): """Update the device firmware.""" - return await self.call("fw_download") + current_fw = self.current_firmware + _LOGGER.debug("Going to upgrade from %s to %s", current_fw, self.firmware_update_info.version) + resp = await self.call("fw_download") + _LOGGER.debug("Update request response: %s", resp) + # TODO: read timeout from get_auto_update_info or from get_fw_download_state? + async with asyncio_timeout(60*5): + while True: + await asyncio.sleep(0.5) + state = await self.get_update_state() + _LOGGER.debug("Update state: %s" % state) + # TODO: this could await a given callable for progress + + if self.firmware_update_info.version != current_fw: + _LOGGER.info("Updated to %s", self.firmware_update_info.version) + break @property def auto_update_enabled(self): @@ -115,4 +152,4 @@ def auto_update_enabled(self): async def set_auto_update_enabled(self, enabled: bool): """Change autoupdate setting.""" data = {**self.data["get_auto_update_info"], "enable": enabled} - await self.call("set_auto_update_info", data) # {"enable": enabled}) + await self.call("set_auto_update_info", data) From fd8689123f6a28bc5782e8db7f00d263f6b7de4a Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Fri, 23 Feb 2024 16:12:28 +0100 Subject: [PATCH 2/8] Fix linting --- kasa/smart/modules/firmware.py | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/kasa/smart/modules/firmware.py b/kasa/smart/modules/firmware.py index d223b0e70..bd5e5ef74 100644 --- a/kasa/smart/modules/firmware.py +++ b/kasa/smart/modules/firmware.py @@ -8,19 +8,22 @@ from pydantic.v1 import BaseModel, Field, validator -from ...exceptions import SmartErrorCode from ...feature import Feature -from ..smartmodule import SmartModule - # When support for cpython older than 3.11 is dropped # async_timeout can be replaced with asyncio.timeout from async_timeout import timeout as asyncio_timeout +from ...exceptions import SmartErrorCode +from ...feature import Feature, FeatureType +from ..smartmodule import SmartModule if TYPE_CHECKING: from ..smartdevice import SmartDevice +_LOGGER = logging.getLogger(__name__) + + class UpdateInfo(BaseModel): """Update info status object.""" @@ -78,10 +81,20 @@ def __init__(self, device: SmartDevice, module: str): ) ) self._add_feature( - Feature(device, "Current firmware version", container=self, attribute_getter="current_firmware") + Feature( + device, + "Current firmware version", + container=self, + attribute_getter="current_firmware", + ) ) self._add_feature( - Feature(device, "Available firmware version", container=self, attribute_getter="latest_firmware") + Feature( + device, + "Available firmware version", + container=self, + attribute_getter="latest_firmware", + ) ) def query(self) -> dict: @@ -96,7 +109,6 @@ def current_firmware(self) -> str: """Return the current firmware version.""" return self._device.hw_info["sw_ver"] - @property def latest_firmware(self) -> str: """Return the latest firmware version.""" @@ -126,11 +138,15 @@ async def get_update_state(self): async def update(self): """Update the device firmware.""" current_fw = self.current_firmware - _LOGGER.debug("Going to upgrade from %s to %s", current_fw, self.firmware_update_info.version) + _LOGGER.debug( + "Going to upgrade from %s to %s", + current_fw, + self.firmware_update_info.version, + ) resp = await self.call("fw_download") _LOGGER.debug("Update request response: %s", resp) # TODO: read timeout from get_auto_update_info or from get_fw_download_state? - async with asyncio_timeout(60*5): + async with asyncio_timeout(60 * 5): while True: await asyncio.sleep(0.5) state = await self.get_update_state() From 39e6aac63b85c14c9ae5ac034d444367e9a71546 Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Tue, 7 May 2024 17:41:38 +0200 Subject: [PATCH 3/8] Fix after rebasing --- kasa/smart/modules/firmware.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/kasa/smart/modules/firmware.py b/kasa/smart/modules/firmware.py index bd5e5ef74..16704ef0b 100644 --- a/kasa/smart/modules/firmware.py +++ b/kasa/smart/modules/firmware.py @@ -1,20 +1,19 @@ """Implementation of firmware module.""" from __future__ import annotations -import asyncio +import asyncio +import logging from datetime import date from typing import TYPE_CHECKING, Any, Optional -from pydantic.v1 import BaseModel, Field, validator - -from ...feature import Feature # When support for cpython older than 3.11 is dropped # async_timeout can be replaced with asyncio.timeout from async_timeout import timeout as asyncio_timeout +from pydantic.v1 import BaseModel, Field, validator from ...exceptions import SmartErrorCode -from ...feature import Feature, FeatureType +from ...feature import Feature from ..smartmodule import SmartModule if TYPE_CHECKING: @@ -83,17 +82,21 @@ def __init__(self, device: SmartDevice, module: str): self._add_feature( Feature( device, - "Current firmware version", + id="current_firmware_version", + name="Current firmware version", container=self, attribute_getter="current_firmware", + category=Feature.Category.Info, ) ) self._add_feature( Feature( device, - "Available firmware version", + id="available_firmware_version", + name="Available firmware version", container=self, attribute_getter="latest_firmware", + category=Feature.Category.Info, ) ) From 06fe4e71cf7ec019c0d214c823479519e474c1d3 Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Fri, 23 Feb 2024 02:32:38 +0100 Subject: [PATCH 4/8] Improve smartdevice update module * Expose current and latest firmware as features * Implement update loop that blocks until the update is complete --- kasa/smart/modules/firmware.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/kasa/smart/modules/firmware.py b/kasa/smart/modules/firmware.py index 16704ef0b..b1302b372 100644 --- a/kasa/smart/modules/firmware.py +++ b/kasa/smart/modules/firmware.py @@ -1,6 +1,7 @@ """Implementation of firmware module.""" from __future__ import annotations +import asyncio import asyncio import logging @@ -16,6 +17,11 @@ from ...feature import Feature from ..smartmodule import SmartModule +# When support for cpython older than 3.11 is dropped +# async_timeout can be replaced with asyncio.timeout +from async_timeout import timeout as asyncio_timeout + + if TYPE_CHECKING: from ..smartdevice import SmartDevice @@ -99,6 +105,12 @@ def __init__(self, device: SmartDevice, module: str): category=Feature.Category.Info, ) ) + self._add_feature( + Feature(device, "Current firmware version", container=self, attribute_getter="current_firmware") + ) + self._add_feature( + Feature(device, "Available firmware version", container=self, attribute_getter="latest_firmware") + ) def query(self) -> dict: """Query to execute during the update cycle.""" @@ -112,6 +124,7 @@ def current_firmware(self) -> str: """Return the current firmware version.""" return self._device.hw_info["sw_ver"] + @property def latest_firmware(self) -> str: """Return the latest firmware version.""" From 3429821b2562d357e2a8a2db79c6a8ece8a7d112 Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Fri, 23 Feb 2024 16:12:28 +0100 Subject: [PATCH 5/8] Fix linting --- kasa/smart/modules/firmware.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/kasa/smart/modules/firmware.py b/kasa/smart/modules/firmware.py index b1302b372..14a23aaae 100644 --- a/kasa/smart/modules/firmware.py +++ b/kasa/smart/modules/firmware.py @@ -1,7 +1,6 @@ """Implementation of firmware module.""" from __future__ import annotations -import asyncio import asyncio import logging @@ -13,14 +12,13 @@ from async_timeout import timeout as asyncio_timeout from pydantic.v1 import BaseModel, Field, validator -from ...exceptions import SmartErrorCode -from ...feature import Feature -from ..smartmodule import SmartModule - # When support for cpython older than 3.11 is dropped # async_timeout can be replaced with asyncio.timeout from async_timeout import timeout as asyncio_timeout +from ...exceptions import SmartErrorCode +from ...feature import Feature, FeatureType +from ..smartmodule import SmartModule if TYPE_CHECKING: from ..smartdevice import SmartDevice @@ -106,10 +104,20 @@ def __init__(self, device: SmartDevice, module: str): ) ) self._add_feature( - Feature(device, "Current firmware version", container=self, attribute_getter="current_firmware") + Feature( + device, + "Current firmware version", + container=self, + attribute_getter="current_firmware", + ) ) self._add_feature( - Feature(device, "Available firmware version", container=self, attribute_getter="latest_firmware") + Feature( + device, + "Available firmware version", + container=self, + attribute_getter="latest_firmware", + ) ) def query(self) -> dict: @@ -124,7 +132,6 @@ def current_firmware(self) -> str: """Return the current firmware version.""" return self._device.hw_info["sw_ver"] - @property def latest_firmware(self) -> str: """Return the latest firmware version.""" From 6dcc2758d7c64bfe80b74ad8e5b1329a6727a932 Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Wed, 3 Apr 2024 17:54:32 +0200 Subject: [PATCH 6/8] Add update interface for iot and expose it through cli --- kasa/__init__.py | 3 + kasa/cli.py | 42 +++++++++++++ kasa/device.py | 6 ++ kasa/firmware.py | 41 ++++++++++++ kasa/iot/iotdevice.py | 6 ++ kasa/iot/modules/cloud.py | 112 +++++++++++++++++++++++++++++---- kasa/smart/modules/firmware.py | 29 +++++++-- kasa/smart/smartdevice.py | 7 +++ 8 files changed, 228 insertions(+), 18 deletions(-) create mode 100644 kasa/firmware.py diff --git a/kasa/__init__.py b/kasa/__init__.py index 62d545025..394fa72e5 100755 --- a/kasa/__init__.py +++ b/kasa/__init__.py @@ -36,6 +36,7 @@ UnsupportedDeviceError, ) from kasa.feature import Feature +from kasa.firmware import Firmware, FirmwareUpdate from kasa.iot.iotbulb import BulbPreset, TurnOnBehavior, TurnOnBehaviors from kasa.iotprotocol import ( IotProtocol, @@ -72,6 +73,8 @@ "ConnectionType", "EncryptType", "DeviceFamilyType", + "Firmware", + "FirmwareUpdate", ] from . import iot diff --git a/kasa/cli.py b/kasa/cli.py index 696dee274..386d2e1c2 100755 --- a/kasa/cli.py +++ b/kasa/cli.py @@ -1252,5 +1252,47 @@ async def feature(dev: Device, child: str, name: str, value): return response +@cli.group(invoke_without_command=True) +@pass_dev +@click.pass_context +async def firmware(ctx: click.Context, dev: Device): + """Firmware update.""" + if ctx.invoked_subcommand is None: + return await ctx.invoke(firmware_info) + + +@firmware.command(name="info") +@pass_dev +@click.pass_context +async def firmware_info(ctx: click.Context, dev: Device): + """Return firmware information.""" + res = await dev.firmware.check_for_updates() + if res.update_available: + echo("[green bold]Update available![/green bold]") + echo(f"Current firmware: {res.current_version}") + echo(f"Version {res.available_version} released at {res.release_date}") + echo("Release notes") + echo("=============") + echo(res.release_notes) + echo("=============") + else: + echo("[red bold]No updates available.[/red bold]") + + +@firmware.command(name="update") +@pass_dev +@click.pass_context +async def firmware_update(ctx: click.Context, dev: Device): + """Perform firmware update.""" + await ctx.invoke(firmware_info) + click.confirm("Are you sure you want to upgrade the firmware?", abort=True) + + async def progress(x): + echo(f"Progress: {x}") + + echo("Going to update %s", dev) + await dev.firmware.update_firmware(progress_cb=progress) # type: ignore + + if __name__ == "__main__": cli() diff --git a/kasa/device.py b/kasa/device.py index ea358a8de..fc400a042 100644 --- a/kasa/device.py +++ b/kasa/device.py @@ -14,6 +14,7 @@ from .emeterstatus import EmeterStatus from .exceptions import KasaException from .feature import Feature +from .firmware import Firmware from .iotprotocol import IotProtocol from .module import Module, ModuleT from .protocol import BaseProtocol @@ -288,6 +289,11 @@ def get_plug_by_index(self, index: int) -> Device: ) return self.children[index] + @property + @abstractmethod + def firmware(self) -> Firmware: + """Return firmware.""" + @property @abstractmethod def time(self) -> datetime: diff --git a/kasa/firmware.py b/kasa/firmware.py new file mode 100644 index 000000000..71592c643 --- /dev/null +++ b/kasa/firmware.py @@ -0,0 +1,41 @@ +"""Interface for firmware updates.""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from dataclasses import dataclass +from datetime import date +from typing import Any, Awaitable, Callable + +UpdateResult = bool + + +@dataclass +class FirmwareUpdate: + """Update info status object.""" + + update_available: bool | None = None + current_version: str | None = None + available_version: str | None = None + release_date: date | None = None + release_notes: str | None = None + + +class Firmware(ABC): + """Interface to access firmware information and perform updates.""" + + @abstractmethod + async def update_firmware( + self, *, progress_cb: Callable[[Any, Any], Awaitable] + ) -> UpdateResult: + """Perform firmware update. + + This "blocks" until the update process has finished. + You can set *progress_cb* to get progress updates. + """ + raise NotImplementedError + + @abstractmethod + async def check_for_updates(self) -> FirmwareUpdate: + """Return information about available updates.""" + raise NotImplementedError diff --git a/kasa/iot/iotdevice.py b/kasa/iot/iotdevice.py index 29ba31554..81735ec20 100755 --- a/kasa/iot/iotdevice.py +++ b/kasa/iot/iotdevice.py @@ -715,3 +715,9 @@ def internal_state(self) -> Any: This should only be used for debugging purposes. """ return self._last_update or self._discovery_info + + @property + @requires_update + def firmware(self) -> Cloud: + """Returns object implementing the firmware handling.""" + return self.modules["cloud"] diff --git a/kasa/iot/modules/cloud.py b/kasa/iot/modules/cloud.py index 5022a68e7..4bcfee5c0 100644 --- a/kasa/iot/modules/cloud.py +++ b/kasa/iot/modules/cloud.py @@ -1,9 +1,25 @@ """Cloud module implementation.""" +from __future__ import annotations + +import logging + from pydantic.v1 import BaseModel +from datetime import date +from typing import Optional + from ...feature import Feature -from ..iotmodule import IotModule +from ...firmware import ( + Firmware, + UpdateResult, +) +from ...firmware import ( + FirmwareUpdate as FirmwareUpdateInterface, +) +from ..iotmodule import IotModule, merge + +_LOGGER = logging.getLogger(__name__) class CloudInfo(BaseModel): @@ -21,7 +37,31 @@ class CloudInfo(BaseModel): username: str -class Cloud(IotModule): +class FirmwareUpdate(BaseModel): + """Update info status object.""" + + status: int = Field(alias="fwType") + version: Optional[str] = Field(alias="fwVer", default=None) # noqa: UP007 + release_date: Optional[date] = Field(alias="fwReleaseDate", default=None) # noqa: UP007 + release_notes: Optional[str] = Field(alias="fwReleaseLog", default=None) # noqa: UP007 + url: Optional[str] = Field(alias="fwUrl", default=None) # noqa: UP007 + + @validator("release_date", pre=True) + def _release_date_optional(cls, v): + if not v: + return None + + return v + + @property + def update_available(self): + """Return True if update available.""" + if self.status != 0: + return True + return False + + +class Cloud(IotModule, Firmware): """Module implementing support for cloud services.""" def __init__(self, device, module): @@ -46,27 +86,73 @@ def is_connected(self) -> bool: def query(self): """Request cloud connectivity info.""" - return self.query_for_command("get_info") + req = self.query_for_command("get_info") + + # TODO: this is problematic, as it will fail the whole query on some + # devices if they are not connected to the internet + if self._module in self._device._last_update and self.is_connected: + req = merge(req, self.get_available_firmwares()) + + return req @property def info(self) -> CloudInfo: """Return information about the cloud connectivity.""" return CloudInfo.parse_obj(self.data["get_info"]) - def get_available_firmwares(self): + async def get_available_firmwares(self): """Return list of available firmwares.""" - return self.query_for_command("get_intl_fw_list") + return await self.call("get_intl_fw_list") + + async def get_firmware_update(self) -> FirmwareUpdate: + """Return firmware update information.""" + try: + available_fws = (await self.get_available_firmwares()).get("fw_list", []) + if not available_fws: + return FirmwareUpdate(fwType=0) + if len(available_fws) > 1: + _LOGGER.warning( + "Got more than one update, using the first one: %s", available_fws + ) + return FirmwareUpdate.parse_obj(next(iter(available_fws))) + except Exception as ex: + _LOGGER.warning("Unable to check for firmware update: %s", ex) + return FirmwareUpdate(fwType=0) - def set_server(self, url: str): + async def set_server(self, url: str): """Set the update server URL.""" - return self.query_for_command("set_server_url", {"server": url}) + return await self.call("set_server_url", {"server": url}) - def connect(self, username: str, password: str): + async def connect(self, username: str, password: str): """Login to the cloud using given information.""" - return self.query_for_command( - "bind", {"username": username, "password": password} - ) + return await self.call("bind", {"username": username, "password": password}) - def disconnect(self): + async def disconnect(self): """Disconnect from the cloud.""" - return self.query_for_command("unbind") + return await self.call("unbind") + + async def update_firmware(self, *, progress_cb=None) -> UpdateResult: + """Perform firmware update.""" + raise NotImplementedError + i = 0 + import asyncio + + while i < 100: + await asyncio.sleep(1) + if progress_cb is not None: + await progress_cb(i) + i += 10 + + return UpdateResult("") + + async def check_for_updates(self) -> FirmwareUpdateInterface: + """Return firmware update information.""" + fw = await self.get_firmware_update() + + return FirmwareUpdateInterface( + update_available=fw.update_available, + current_version=self._device.hw_info.get("sw_ver"), + available_version=fw.version, + release_date=fw.release_date, + release_notes=fw.release_notes, + ) diff --git a/kasa/smart/modules/firmware.py b/kasa/smart/modules/firmware.py index 14a23aaae..07616cf42 100644 --- a/kasa/smart/modules/firmware.py +++ b/kasa/smart/modules/firmware.py @@ -11,13 +11,15 @@ # async_timeout can be replaced with asyncio.timeout from async_timeout import timeout as asyncio_timeout from pydantic.v1 import BaseModel, Field, validator - # When support for cpython older than 3.11 is dropped # async_timeout can be replaced with asyncio.timeout from async_timeout import timeout as asyncio_timeout from ...exceptions import SmartErrorCode from ...feature import Feature, FeatureType +from ...firmware import Firmware as FirmwareInterface +from ...firmware import FirmwareUpdate as FirmwareUpdateInterface +from ...firmware import UpdateResult from ..smartmodule import SmartModule if TYPE_CHECKING: @@ -27,7 +29,7 @@ _LOGGER = logging.getLogger(__name__) -class UpdateInfo(BaseModel): +class FirmwareUpdate(BaseModel): """Update info status object.""" status: int = Field(alias="type") @@ -53,7 +55,7 @@ def update_available(self): return False -class Firmware(SmartModule): +class Firmware(SmartModule, FirmwareInterface): """Implementation of firmware module.""" REQUIRED_COMPONENT = "firmware" @@ -143,9 +145,9 @@ def firmware_update_info(self): fw = self.data.get("get_latest_fw") or self.data if not self._device.is_cloud_connected or isinstance(fw, SmartErrorCode): # Error in response, probably disconnected from the cloud. - return UpdateInfo(type=0, need_to_upgrade=False) + return FirmwareUpdate(type=0, need_to_upgrade=False) - return UpdateInfo.parse_obj(fw) + return FirmwareUpdate.parse_obj(fw) @property def update_available(self) -> bool | None: @@ -192,3 +194,20 @@ async def set_auto_update_enabled(self, enabled: bool): """Change autoupdate setting.""" data = {**self.data["get_auto_update_info"], "enable": enabled} await self.call("set_auto_update_info", data) + + async def update_firmware(self, *, progress_cb) -> UpdateResult: + """Update the firmware.""" + # TODO: implement, this is part of the common firmware API + raise NotImplementedError + + async def check_for_updates(self) -> FirmwareUpdateInterface: + """Return firmware update information.""" + # TODO: naming of the common firmware API methods + info = self.firmware_update_info + return FirmwareUpdateInterface( + current_version=self.current_firmware, + update_available=info.update_available, + available_version=info.version, + release_date=info.release_date, + release_notes=info.release_notes, + ) diff --git a/kasa/smart/smartdevice.py b/kasa/smart/smartdevice.py index 898133878..1e53bc0d4 100644 --- a/kasa/smart/smartdevice.py +++ b/kasa/smart/smartdevice.py @@ -625,6 +625,13 @@ def device_type(self) -> DeviceType: return self._device_type + @property + def firmware(self) -> FirmwareInterface: + """Return firmware module.""" + # TODO: open question: does it make sense to expose common modules? + fw = cast(FirmwareInterface, self.modules["Firmware"]) + return fw + @staticmethod def _get_device_type_from_components( components: list[str], device_type: str From fc122eac6ba54c9a986b106b0e04419d6f6a0fc6 Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Tue, 7 May 2024 17:55:14 +0200 Subject: [PATCH 7/8] Fix issues after rebasing --- kasa/iot/iotdevice.py | 5 +++-- kasa/iot/modules/cloud.py | 5 ++--- kasa/smart/modules/firmware.py | 21 ++------------------- kasa/smart/smartdevice.py | 10 ++++++---- 4 files changed, 13 insertions(+), 28 deletions(-) diff --git a/kasa/iot/iotdevice.py b/kasa/iot/iotdevice.py index 81735ec20..144d803df 100755 --- a/kasa/iot/iotdevice.py +++ b/kasa/iot/iotdevice.py @@ -26,6 +26,7 @@ from ..emeterstatus import EmeterStatus from ..exceptions import KasaException from ..feature import Feature +from ..firmware import Firmware from ..module import ModuleT from ..protocol import BaseProtocol from .iotmodule import IotModule @@ -718,6 +719,6 @@ def internal_state(self) -> Any: @property @requires_update - def firmware(self) -> Cloud: + def firmware(self) -> Firmware: """Returns object implementing the firmware handling.""" - return self.modules["cloud"] + return cast(Firmware, self.modules["cloud"]) diff --git a/kasa/iot/modules/cloud.py b/kasa/iot/modules/cloud.py index 4bcfee5c0..4606442eb 100644 --- a/kasa/iot/modules/cloud.py +++ b/kasa/iot/modules/cloud.py @@ -3,12 +3,11 @@ from __future__ import annotations import logging - -from pydantic.v1 import BaseModel - from datetime import date from typing import Optional +from pydantic.v1 import BaseModel, Field, validator + from ...feature import Feature from ...firmware import ( Firmware, diff --git a/kasa/smart/modules/firmware.py b/kasa/smart/modules/firmware.py index 07616cf42..81c7b62b0 100644 --- a/kasa/smart/modules/firmware.py +++ b/kasa/smart/modules/firmware.py @@ -9,14 +9,13 @@ # When support for cpython older than 3.11 is dropped # async_timeout can be replaced with asyncio.timeout -from async_timeout import timeout as asyncio_timeout -from pydantic.v1 import BaseModel, Field, validator # When support for cpython older than 3.11 is dropped # async_timeout can be replaced with asyncio.timeout from async_timeout import timeout as asyncio_timeout +from pydantic.v1 import BaseModel, Field, validator from ...exceptions import SmartErrorCode -from ...feature import Feature, FeatureType +from ...feature import Feature from ...firmware import Firmware as FirmwareInterface from ...firmware import FirmwareUpdate as FirmwareUpdateInterface from ...firmware import UpdateResult @@ -105,22 +104,6 @@ def __init__(self, device: SmartDevice, module: str): category=Feature.Category.Info, ) ) - self._add_feature( - Feature( - device, - "Current firmware version", - container=self, - attribute_getter="current_firmware", - ) - ) - self._add_feature( - Feature( - device, - "Available firmware version", - container=self, - attribute_getter="latest_firmware", - ) - ) def query(self) -> dict: """Query to execute during the update cycle.""" diff --git a/kasa/smart/smartdevice.py b/kasa/smart/smartdevice.py index 1e53bc0d4..17f9d359f 100644 --- a/kasa/smart/smartdevice.py +++ b/kasa/smart/smartdevice.py @@ -16,6 +16,7 @@ from ..exceptions import AuthenticationError, DeviceError, KasaException, SmartErrorCode from ..fan import Fan from ..feature import Feature +from ..firmware import Firmware from ..module import ModuleT from ..smartprotocol import SmartProtocol from .modules import ( @@ -26,9 +27,11 @@ DeviceModule, EnergyModule, FanModule, - Firmware, TimeModule, ) +from .modules import ( + Firmware as FirmwareModule, +) from .smartmodule import SmartModule _LOGGER = logging.getLogger(__name__) @@ -626,11 +629,10 @@ def device_type(self) -> DeviceType: return self._device_type @property - def firmware(self) -> FirmwareInterface: + def firmware(self) -> Firmware: """Return firmware module.""" # TODO: open question: does it make sense to expose common modules? - fw = cast(FirmwareInterface, self.modules["Firmware"]) - return fw + return cast(Firmware, self.get_module(FirmwareModule)) @staticmethod def _get_device_type_from_components( From 64eed4fbdd16ef61120c31fef1ef5531cb296345 Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Sat, 31 Aug 2024 15:31:25 +0200 Subject: [PATCH 8/8] Fix update for smart devices --- kasa/cli.py | 2 +- kasa/smart/modules/firmware.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/kasa/cli.py b/kasa/cli.py index 5a60bf191..dce38def1 100755 --- a/kasa/cli.py +++ b/kasa/cli.py @@ -1301,7 +1301,7 @@ async def progress(x): echo(f"Progress: {x}") echo("Going to update %s", dev) - await dev.modules.get[Module.Firmware].update_firmware(progress_cb=progress) # type: ignore + await dev.modules[Module.Firmware].update_firmware(progress_cb=progress) # type: ignore if __name__ == "__main__": diff --git a/kasa/smart/modules/firmware.py b/kasa/smart/modules/firmware.py index 207221225..cdad5f6a5 100644 --- a/kasa/smart/modules/firmware.py +++ b/kasa/smart/modules/firmware.py @@ -228,8 +228,7 @@ async def update_firmware( | None = None, ) -> UpdateResult: """Update the firmware.""" - # TODO: implement, this is part of the common firmware API - raise NotImplementedError + return await self.update(progress_cb) async def check_for_updates(self) -> FirmwareUpdateInfoInterface: """Return firmware update information."""