8000 Disable automatic updating of latest firmware (#1103) · msz-coder/python-kasa@520b9d7 · GitHub
[go: up one dir, main page]

Skip to content

Commit 520b9d7

Browse files
authored
Disable automatic updating of latest firmware (python-kasa#1103)
To resolve issues with the calls to the tplink cloud to get the latest firmware. Disables the automatic calling of `get_latest_fw` and requires firmware update checks to be triggered manually.
1 parent 6a86ffb commit 520b9d7

File tree

4 files changed

+88
-29
lines changed

4 files changed

+88
-29
lines changed

docs/tutorial.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,5 +91,5 @@
9191
True
9292
>>> for feat in dev.features.values():
9393
>>> print(f"{feat.name}: {feat.value}")
94-
Device ID: 0000000000000000000000000000000000000000\nState: True\nSignal Level: 2\nRSSI: -52\nSSID: #MASKED_SSID#\nOverheated: False\nReboot: <Action>\nBrightness: 50\nCloud connection: True\nHSV: HSV(hue=0, saturation=100, value=50)\nColor temperature: 2700\nAuto update enabled: True\nUpdate available: False\nCurrent firmware version: 1.1.6 Build 240130 Rel.173828\nAvailable firmware version: 1.1.6 Build 240130 Rel.173828\nLight effect: Party\nLight preset: Light preset 1\nSmooth transition on: 2\nSmooth transition off: 2\nDevice time: 2024-02-23 02:40:15+01:00
94+
Device ID: 0000000000000000000000000000000000000000\nState: True\nSignal Level: 2\nRSSI: -52\nSSID: #MASKED_SSID#\nOverheated: False\nReboot: <Action>\nBrightness: 50\nCloud connection: True\nHSV: HSV(hue=0, saturation=100, value=50)\nColor temperature: 2700\nAuto update enabled: True\nUpdate available: None\nCurrent firmware version: 1.1.6 Build 240130 Rel.173828\nAvailable firmware version: None\nCheck latest firmware: <Action>\nLight effect: Party\nLight preset: Light preset 1\nSmooth transition on: 2\nSmooth transition off: 2\nDevice time: 2024-02-23 02:40:15+01:00
9595
"""

kasa/feature.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,10 @@
3131
HSV (hsv): HSV(hue=0, saturation=100, value=100)
3232
Color temperature (color_temperature): 2700
3333
Auto update enabled (auto_update_enabled): False
34-
Update available (update_available): False
34+
Update available (update_available): None
3535
Current firmware version (current_firmware_version): 1.1.6 Build 240130 Rel.173828
36-
Available firmware version (available_firmware_version): 1.1.6 Build 240130 Rel.173828
36+
Available firmware version (available_firmware_version): None
37+
Check latest firmware (check_latest_firmware): <Action>
3738
Light effect (light_effect): Off
3839
Light preset (light_preset): Not set
3940
Smooth transition on (smooth_transition_on): 2

kasa/smart/modules/firmware.py

Lines changed: 50 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@
66
import logging
77
from collections.abc import Coroutine
88
from datetime import date
9-
from typing import TYPE_CHECKING, Any, Callable, Optional
9+
from typing import TYPE_CHECKING, Callable, Optional
1010

1111
# When support for cpython older than 3.11 is dropped
1212
# async_timeout can be replaced with asyncio.timeout
1313
from async_timeout import timeout as asyncio_timeout
1414
from pydantic.v1 import BaseModel, Field, validator
1515

16+
from ...exceptions import KasaException
1617
from ...feature import Feature
1718
from ..smartmodule import SmartModule, allow_update_after
1819

@@ -70,6 +71,11 @@ class Firmware(SmartModule):
7071

7172
def __init__(self, device: SmartDevice, module: str):
7273
super().__init__(device, module)
74+
self._firmware_update_info: UpdateInfo | None = None
75+
76+
def _initialize_features(self):
77+
"""Initialize features."""
78+
device = self._device
7379
if self.supported_version > 1:
7480
self._add_feature(
7581
Feature(
@@ -115,40 +121,58 @@ def __init__(self, device: SmartDevice, module: str):
115121
type=Feature.Type.Sensor,
116122
)
117123
)
124+
self._add_feature(
125+
Feature(
126+
device,
127+
id="check_latest_firmware",
128+
name="Check latest firmware",
129+
container=self,
130+
attribute_setter="check_latest_firmware",
131+
category=Feature.Category.Info,
132+
type=Feature.Type.Action,
133+
)
134+
)
118135

119136
def query(self) -> dict:
120137
"""Query to execute during the update cycle."""
121-
req: dict[str, Any] = {"get_latest_fw": None}
122138
if self.supported_version > 1:
123-
req["get_auto_update_info"] = None
124-
return req
139+
return {"get_auto_update_info": None}
140+
return {}
141+
142+
async def check_latest_firmware(self) -> UpdateInfo | None:
143+
"""Check for the latest firmware for the device."""
144+
try:
145+
fw = await self.call("get_latest_fw")
146+
self._firmware_update_info = UpdateInfo.parse_obj(fw["get_latest_fw"])
147+
return self._firmware_update_info
148+
except Exception:
149+
_LOGGER.exception("Error getting latest firmware for %s:", self._device)
150+
self._firmware_update_info = None
151+
return None
125152

126153
@property
127154
def current_firmware(self) -> str:
128155
"""Return the current firmware version."""
129156
return self._device.hw_info["sw_ver"]
130157

131158
@property
132-
def latest_firmware(self) -> str:
159+
def latest_firmware(self) -> str | None:
133160
"""Return the latest firmware version."""
134-
return self.firmware_update_info.version
161+
if not self._firmware_update_info:
162+
return None
163+
return self._firmware_update_info.version
135164

136165
@property
137-
def firmware_update_info(self):
166+
def firmware_update_info(self) -> UpdateInfo | None:
138167
"""Return latest firmware information."""
139-
if not self._device.is_cloud_connected or self._has_data_error():
140-
# Error in response, probably disconnected from the cloud.
141-
return UpdateInfo(type=0, need_to_upgrade=False)
142-
143-
fw = self.data.get("get_latest_fw") or self.data
144-
return UpdateInfo.parse_obj(fw)
168+
return self._firmware_update_info
145169

146170
@property
147171
def update_available(self) -> bool | None:
148172
"""Return True if update is available."""
149-
if not self._device.is_cloud_connected:
173+
if not self._device.is_cloud_connected or not self._firmware_update_info:
150174
return None
151-
return self.firmware_update_info.update_available
175+
return self._firmware_update_info.update_available
152176

153177
async def get_update_state(self) -> DownloadState:
154178
"""Return update state."""
@@ -161,11 +185,17 @@ async def update(
161185
self, progress_cb: Callable[[DownloadState], Coroutine] | None = None
162186
):
163187
"""Update the device firmware."""
188+
if not self._firmware_update_info:
189+
raise KasaException(
190+
"You must call check_latest_firmware before calling update"
191+
)
192+
if not self.update_available:
193+
raise KasaException("A new update must be available to call update")
164194
current_fw = self.current_firmware
165195
_LOGGER.info(
166196
"Going to upgrade from %s to %s",
167197
current_fw,
168-
self.firmware_update_info.version,
198+
self._firmware_update_info.version,
169199
)
170200
await self.call("fw_download")
171201

@@ -188,7 +218,7 @@ async def update(
188218
if state.status == 0:
189219
_LOGGER.info(
190220
"Update idle, hopefully updated to %s",
191-
self.firmware_update_info.version,
221+
self._firmware_update_info.version,
192222
)
193223
break
194224
elif state.status == 2:
@@ -207,15 +237,12 @@ async def update(
207237
_LOGGER.warning("Unhandled state code: %s", state)
208238

209239
@property
210-
def auto_update_enabled(self):
240+
def auto_update_enabled(self) -> bool:
211241
"""Return True if autoupdate is enabled."""
212-
return (
213-
"get_auto_update_info" in self.data
214-
and self.data["get_auto_update_info"]["enable"]
215-
)
242+
return "enable" in self.data and self.data["enable"]
216243

217244
@allow_update_after
218245
async def set_auto_update_enabled(self, enabled: bool):
219246
"""Change autoupdate setting."""
220-
data = {**self.data["get_auto_update_info"], "enable": enabled}
247+
data = {**self.data, "enable": enabled}
221248
await self.call("set_auto_update_info", data)

kasa/tests/smart/modules/test_firmware.py

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22

33
import asyncio
44
import logging
5+
from contextlib import nullcontext
56
from typing import TypedDict
67

78
import pytest
89
from pytest_mock import MockerFixture
910

10-
from kasa import Module
11+
from kasa import KasaException, Module
1112
from kasa.smart import SmartDevice
1213
from kasa.smart.modules.firmware import DownloadState
1314
from kasa.tests.device_fixtures import parametrize
@@ -33,10 +34,12 @@ async def test_firmware_features(
3334
"""Test light effect."""
3435
fw = dev.modules.get(Module.Firmware)
3536
assert fw
37+
assert fw.firmware_update_info is None
3638

3739
if not dev.is_cloud_connected:
3840
pytest.skip("Device is not cloud connected, skipping test")
3941

42+
await fw.check_latest_firmware()
4043
if fw.supported_version < required_version:
4144
pytest.skip("Feature %s requires newer version" % feature)
4245

@@ -53,20 +56,36 @@ async def test_update_available_without_cloud(dev: SmartDevice):
5356
"""Test that update_available returns None when disconnected."""
5457
fw = dev.modules.get(Module.Firmware)
5558
assert fw
59+
assert fw.firmware_update_info is None
5660

5761
if dev.is_cloud_connected:
62+
await fw.check_latest_firmware()
5863
assert isinstance(fw.update_available, bool)
5964
else:
6065
assert fw.update_available is None
6166

6267

6368
@firmware
69+
@pytest.mark.parametrize(
70+
("update_available", "expected_result"),
71+
[
72+
pytest.param(True, nullcontext(), id="available"),
73+
pytest.param(False, pytest.raises(KasaException), id="not-available"),
74+
],
75+
)
6476
async def test_firmware_update(
65-
dev: SmartDevice, mocker: MockerFixture, caplog: pytest.LogCaptureFixture
77+
dev: SmartDevice,
78+
mocker: MockerFixture,
79+
caplog: pytest.LogCaptureFixture,
80+
update_available,
81+
expected_result,
6682
):
6783
"""Test updating firmware."""
6884
caplog.set_level(logging.INFO)
6985

86+
if not dev.is_cloud_connected:
87+
pytest.skip("Device is not cloud connected, skipping test")
88+
7089
fw = dev.modules.get(Module.Firmware)
7190
assert fw
7291

@@ -101,7 +120,19 @@ class Extras(TypedDict):
101120

102121
cb_mock = mocker.AsyncMock()
103122

104-
await fw.update(progress_cb=cb_mock)
123+
assert fw.firmware_update_info is None
124+
with pytest.raises(KasaException):
125+
await fw.update(progress_cb=cb_mock)
126+
await fw.check_latest_firmware()
127+
assert fw.firmware_update_info is not None
128+
129+
fw._firmware_update_info.status = 1 if update_available else 0
130+
131+
with expected_result:
132+
await fw.update(progress_cb=cb_mock)
133+
134+
if not update_available:
135+
return
105136

106137
# This is necessary to allow the eventloop to process the created tasks
107138
await asyncio_sleep(0)

0 commit comments

Comments
 (0)
0