8000 Update cli to use common modules and remove iot specific cli testing … · python-kasa/python-kasa@67b5d7d · GitHub
[go: up one dir, main page]

Skip to content

Commit 67b5d7d

Browse files
authored
Update cli to use common modules and remove iot specific cli testing (#913)
1 parent ef49f44 commit 67b5d7d

File tree

2 files changed

+154
-45
lines changed

2 files changed

+154
-45
lines changed

kasa/cli.py

Lines changed: 40 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
EncryptType,
2828
Feature,
2929
KasaException,
30-
Light,
30+
Module,
3131
UnsupportedDeviceError,
3232
)
3333
from kasa.discover import DiscoveryResult
@@ -859,18 +859,18 @@ async def usage(dev: Device, year, month, erase):
859859
@click.argument("brightness", type=click.IntRange(0, 100), default=None, required=False)
860860
@click.option("--transition", type=int, required=False)
861861
@pass_dev
862-
async def brightness(dev: Light, brightness: int, transition: int):
862+
async def brightness(dev: Device, brightness: int, transition: int):
863863
"""Get or set brightness."""
864-
if not dev.is_dimmable:
864+
if not (light := dev.modules.get(Module.Light)) or not light.is_dimmable:
865865
echo("This device does not support brightness.")
866866
return
867867

868868
if brightness is None:
869-
echo(f"Brightness: {dev.brightness}")
870-
return dev.brightness
869+
echo(f"Brightness: {light.brightness}")
870+
return light.brightness
871871
else:
872872
echo(f"Setting brightness to {brightness}")
873-
return await dev.set_brightness(brightness, transition= 57AE transition)
873+
return await light.set_brightness(brightness, transition=transition)
874874

875875

876876
@cli.command()
@@ -879,47 +879,50 @@ async def brightness(dev: Light, brightness: int, transition: int):
879879
)
880880
@click.option("--transition", type=int, required=False)
881881
@pass_dev
882-
async def temperature(dev: Light, temperature: int, transition: int):
882+
async def temperature(dev: Device, temperature: int, transition: int):
883883
"""Get or set color temperature."""
884-
if not dev.is_variable_color_temp:
884+
if not (light := dev.modules.get(Module.Light)) or not light.is_variable_color_temp:
885885
echo("Device does not support color temperature")
886886
return
887887

888888
if temperature is None:
889-
echo(f"Color temperature: {dev.color_temp}")
890-
valid_temperature_range = dev.valid_temperature_range
889+
echo(f"Color temperature: {light.color_temp}")
890+
valid_temperature_range = light.valid_temperature_range
891891
if valid_temperature_range != (0, 0):
892892
echo("(min: {}, max: {})".format(*valid_temperature_range))
893893
else:
894894
echo(
895895
"Temperature range unknown, please open a github issue"
896896
f" or a pull request for model '{dev.model}'"
897897
)
898-
return dev.valid_temperature_range
898+
return light.valid_temperature_range
899899
else:
900900
echo(f"Setting color temperature to {temperature}")
901-
return await dev.set_color_temp(temperature, transition=transition)
901+
return await light.set_color_temp(temperature, transition=transition)
902902

903903

904904
@cli.command()
905905
@click.argument("effect", type=click.STRING, default=None, required=False)
906906
@click.pass_context
907907
@pass_dev
908-
async def effect(dev, ctx, effect):
908+
async def effect(dev: Device, ctx, effect):
909909
"""Set an effect."""
910-
if not dev.has_effects:
910+
if not (light_effect := dev.modules.get(Module.LightEffect)):
911911
echo("Device does not support effects")
912912
return
913913
if effect is None:
914914
raise click.BadArgumentUsage(
915-
f"Setting an effect requires a named built-in effect: {dev.effect_list}",
915+
"Setting an effect requires a named built-in effect: "
916+
+ f"{light_effect.effect_list}",
916917
ctx,
917918
)
918-
if effect not in dev.effect_list:
919-
raise click.BadArgumentUsage(f"Effect must be one of: {dev.effect_list}", ctx)
919+
if effect not in light_effect.effect_list:
920+
raise click.BadArgumentUsage(
921+
f"Effect must be one of: {light_effect.effect_list}", ctx
922+
)
920923

921924
echo(f"Setting Effect: {effect}")
922-
return await dev.set_effect(effect)
925+
return await light_effect.set_effect(effect)
923926

924927

925928
@cli.command()
@@ -929,33 +932,36 @@ async def effect(dev, ctx, effect):
929932
@click.option("--transition", type=int, required=False)
930933
@click.pass_context
931934
@pass_dev
932-
async def hsv(dev, ctx, h, s, v, transition):
935+
async def hsv(dev: Device, ctx, h, s, v, transition):
933936
"""Get or set color in HSV."""
934-
if not dev.is_color:
937+
if not (light := dev.modules.get(Module.Light)) or not light.is_color:
935938
echo("Device does not support colors")
936939
return
937940

