mirror of
https://github.com/python-kasa/python-kasa.git
synced 2024-12-25 12:33:34 +00:00
9473d97ad2
Introduce common module interfaces across smart and iot devices and provide better typing implementation for getting modules to support this.
287 lines
7.5 KiB
Python
287 lines
7.5 KiB
Python
"""Module for common iotdevice tests."""
|
|
|
|
import re
|
|
from datetime import datetime
|
|
|
|
import pytest
|
|
from voluptuous import (
|
|
REMOVE_EXTRA,
|
|
All,
|
|
Any,
|
|
Boolean,
|
|
In,
|
|
Invalid,
|
|
Optional,
|
|
Range,
|
|
Schema,
|
|
)
|
|
|
|
from kasa import KasaException, Module
|
|
from kasa.iot import IotDevice
|
|
|
|
from .conftest import get_device_for_fixture_protocol, handle_turn_on, turn_on
|
|
from .device_fixtures import device_iot, has_emeter_iot, no_emeter_iot
|
|
from .fakeprotocol_iot import FakeIotProtocol
|
|
|
|
TZ_SCHEMA = Schema(
|
|
{"zone_str": str, "dst_offset": int, "index": All(int, Range(min=0)), "tz_str": str}
|
|
)
|
|
|
|
|
|
def check_mac(x):
|
|
if re.match("[0-9a-f]{2}([-:])[0-9a-f]{2}(\\1[0-9a-f]{2}){4}$", x.lower()):
|
|
return x
|
|
raise Invalid(x)
|
|
|
|
|
|
SYSINFO_SCHEMA = Schema(
|
|
{
|
|
"active_mode": In(["schedule", "none", "count_down"]),
|
|
"alias": str,
|
|
"dev_name": str,
|
|
"deviceId": str,
|
|
"feature": str,
|
|
"fwId": str,
|
|
"hwId": str,
|
|
"hw_ver": str,
|
|
"icon_hash": str,
|
|
"led_off": Boolean,
|
|
"latitude": Any(All(float, Range(min=-90, max=90)), 0, None),
|
|
"latitude_i": Any(
|
|
All(int, Range(min=-900000, max=900000)),
|
|
All(float, Range(min=-900000, max=900000)),
|
|
0,
|
|
None,
|
|
),
|
|
"longitude": Any(All(float, Range(min=-180, max=180)), 0, None),
|
|
"longitude_i": Any(
|
|
All(int, Range(min=-18000000, max=18000000)),
|
|
All(float, Range(min=-18000000, max=18000000)),
|
|
0,
|
|
None,
|
|
),
|
|
"mac": check_mac,
|
|
"model": str,
|
|
"oemId": str,
|
|
"on_time": int,
|
|
"relay_state": int,
|
|
"rssi": Any(int, None), # rssi can also be positive, see #54
|
|
"sw_ver": str,
|
|
"type": str,
|
|
"mic_type": str,
|
|
"updating": Boolean,
|
|
# these are available on hs220
|
|
"brightness": int,
|
|
"preferred_state": [
|
|
{"brightness": All(int, Range(min=0, max=100)), "index": int}
|
|
],
|
|
"next_action": {"type": int},
|
|
"child_num": Optional(Any(None, int)),
|
|
"children": Optional(list),
|
|
},
|
|
extra=REMOVE_EXTRA,
|
|
)
|
|
|
|
|
|
@device_iot
|
|
async def test_state_info(dev):
|
|
assert isinstance(dev.state_information, dict)
|
|
|
|
|
|
@pytest.mark.requires_dummy
|
|
@device_iot
|
|
async def test_invalid_connection(mocker, dev):
|
|
with mocker.patch.object(
|
|
FakeIotProtocol, "query", side_effect=KasaException
|
|
), pytest.raises(KasaException):
|
|
await dev.update()
|
|
|
|
|
|
@has_emeter_iot
|
|
async def test_initial_update_emeter(dev, mocker):
|
|
"""Test that the initial update performs second query if emeter is available."""
|
|
dev._last_update = None
|
|
dev._legacy_features = set()
|
|
spy = mocker.spy(dev.protocol, "query")
|
|
await dev.update()
|
|
# Devices with small buffers may require 3 queries
|
|
expected_queries = 2 if dev.max_device_response_size > 4096 else 3
|
|
assert spy.call_count == expected_queries + len(dev.children)
|
|
|
|
|
|
@no_emeter_iot
|
|
async def test_initial_update_no_emeter(dev, mocker):
|
|
"""Test that the initial update performs second query if emeter is available."""
|
|
dev._last_update = None
|
|
dev._legacy_features = set()
|
|
spy = mocker.spy(dev.protocol, "query")
|
|
await dev.update()
|
|
# 2 calls are necessary as some devices crash on unexpected modules
|
|
# See #105, #120, #161
|
|
assert spy.call_count == 2
|
|
|
|
|
|
@device_iot
|
|
async def test_query_helper(dev):
|
|
with pytest.raises(KasaException):
|
|
await dev._query_helper("test", "testcmd", {})
|
|
# TODO check for unwrapping?
|
|
|
|
|
|
@device_iot
|
|
@turn_on
|
|
async def test_state(dev, turn_on):
|
|
await handle_turn_on(dev, turn_on)
|
|
orig_state = dev.is_on
|
|
if orig_state:
|
|
await dev.turn_off()
|
|
await dev.update()
|
|
assert not dev.is_on
|
|
assert dev.is_off
|
|
|
|
await dev.turn_on()
|
|
await dev.update()
|
|
assert dev.is_on
|
|
assert not dev.is_off
|
|
else:
|
|
await dev.turn_on()
|
|
await dev.update()
|
|
assert dev.is_on
|
|
assert not dev.is_off
|
|
|
|
await dev.turn_off()
|
|
await dev.update()
|
|
assert not dev.is_on
|
|
assert dev.is_off
|
|
|
|
|
|
@device_iot
|
|
@turn_on
|
|
async def test_on_since(dev, turn_on):
|
|
await handle_turn_on(dev, turn_on)
|
|
orig_state = dev.is_on
|
|
if "on_time" not in dev.sys_info and not dev.is_strip:
|
|
assert dev.on_since is None
|
|
elif orig_state:
|
|
assert isinstance(dev.on_since, datetime)
|
|
else:
|
|
assert dev.on_since is None
|
|
|
|
|
|
@device_iot
|
|
async def test_time(dev):
|
|
assert isinstance(await dev.get_time(), datetime)
|
|
|
|
|
|
@device_iot
|
|
async def test_timezone(dev):
|
|
TZ_SCHEMA(await dev.get_timezone())
|
|
|
|
|
|
@device_iot
|
|
async def test_hw_info(dev):
|
|
SYSINFO_SCHEMA(dev.hw_info)
|
|
|
|
|
|
@device_iot
|
|
async def test_location(dev):
|
|
SYSINFO_SCHEMA(dev.location)
|
|
|
|
|
|
@device_iot
|
|
async def test_rssi(dev):
|
|
SYSINFO_SCHEMA({"rssi": dev.rssi}) # wrapping for vol
|
|
|
|
|
|
@device_iot
|
|
async def test_mac(dev):
|
|
SYSINFO_SCHEMA({"mac": dev.mac}) # wrapping for val
|
|
|
|
|
|
@device_iot
|
|
async def test_representation(dev):
|
|
pattern = re.compile("<DeviceType\..+ at .+? - .*? \(.+?\)>")
|
|
assert pattern.match(str(dev))
|
|
|
|
|
|
@device_iot
|
|
async def test_children(dev):
|
|
"""Make sure that children property is exposed by every device."""
|
|
if dev.is_strip:
|
|
assert len(dev.children) > 0
|
|
else:
|
|
assert len(dev.children) == 0
|
|
|
|
|
|
@device_iot
|
|
async def test_modules_preserved(dev: IotDevice):
|
|
"""Make modules that are not being updated are preserved between updates."""
|
|
dev._last_update["some_module_not_being_updated"] = "should_be_kept"
|
|
await dev.update()
|
|
assert dev._last_update["some_module_not_being_updated"] == "should_be_kept"
|
|
|
|
|
|
@device_iot
|
|
async def test_internal_state(dev):
|
|
"""Make sure the internal state returns the last update results."""
|
|
assert dev.internal_state == dev._last_update
|
|
|
|
|
|
@device_iot
|
|
async def test_features(dev):
|
|
"""Make sure features is always accessible."""
|
|
sysinfo = dev._last_update["system"]["get_sysinfo"]
|
|
if "feature" in sysinfo:
|
|
assert dev._legacy_features == set(sysinfo["feature"].split(":"))
|
|
else:
|
|
assert dev._legacy_features == set()
|
|
|
|
|
|
@device_iot
|
|
async def test_max_device_response_size(dev):
|
|
"""Make sure every device return has a set max response size."""
|
|
assert dev.max_device_response_size > 0
|
|
|
|
|
|
@device_iot
|
|
async def test_estimated_response_sizes(dev):
|
|
"""Make sure every module has an estimated response size set."""
|
|
for mod in dev.modules.values():
|
|
assert mod.estimated_query_response_size > 0
|
|
|
|
|
|
@device_iot
|
|
async def test_modules_not_supported(dev: IotDevice):
|
|
"""Test that unsupported modules do not break the device."""
|
|
for module in dev.modules.values():
|
|
assert module.is_supported is not None
|
|
await dev.update()
|
|
for module in dev.modules.values():
|
|
assert module.is_supported is not None
|
|
|
|
|
|
async def test_get_modules():
|
|
"""Test getting modules for child and parent modules."""
|
|
dummy_device = await get_device_for_fixture_protocol(
|
|
"HS100(US)_2.0_1.5.6.json", "IOT"
|
|
)
|
|
from kasa.iot.modules import Cloud
|
|
|
|
# Modules on device
|
|
module = dummy_device.modules.get("cloud")
|
|
assert module
|
|
assert module._device == dummy_device
|
|
assert isinstance(module, Cloud)
|
|
|
|
module = dummy_device.modules.get(Module.IotCloud)
|
|
assert module
|
|
assert module._device == dummy_device
|
|
assert isinstance(module, Cloud)
|
|
|
|
# Invalid modules
|
|
module = dummy_device.modules.get("DummyModule")
|
|
assert module is None
|
|
|
|
module = dummy_device.modules.get(Module.Cloud)
|
|
assert module is None
|