Refactor devices into subpackages and deprecate old names (#716)

* Refactor devices into subpackages and deprecate old names

* Tweak and add tests

* Fix linting

* Remove duplicate implementations affecting project coverage

* Update post review

* Add device base class attributes and rename subclasses

* Rename Module to BaseModule

* Remove has_emeter_history

* Fix missing _time in init

* Update post review

* Fix test_readmeexamples

* Fix erroneously duped files

* Clean up iot and smart imports

* Update post latest review

* Tweak Device docstring
This commit is contained in:
Steven B
2024-02-04 15:20:08 +00:00
committed by GitHub
parent 6afd05be59
commit 0d119e63d0
49 changed files with 1046 additions and 606 deletions

View File

@@ -13,18 +13,14 @@ import pytest # type: ignore # see https://github.com/pytest-dev/pytest/issues/
from kasa import (
Credentials,
Device,
DeviceConfig,
Discover,
SmartBulb,
SmartDevice,
SmartDimmer,
SmartLightStrip,
SmartPlug,
SmartProtocol,
SmartStrip,
)
from kasa.iot import IotBulb, IotDimmer, IotLightStrip, IotPlug, IotStrip
from kasa.protocol import BaseTransport
from kasa.tapo import TapoBulb, TapoPlug
from kasa.smart import SmartBulb, SmartPlug
from kasa.xortransport import XorEncryption
from .fakeprotocol_iot import FakeIotProtocol
@@ -350,37 +346,37 @@ def device_for_file(model, protocol):
if protocol == "SMART":
for d in PLUGS_SMART:
if d in model:
return TapoPlug
return SmartPlug
for d in BULBS_SMART:
if d in model:
return TapoBulb
return SmartBulb
for d in DIMMERS_SMART:
if d in model:
return TapoBulb
return SmartBulb
for d in STRIPS_SMART:
if d in model:
return TapoPlug
return SmartPlug
else:
for d in STRIPS_IOT:
if d in model:
return SmartStrip
return IotStrip
for d in PLUGS_IOT:
if d in model:
return SmartPlug
return IotPlug
# Light strips are recognized also as bulbs, so this has to go first
for d in BULBS_IOT_LIGHT_STRIP:
if d in model:
return SmartLightStrip
return IotLightStrip
for d in BULBS_IOT:
if d in model:
return SmartBulb
return IotBulb
for d in DIMMERS_IOT:
if d in model:
return SmartDimmer
return IotDimmer
raise Exception("Unable to find type for %s", model)
@@ -446,11 +442,11 @@ async def dev(request):
IP_MODEL_CACHE[ip] = model = d.model
if model not in file:
pytest.skip(f"skipping file {file}")
dev: SmartDevice = (
dev: Device = (
d if d else await _discover_update_and_close(ip, username, password)
)
else:
dev: SmartDevice = await get_device_for_file(file, protocol)
dev: Device = await get_device_for_file(file, protocol)
yield dev

View File

@@ -7,7 +7,8 @@ from voluptuous import (
Schema,
)
from kasa import DeviceType, SmartBulb, SmartBulbPreset, SmartDeviceException
from kasa import Bulb, BulbPreset, DeviceType, SmartDeviceException
from kasa.iot import IotBulb
from .conftest import (
bulb,
@@ -27,7 +28,7 @@ from .test_smartdevice import SYSINFO_SCHEMA
@bulb
async def test_bulb_sysinfo(dev: SmartBulb):
async def test_bulb_sysinfo(dev: Bulb):
assert dev.sys_info is not None
SYSINFO_SCHEMA_BULB(dev.sys_info)
@@ -40,7 +41,7 @@ async def test_bulb_sysinfo(dev: SmartBulb):
@bulb
async def test_state_attributes(dev: SmartBulb):
async def test_state_attributes(dev: Bulb):
assert "Brightness" in dev.state_information
assert dev.state_information["Brightness"] == dev.brightness
@@ -49,7 +50,7 @@ async def test_state_attributes(dev: SmartBulb):
@bulb_iot
async def test_light_state_without_update(dev: SmartBulb, monkeypatch):
async def test_light_state_without_update(dev: IotBulb, monkeypatch):
with pytest.raises(SmartDeviceException):
monkeypatch.setitem(
dev._last_update["system"]["get_sysinfo"], "light_state", None
@@ -58,13 +59,13 @@ async def test_light_state_without_update(dev: SmartBulb, monkeypatch):
@bulb_iot
async def test_get_light_state(dev: SmartBulb):
async def test_get_light_state(dev: IotBulb):
LIGHT_STATE_SCHEMA(await dev.get_light_state())
@color_bulb
@turn_on
async def test_hsv(dev: SmartBulb, turn_on):
async def test_hsv(dev: Bulb, turn_on):
await handle_turn_on(dev, turn_on)
assert dev.is_color
@@ -83,8 +84,8 @@ async def test_hsv(dev: SmartBulb, turn_on):
@color_bulb_iot
async def test_set_hsv_transition(dev: SmartBulb, mocker):
set_light_state = mocker.patch("kasa.SmartBulb.set_light_state")
async def test_set_hsv_transition(dev: IotBulb, mocker):
set_light_state = mocker.patch("kasa.iot.IotBulb.set_light_state")
await dev.set_hsv(10, 10, 100, transition=1000)
set_light_state.assert_called_with(
@@ -95,31 +96,31 @@ async def test_set_hsv_transition(dev: SmartBulb, mocker):
@color_bulb
@turn_on
async def test_invalid_hsv(dev: SmartBulb, turn_on):
async def test_invalid_hsv(dev: Bulb, turn_on):
await handle_turn_on(dev, turn_on)
assert dev.is_color
for invalid_hue in [-1, 361, 0.5]:
with pytest.raises(ValueError):
await dev.set_hsv(invalid_hue, 0, 0)
await dev.set_hsv(invalid_hue, 0, 0) # type: ignore[arg-type]
for invalid_saturation in [-1, 101, 0.5]:
with pytest.raises(ValueError):
await dev.set_hsv(0, invalid_saturation, 0)
await dev.set_hsv(0, invalid_saturation, 0) # type: ignore[arg-type]
for invalid_brightness in [-1, 101, 0.5]:
with pytest.raises(ValueError):
await dev.set_hsv(0, 0, invalid_brightness)
await dev.set_hsv(0, 0, invalid_brightness) # type: ignore[arg-type]
@color_bulb
async def test_color_state_information(dev: SmartBulb):
async def test_color_state_information(dev: Bulb):
assert "HSV" in dev.state_information
assert dev.state_information["HSV"] == dev.hsv
@non_color_bulb
async def test_hsv_on_non_color(dev: SmartBulb):
async def test_hsv_on_non_color(dev: Bulb):
assert not dev.is_color
with pytest.raises(SmartDeviceException):
@@ -129,7 +130,7 @@ async def test_hsv_on_non_color(dev: SmartBulb):
@variable_temp
async def test_variable_temp_state_information(dev: SmartBulb):
async def test_variable_temp_state_information(dev: Bulb):
assert "Color temperature" in dev.state_information
assert dev.state_information["Color temperature"] == dev.color_temp
@@ -141,7 +142,7 @@ async def test_variable_temp_state_information(dev: SmartBulb):
@variable_temp
@turn_on
async def test_try_set_colortemp(dev: SmartBulb, turn_on):
async def test_try_set_colortemp(dev: Bulb, turn_on):
await handle_turn_on(dev, turn_on)
await dev.set_color_temp(2700)
await dev.update()
@@ -149,15 +150,15 @@ async def test_try_set_colortemp(dev: SmartBulb, turn_on):
@variable_temp_iot
async def test_set_color_temp_transition(dev: SmartBulb, mocker):
set_light_state = mocker.patch("kasa.SmartBulb.set_light_state")
async def test_set_color_temp_transition(dev: IotBulb, mocker):
set_light_state = mocker.patch("kasa.iot.IotBulb.set_light_state")
await dev.set_color_temp(2700, transition=100)
set_light_state.assert_called_with({"color_temp": 2700}, transition=100)
@variable_temp_iot
async def test_unknown_temp_range(dev: SmartBulb, monkeypatch, caplog):
async def test_unknown_temp_range(dev: IotBulb, monkeypatch, caplog):
monkeypatch.setitem(dev._sys_info, "model", "unknown bulb")
assert dev.valid_temperature_range == (2700, 5000)
@@ -165,7 +166,7 @@ async def test_unknown_temp_range(dev: SmartBulb, monkeypatch, caplog):
@variable_temp
async def test_out_of_range_temperature(dev: SmartBulb):
async def test_out_of_range_temperature(dev: Bulb):
with pytest.raises(ValueError):
await dev.set_color_temp(1000)
with pytest.raises(ValueError):
@@ -173,7 +174,7 @@ async def test_out_of_range_temperature(dev: SmartBulb):
@non_variable_temp
async def test_non_variable_temp(dev: SmartBulb):
async def test_non_variable_temp(dev: Bulb):
with pytest.raises(SmartDeviceException):
await dev.set_color_temp(2700)
@@ -186,7 +187,7 @@ async def test_non_variable_temp(dev: SmartBulb):
@dimmable
@turn_on
async def test_dimmable_brightness(dev: SmartBulb, turn_on):
async def test_dimmable_brightness(dev: Bulb, turn_on):
await handle_turn_on(dev, turn_on)
assert dev.is_dimmable
@@ -199,12 +200,12 @@ async def test_dimmable_brightness(dev: SmartBulb, turn_on):
assert dev.brightness == 10
with pytest.raises(ValueError):
await dev.set_brightness("foo")
await dev.set_brightness("foo") # type: ignore[arg-type]
@bulb_iot
async def test_turn_on_transition(dev: SmartBulb, mocker):
set_light_state = mocker.patch("kasa.SmartBulb.set_light_state")
async def test_turn_on_transition(dev: IotBulb, mocker):
set_light_state = mocker.patch("kasa.iot.IotBulb.set_light_state")
await dev.turn_on(transition=1000)
set_light_state.assert_called_with({"on_off": 1}, transition=1000)
@@ -215,15 +216,15 @@ async def test_turn_on_transition(dev: SmartBulb, mocker):
@bulb_iot
async def test_dimmable_brightness_transition(dev: SmartBulb, mocker):
set_light_state = mocker.patch("kasa.SmartBulb.set_light_state")
async def test_dimmable_brightness_transition(dev: IotBulb, mocker):
set_light_state = mocker.patch("kasa.iot.IotBulb.set_light_state")
await dev.set_brightness(10, transition=1000)
set_light_state.assert_called_with({"brightness": 10}, transition=1000)
@dimmable
async def test_invalid_brightness(dev: SmartBulb):
async def test_invalid_brightness(dev: Bulb):
assert dev.is_dimmable
with pytest.raises(ValueError):
@@ -234,7 +235,7 @@ async def test_invalid_brightness(dev: SmartBulb):
@non_dimmable
async def test_non_dimmable(dev: SmartBulb):
async def test_non_dimmable(dev: Bulb):
assert not dev.is_dimmable
with pytest.raises(SmartDeviceException):
@@ -245,9 +246,9 @@ async def test_non_dimmable(dev: SmartBulb):
@bulb_iot
async def test_ignore_default_not_set_without_color_mode_change_turn_on(
dev: SmartBulb, mocker
dev: IotBulb, mocker
):
query_helper = mocker.patch("kasa.SmartBulb._query_helper")
query_helper = mocker.patch("kasa.iot.IotBulb._query_helper")
# When turning back without settings, ignore default to restore the state
await dev.turn_on()
args, kwargs = query_helper.call_args_list[0]
@@ -259,7 +260,7 @@ async def test_ignore_default_not_set_without_color_mode_change_turn_on(
@bulb_iot
async def test_list_presets(dev: SmartBulb):
async def test_list_presets(dev: IotBulb):
presets = dev.presets
assert len(presets) == len(dev.sys_info["preferred_state"])
@@ -272,7 +273,7 @@ async def test_list_presets(dev: SmartBulb):
@bulb_iot
async def test_modify_preset(dev: SmartBulb, mocker):
async def test_modify_preset(dev: IotBulb, mocker):
"""Verify that modifying preset calls the and exceptions are raised properly."""
if not dev.presets:
pytest.skip("Some strips do not support presets")
@@ -284,7 +285,7 @@ async def test_modify_preset(dev: SmartBulb, mocker):
"saturation": 0,
"color_temp": 0,
}
preset = SmartBulbPreset(**data)
preset = BulbPreset(**data)
assert preset.index == 0
assert preset.brightness == 10
@@ -297,7 +298,7 @@ async def test_modify_preset(dev: SmartBulb, mocker):
with pytest.raises(SmartDeviceException):
await dev.save_preset(
SmartBulbPreset(index=5, hue=0, brightness=0, saturation=0, color_temp=0)
BulbPreset(index=5, hue=0, brightness=0, saturation=0, color_temp=0)
)
@@ -306,21 +307,21 @@ async def test_modify_preset(dev: SmartBulb, mocker):
("preset", "payload"),
[
(
SmartBulbPreset(index=0, hue=0, brightness=1, saturation=0),
BulbPreset(index=0, hue=0, brightness=1, saturation=0),
{"index": 0, "hue": 0, "brightness": 1, "saturation": 0},
),
(
SmartBulbPreset(index=0, brightness=1, id="testid", mode=2, custom=0),
BulbPreset(index=0, brightness=1, id="testid", mode=2, custom=0),
{"index": 0, "brightness": 1, "id": "testid", "mode": 2, "custom": 0},
),
],
)
async def test_modify_preset_payloads(dev: SmartBulb, preset, payload, mocker):
async def test_modify_preset_payloads(dev: IotBulb, preset, payload, mocker):
"""Test that modify preset payloads ignore none values."""
if not dev.presets:
pytest.skip("Some strips do not support presets")
query_helper = mocker.patch("kasa.SmartBulb._query_helper")
query_helper = mocker.patch("kasa.iot.IotBulb._query_helper")
await dev.save_preset(preset)
query_helper.assert_called_with(dev.LIGHT_SERVICE, "set_preferred_state", payload)

View File

@@ -3,8 +3,8 @@ import sys
import pytest
from kasa.smart.smartchilddevice import SmartChildDevice
from kasa.smartprotocol import _ChildProtocolWrapper
from kasa.tapo.childdevice import ChildDevice
from .conftest import strip_smart
@@ -42,7 +42,7 @@ async def test_childdevice_update(dev, dummy_protocol, mocker):
sys.version_info < (3, 11),
reason="exceptiongroup requires python3.11+",
)
async def test_childdevice_properties(dev: ChildDevice):
async def test_childdevice_properties(dev: SmartChildDevice):
"""Check that accessing childdevice properties do not raise exceptions."""
assert len(dev.children) > 0

View File

@@ -7,8 +7,8 @@ from asyncclick.testing import CliRunner
from kasa import (
AuthenticationException,
Device,
EmeterStatus,
SmartDevice,
SmartDeviceException,
UnsupportedDeviceException,
)
@@ -27,6 +27,7 @@ from kasa.cli import (
wifi,
)
from kasa.discover import Discover, DiscoveryResult
from kasa.iot import IotDevice
from .conftest import device_iot, device_smart, handle_turn_on, new_discovery, turn_on
@@ -107,9 +108,9 @@ async def test_alias(dev):
async def test_raw_command(dev, mocker):
runner = CliRunner()
update = mocker.patch.object(dev, "update")
from kasa.tapo import TapoDevice
from kasa.smart import SmartDevice
if isinstance(dev, TapoDevice):
if isinstance(dev, SmartDevice):
params = ["na", "get_device_info"]
else:
params = ["system", "get_sysinfo"]
@@ -216,7 +217,7 @@ async def test_update_credentials(dev):
)
async def test_emeter(dev: SmartDevice, mocker):
async def test_emeter(dev: Device, mocker):
runner = CliRunner()
res = await runner.invoke(emeter, obj=dev)
@@ -245,16 +246,24 @@ async def test_emeter(dev: SmartDevice, mocker):
assert "Voltage: 122.066 V" in res.output
assert realtime_emeter.call_count == 2
monthly = mocker.patch.object(dev, "get_emeter_monthly")
monthly.return_value = {1: 1234}
if isinstance(dev, IotDevice):
monthly = mocker.patch.object(dev, "get_emeter_monthly")
monthly.return_value = {1: 1234}
res = await runner.invoke(emeter, ["--year", "1900"], obj=dev)
if not isinstance(dev, IotDevice):
assert "Device has no historical statistics" in res.output
return
assert "For year" in res.output
assert "1, 1234" in res.output
monthly.assert_called_with(year=1900)
daily = mocker.patch.object(dev, "get_emeter_daily")
daily.return_value = {1: 1234}
if isinstance(dev, IotDevice):
daily = mocker.patch.object(dev, "get_emeter_daily")
daily.return_value = {1: 1234}
res = await runner.invoke(emeter, ["--month", "1900-12"], obj=dev)
if not isinstance(dev, IotDevice):
assert "Device has no historical statistics" in res.output
return
assert "For month" in res.output
assert "1, 1234" in res.output
daily.assert_called_with(year=1900, month=12)
@@ -279,7 +288,7 @@ async def test_brightness(dev):
@device_iot
async def test_json_output(dev: SmartDevice, mocker):
async def test_json_output(dev: Device, mocker):
"""Test that the json output produces correct output."""
mocker.patch("kasa.Discover.discover", return_value=[dev])
runner = CliRunner()
@@ -292,10 +301,10 @@ async def test_json_output(dev: SmartDevice, mocker):
async def test_credentials(discovery_mock, mocker):
"""Test credentials are passed correctly from cli to device."""
# Patch state to echo username and password
pass_dev = click.make_pass_decorator(SmartDevice)
pass_dev = click.make_pass_decorator(Device)
@pass_dev
async def _state(dev: SmartDevice):
async def _state(dev: Device):
if dev.credentials:
click.echo(
f"Username:{dev.credentials.username} Password:{dev.credentials.password}"
@@ -513,10 +522,10 @@ async def test_type_param(device_type, mocker):
runner = CliRunner()
result_device = FileNotFoundError
pass_dev = click.make_pass_decorator(SmartDevice)
pass_dev = click.make_pass_decorator(Device)
@pass_dev
async def _state(dev: SmartDevice):
async def _state(dev: Device):
nonlocal result_device
result_device = dev

View File

@@ -6,8 +6,8 @@ import pytest # type: ignore # https://github.com/pytest-dev/pytest/issues/3342
from kasa import (
Credentials,
Device,
Discover,
SmartDevice,
SmartDeviceException,
)
from kasa.device_factory import connect, get_protocol
@@ -83,7 +83,7 @@ async def test_connect_custom_port(all_fixture_data: dict, mocker, custom_port):
mocker.patch("kasa.IotProtocol.query", return_value=all_fixture_data)
mocker.patch("kasa.SmartProtocol.query", return_value=all_fixture_data)
dev = await connect(config=config)
assert issubclass(dev.__class__, SmartDevice)
assert issubclass(dev.__class__, Device)
assert dev.port == custom_port or dev.port == default_port

View File

@@ -1,4 +1,4 @@
from kasa.smartdevice import DeviceType
from kasa.device_type import DeviceType
async def test_device_type_from_value():

View File

@@ -1,6 +1,6 @@
import pytest
from kasa import SmartDimmer
from kasa.iot import IotDimmer
from .conftest import dimmer, handle_turn_on, turn_on
@@ -23,7 +23,7 @@ async def test_set_brightness(dev, turn_on):
@turn_on
async def test_set_brightness_transition(dev, turn_on, mocker):
await handle_turn_on(dev, turn_on)
query_helper = mocker.spy(SmartDimmer, "_query_helper")
query_helper = mocker.spy(IotDimmer, "_query_helper")
await dev.set_brightness(99, transition=1000)
@@ -53,7 +53,7 @@ async def test_set_brightness_invalid(dev):
@dimmer
async def test_turn_on_transition(dev, mocker):
query_helper = mocker.spy(SmartDimmer, "_query_helper")
query_helper = mocker.spy(IotDimmer, "_query_helper")
original_brightness = dev.brightness
await dev.turn_on(transition=1000)
@@ -71,7 +71,7 @@ async def test_turn_on_transition(dev, mocker):
@dimmer
async def test_turn_off_transition(dev, mocker):
await handle_turn_on(dev, True)
query_helper = mocker.spy(SmartDimmer, "_query_helper")
query_helper = mocker.spy(IotDimmer, "_query_helper")
original_brightness = dev.brightness
await dev.turn_off(transition=1000)
@@ -90,7 +90,7 @@ async def test_turn_off_transition(dev, mocker):
@turn_on
async def test_set_dimmer_transition(dev, turn_on, mocker):
await handle_turn_on(dev, turn_on)
query_helper = mocker.spy(SmartDimmer, "_query_helper")
query_helper = mocker.spy(IotDimmer, "_query_helper")
await dev.set_dimmer_transition(99, 1000)
@@ -109,7 +109,7 @@ async def test_set_dimmer_transition(dev, turn_on, mocker):
async def test_set_dimmer_transition_to_off(dev, turn_on, mocker):
await handle_turn_on(dev, turn_on)
original_brightness = dev.brightness
query_helper = mocker.spy(SmartDimmer, "_query_helper")
query_helper = mocker.spy(IotDimmer, "_query_helper")
await dev.set_dimmer_transition(0, 1000)

View File

@@ -10,9 +10,9 @@ from async_timeout import timeout as asyncio_timeout
from kasa import (
Credentials,
Device,
DeviceType,
Discover,
SmartDevice,
SmartDeviceException,
)
from kasa.deviceconfig import (
@@ -21,6 +21,7 @@ from kasa.deviceconfig import (
)
from kasa.discover import DiscoveryResult, _DiscoverProtocol, json_dumps
from kasa.exceptions import AuthenticationException, UnsupportedDeviceException
from kasa.iot import IotDevice
from kasa.xortransport import XorEncryption
from .conftest import (
@@ -55,14 +56,14 @@ UNSUPPORTED = {
@plug
async def test_type_detection_plug(dev: SmartDevice):
async def test_type_detection_plug(dev: Device):
d = Discover._get_device_class(dev._last_update)("localhost")
assert d.is_plug
assert d.device_type == DeviceType.Plug
@bulb_iot
async def test_type_detection_bulb(dev: SmartDevice):
async def test_type_detection_bulb(dev: Device):
d = Discover._get_device_class(dev._last_update)("localhost")
# TODO: light_strip is a special case for now to force bulb tests on it
if not d.is_light_strip:
@@ -71,21 +72,21 @@ async def test_type_detection_bulb(dev: SmartDevice):
@strip_iot
async def test_type_detection_strip(dev: SmartDevice):
async def test_type_detection_strip(dev: Device):
d = Discover._get_device_class(dev._last_update)("localhost")
assert d.is_strip
assert d.device_type == DeviceType.Strip
@dimmer
async def test_type_detection_dimmer(dev: SmartDevice):
async def test_type_detection_dimmer(dev: Device):
d = Discover._get_device_class(dev._last_update)("localhost")
assert d.is_dimmer
assert d.device_type == DeviceType.Dimmer
@lightstrip
async def test_type_detection_lightstrip(dev: SmartDevice):
async def test_type_detection_lightstrip(dev: Device):
d = Discover._get_device_class(dev._last_update)("localhost")
assert d.is_light_strip
assert d.device_type == DeviceType.LightStrip
@@ -111,7 +112,7 @@ async def test_discover_single(discovery_mock, custom_port, mocker):
x = await Discover.discover_single(
host, port=custom_port, credentials=Credentials()
)
assert issubclass(x.__class__, SmartDevice)
assert issubclass(x.__class__, Device)
assert x._discovery_info is not None
assert x.port == custom_port or x.port == discovery_mock.default_port
assert update_mock.call_count == 0
@@ -144,7 +145,7 @@ async def test_discover_single_hostname(discovery_mock, mocker):
update_mock = mocker.patch.object(device_class, "update")
x = await Discover.discover_single(host, credentials=Credentials())
assert issubclass(x.__class__, SmartDevice)
assert issubclass(x.__class__, Device)
assert x._discovery_info is not None
assert x.host == host
assert update_mock.call_count == 0
@@ -232,7 +233,7 @@ async def test_discover_datagram_received(mocker, discovery_data):
# Check that unsupported device is 1
assert len(proto.unsupported_device_exceptions) == 1
dev = proto.discovered_devices[addr]
assert issubclass(dev.__class__, SmartDevice)
assert issubclass(dev.__class__, Device)
assert dev.host == addr
@@ -298,7 +299,7 @@ async def test_discover_single_authentication(discovery_mock, mocker):
@new_discovery
async def test_device_update_from_new_discovery_info(discovery_data):
device = SmartDevice("127.0.0.7")
device = IotDevice("127.0.0.7")
discover_info = DiscoveryResult(**discovery_data["result"])
discover_dump = discover_info.get_dict()
discover_dump["alias"] = "foobar"
@@ -323,7 +324,7 @@ async def test_discover_single_http_client(discovery_mock, mocker):
http_client = aiohttp.ClientSession()
x: SmartDevice = await Discover.discover_single(host)
x: Device = await Discover.discover_single(host)
assert x.config.uses_http == (discovery_mock.default_port == 80)
@@ -341,7 +342,7 @@ async def test_discover_http_client(discovery_mock, mocker):
http_client = aiohttp.ClientSession()
devices = await Discover.discover(discovery_timeout=0)
x: SmartDevice = devices[host]
x: Device = devices[host]
assert x.config.uses_http == (discovery_mock.default_port == 80)
if discovery_mock.default_port == 80:

View File

@@ -11,7 +11,8 @@ from voluptuous import (
)
from kasa import EmeterStatus, SmartDeviceException
from kasa.modules.emeter import Emeter
from kasa.iot import IotDevice
from kasa.iot.modules.emeter import Emeter
from .conftest import has_emeter, has_emeter_iot, no_emeter
@@ -39,12 +40,15 @@ async def test_no_emeter(dev):
with pytest.raises(SmartDeviceException):
await dev.get_emeter_realtime()
with pytest.raises(SmartDeviceException):
await dev.get_emeter_daily()
with pytest.raises(SmartDeviceException):
await dev.get_emeter_monthly()
with pytest.raises(SmartDeviceException):
await dev.erase_emeter_stats()
# Only iot devices support the historical stats so other
# devices will not implement the methods below
if isinstance(dev, IotDevice):
with pytest.raises(SmartDeviceException):
await dev.get_emeter_daily()
with pytest.raises(SmartDeviceException):
await dev.get_emeter_monthly()
with pytest.raises(SmartDeviceException):
await dev.erase_emeter_stats()
@has_emeter
@@ -121,7 +125,7 @@ async def test_erase_emeter_stats(dev):
await dev.erase_emeter()
@has_emeter
@has_emeter_iot
async def test_current_consumption(dev):
if dev.has_emeter:
x = await dev.current_consumption()

View File

@@ -1,27 +1,28 @@
import pytest
from kasa import DeviceType, SmartLightStrip
from kasa import DeviceType
from kasa.exceptions import SmartDeviceException
from kasa.iot import IotLightStrip
from .conftest import lightstrip
@lightstrip
async def test_lightstrip_length(dev: SmartLightStrip):
async def test_lightstrip_length(dev: IotLightStrip):
assert dev.is_light_strip
assert dev.device_type == DeviceType.LightStrip
assert dev.length == dev.sys_info["length"]
@lightstrip
async def test_lightstrip_effect(dev: SmartLightStrip):
async def test_lightstrip_effect(dev: IotLightStrip):
assert isinstance(dev.effect, dict)
for k in ["brightness", "custom", "enable", "id", "name"]:
assert k in dev.effect
@lightstrip
async def test_effects_lightstrip_set_effect(dev: SmartLightStrip):
async def test_effects_lightstrip_set_effect(dev: IotLightStrip):
with pytest.raises(SmartDeviceException):
await dev.set_effect("Not real")
@@ -33,9 +34,9 @@ async def test_effects_lightstrip_set_effect(dev: SmartLightStrip):
@lightstrip
@pytest.mark.parametrize("brightness", [100, 50])
async def test_effects_lightstrip_set_effect_brightness(
dev: SmartLightStrip, brightness, mocker
dev: IotLightStrip, brightness, mocker
):
query_helper = mocker.patch("kasa.SmartLightStrip._query_helper")
query_helper = mocker.patch("kasa.iot.IotLightStrip._query_helper")
# test that default brightness works (100 for candy cane)
if brightness == 100:
@@ -51,9 +52,9 @@ async def test_effects_lightstrip_set_effect_brightness(
@lightstrip
@pytest.mark.parametrize("transition", [500, 1000])
async def test_effects_lightstrip_set_effect_transition(
dev: SmartLightStrip, transition, mocker
dev: IotLightStrip, transition, mocker
):
query_helper = mocker.patch("kasa.SmartLightStrip._query_helper")
query_helper = mocker.patch("kasa.iot.IotLightStrip._query_helper")
# test that default (500 for candy cane) transition works
if transition == 500:
@@ -67,6 +68,6 @@ async def test_effects_lightstrip_set_effect_transition(
@lightstrip
async def test_effects_lightstrip_has_effects(dev: SmartLightStrip):
async def test_effects_lightstrip_has_effects(dev: IotLightStrip):
assert dev.has_effects is True
assert dev.effect_list

View File

@@ -8,54 +8,54 @@ from kasa.tests.conftest import get_device_for_file
def test_bulb_examples(mocker):
"""Use KL130 (bulb with all features) to test the doctests."""
p = asyncio.run(get_device_for_file("KL130(US)_1.0_1.8.11.json", "IOT"))
mocker.patch("kasa.smartbulb.SmartBulb", return_value=p)
mocker.patch("kasa.smartbulb.SmartBulb.update")
res = xdoctest.doctest_module("kasa.smartbulb", "all")
mocker.patch("kasa.iot.iotbulb.IotBulb", return_value=p)
mocker.patch("kasa.iot.iotbulb.IotBulb.update")
res = xdoctest.doctest_module("kasa.iot.iotbulb", "all")
assert not res["failed"]
def test_smartdevice_examples(mocker):
"""Use HS110 for emeter examples."""
p = asyncio.run(get_device_for_file("HS110(EU)_1.0_1.2.5.json", "IOT"))
mocker.patch("kasa.smartdevice.SmartDevice", return_value=p)
mocker.patch("kasa.smartdevice.SmartDevice.update")
res = xdoctest.doctest_module("kasa.smartdevice", "all")
mocker.patch("kasa.iot.iotdevice.IotDevice", return_value=p)
mocker.patch("kasa.iot.iotdevice.IotDevice.update")
res = xdoctest.doctest_module("kasa.iot.iotdevice", "all")
assert not res["failed"]
def test_plug_examples(mocker):
"""Test plug examples."""
p = asyncio.run(get_device_for_file("HS110(EU)_1.0_1.2.5.json", "IOT"))
mocker.patch("kasa.smartplug.SmartPlug", return_value=p)
mocker.patch("kasa.smartplug.SmartPlug.update")
res = xdoctest.doctest_module("kasa.smartplug", "all")
mocker.patch("kasa.iot.iotplug.IotPlug", return_value=p)
mocker.patch("kasa.iot.iotplug.IotPlug.update")
res = xdoctest.doctest_module("kasa.iot.iotplug", "all")
assert not res["failed"]
def test_strip_examples(mocker):
"""Test strip examples."""
p = asyncio.run(get_device_for_file("KP303(UK)_1.0_1.0.3.json", "IOT"))
mocker.patch("kasa.smartstrip.SmartStrip", return_value=p)
mocker.patch("kasa.smartstrip.SmartStrip.update")
res = xdoctest.doctest_module("kasa.smartstrip", "all")
mocker.patch("kasa.iot.iotstrip.IotStrip", return_value=p)
mocker.patch("kasa.iot.iotstrip.IotStrip.update")
res = xdoctest.doctest_module("kasa.iot.iotstrip", "all")
assert not res["failed"]
def test_dimmer_examples(mocker):
"""Test dimmer examples."""
p = asyncio.run(get_device_for_file("HS220(US)_1.0_1.5.7.json", "IOT"))
mocker.patch("kasa.smartdimmer.SmartDimmer", return_value=p)
mocker.patch("kasa.smartdimmer.SmartDimmer.update")
res = xdoctest.doctest_module("kasa.smartdimmer", "all")
mocker.patch("kasa.iot.iotdimmer.IotDimmer", return_value=p)
mocker.patch("kasa.iot.iotdimmer.IotDimmer.update")
res = xdoctest.doctest_module("kasa.iot.iotdimmer", "all")
assert not res["failed"]
def test_lightstrip_examples(mocker):
"""Test lightstrip examples."""
p = asyncio.run(get_device_for_file("KL430(US)_1.0_1.0.10.json", "IOT"))
mocker.patch("kasa.smartlightstrip.SmartLightStrip", return_value=p)
mocker.patch("kasa.smartlightstrip.SmartLightStrip.update")
res = xdoctest.doctest_module("kasa.smartlightstrip", "all")
mocker.patch("kasa.iot.iotlightstrip.IotLightStrip", return_value=p)
mocker.patch("kasa.iot.iotlightstrip.IotLightStrip.update")
res = xdoctest.doctest_module("kasa.iot.iotlightstrip", "all")
assert not res["failed"]

View File

@@ -1,5 +1,8 @@
import importlib
import inspect
import pkgutil
import re
import sys
from datetime import datetime
from unittest.mock import Mock, patch
@@ -17,20 +20,33 @@ from voluptuous import (
)
import kasa
from kasa import Credentials, DeviceConfig, SmartDevice, SmartDeviceException
from kasa import Credentials, Device, DeviceConfig, SmartDeviceException
from kasa.iot import IotDevice
from kasa.smart import SmartChildDevice, SmartDevice
from .conftest import device_iot, handle_turn_on, has_emeter_iot, no_emeter_iot, turn_on
from .fakeprotocol_iot import FakeIotProtocol
# List of all SmartXXX classes including the SmartDevice base class
smart_device_classes = [
dc
for (mn, dc) in inspect.getmembers(
kasa,
lambda member: inspect.isclass(member)
and (member == SmartDevice or issubclass(member, SmartDevice)),
)
]
def _get_subclasses(of_class):
package = sys.modules["kasa"]
subclasses = set()
for _, modname, _ in pkgutil.iter_modules(package.__path__):
importlib.import_module("." + modname, package="kasa")
module = sys.modules["kasa." + modname]
for name, obj in inspect.getmembers(module):
if (
inspect.isclass(obj)
and issubclass(obj, of_class)
and module.__package__ != "kasa"
):
subclasses.add((module.__package__ + "." + name, obj))
return subclasses
device_classes = pytest.mark.parametrize(
"device_class_name_obj", _get_subclasses(Device), ids=lambda t: t[0]
)
@device_iot
@@ -220,21 +236,26 @@ async def test_estimated_response_sizes(dev):
assert mod.estimated_query_response_size > 0
@pytest.mark.parametrize("device_class", smart_device_classes)
def test_device_class_ctors(device_class):
@device_classes
async def test_device_class_ctors(device_class_name_obj):
"""Make sure constructor api not broken for new and existing SmartDevices."""
host = "127.0.0.2"
port = 1234
credentials = Credentials("foo", "bar")
config = DeviceConfig(host, port_override=port, credentials=credentials)
dev = device_class(host, config=config)
klass = device_class_name_obj[1]
if issubclass(klass, SmartChildDevice):
parent = SmartDevice(host, config=config)
dev = klass(parent, 1)
else:
dev = klass(host, config=config)
assert dev.host == host
assert dev.port == port
assert dev.credentials == credentials
@device_iot
async def test_modules_preserved(dev: SmartDevice):
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()
@@ -244,6 +265,8 @@ async def test_modules_preserved(dev: SmartDevice):
async def test_create_smart_device_with_timeout():
"""Make sure timeout is passed to the protocol."""
host = "127.0.0.1"
dev = IotDevice(host, config=DeviceConfig(host, timeout=100))
assert dev.protocol._transport._timeout == 100
dev = SmartDevice(host, config=DeviceConfig(host, timeout=100))
assert dev.protocol._transport._timeout == 100
@@ -258,7 +281,7 @@ async def test_create_thin_wrapper():
credentials=Credentials("username", "password"),
)
with patch("kasa.device_factory.connect", return_value=mock) as connect:
dev = await SmartDevice.connect(config=config)
dev = await Device.connect(config=config)
assert dev is mock
connect.assert_called_once_with(
@@ -268,7 +291,7 @@ async def test_create_thin_wrapper():
@device_iot
async def test_modules_not_supported(dev: SmartDevice):
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
@@ -277,6 +300,21 @@ async def test_modules_not_supported(dev: SmartDevice):
assert module.is_supported is not None
@pytest.mark.parametrize(
"device_class, use_class", kasa.deprecated_smart_devices.items()
)
def test_deprecated_devices(device_class, use_class):
package_name = ".".join(use_class.__module__.split(".")[:-1])
msg = f"{device_class} is deprecated, use {use_class.__name__} from package {package_name} instead"
with pytest.deprecated_call(match=msg):
getattr(kasa, device_class)
packages = package_name.split(".")
module = __import__(packages[0])
for _ in packages[1:]:
module = importlib.import_module(package_name, package=module.__name__)
getattr(module, use_class.__name__)
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

View File

@@ -2,7 +2,8 @@ from datetime import datetime
import pytest
from kasa import SmartDeviceException, SmartStrip
from kasa import SmartDeviceException
from kasa.iot import IotStrip
from .conftest import handle_turn_on, strip, turn_on
@@ -68,7 +69,7 @@ async def test_children_on_since(dev):
@strip
async def test_get_plug_by_name(dev: SmartStrip):
async def test_get_plug_by_name(dev: IotStrip):
name = dev.children[0].alias
assert dev.get_plug_by_name(name) == dev.children[0] # type: ignore[arg-type]
@@ -77,7 +78,7 @@ async def test_get_plug_by_name(dev: SmartStrip):
@strip
async def test_get_plug_by_index(dev: SmartStrip):
async def test_get_plug_by_index(dev: IotStrip):
assert dev.get_plug_by_index(0) == dev.children[0]
with pytest.raises(SmartDeviceException):

View File

@@ -1,7 +1,7 @@
import datetime
from unittest.mock import Mock
from kasa.modules import Usage
from kasa.iot.modules import Usage
def test_usage_convert_stat_data():