938-
if h is None or s is None or v is None:
939-
echo(f"Current HSV: {dev.hsv}")
940-
return dev.hsv
941+
if h is None and s is None and v is None:
942+
echo(f"Current HSV: {light.hsv}")
943+
return light.hsv
941944
elif s is None or v is None:
942945
raise click.BadArgumentUsage("Setting a color requires 3 values.", ctx)
943946
else:
944947
echo(f"Setting HSV: {h} {s} {v}")
945-
return await dev.set_hsv(h, s, v, transition=transition)
948+
return await light.set_hsv(h, s, v, transition=transition)
946949

947950

948951
@cli.command()
949952
@click.argument("state", type=bool, required=False)
950953
@pass_dev
951-
async def led(dev, state):
954+
async def led(dev: Device, state):
952955
"""Get or set (Plug's) led state."""
956+
if not (led := dev.modules.get(Module.Led)):
957+
echo("Device does not support led.")
958+
return
953959
if state is not None:
954960
echo(f"Turning led to {state}")
955-
return await dev.set_led(state)
961+
return await led.set_led(state)
956962
else:
957-
echo(f"LED state: {dev.led}")
958-
return dev.led
963+
echo(f"LED state: {led.led}")
964+
return led.led
959965

960966

961967
@cli.command()
@@ -975,8 +981,8 @@ async def time(dev):
975981
async def on(dev: Device, index: int, name: str, transition: int):
976982
"""Turn the device on."""
977983
if index is not None or name is not None:
978-
if not dev.is_strip:
979-
echo("Index and name are only for power strips!")
984+
if not dev.children:
985+
echo("Index and name are only for devices with children.")
980986
return
981987

982988
if index is not None:
@@ -996,8 +1002,8 @@ async def on(dev: Device, index: int, name: str, transition: int):
9961002
async def off(dev: Device, index: int, name: str, transition: int):
9971003
"""Turn the device off."""
9981004
if index is not None or name is not None:
999-
1C6A if not dev.is_strip:
1000-
echo("Index and name are only for power strips!")
1005+
if not dev.children:
1006+
echo("Index and name are only for devices with children.")
10011007
return
10021008

10031009
if index is not None:
@@ -1017,8 +1023,8 @@ async def off(dev: Device, index: int, name: str, transition: int):
10171023
async def toggle(dev: Device, index: int, name: str, transition: int):
10181024
"""Toggle the device on/off."""
10191025
if index is not None or name is not None:
1020-
if not dev.is_strip:
1021-
echo("Index and name are only for power strips!")
1026+
if not dev.children:
1027+
echo("Index and name are only for devices with children.")
10221028
return
10231029

10241030
if index is not None:

kasa/tests/test_cli.py

