mirror of
https://github.com/python-kasa/python-kasa.git
synced 2025-12-31 17:02:45 +00:00
Merge remote-tracking branch 'upstream/master' into feat/power_protection
This commit is contained in:
44
tests/smart/modules/test_childlock.py
Normal file
44
tests/smart/modules/test_childlock.py
Normal file
@@ -0,0 +1,44 @@
|
||||
import pytest
|
||||
|
||||
from kasa import Module
|
||||
from kasa.smart.modules import ChildLock
|
||||
|
||||
from ...device_fixtures import parametrize
|
||||
|
||||
childlock = parametrize(
|
||||
"has child lock",
|
||||
component_filter="button_and_led",
|
||||
protocol_filter={"SMART"},
|
||||
)
|
||||
|
||||
|
||||
@childlock
|
||||
@pytest.mark.parametrize(
|
||||
("feature", "prop_name", "type"),
|
||||
[
|
||||
("child_lock", "enabled", bool),
|
||||
],
|
||||
)
|
||||
async def test_features(dev, feature, prop_name, type):
|
||||
"""Test that features are registered and work as expected."""
|
||||
protect: ChildLock = dev.modules[Module.ChildLock]
|
||||
assert protect is not None
|
||||
|
||||
prop = getattr(protect, prop_name)
|
||||
assert isinstance(prop, type)
|
||||
|
||||
feat = protect._device.features[feature]
|
||||
assert feat.value == prop
|
||||
assert isinstance(feat.value, type)
|
||||
|
||||
|
||||
@childlock
|
||||
async def test_enabled(dev):
|
||||
"""Test the API."""
|
||||
protect: ChildLock = dev.modules[Module.ChildLock]
|
||||
assert protect is not None
|
||||
|
||||
assert isinstance(protect.enabled, bool)
|
||||
await protect.set_enabled(False)
|
||||
await dev.update()
|
||||
assert protect.enabled is False
|
||||
69
tests/smart/modules/test_childsetup.py
Normal file
69
tests/smart/modules/test_childsetup.py
Normal file
@@ -0,0 +1,69 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from kasa import Feature, Module, SmartDevice
|
||||
|
||||
from ...device_fixtures import parametrize
|
||||
|
||||
childsetup = parametrize(
|
||||
"supports pairing", component_filter="child_quick_setup", protocol_filter={"SMART"}
|
||||
)
|
||||
|
||||
|
||||
@childsetup
|
||||
async def test_childsetup_features(dev: SmartDevice):
|
||||
"""Test the exposed features."""
|
||||
cs = dev.modules.get(Module.ChildSetup)
|
||||
assert cs
|
||||
|
||||
assert "pair" in cs._module_features
|
||||
pair = cs._module_features["pair"]
|
||||
assert pair.type == Feature.Type.Action
|
||||
|
||||
|
||||
@childsetup
|
||||
async def test_childsetup_pair(
|
||||
dev: SmartDevice, mocker: MockerFixture, caplog: pytest.LogCaptureFixture
|
||||
):
|
||||
"""Test device pairing."""
|
||||
caplog.set_level(logging.INFO)
|
||||
mock_query_helper = mocker.spy(dev, "_query_helper")
|
||||
mocker.patch("asyncio.sleep")
|
||||
|
||||
cs = dev.modules.get(Module.ChildSetup)
|
||||
assert cs
|
||||
|
||||
await cs.pair()
|
||||
|
||||
mock_query_helper.assert_has_awaits(
|
||||
[
|
||||
mocker.call("begin_scanning_child_device", None),
|
||||
mocker.call("get_support_child_device_category", None),
|
||||
mocker.call("get_scan_child_device_list", params=mocker.ANY),
|
||||
mocker.call("add_child_device_list", params=mocker.ANY),
|
||||
]
|
||||
)
|
||||
assert "Discovery done" in caplog.text
|
||||
|
||||
|
||||
@childsetup
|
||||
async def test_childsetup_unpair(
|
||||
dev: SmartDevice, mocker: MockerFixture, caplog: pytest.LogCaptureFixture
|
||||
):
|
||||
"""Test unpair."""
|
||||
mock_query_helper = mocker.spy(dev, "_query_helper")
|
||||
DUMMY_ID = "dummy_id"
|
||||
|
||||
cs = dev.modules.get(Module.ChildSetup)
|
||||
assert cs
|
||||
|
||||
await cs.unpair(DUMMY_ID)
|
||||
|
||||
mock_query_helper.assert_awaited_with(
|
||||
"remove_child_device_list",
|
||||
params={"child_device_list": [{"device_id": DUMMY_ID}]},
|
||||
)
|
||||
248
tests/smart/modules/test_clean.py
Normal file
248
tests/smart/modules/test_clean.py
Normal file
@@ -0,0 +1,248 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from kasa import Module
|
||||
from kasa.smart import SmartDevice
|
||||
from kasa.smart.modules.clean import ErrorCode, Status
|
||||
|
||||
from ...device_fixtures import get_parent_and_child_modules, parametrize
|
||||
|
||||
clean = parametrize("clean module", component_filter="clean", protocol_filter={"SMART"})
|
||||
|
||||
|
||||
@clean
|
||||
@pytest.mark.parametrize(
|
||||
("feature", "prop_name", "type"),
|
||||
[
|
||||
("vacuum_status", "status", Status),
|
||||
("vacuum_error", "error", ErrorCode),
|
||||
("vacuum_fan_speed", "fan_speed_preset", str),
|
||||
("carpet_clean_mode", "carpet_clean_mode", str),
|
||||
("battery_level", "battery", int),
|
||||
],
|
||||
)
|
||||
async def test_features(dev: SmartDevice, feature: str, prop_name: str, type: type):
|
||||
"""Test that features are registered and work as expected."""
|
||||
clean = next(get_parent_and_child_modules(dev, Module.Clean))
|
||||
assert clean is not None
|
||||
|
||||
prop = getattr(clean, prop_name)
|
||||
assert isinstance(prop, type)
|
||||
|
||||
feat = clean._device.features[feature]
|
||||
assert feat.value == prop
|
||||
assert isinstance(feat.value, type)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("feature", "value", "method", "params"),
|
||||
[
|
||||
pytest.param(
|
||||
"vacuum_start",
|
||||
1,
|
||||
"setSwitchClean",
|
||||
{
|
||||
"clean_mode": 0,
|
||||
"clean_on": True,
|
||||
"clean_order": True,
|
||||
"force_clean": False,
|
||||
},
|
||||
id="vacuum_start",
|
||||
),
|
||||
pytest.param(
|
||||
"vacuum_pause", 1, "setRobotPause", {"pause": True}, id="vacuum_pause"
|
||||
),
|
||||
pytest.param(
|
||||
"vacuum_return_home",
|
||||
1,
|
||||
"setSwitchCharge",
|
||||
{"switch_charge": True},
|
||||
id="vacuum_return_home",
|
||||
),
|
||||
pytest.param(
|
||||
"vacuum_fan_speed",
|
||||
"Quiet",
|
||||
"setCleanAttr",
|
||||
{"suction": 1, "type": "global"},
|
||||
id="vacuum_fan_speed",
|
||||
),
|
||||
pytest.param(
|
||||
"carpet_clean_mode",
|
||||
"Boost",
|
||||
"setCarpetClean",
|
||||
{"carpet_clean_prefer": "boost"},
|
||||
id="carpet_clean_mode",
|
||||
),
|
||||
pytest.param(
|
||||
"clean_count",
|
||||
2,
|
||||
"setCleanAttr",
|
||||
{"clean_number": 2, "type": "global"},
|
||||
id="clean_count",
|
||||
),
|
||||
],
|
||||
)
|
||||
@clean
|
||||
async def test_actions(
|
||||
dev: SmartDevice,
|
||||
mocker: MockerFixture,
|
||||
feature: str,
|
||||
value: str | int,
|
||||
method: str,
|
||||
params: dict,
|
||||
):
|
||||
"""Test the clean actions."""
|
||||
clean = next(get_parent_and_child_modules(dev, Module.Clean))
|
||||
call = mocker.spy(clean, "call")
|
||||
|
||||
await dev.features[feature].set_value(value)
|
||||
call.assert_called_with(method, params)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("err_status", "error", "warning_msg"),
|
||||
[
|
||||
pytest.param([], ErrorCode.Ok, None, id="empty error"),
|
||||
pytest.param([0], ErrorCode.Ok, None, id="no error"),
|
||||
pytest.param([3], ErrorCode.MainBrushStuck, None, id="known error"),
|
||||
pytest.param(
|
||||
[123],
|
||||
ErrorCode.UnknownInternal,
|
||||
"Unknown error code, please create an issue describing the error: 123",
|
||||
id="unknown error",
|
||||
),
|
||||
pytest.param(
|
||||
[3, 4],
|
||||
ErrorCode.MainBrushStuck,
|
||||
"Multiple error codes, using the first one only: [3, 4]",
|
||||
id="multi-error",
|
||||
),
|
||||
],
|
||||
)
|
||||
@clean
|
||||
async def test_post_update_hook(
|
||||
dev: SmartDevice,
|
||||
err_status: list,
|
||||
error: ErrorCode,
|
||||
warning_msg: str | None,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
):
|
||||
"""Test that post update hook sets error states correctly."""
|
||||
clean = next(get_parent_and_child_modules(dev, Module.Clean))
|
||||
assert clean
|
||||
|
||||
caplog.set_level(logging.DEBUG)
|
||||
|
||||
# _post_update_hook will pop an item off the status list so create a copy.
|
||||
err_status = [e for e in err_status]
|
||||
clean.data["getVacStatus"]["err_status"] = err_status
|
||||
|
||||
await clean._post_update_hook()
|
||||
|
||||
assert clean._error_code is error
|
||||
|
||||
if error is not ErrorCode.Ok:
|
||||
assert clean.status is Status.Error
|
||||
|
||||
if warning_msg:
|
||||
assert warning_msg in caplog.text
|
||||
|
||||
# Check doesn't log twice
|
||||
caplog.clear()
|
||||
await clean._post_update_hook()
|
||||
|
||||
if warning_msg:
|
||||
assert warning_msg not in caplog.text
|
||||
|
||||
|
||||
@clean
|
||||
async def test_resume(dev: SmartDevice, mocker: MockerFixture):
|
||||
"""Test that start calls resume if the state is paused."""
|
||||
clean = next(get_parent_and_child_modules(dev, Module.Clean))
|
||||
|
||||
call = mocker.spy(clean, "call")
|
||||
resume = mocker.spy(clean, "resume")
|
||||
|
||||
mocker.patch.object(
|
||||
type(clean),
|
||||
"status",
|
||||
new_callable=mocker.PropertyMock,
|
||||
return_value=Status.Paused,
|
||||
)
|
||||
await clean.start()
|
||||
|
||||
call.assert_called_with("setRobotPause", {"pause": False})
|
||||
resume.assert_awaited()
|
||||
|
||||
|
||||
@clean
|
||||
async def test_unknown_status(
|
||||
dev: SmartDevice, mocker: MockerFixture, caplog: pytest.LogCaptureFixture
|
||||
):
|
||||
"""Test that unknown status is logged."""
|
||||
clean = next(get_parent_and_child_modules(dev, Module.Clean))
|
||||
|
||||
caplog.set_level(logging.DEBUG)
|
||||
clean.data["getVacStatus"]["status"] = 123
|
||||
|
||||
assert clean.status is Status.UnknownInternal
|
||||
assert "Got unknown status code: 123" in caplog.text
|
||||
|
||||
# Check only logs once
|
||||
caplog.clear()
|
||||
|
||||
assert clean.status is Status.UnknownInternal
|
||||
assert "Got unknown status code: 123" not in caplog.text
|
||||
|
||||
# Check logs again for other errors
|
||||
|
||||
caplog.clear()
|
||||
clean.data["getVacStatus"]["status"] = 123456
|
||||
|
||||
assert clean.status is Status.UnknownInternal
|
||||
assert "Got unknown status code: 123456" in caplog.text
|
||||
|
||||
|
||||
@clean
|
||||
@pytest.mark.parametrize(
|
||||
("setting", "value", "exc", "exc_message"),
|
||||
[
|
||||
pytest.param(
|
||||
"vacuum_fan_speed",
|
||||
"invalid speed",
|
||||
ValueError,
|
||||
"Invalid fan speed",
|
||||
id="vacuum_fan_speed",
|
||||
),
|
||||
pytest.param(
|
||||
"carpet_clean_mode",
|
||||
"invalid mode",
|
||||
ValueError,
|
||||
"Invalid carpet clean mode",
|
||||
id="carpet_clean_mode",
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_invalid_settings(
|
||||
dev: SmartDevice,
|
||||
mocker: MockerFixture,
|
||||
setting: str,
|
||||
value: str,
|
||||
exc: type[Exception],
|
||||
exc_message: str,
|
||||
):
|
||||
"""Test invalid settings."""
|
||||
clean = next(get_parent_and_child_modules(dev, Module.Clean))
|
||||
|
||||
# Not using feature.set_value() as it checks for valid values
|
||||
setter_name = dev.features[setting].attribute_setter
|
||||
assert isinstance(setter_name, str)
|
||||
|
||||
setter = getattr(clean, setter_name)
|
||||
|
||||
with pytest.raises(exc, match=exc_message):
|
||||
await setter(value)
|
||||
59
tests/smart/modules/test_cleanrecords.py
Normal file
59
tests/smart/modules/test_cleanrecords.py
Normal file
@@ -0,0 +1,59 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
import pytest
|
||||
|
||||
from kasa import Module
|
||||
from kasa.smart import SmartDevice
|
||||
|
||||
from ...device_fixtures import get_parent_and_child_modules, parametrize
|
||||
|
||||
cleanrecords = parametrize(
|
||||
"has clean records", component_filter="clean_percent", protocol_filter={"SMART"}
|
||||
)
|
||||
|
||||
|
||||
@cleanrecords
|
||||
@pytest.mark.parametrize(
|
||||
("feature", "prop_name", "type"),
|
||||
[
|
||||
("total_clean_area", "total_clean_area", int),
|
||||
("total_clean_time", "total_clean_time", timedelta),
|
||||
("last_clean_area", "last_clean_area", int),
|
||||
("last_clean_time", "last_clean_time", timedelta),
|
||||
("total_clean_count", "total_clean_count", int),
|
||||
("last_clean_timestamp", "last_clean_timestamp", datetime),
|
||||
],
|
||||
)
|
||||
async def test_features(dev: SmartDevice, feature: str, prop_name: str, type: type):
|
||||
"""Test that features are registered and work as expected."""
|
||||
records = next(get_parent_and_child_modules(dev, Module.CleanRecords))
|
||||
assert records is not None
|
||||
|
||||
prop = getattr(records, prop_name)
|
||||
assert isinstance(prop, type)
|
||||
|
||||
feat = records._device.features[feature]
|
||||
assert feat.value == prop
|
||||
assert isinstance(feat.value, type)
|
||||
|
||||
|
||||
@cleanrecords
|
||||
async def test_timezone(dev: SmartDevice):
|
||||
"""Test that timezone is added to timestamps."""
|
||||
clean_records = next(get_parent_and_child_modules(dev, Module.CleanRecords))
|
||||
assert clean_records is not None
|
||||
|
||||
assert isinstance(clean_records.last_clean_timestamp, datetime)
|
||||
assert clean_records.last_clean_timestamp.tzinfo
|
||||
|
||||
# Check for zone info to ensure that this wasn't picking upthe default
|
||||
# of utc before the time module is updated.
|
||||
assert isinstance(clean_records.last_clean_timestamp.tzinfo, ZoneInfo)
|
||||
|
||||
for record in clean_records.parsed_data.records:
|
||||
assert isinstance(record.timestamp, datetime)
|
||||
assert record.timestamp.tzinfo
|
||||
assert isinstance(record.timestamp.tzinfo, ZoneInfo)
|
||||
53
tests/smart/modules/test_consumables.py
Normal file
53
tests/smart/modules/test_consumables.py
Normal file
@@ -0,0 +1,53 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
import pytest
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from kasa import Module
|
||||
from kasa.smart import SmartDevice
|
||||
from kasa.smart.modules.consumables import CONSUMABLE_METAS
|
||||
|
||||
from ...device_fixtures import get_parent_and_child_modules, parametrize
|
||||
|
||||
consumables = parametrize(
|
||||
"has consumables", component_filter="consumables", protocol_filter={"SMART"}
|
||||
)
|
||||
|
||||
|
||||
@consumables
|
||||
@pytest.mark.parametrize(
|
||||
"consumable_name", [consumable.id for consumable in CONSUMABLE_METAS]
|
||||
)
|
||||
@pytest.mark.parametrize("postfix", ["used", "remaining"])
|
||||
async def test_features(dev: SmartDevice, consumable_name: str, postfix: str):
|
||||
"""Test that features are registered and work as expected."""
|
||||
consumables = next(get_parent_and_child_modules(dev, Module.Consumables))
|
||||
assert consumables is not None
|
||||
|
||||
feature_name = f"{consumable_name}_{postfix}"
|
||||
|
||||
feat = consumables._device.features[feature_name]
|
||||
assert isinstance(feat.value, timedelta)
|
||||
|
||||
|
||||
@consumables
|
||||
@pytest.mark.parametrize(
|
||||
("consumable_name", "data_key"),
|
||||
[(consumable.id, consumable.data_key) for consumable in CONSUMABLE_METAS],
|
||||
)
|
||||
async def test_erase(
|
||||
dev: SmartDevice, mocker: MockerFixture, consumable_name: str, data_key: str
|
||||
):
|
||||
"""Test autocollection switch."""
|
||||
consumables = next(get_parent_and_child_modules(dev, Module.Consumables))
|
||||
call = mocker.spy(consumables, "call")
|
||||
|
||||
feature_name = f"{consumable_name}_reset"
|
||||
feat = dev._features[feature_name]
|
||||
await feat.set_value(True)
|
||||
|
||||
call.assert_called_with(
|
||||
"resetConsumablesTime", {"reset_list": [data_key.removesuffix("_time")]}
|
||||
)
|
||||
92
tests/smart/modules/test_dustbin.py
Normal file
92
tests/smart/modules/test_dustbin.py
Normal file
@@ -0,0 +1,92 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from kasa import Module
|
||||
from kasa.smart import SmartDevice
|
||||
from kasa.smart.modules.dustbin import Mode
|
||||
|
||||
from ...device_fixtures import get_parent_and_child_modules, parametrize
|
||||
|
||||
dustbin = parametrize(
|
||||
"has dustbin", component_filter="dust_bucket", protocol_filter={"SMART"}
|
||||
)
|
||||
|
||||
|
||||
@dustbin
|
||||
@pytest.mark.parametrize(
|
||||
("feature", "prop_name", "type"),
|
||||
[
|
||||
("dustbin_autocollection_enabled", "auto_collection", bool),
|
||||
("dustbin_mode", "mode", str),
|
||||
],
|
||||
)
|
||||
async def test_features(dev: SmartDevice, feature: str, prop_name: str, type: type):
|
||||
"""Test that features are registered and work as expected."""
|
||||
dustbin = next(get_parent_and_child_modules(dev, Module.Dustbin))
|
||||
assert dustbin is not None
|
||||
|
||||
prop = getattr(dustbin, prop_name)
|
||||
assert isinstance(prop, type)
|
||||
|
||||
feat = dustbin._device.features[feature]
|
||||
assert feat.value == prop
|
||||
assert isinstance(feat.value, type)
|
||||
|
||||
|
||||
@dustbin
|
||||
async def test_dustbin_mode(dev: SmartDevice, mocker: MockerFixture):
|
||||
"""Test dust mode."""
|
||||
dustbin = next(get_parent_and_child_modules(dev, Module.Dustbin))
|
||||
call = mocker.spy(dustbin, "call")
|
||||
|
||||
mode_feature = dustbin._device.features["dustbin_mode"]
|
||||
assert dustbin.mode == mode_feature.value
|
||||
|
||||
new_mode = Mode.Max
|
||||
await dustbin.set_mode(new_mode.name)
|
||||
|
||||
params = dustbin._settings.copy()
|
||||
params["dust_collection_mode"] = new_mode.value
|
||||
|
||||
call.assert_called_with("setDustCollectionInfo", params)
|
||||
|
||||
await dev.update()
|
||||
|
||||
assert dustbin.mode == new_mode.name
|
||||
|
||||
with pytest.raises(ValueError, match="Invalid auto/emptying mode speed"):
|
||||
await dustbin.set_mode("invalid")
|
||||
|
||||
|
||||
@dustbin
|
||||
async def test_autocollection(dev: SmartDevice, mocker: MockerFixture):
|
||||
"""Test autocollection switch."""
|
||||
dustbin = next(get_parent_and_child_modules(dev, Module.Dustbin))
|
||||
call = mocker.spy(dustbin, "call")
|
||||
|
||||
auto_collection = dustbin._device.features["dustbin_autocollection_enabled"]
|
||||
assert dustbin.auto_collection == auto_collection.value
|
||||
|
||||
await auto_collection.set_value(True)
|
||||
|
||||
params = dustbin._settings.copy()
|
||||
params["auto_dust_collection"] = True
|
||||
|
||||
call.assert_called_with("setDustCollectionInfo", params)
|
||||
|
||||
await dev.update()
|
||||
|
||||
assert dustbin.auto_collection is True
|
||||
|
||||
|
||||
@dustbin
|
||||
async def test_empty_dustbin(dev: SmartDevice, mocker: MockerFixture):
|
||||
"""Test the empty dustbin feature."""
|
||||
dustbin = next(get_parent_and_child_modules(dev, Module.Dustbin))
|
||||
call = mocker.spy(dustbin, "call")
|
||||
|
||||
await dustbin.start_emptying()
|
||||
|
||||
call.assert_called_with("setSwitchDustCollection", {"switch_dust_collection": True})
|
||||
@@ -1,7 +1,14 @@
|
||||
import copy
|
||||
import logging
|
||||
from contextlib import nullcontext as does_not_raise
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from kasa import Module, SmartDevice
|
||||
from kasa import DeviceError, Module
|
||||
from kasa.exceptions import SmartErrorCode
|
||||
from kasa.interfaces.energy import Energy
|
||||
from kasa.smart import SmartDevice
|
||||
from kasa.smart.modules import Energy as SmartEnergyModule
|
||||
from tests.conftest import has_emeter_smart
|
||||
|
||||
@@ -19,3 +26,84 @@ async def test_supported(dev: SmartDevice):
|
||||
assert energy_module.supports(Energy.ModuleFeature.VOLTAGE_CURRENT) is False
|
||||
else:
|
||||
assert energy_module.supports(Energy.ModuleFeature.VOLTAGE_CURRENT) is True
|
||||
|
||||
|
||||
@has_emeter_smart
|
||||
async def test_get_energy_usage_error(
|
||||
dev: SmartDevice, caplog: pytest.LogCaptureFixture
|
||||
):
|
||||
"""Test errors on get_energy_usage."""
|
||||
caplog.set_level(logging.DEBUG)
|
||||
|
||||
energy_module = dev.modules.get(Module.Energy)
|
||||
if not energy_module:
|
||||
pytest.skip(f"Energy module not supported for {dev}.")
|
||||
|
||||
version = dev._components["energy_monitoring"]
|
||||
|
||||
expected_raise = does_not_raise() if version > 1 else pytest.raises(DeviceError)
|
||||
if version > 1:
|
||||
expected = "get_energy_usage"
|
||||
expected_current_consumption = 2.002
|
||||
else:
|
||||
expected = "current_power"
|
||||
expected_current_consumption = None
|
||||
|
||||
assert expected in energy_module.data
|
||||
assert energy_module.current_consumption is not None
|
||||
assert energy_module.consumption_today is not None
|
||||
assert energy_module.consumption_this_month is not None
|
||||
|
||||
last_update = copy.deepcopy(dev._last_update)
|
||||
resp = copy.deepcopy(last_update)
|
||||
|
||||
if ed := resp.get("get_emeter_data"):
|
||||
ed["power_mw"] = 2002
|
||||
if cp := resp.get("get_current_power"):
|
||||
cp["current_power"] = 2.002
|
||||
resp["get_energy_usage"] = SmartErrorCode.JSON_DECODE_FAIL_ERROR
|
||||
|
||||
# version 1 only has get_energy_usage so module should raise an error if
|
||||
# version 1 and get_energy_usage is in error
|
||||
with patch.object(dev.protocol, "query", return_value=resp):
|
||||
await dev.update()
|
||||
|
||||
with expected_raise:
|
||||
assert "get_energy_usage" not in energy_module.data
|
||||
|
||||
assert energy_module.current_consumption == expected_current_consumption
|
||||
assert energy_module.consumption_today is None
|
||||
assert energy_module.consumption_this_month is None
|
||||
|
||||
msg = (
|
||||
f"Removed key get_energy_usage from response for device {dev.host}"
|
||||
" as it returned error: JSON_DECODE_FAIL_ERROR"
|
||||
)
|
||||
if version > 1:
|
||||
assert msg in caplog.text
|
||||
|
||||
# Now test with no get_emeter_data
|
||||
# This may not be valid scenario but we have a fallback to get_current_power
|
||||
# just in case that should be tested.
|
||||
caplog.clear()
|
||||
resp = copy.deepcopy(last_update)
|
||||
|
||||
if cp := resp.get("get_current_power"):
|
||||
cp["current_power"] = 2.002
|
||||
resp["get_energy_usage"] = SmartErrorCode.JSON_DECODE_FAIL_ERROR
|
||||
|
||||
# Remove get_emeter_data from the response and from the device which will
|
||||
# remember it otherwise.
|
||||
resp.pop("get_emeter_data", None)
|
||||
dev._last_update.pop("get_emeter_data", None)
|
||||
|
||||
with patch.object(dev.protocol, "query", return_value=resp):
|
||||
await dev.update()
|
||||
|
||||
with expected_raise:
|
||||
assert "get_energy_usage" not in energy_module.data
|
||||
|
||||
assert energy_module.current_consumption == expected_current_consumption
|
||||
|
||||
# message should only be logged once
|
||||
assert msg not in caplog.text
|
||||
|
||||
16
tests/smart/modules/test_homekit.py
Normal file
16
tests/smart/modules/test_homekit.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from kasa import Module
|
||||
from kasa.smart import SmartDevice
|
||||
|
||||
from ...device_fixtures import parametrize
|
||||
|
||||
homekit = parametrize(
|
||||
"has homekit", component_filter="homekit", protocol_filter={"SMART"}
|
||||
)
|
||||
|
||||
|
||||
@homekit
|
||||
async def test_info(dev: SmartDevice):
|
||||
"""Test homekit info."""
|
||||
homekit = dev.modules.get(Module.HomeKit)
|
||||
assert homekit
|
||||
assert homekit.info
|
||||
20
tests/smart/modules/test_matter.py
Normal file
20
tests/smart/modules/test_matter.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from kasa import Module
|
||||
from kasa.smart import SmartDevice
|
||||
|
||||
from ...device_fixtures import parametrize
|
||||
|
||||
matter = parametrize(
|
||||
"has matter", component_filter="matter", protocol_filter={"SMART", "SMARTCAM"}
|
||||
)
|
||||
|
||||
|
||||
@matter
|
||||
async def test_info(dev: SmartDevice):
|
||||
"""Test matter info."""
|
||||
matter = dev.modules.get(Module.Matter)
|
||||
assert matter
|
||||
assert matter.info
|
||||
setup_code = dev.features.get("matter_setup_code")
|
||||
assert setup_code
|
||||
setup_payload = dev.features.get("matter_setup_payload")
|
||||
assert setup_payload
|
||||
58
tests/smart/modules/test_mop.py
Normal file
58
tests/smart/modules/test_mop.py
Normal file
@@ -0,0 +1,58 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from kasa import Module
|
||||
from kasa.smart import SmartDevice
|
||||
from kasa.smart.modules.mop import Waterlevel
|
||||
|
||||
from ...device_fixtures import get_parent_and_child_modules, parametrize
|
||||
|
||||
mop = parametrize("has mop", component_filter="mop", protocol_filter={"SMART"})
|
||||
|
||||
|
||||
@mop
|
||||
@pytest.mark.parametrize(
|
||||
("feature", "prop_name", "type"),
|
||||
[
|
||||
("mop_attached", "mop_attached", bool),
|
||||
("mop_waterlevel", "waterlevel", str),
|
||||
],
|
||||
)
|
||||
async def test_features(dev: SmartDevice, feature: str, prop_name: str, type: type):
|
||||
"""Test that features are registered and work as expected."""
|
||||
mod = next(get_parent_and_child_modules(dev, Module.Mop))
|
||||
assert mod is not None
|
||||
|
||||
prop = getattr(mod, prop_name)
|
||||
assert isinstance(prop, type)
|
||||
|
||||
feat = mod._device.features[feature]
|
||||
assert feat.value == prop
|
||||
assert isinstance(feat.value, type)
|
||||
|
||||
|
||||
@mop
|
||||
async def test_mop_waterlevel(dev: SmartDevice, mocker: MockerFixture):
|
||||
"""Test dust mode."""
|
||||
mop_module = next(get_parent_and_child_modules(dev, Module.Mop))
|
||||
call = mocker.spy(mop_module, "call")
|
||||
|
||||
waterlevel = mop_module._device.features["mop_waterlevel"]
|
||||
assert mop_module.waterlevel == waterlevel.value
|
||||
|
||||
new_level = Waterlevel.High
|
||||
await mop_module.set_waterlevel(new_level.name)
|
||||
|
||||
params = mop_module._settings.copy()
|
||||
params["cistern"] = new_level.value
|
||||
|
||||
call.assert_called_with("setCleanAttr", params)
|
||||
|
||||
await dev.update()
|
||||
|
||||
assert mop_module.waterlevel == new_level.name
|
||||
|
||||
with pytest.raises(ValueError, match="Invalid waterlevel"):
|
||||
await mop_module.set_waterlevel("invalid")
|
||||
71
tests/smart/modules/test_speaker.py
Normal file
71
tests/smart/modules/test_speaker.py
Normal file
@@ -0,0 +1,71 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from kasa import Module
|
||||
from kasa.smart import SmartDevice
|
||||
|
||||
from ...device_fixtures import get_parent_and_child_modules, parametrize
|
||||
|
||||
speaker = parametrize(
|
||||
"has speaker", component_filter="speaker", protocol_filter={"SMART"}
|
||||
)
|
||||
|
||||
|
||||
@speaker
|
||||
@pytest.mark.parametrize(
|
||||
("feature", "prop_name", "type"),
|
||||
[
|
||||
("volume", "volume", int),
|
||||
],
|
||||
)
|
||||
async def test_features(dev: SmartDevice, feature: str, prop_name: str, type: type):
|
||||
"""Test that features are registered and work as expected."""
|
||||
speaker = next(get_parent_and_child_modules(dev, Module.Speaker))
|
||||
assert speaker is not None
|
||||
|
||||
prop = getattr(speaker, prop_name)
|
||||
assert isinstance(prop, type)
|
||||
|
||||
feat = speaker._device.features[feature]
|
||||
assert feat.value == prop
|
||||
assert isinstance(feat.value, type)
|
||||
|
||||
|
||||
@speaker
|
||||
async def test_set_volume(dev: SmartDevice, mocker: MockerFixture):
|
||||
"""Test speaker settings."""
|
||||
speaker = next(get_parent_and_child_modules(dev, Module.Speaker))
|
||||
assert speaker is not None
|
||||
|
||||
call = mocker.spy(speaker, "call")
|
||||
|
||||
volume = speaker._device.features["volume"]
|
||||
assert speaker.volume == volume.value
|
||||
|
||||
new_volume = 15
|
||||
await speaker.set_volume(new_volume)
|
||||
|
||||
call.assert_called_with("setVolume", {"volume": new_volume})
|
||||
|
||||
await dev.update()
|
||||
|
||||
assert speaker.volume == new_volume
|
||||
|
||||
with pytest.raises(ValueError, match="Volume must be between 0 and 100"):
|
||||
await speaker.set_volume(-10)
|
||||
|
||||
with pytest.raises(ValueError, match="Volume must be between 0 and 100"):
|
||||
await speaker.set_volume(110)
|
||||
|
||||
|
||||
@speaker
|
||||
async def test_locate(dev: SmartDevice, mocker: MockerFixture):
|
||||
"""Test the locate method."""
|
||||
speaker = next(get_parent_and_child_modules(dev, Module.Speaker))
|
||||
call = mocker.spy(speaker, "call")
|
||||
|
||||
await speaker.locate()
|
||||
|
||||
call.assert_called_with("playSelectAudio", {"audio_type": "seek_me"})
|
||||
Reference in New Issue
Block a user