2024-05-08 23:43:07 +00:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
import asyncio
|
|
|
|
import logging
|
2024-08-30 17:01:54 +00:00
|
|
|
from contextlib import nullcontext
|
2024-11-20 11:54:13 +00:00
|
|
|
from datetime import date
|
2024-06-19 13:07:59 +00:00
|
|
|
from typing import TypedDict
|
2024-05-08 23:43:07 +00:00
|
|
|
|
|
|
|
import pytest
|
|
|
|
from pytest_mock import MockerFixture
|
|
|
|
|
2024-08-30 17:01:54 +00:00
|
|
|
from kasa import KasaException, Module
|
2024-05-08 23:43:07 +00:00
|
|
|
from kasa.smart import SmartDevice
|
|
|
|
from kasa.smart.modules.firmware import DownloadState
|
2024-11-11 10:11:31 +00:00
|
|
|
|
|
|
|
from ...device_fixtures import parametrize
|
2024-05-08 23:43:07 +00:00
|
|
|
|
|
|
|
firmware = parametrize(
|
|
|
|
"has firmware", component_filter="firmware", protocol_filter={"SMART"}
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@firmware
|
|
|
|
@pytest.mark.parametrize(
|
2024-08-30 15:30:07 +00:00
|
|
|
("feature", "prop_name", "type", "required_version"),
|
2024-05-08 23:43:07 +00:00
|
|
|
[
|
|
|
|
("auto_update_enabled", "auto_update_enabled", bool, 2),
|
|
|
|
("update_available", "update_available", bool, 1),
|
|
|
|
("current_firmware_version", "current_firmware", str, 1),
|
|
|
|
("available_firmware_version", "latest_firmware", str, 1),
|
|
|
|
],
|
|
|
|
)
|
|
|
|
async def test_firmware_features(
|
|
|
|
dev: SmartDevice, feature, prop_name, type, required_version, mocker: MockerFixture
|
|
|
|
):
|
|
|
|
"""Test light effect."""
|
2024-05-10 18:29:28 +00:00
|
|
|
fw = dev.modules.get(Module.Firmware)
|
2024-05-08 23:43:07 +00:00
|
|
|
assert fw
|
2024-08-30 17:01:54 +00:00
|
|
|
assert fw.firmware_update_info is None
|
2024-05-08 23:43:07 +00:00
|
|
|
|
|
|
|
if not dev.is_cloud_connected:
|
|
|
|
pytest.skip("Device is not cloud connected, skipping test")
|
|
|
|
|
2024-08-30 17:01:54 +00:00
|
|
|
await fw.check_latest_firmware()
|
2024-05-08 23:43:07 +00:00
|
|
|
if fw.supported_version < required_version:
|
2024-11-10 18:55:13 +00:00
|
|
|
pytest.skip(f"Feature {feature} requires newer version")
|
2024-05-08 23:43:07 +00:00
|
|
|
|
|
|
|
prop = getattr(fw, prop_name)
|
|
|
|
assert isinstance(prop, type)
|
|
|
|
|
2024-05-13 16:34:44 +00:00
|
|
|
feat = dev.features[feature]
|
2024-05-08 23:43:07 +00:00
|
|
|
assert feat.value == prop
|
|
|
|
assert isinstance(feat.value, type)
|
|
|
|
|
|
|
|
|
2024-11-20 11:54:13 +00:00
|
|
|
@firmware
|
|
|
|
async def test_firmware_update_info(dev: SmartDevice):
|
|
|
|
"""Test that the firmware UpdateInfo object deserializes correctly."""
|
|
|
|
fw = dev.modules.get(Module.Firmware)
|
|
|
|
assert fw
|
|
|
|
|
|
|
|
if not dev.is_cloud_connected:
|
|
|
|
pytest.skip("Device is not cloud connected, skipping test")
|
|
|
|
assert fw.firmware_update_info is None
|
|
|
|
await fw.check_latest_firmware()
|
|
|
|
assert fw.firmware_update_info is not None
|
|
|
|
assert isinstance(fw.firmware_update_info.release_date, date | None)
|
|
|
|
|
|
|
|
|
2024-05-08 23:43:07 +00:00
|
|
|
@firmware
|
|
|
|
async def test_update_available_without_cloud(dev: SmartDevice):
|
|
|
|
"""Test that update_available returns None when disconnected."""
|
2024-05-10 18:29:28 +00:00
|
|
|
fw = dev.modules.get(Module.Firmware)
|
2024-05-08 23:43:07 +00:00
|
|
|
assert fw
|
2024-08-30 17:01:54 +00:00
|
|
|
assert fw.firmware_update_info is None
|
2024-05-08 23:43:07 +00:00
|
|
|
|
|
|
|
if dev.is_cloud_connected:
|
2024-08-30 17:01:54 +00:00
|
|
|
await fw.check_latest_firmware()
|
2024-05-08 23:43:07 +00:00
|
|
|
assert isinstance(fw.update_available, bool)
|
|
|
|
else:
|
|
|
|
assert fw.update_available is None
|
|
|
|
|
|
|
|
|
|
|
|
@firmware
|
2024-08-30 17:01:54 +00:00
|
|
|
@pytest.mark.parametrize(
|
|
|
|
("update_available", "expected_result"),
|
|
|
|
[
|
|
|
|
pytest.param(True, nullcontext(), id="available"),
|
|
|
|
pytest.param(False, pytest.raises(KasaException), id="not-available"),
|
|
|
|
],
|
|
|
|
)
|
2024-11-18 18:46:36 +00:00
|
|
|
@pytest.mark.requires_dummy
|
2024-11-23 12:20:51 +00:00
|
|
|
@pytest.mark.xdist_group(name="caplog")
|
2024-05-08 23:43:07 +00:00
|
|
|
async def test_firmware_update(
|
2024-08-30 17:01:54 +00:00
|
|
|
dev: SmartDevice,
|
|
|
|
mocker: MockerFixture,
|
|
|
|
caplog: pytest.LogCaptureFixture,
|
|
|
|
update_available,
|
|
|
|
expected_result,
|
2024-05-08 23:43:07 +00:00
|
|
|
):
|
|
|
|
"""Test updating firmware."""
|
|
|
|
caplog.set_level(logging.INFO)
|
|
|
|
|
2024-08-30 17:01:54 +00:00
|
|
|
if not dev.is_cloud_connected:
|
|
|
|
pytest.skip("Device is not cloud connected, skipping test")
|
|
|
|
|
2024-05-10 18:29:28 +00:00
|
|
|
fw = dev.modules.get(Module.Firmware)
|
2024-05-08 23:43:07 +00:00
|
|
|
assert fw
|
|
|
|
|
|
|
|
upgrade_time = 5
|
2024-06-19 13:07:59 +00:00
|
|
|
|
|
|
|
class Extras(TypedDict):
|
|
|
|
reboot_time: int
|
|
|
|
upgrade_time: int
|
|
|
|
auto_upgrade: bool
|
|
|
|
|
|
|
|
extras: Extras = {
|
|
|
|
"reboot_time": 5,
|
|
|
|
"upgrade_time": upgrade_time,
|
|
|
|
"auto_upgrade": False,
|
|
|
|
}
|
2024-05-08 23:43:07 +00:00
|
|
|
update_states = [
|
|
|
|
# Unknown 1
|
2024-11-20 11:54:13 +00:00
|
|
|
DownloadState(status=1, progress=0, **extras),
|
2024-05-08 23:43:07 +00:00
|
|
|
# Downloading
|
2024-11-20 11:54:13 +00:00
|
|
|
DownloadState(status=2, progress=10, **extras),
|
|
|
|
DownloadState(status=2, progress=100, **extras),
|
2024-05-08 23:43:07 +00:00
|
|
|
# Flashing
|
2024-11-20 11:54:13 +00:00
|
|
|
DownloadState(status=3, progress=100, **extras),
|
|
|
|
DownloadState(status=3, progress=100, **extras),
|
2024-05-08 23:43:07 +00:00
|
|
|
# Done
|
2024-11-20 11:54:13 +00:00
|
|
|
DownloadState(status=0, progress=100, **extras),
|
2024-05-08 23:43:07 +00:00
|
|
|
]
|
|
|
|
|
|
|
|
asyncio_sleep = asyncio.sleep
|
|
|
|
sleep = mocker.patch("asyncio.sleep")
|
|
|
|
mocker.patch.object(fw, "get_update_state", side_effect=update_states)
|
|
|
|
|
|
|
|
cb_mock = mocker.AsyncMock()
|
|
|
|
|
2024-08-30 17:01:54 +00:00
|
|
|
assert fw.firmware_update_info is None
|
|
|
|
with pytest.raises(KasaException):
|
|
|
|
await fw.update(progress_cb=cb_mock)
|
|
|
|
await fw.check_latest_firmware()
|
|
|
|
assert fw.firmware_update_info is not None
|
|
|
|
|
|
|
|
fw._firmware_update_info.status = 1 if update_available else 0
|
|
|
|
|
|
|
|
with expected_result:
|
|
|
|
await fw.update(progress_cb=cb_mock)
|
|
|
|
|
|
|
|
if not update_available:
|
|
|
|
return
|
2024-05-08 23:43:07 +00:00
|
|
|
|
|
|
|
# This is necessary to allow the eventloop to process the created tasks
|
|
|
|
await asyncio_sleep(0)
|
|
|
|
|
|
|
|
assert "Unhandled state code" in caplog.text
|
|
|
|
assert "Downloading firmware, progress: 10" in caplog.text
|
|
|
|
assert "Flashing firmware, sleeping" in caplog.text
|
|
|
|
assert "Update idle" in caplog.text
|
|
|
|
|
|
|
|
for state in update_states:
|
|
|
|
cb_mock.assert_any_await(state)
|
|
|
|
|
|
|
|
# sleep based on the upgrade_time
|
|
|
|
sleep.assert_any_call(upgrade_time)
|