Lines changed: 114 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
DeviceError,
1414
EmeterStatus,
1515
KasaException,
16+
Module,
1617
UnsupportedDeviceError,
1718
)
1819
from kasa.cli import (
@@ -21,11 +22,15 @@
2122
brightness,
2223
cli,
2324
cmd_command,
25+
effect,
2426
emeter,
27+
hsv,
28+
led,
2529
raw_command,
2630
reboot,
2731
state,
2832
sysinfo,
33+
temperature,
2934
toggle,
3035
update_credentials,
3136
wifi,
@@ -34,7 +39,6 @@
3439
from kasa.iot import IotDevice
3540

3641
from .conftest import (
37-
device_iot,
3842
device_smart,
3943
get_device_for_fixture_protocol,
4044
handle_turn_on,
@@ -78,11 +82,10 @@ async def test_update_called_by_cli(dev, mocker, runner):
7882
update.assert_called()
7983

8084

81-
@device_iot
82-
async def test_sysinfo(dev, runner):
85+
async def test_sysinfo(dev: Device, runner):
8386
res = await runner.invoke(sysinfo, obj=dev)
8487
assert "System info" in res.output
85-
assert dev.alias in res.output
88+
assert dev.model in res.output
8689

8790

8891
@turn_on
@@ -108,7 +111,6 @@ async def test_toggle(dev, turn_on, runner):
108111
assert dev.is_on != turn_on
109112

110113

111-
@device_iot
112114
async def test_alias(dev, runner):
113115
res = await runner.invoke(alias, obj=dev)
114116
assert f"Alias: {dev.alias}" in res.output
@@ -308,15 +310,14 @@ async def test_emeter(dev: Device, mocker, runner):
308310
daily.assert_called_with(year=1900, month=12)
309311

310312

311-
@device_iot
312-
async def test_brightness(dev, runner):
313+
async def test_brightness(dev: Device, runner):
313314
res = await runner.invoke(brightness, obj=dev)
314-
if not dev.is_dimmable:
315+
if not (light := dev.modules.get(Module.Light)) or not light.is_dimmable:
315316
assert "This device does not support brightness." in res.output
316317
return
317318

318319
res = await runner.invoke(brightness, obj=dev)
319-
assert f"Brightness: {dev.brightness}" in res.output
320+
assert f"Brightness: {light.brightness}" in res.output
320321

321322
res = await runner.invoke(brightness, ["12"], obj=dev)
322323
assert "Setting brightness" in res.output
@@ -326,7 +327,110 @@ async def test_brightness(dev, runner):
326327
assert "Brightness: 12" in res.output
327328

328329

329-
@device_iot
330+
async def test_color_temperature(dev: Device, runner):
331+
res = await runner.invoke(temperature, obj=dev)
332+
if not (light := dev.modules.get(Module.Light)) or not light.is_variable_color_temp:
333+
assert "Device does not support color temperature" in res.output
334+
return
335+
336+
res = await runner.invoke(temperature, obj=dev)
337+
assert f"Color temperature: {light.color_temp}" in res.output
338+
valid_range = light.valid_temperature_range
339+
assert f"(min: {valid_range.min}, max: {valid_range.max})" in res.output
340+
341+
val = int((valid_range.min + valid_range.max) / 2)
342+
res = await runner.invoke(temperature, [str(val)], obj=dev)
343+
assert "Setting color temperature to " in res.output
344+
await dev.update()
345+
346+
res = await runner.invoke(temperature, obj=dev)
347+
assert f"Color temperature: {val}" in res.output
348+
assert res.exit_code == 0
349+
350+
invalid_max = valid_range.max + 100
351+
# Lights that support the maximum range will not get past the click cli range check
352+
# So can't be tested for the internal range check.
353+
if invalid_max < 9000:
354+
res = await runner.invoke(temperature, [str(invalid_max)], obj=dev)
355+
assert res.exit_code == 1
356+
assert isinstance(res.exception, ValueError)
357+
358+
res = await runner.invoke(temperature, [str(9100)], obj=dev)
359+
assert res.exit_code == 2
360+
361+
362+
async def test_color_hsv(dev: Device, runner: CliRunner):
363+
res = await runner.invoke(hsv, obj=dev)
364+
if not (light := dev.modules.get(Module.Light)) or not light.is_color:
365+
assert "Device does not support colors" in res.output
366+
return
367+
368+
res = await runner.invoke(hsv, obj=dev)
369+
assert f"Current HSV: {light.hsv}" in res.output
370+
371+
res = await runner.invoke(hsv, ["180", "50", "50"], obj=dev)
372+
assert "Setting HSV: 180 50 50" in res.output
373+
assert res.exit_code == 0
374+
await dev.update()
375+
376+
res = await runner.invoke(hsv, ["180", "50"], obj=dev)
377+
assert "Setting a color requires 3 values." in res.output
378+
assert res.exit_code == 2
379+
380+
381+
async def test_light_effect(dev: Device, runner: CliRunner):
382+
res = await runner.invoke(effect, obj=dev)
383+
if not (light_effect := dev.modules.get(Module.LightEffect)):
384+
assert "Device does not support effects" in res.output
385+
return
386+
387+
# Start off with a known state of off
388+
await light_effect.set_effect(light_effect.LIGHT_EFFECTS_OFF)
389+
await dev.update()
390+
assert light_effect.effect == light_effect.LIGHT_EFFECTS_OFF
391+
392+
res = await runner.invoke(effect, obj=dev)
393+
msg = (
394+
"Setting an effect requires a named built-in effect: "
395+
+ f"{light_effect.effect_list}"
396+
)
397+
assert msg in res.output
398+
assert res.exit_code == 2
399+
400+
res = await runner.invoke(effect, [light_effect.effect_list[1]], obj=dev)
401+
assert f"Setting Effect: {light_effect.effect_list[1]}" in res.output
402+
assert res.exit_code == 0
403+
await dev.update()
404+
assert light_effect.effect == light_effect.effect_list[1]
405+
406+
res = await runner.invoke(effect, ["foobar"], obj=dev)
407+
assert f"Effect must be one of: {light_effect.effect_list}" in res.output
408+
assert res.exit_code == 2
409+
410+
411+
async def test_led(dev: Device, runner: CliRunner):
412+
res = await runner.invoke(led, obj=dev)
413+
if not (led_module := dev.modules.get(Module.Led)):
414+
assert "Device does not support led" in res.output
415+
return
416+
417+
res = await runner.invoke(led, obj=dev)
418+
assert f"LED state: {led_module.led}" in res.output
419+
assert res.exit_code == 0
420+
421+
res = await runner.invoke(led, ["on"], obj=dev)
422+
assert "Turning led to True" in res.output
423+
assert res.exit_code == 0
424+
await dev.update()
425+
assert led_module.led is True
426+
427+
res = await runner.invoke(led, ["off"], obj=dev)
428+
assert "Turning led to False" in res.output
429+
assert res.exit_code == 0
430+
await dev.update()
431+
assert led_module.led is False
432+
433+
330434
async def test_json_output(dev: Device, mocker, runner):
331435
"""Test that the json output produces correct output."""
332436
mocker.patch("kasa.Discover.discover", return_value={"127.0.0.1": dev})
@@ -375,7 +479,6 @@ async def _state(dev: Device):
375479
assert "Username:foo Password:bar\n" in res.output
376480

377481

378-
@device_iot
379482
async def test_without_device_type(dev, mocker, runner):
380483
"""Test connecting without the device type."""
381484
discovery_mock = mocker.patch(

0 commit comments

Comments
 (0)
0