mirror of
https://github.com/python-kasa/python-kasa.git
synced 2025-02-02 10:07:03 +00:00
async++, small powerstrip improvements (#46)
* async++, small powerstrip improvements * use asyncclick instead of click, allows defining the commands with async def to avoid manual eventloop/asyncio.run handling * improve powerstrip support: * new powerstrip api: turn_{on,off}_by_{name,index} methods * cli: fix on/off for powerstrip using the new apis * add missing update()s for cli's hsv, led, temperature (fixes #43) * prettyprint the received payloads when debug mode in use * cli: debug mode can be activated now with '-d' * update requirements_test.txt * remove outdated click-datetime, replace click with asyncclick * debug is a flag * make smartstripplug to inherit the sysinfo from its parent, allows for simple access of general plug properties * proper bound checking for index accesses, allow controlling the plug at index 0 * remove the mess of turn_{on,off}_by_{name,index}, get_plug_by_{name,index} are enough. * adapt cli to use that * allow changing the alias per index * use f-strings consistently everywhere in the cli * add tests for get_plug_by_{index,name}
This commit is contained in:
parent
852ae494af
commit
3fe578cf26
181
kasa/cli.py
181
kasa/cli.py
@ -4,12 +4,13 @@ import json
|
||||
import logging
|
||||
import re
|
||||
from pprint import pformat as pf
|
||||
from typing import cast
|
||||
|
||||
import click
|
||||
import asyncclick as click
|
||||
from kasa import Discover, SmartBulb, SmartDevice, SmartPlug, SmartStrip
|
||||
|
||||
from kasa import Discover, SmartBulb, SmartDevice, SmartStrip
|
||||
click.anyio_backend = "asyncio"
|
||||
|
||||
from kasa import SmartPlug # noqa: E402; noqa: E402
|
||||
|
||||
pass_dev = click.make_pass_decorator(SmartDevice)
|
||||
|
||||
@ -33,13 +34,13 @@ pass_dev = click.make_pass_decorator(SmartDevice)
|
||||
required=False,
|
||||
help="The broadcast address to be used for discovery.",
|
||||
)
|
||||
@click.option("--debug/--normal", default=False)
|
||||
@click.option("-d", "--debug", default=False, is_flag=True)
|
||||
@click.option("--bulb", default=False, is_flag=True)
|
||||
@click.option("--plug", default=False, is_flag=True)
|
||||
@click.option("--strip", default=False, is_flag=True)
|
||||
@click.version_option()
|
||||
@click.pass_context
|
||||
def cli(ctx, host, alias, target, debug, bulb, plug, strip):
|
||||
async def cli(ctx, host, alias, target, debug, bulb, plug, strip):
|
||||
"""A cli tool for controlling TP-Link smart home plugs.""" # noqa
|
||||
if debug:
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
@ -51,7 +52,7 @@ def cli(ctx, host, alias, target, debug, bulb, plug, strip):
|
||||
|
||||
if alias is not None and host is None:
|
||||
click.echo(f"Alias is given, using discovery to find host {alias}")
|
||||
host = find_host_from_alias(alias=alias, target=target)
|
||||
host = await find_host_from_alias(alias=alias, target=target)
|
||||
if host:
|
||||
click.echo(f"Found hostname is {host}")
|
||||
else:
|
||||
@ -60,12 +61,12 @@ def cli(ctx, host, alias, target, debug, bulb, plug, strip):
|
||||
|
||||
if host is None:
|
||||
click.echo("No host name given, trying discovery..")
|
||||
ctx.invoke(discover)
|
||||
await ctx.invoke(discover)
|
||||
return
|
||||
else:
|
||||
if not bulb and not plug and not strip:
|
||||
click.echo("No --strip nor --bulb nor --plug given, discovering..")
|
||||
dev = asyncio.run(Discover.discover_single(host))
|
||||
dev = await Discover.discover_single(host)
|
||||
elif bulb:
|
||||
dev = SmartBulb(host)
|
||||
elif plug:
|
||||
@ -78,7 +79,7 @@ def cli(ctx, host, alias, target, debug, bulb, plug, strip):
|
||||
ctx.obj = dev
|
||||
|
||||
if ctx.invoked_subcommand is None:
|
||||
ctx.invoke(state)
|
||||
await ctx.invoke(state)
|
||||
|
||||
|
||||
@cli.group()
|
||||
@ -89,10 +90,10 @@ def wifi(dev):
|
||||
|
||||
@wifi.command()
|
||||
@pass_dev
|
||||
def scan(dev):
|
||||
async def scan(dev):
|
||||
"""Scan for available wifi networks."""
|
||||
click.echo("Scanning for wifi networks, wait a second..")
|
||||
devs = asyncio.run(dev.wifi_scan())
|
||||
devs = await dev.wifi_scan()
|
||||
click.echo(f"Found {len(devs)} wifi networks!")
|
||||
for dev in devs:
|
||||
click.echo(f"\t {dev}")
|
||||
@ -103,10 +104,10 @@ def scan(dev):
|
||||
@click.option("--password", prompt=True, hide_input=True)
|
||||
@click.option("--keytype", default=3)
|
||||
@pass_dev
|
||||
def join(dev: SmartDevice, ssid, password, keytype):
|
||||
async def join(dev: SmartDevice, ssid, password, keytype):
|
||||
"""Join the given wifi network."""
|
||||
click.echo("Asking the device to connect to {ssid}.." % (ssid))
|
||||
res = asyncio.run(dev.wifi_join(ssid, password, keytype=keytype))
|
||||
res = await dev.wifi_join(ssid, password, keytype=keytype)
|
||||
click.echo(
|
||||
f"Response: {res} - if the device is not able to join the network, it will revert back to its previous state."
|
||||
)
|
||||
@ -115,7 +116,7 @@ def join(dev: SmartDevice, ssid, password, keytype):
|
||||
@cli.command()
|
||||
@click.option("--scrub/--no-scrub", default=True)
|
||||
@click.pass_context
|
||||
def dump_discover(ctx, scrub):
|
||||
async def dump_discover(ctx, scrub):
|
||||
"""Dump discovery information.
|
||||
|
||||
Useful for dumping into a file to be added to the test suite.
|
||||
@ -132,7 +133,7 @@ def dump_discover(ctx, scrub):
|
||||
"latitude",
|
||||
"longitude",
|
||||
]
|
||||
devs = asyncio.run(Discover.discover(target=target, return_raw=True))
|
||||
devs = await Discover.discover(target=target, return_raw=True)
|
||||
if scrub:
|
||||
click.echo("Scrubbing personal data before writing")
|
||||
for dev in devs.values():
|
||||
@ -160,35 +161,35 @@ def dump_discover(ctx, scrub):
|
||||
@click.option("--discover-only", default=False)
|
||||
@click.option("--dump-raw", is_flag=True)
|
||||
@click.pass_context
|
||||
def discover(ctx, timeout, discover_only, dump_raw):
|
||||
async def discover(ctx, timeout, discover_only, dump_raw):
|
||||
"""Discover devices in the network."""
|
||||
target = ctx.parent.params["target"]
|
||||
click.echo(f"Discovering devices for {timeout} seconds")
|
||||
found_devs = asyncio.run(
|
||||
Discover.discover(target=target, timeout=timeout, return_raw=dump_raw)
|
||||
found_devs = await Discover.discover(
|
||||
target=target, timeout=timeout, return_raw=dump_raw
|
||||
)
|
||||
if not discover_only:
|
||||
for ip, dev in found_devs.items():
|
||||
asyncio.run(dev.update())
|
||||
await dev.update()
|
||||
if dump_raw:
|
||||
click.echo(dev)
|
||||
continue
|
||||
ctx.obj = dev
|
||||
ctx.invoke(state)
|
||||
print()
|
||||
await ctx.invoke(state)
|
||||
click.echo()
|
||||
|
||||
return found_devs
|
||||
|
||||
|
||||
def find_host_from_alias(alias, target="255.255.255.255", timeout=1, attempts=3):
|
||||
async def find_host_from_alias(alias, target="255.255.255.255", timeout=1, attempts=3):
|
||||
"""Discover a device identified by its alias."""
|
||||
host = None
|
||||
click.echo(
|
||||
f"Trying to discover {alias} using {attempts} attempts of {timeout} seconds"
|
||||
)
|
||||
for attempt in range(1, attempts):
|
||||
click.echo(f"Attempt {attempt} of {attempts}")
|
||||
found_devs = Discover.discover(target=target, timeout=timeout).items()
|
||||
found_devs = await Discover.discover(target=target, timeout=timeout)
|
||||
found_devs = found_devs.items()
|
||||
for ip, dev in found_devs:
|
||||
if dev.alias.lower() == alias.lower():
|
||||
host = dev.host
|
||||
@ -198,9 +199,9 @@ def find_host_from_alias(alias, target="255.255.255.255", timeout=1, attempts=3)
|
||||
|
||||
@cli.command()
|
||||
@pass_dev
|
||||
def sysinfo(dev):
|
||||
async def sysinfo(dev):
|
||||
"""Print out full system information."""
|
||||
asyncio.run(dev.update())
|
||||
await dev.update()
|
||||
click.echo(click.style("== System info ==", bold=True))
|
||||
click.echo(pf(dev.sys_info))
|
||||
|
||||
@ -208,9 +209,9 @@ def sysinfo(dev):
|
||||
@cli.command()
|
||||
@pass_dev
|
||||
@click.pass_context
|
||||
def state(ctx, dev: SmartDevice):
|
||||
async def state(ctx, dev: SmartDevice):
|
||||
"""Print out device state and versions."""
|
||||
asyncio.run(dev.update())
|
||||
await dev.update()
|
||||
click.echo(click.style(f"== {dev.alias} - {dev.model} ==", bold=True))
|
||||
|
||||
click.echo(
|
||||
@ -221,12 +222,13 @@ def state(ctx, dev: SmartDevice):
|
||||
)
|
||||
if dev.is_strip:
|
||||
for plug in dev.plugs: # type: ignore
|
||||
asyncio.run(plug.update())
|
||||
is_on = plug.is_on
|
||||
alias = plug.alias
|
||||
click.echo(
|
||||
click.style(
|
||||
" * {} state: {}".format(alias, ("ON" if is_on else "OFF")),
|
||||
" * Socket '{}' state: {} on_since: {}".format(
|
||||
alias, ("ON" if is_on else "OFF"), plug.on_since
|
||||
),
|
||||
fg="green" if is_on else "red",
|
||||
)
|
||||
)
|
||||
@ -235,25 +237,37 @@ def state(ctx, dev: SmartDevice):
|
||||
for k, v in dev.state_information.items():
|
||||
click.echo(f"{k}: {v}")
|
||||
click.echo(click.style("== Generic information ==", bold=True))
|
||||
click.echo(f"Time: {asyncio.run(dev.get_time())}")
|
||||
click.echo(f"Time: {await dev.get_time()}")
|
||||
click.echo(f"Hardware: {dev.hw_info['hw_ver']}")
|
||||
click.echo(f"Software: {dev.hw_info['sw_ver']}")
|
||||
click.echo(f"MAC (rssi): {dev.mac} ({dev.rssi})")
|
||||
click.echo(f"Location: {dev.location}")
|
||||
|
||||
ctx.invoke(emeter)
|
||||
await ctx.invoke(emeter)
|
||||
|
||||
|
||||
@cli.command()
|
||||
@pass_dev
|
||||
@click.argument("new_alias", required=False, default=None)
|
||||
def alias(dev, new_alias):
|
||||
"""Get or set the device alias."""
|
||||
@click.option("--index", type=int)
|
||||
async def alias(dev, new_alias, index):
|
||||
"""Get or set the device (or plug) alias."""
|
||||
await dev.update()
|
||||
if index is not None:
|
||||
if not dev.is_strip:
|
||||
click.echo("Index can only used for power strips!")
|
||||
return
|
||||
dev = cast(SmartStrip, dev)
|
||||
dev = dev.get_plug_by_index(index)
|
||||
|
||||
if new_alias is not None:
|
||||
click.echo(f"Setting alias to {new_alias}")
|
||||
asyncio.run(dev.set_alias(new_alias))
|
||||
click.echo(await dev.set_alias(new_alias))
|
||||
|
||||
click.echo(f"Alias: {dev.alias}")
|
||||
if dev.is_strip:
|
||||
for plug in dev.plugs:
|
||||
click.echo(f" * {plug.alias}")
|
||||
|
||||
|
||||
@cli.command()
|
||||
@ -261,14 +275,14 @@ def alias(dev, new_alias):
|
||||
@click.argument("module")
|
||||
@click.argument("command")
|
||||
@click.argument("parameters", default=None, required=False)
|
||||
def raw_command(dev: SmartDevice, module, command, parameters):
|
||||
async def raw_command(dev: SmartDevice, module, command, parameters):
|
||||
"""Run a raw command on the device."""
|
||||
import ast
|
||||
|
||||
if parameters is not None:
|
||||
parameters = ast.literal_eval(parameters)
|
||||
res = asyncio.run(dev._query_helper(module, command, parameters))
|
||||
asyncio.run(dev.update())
|
||||
res = await dev._query_helper(module, command, parameters)
|
||||
await dev.update() # TODO: is this needed?
|
||||
click.echo(res)
|
||||
|
||||
|
||||
@ -277,34 +291,34 @@ def raw_command(dev: SmartDevice, module, command, parameters):
|
||||
@click.option("--year", type=click.DateTime(["%Y"]), default=None, required=False)
|
||||
@click.option("--month", type=click.DateTime(["%Y-%m"]), default=None, required=False)
|
||||
@click.option("--erase", is_flag=True)
|
||||
def emeter(dev, year, month, erase):
|
||||
async def emeter(dev, year, month, erase):
|
||||
"""Query emeter for historical consumption."""
|
||||
click.echo(click.style("== Emeter ==", bold=True))
|
||||
asyncio.run(dev.update())
|
||||
await dev.update()
|
||||
if not dev.has_emeter:
|
||||
click.echo("Device has no emeter")
|
||||
return
|
||||
|
||||
if erase:
|
||||
click.echo("Erasing emeter statistics..")
|
||||
asyncio.run(dev.erase_emeter_stats())
|
||||
click.echo(await dev.erase_emeter_stats())
|
||||
return
|
||||
|
||||
if year:
|
||||
click.echo(f"== For year {year.year} ==")
|
||||
emeter_status = asyncio.run(dev.get_emeter_monthly(year.year))
|
||||
emeter_status = await dev.get_emeter_monthly(year.year)
|
||||
elif month:
|
||||
click.echo(f"== For month {month.month} of {month.year} ==")
|
||||
emeter_status = asyncio.run(
|
||||
dev.get_emeter_daily(year=month.year, month=month.month)
|
||||
)
|
||||
emeter_status = await dev.get_emeter_daily(year=month.year, month=month.month)
|
||||
|
||||
else:
|
||||
emeter_status = asyncio.run(dev.get_emeter_realtime())
|
||||
emeter_status = await dev.get_emeter_realtime()
|
||||
click.echo("== Current State ==")
|
||||
|
||||
if isinstance(emeter_status, list):
|
||||
for plug in emeter_status:
|
||||
click.echo("Plug %d: %s" % (emeter_status.index(plug) + 1, plug))
|
||||
index = emeter_status.index(plug) + 1
|
||||
click.echo(f"Plug {index}: {plug}")
|
||||
else:
|
||||
click.echo(str(emeter_status))
|
||||
|
||||
@ -312,9 +326,9 @@ def emeter(dev, year, month, erase):
|
||||
@cli.command()
|
||||
@click.argument("brightness", type=click.IntRange(0, 100), default=None, required=False)
|
||||
@pass_dev
|
||||
def brightness(dev, brightness):
|
||||
async def brightness(dev, brightness):
|
||||
"""Get or set brightness."""
|
||||
asyncio.run(dev.update())
|
||||
await dev.update()
|
||||
if not dev.is_dimmable:
|
||||
click.echo("This device does not support brightness.")
|
||||
return
|
||||
@ -322,7 +336,7 @@ def brightness(dev, brightness):
|
||||
click.echo(f"Brightness: {dev.brightness}")
|
||||
else:
|
||||
click.echo(f"Setting brightness to {brightness}")
|
||||
asyncio.run(dev.set_brightness(brightness))
|
||||
click.echo(await dev.set_brightness(brightness))
|
||||
|
||||
|
||||
@cli.command()
|
||||
@ -330,8 +344,9 @@ def brightness(dev, brightness):
|
||||
"temperature", type=click.IntRange(2500, 9000), default=None, required=False
|
||||
)
|
||||
@pass_dev
|
||||
def temperature(dev: SmartBulb, temperature):
|
||||
async def temperature(dev: SmartBulb, temperature):
|
||||
"""Get or set color temperature."""
|
||||
await dev.update()
|
||||
if temperature is None:
|
||||
click.echo(f"Color temperature: {dev.color_temp}")
|
||||
valid_temperature_range = dev.valid_temperature_range
|
||||
@ -353,67 +368,87 @@ def temperature(dev: SmartBulb, temperature):
|
||||
@click.argument("v", type=click.IntRange(0, 100), default=None, required=False)
|
||||
@click.pass_context
|
||||
@pass_dev
|
||||
def hsv(dev, ctx, h, s, v):
|
||||
async def hsv(dev, ctx, h, s, v):
|
||||
"""Get or set color in HSV. (Bulb only)."""
|
||||
await dev.update()
|
||||
if h is None or s is None or v is None:
|
||||
click.echo(f"Current HSV: {dev.hsv}")
|
||||
elif s is None or v is None:
|
||||
raise click.BadArgumentUsage("Setting a color requires 3 values.", ctx)
|
||||
else:
|
||||
click.echo(f"Setting HSV: {h} {s} {v}")
|
||||
asyncio.run(dev.set_hsv(h, s, v))
|
||||
click.echo(await dev.set_hsv(h, s, v))
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.argument("state", type=bool, required=False)
|
||||
@pass_dev
|
||||
def led(dev, state):
|
||||
async def led(dev, state):
|
||||
"""Get or set (Plug's) led state."""
|
||||
await dev.update()
|
||||
if state is not None:
|
||||
click.echo(f"Turning led to {state}")
|
||||
asyncio.run(dev.set_led(state))
|
||||
click.echo(await dev.set_led(state))
|
||||
else:
|
||||
click.echo(f"LED state: {dev.led}")
|
||||
|
||||
|
||||
@cli.command()
|
||||
@pass_dev
|
||||
def time(dev):
|
||||
async def time(dev):
|
||||
"""Get the device time."""
|
||||
click.echo(asyncio.run(dev.get_time()))
|
||||
click.echo(await dev.get_time())
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.argument("index", type=int, required=False)
|
||||
@click.option("--index", type=int, required=False)
|
||||
@click.option("--name", type=str, required=False)
|
||||
@pass_dev
|
||||
def on(plug, index):
|
||||
async def on(dev: SmartDevice, index, name):
|
||||
"""Turn the device on."""
|
||||
click.echo("Turning on..")
|
||||
if index is None:
|
||||
asyncio.run(plug.turn_on())
|
||||
else:
|
||||
asyncio.run(plug.turn_on(index=(index - 1)))
|
||||
await dev.update()
|
||||
if index is not None or name is not None:
|
||||
if not dev.is_strip:
|
||||
click.echo("Index and name are only for power strips!")
|
||||
return
|
||||
dev = cast(SmartStrip, dev)
|
||||
if index is not None:
|
||||
dev = dev.get_plug_by_index(index)
|
||||
elif name:
|
||||
dev = dev.get_plug_by_name(name)
|
||||
|
||||
click.echo(f"Turning on {dev.alias}")
|
||||
await dev.turn_on()
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.argument("index", type=int, required=False)
|
||||
@click.option("--index", type=int, required=False)
|
||||
@click.option("--name", type=str, required=False)
|
||||
@pass_dev
|
||||
def off(plug, index):
|
||||
async def off(dev, index, name):
|
||||
"""Turn the device off."""
|
||||
click.echo("Turning off..")
|
||||
if index is None:
|
||||
asyncio.run(plug.turn_off())
|
||||
else:
|
||||
asyncio.run(plug.turn_off(index=(index - 1)))
|
||||
await dev.update()
|
||||
if index is not None or name is not None:
|
||||
if not dev.is_strip:
|
||||
click.echo("Index and name are only for power strips!")
|
||||
return
|
||||
dev = cast(SmartStrip, dev)
|
||||
if index is not None:
|
||||
dev = dev.get_plug_by_index(index)
|
||||
elif name:
|
||||
dev = dev.get_plug_by_name(name)
|
||||
|
||||
click.echo(f"Turning off {dev.alias}")
|
||||
await dev.turn_off()
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option("--delay", default=1)
|
||||
@pass_dev
|
||||
def reboot(plug, delay):
|
||||
async def reboot(plug, delay):
|
||||
"""Reboot the device."""
|
||||
click.echo("Rebooting the device..")
|
||||
asyncio.run(plug.reboot(delay))
|
||||
click.echo(await plug.reboot(delay))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
@ -13,6 +13,7 @@ import asyncio
|
||||
import json
|
||||
import logging
|
||||
import struct
|
||||
from pprint import pformat as pf
|
||||
from typing import Any, Dict, Union
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -67,9 +68,10 @@ class TPLinkSmartHomeProtocol:
|
||||
await writer.wait_closed()
|
||||
|
||||
response = TPLinkSmartHomeProtocol.decrypt(buffer[4:])
|
||||
_LOGGER.debug("< (%i) %s", len(response), response)
|
||||
json_payload = json.loads(response)
|
||||
_LOGGER.debug("< (%i) %s", len(response), pf(json_payload))
|
||||
|
||||
return json.loads(response)
|
||||
return json_payload
|
||||
|
||||
@staticmethod
|
||||
def encrypt(request: str) -> bytes:
|
||||
|
@ -122,7 +122,7 @@ class SmartDevice:
|
||||
|
||||
self.protocol = TPLinkSmartHomeProtocol()
|
||||
self.emeter_type = "emeter"
|
||||
_LOGGER.debug("Initializing %s", self.host)
|
||||
_LOGGER.debug("Initializing %s of type %s", self.host, type(self))
|
||||
self._device_type = DeviceType.Unknown
|
||||
self._sys_info: Optional[Dict] = None
|
||||
|
||||
|
@ -93,6 +93,22 @@ class SmartStrip(SmartDevice):
|
||||
await self._query_helper("system", "set_relay_state", {"state": 0})
|
||||
await self.update()
|
||||
|
||||
def get_plug_by_name(self, name: str) -> "SmartStripPlug":
|
||||
"""Return child plug for given name."""
|
||||
for p in self.plugs:
|
||||
if p.alias == name:
|
||||
return p
|
||||
|
||||
raise SmartDeviceException(f"Device has no child with {name}")
|
||||
|
||||
def get_plug_by_index(self, index: int) -> "SmartStripPlug":
|
||||
"""Return child plug for given index."""
|
||||
if index + 1 > len(self.plugs) or index < 0:
|
||||
raise SmartDeviceException(
|
||||
f"Invalid index {index}, device has {len(self.plugs)} plugs"
|
||||
)
|
||||
return self.plugs[index]
|
||||
|
||||
@property # type: ignore
|
||||
@requires_update
|
||||
def on_since(self) -> datetime.datetime:
|
||||
@ -107,8 +123,6 @@ class SmartStrip(SmartDevice):
|
||||
:return: True if led is on, False otherwise
|
||||
:rtype: bool
|
||||
"""
|
||||
# TODO this is a copypaste from smartplug,
|
||||
# check if led value is per socket or per device..
|
||||
sys_info = self.sys_info
|
||||
return bool(1 - sys_info["led_off"])
|
||||
|
||||
@ -118,8 +132,6 @@ class SmartStrip(SmartDevice):
|
||||
:param bool state: True to set led on, False to set led off
|
||||
:raises SmartDeviceException: on error
|
||||
"""
|
||||
# TODO this is a copypaste from smartplug,
|
||||
# check if led value is per socket or per device..
|
||||
await self._query_helper("system", "set_led_off", {"off": int(not state)})
|
||||
await self.update()
|
||||
|
||||
@ -221,6 +233,8 @@ class SmartStripPlug(SmartPlug):
|
||||
This allows you to use the sockets as they were SmartPlug objects.
|
||||
Instead of calling an update on any of these, you should call an update
|
||||
on the parent device before accessing the properties.
|
||||
|
||||
The plug inherits (most of) the system information from the parent.
|
||||
"""
|
||||
|
||||
def __init__(self, host: str, parent: "SmartStrip", child_id: str) -> None:
|
||||
@ -228,7 +242,7 @@ class SmartStripPlug(SmartPlug):
|
||||
|
||||
self.parent = parent
|
||||
self.child_id = child_id
|
||||
self._sys_info = self._get_child_info()
|
||||
self._sys_info = {**self.parent.sys_info, **self._get_child_info()}
|
||||
|
||||
async def update(self):
|
||||
"""Override the update to no-op and inform the user."""
|
||||
@ -268,6 +282,12 @@ class SmartStripPlug(SmartPlug):
|
||||
"""
|
||||
return False
|
||||
|
||||
@property # type: ignore
|
||||
@requires_update
|
||||
def has_emeter(self) -> bool:
|
||||
"""Children have no emeter to my knowledge."""
|
||||
return False
|
||||
|
||||
@property # type: ignore
|
||||
@requires_update
|
||||
def device_id(self) -> str:
|
||||
@ -288,6 +308,13 @@ class SmartStripPlug(SmartPlug):
|
||||
info = self._get_child_info()
|
||||
return info["alias"]
|
||||
|
||||
@property # type: ignore
|
||||
@requires_update
|
||||
def next_action(self) -> Dict:
|
||||
"""Return next scheduled(?) action."""
|
||||
info = self._get_child_info()
|
||||
return info["next_action"]
|
||||
|
||||
@property # type: ignore
|
||||
@requires_update
|
||||
def on_since(self) -> datetime.datetime:
|
||||
|
@ -92,13 +92,12 @@ def dev(request):
|
||||
Provides a device (given --ip) or parametrized fixture for the supported devices.
|
||||
The initial update is called automatically before returning the device.
|
||||
"""
|
||||
loop = asyncio.get_event_loop()
|
||||
file = request.param
|
||||
|
||||
ip = request.config.getoption("--ip")
|
||||
if ip:
|
||||
d = loop.run_until_complete(Discover.discover_single(ip))
|
||||
loop.run_until_complete(d.update())
|
||||
d = asyncio.run(Discover.discover_single(ip))
|
||||
asyncio.run(d.update())
|
||||
print(d.model)
|
||||
if d.model in file:
|
||||
return d
|
||||
@ -125,7 +124,7 @@ def dev(request):
|
||||
model = basename(file)
|
||||
p = device_for_file(model)(host="123.123.123.123")
|
||||
p.protocol = FakeTransportProtocol(sysinfo)
|
||||
loop.run_until_complete(p.update())
|
||||
asyncio.run(p.update())
|
||||
yield p
|
||||
|
||||
|
||||
|
@ -1,25 +1,26 @@
|
||||
import asyncio
|
||||
|
||||
from click.testing import CliRunner
|
||||
import pytest
|
||||
|
||||
from asyncclick.testing import CliRunner
|
||||
from kasa import SmartDevice
|
||||
from kasa.cli import alias, brightness, emeter, raw_command, state, sysinfo
|
||||
|
||||
from .conftest import handle_turn_on, turn_on
|
||||
|
||||
pytestmark = pytest.mark.asyncio
|
||||
|
||||
def test_sysinfo(dev):
|
||||
|
||||
async def test_sysinfo(dev):
|
||||
runner = CliRunner()
|
||||
res = runner.invoke(sysinfo, obj=dev)
|
||||
res = await runner.invoke(sysinfo, obj=dev)
|
||||
assert "System info" in res.output
|
||||
assert dev.alias in res.output
|
||||
|
||||
|
||||
@turn_on
|
||||
def test_state(dev, turn_on):
|
||||
asyncio.run(handle_turn_on(dev, turn_on))
|
||||
async def test_state(dev, turn_on):
|
||||
await handle_turn_on(dev, turn_on)
|
||||
runner = CliRunner()
|
||||
res = runner.invoke(state, obj=dev)
|
||||
res = await runner.invoke(state, obj=dev)
|
||||
print(res.output)
|
||||
|
||||
if dev.is_on:
|
||||
@ -31,36 +32,36 @@ def test_state(dev, turn_on):
|
||||
assert "Device has no emeter" in res.output
|
||||
|
||||
|
||||
def test_alias(dev):
|
||||
async def test_alias(dev):
|
||||
runner = CliRunner()
|
||||
|
||||
res = runner.invoke(alias, obj=dev)
|
||||
res = await runner.invoke(alias, obj=dev)
|
||||
assert f"Alias: {dev.alias}" in res.output
|
||||
|
||||
new_alias = "new alias"
|
||||
res = runner.invoke(alias, [new_alias], obj=dev)
|
||||
res = await runner.invoke(alias, [new_alias], obj=dev)
|
||||
assert f"Setting alias to {new_alias}" in res.output
|
||||
|
||||
res = runner.invoke(alias, obj=dev)
|
||||
res = await runner.invoke(alias, obj=dev)
|
||||
assert f"Alias: {new_alias}" in res.output
|
||||
|
||||
|
||||
def test_raw_command(dev):
|
||||
async def test_raw_command(dev):
|
||||
runner = CliRunner()
|
||||
res = runner.invoke(raw_command, ["system", "get_sysinfo"], obj=dev)
|
||||
res = await runner.invoke(raw_command, ["system", "get_sysinfo"], obj=dev)
|
||||
|
||||
assert res.exit_code == 0
|
||||
assert dev.alias in res.output
|
||||
|
||||
res = runner.invoke(raw_command, obj=dev)
|
||||
res = await runner.invoke(raw_command, obj=dev)
|
||||
assert res.exit_code != 0
|
||||
assert "Usage" in res.output
|
||||
|
||||
|
||||
def test_emeter(dev: SmartDevice, mocker):
|
||||
async def test_emeter(dev: SmartDevice, mocker):
|
||||
runner = CliRunner()
|
||||
|
||||
res = runner.invoke(emeter, obj=dev)
|
||||
res = await runner.invoke(emeter, obj=dev)
|
||||
if not dev.has_emeter:
|
||||
assert "Device has no emeter" in res.output
|
||||
return
|
||||
@ -68,52 +69,52 @@ def test_emeter(dev: SmartDevice, mocker):
|
||||
assert "Current State" in res.output
|
||||
|
||||
monthly = mocker.patch.object(dev, "get_emeter_monthly")
|
||||
res = runner.invoke(emeter, ["--year", "1900"], obj=dev)
|
||||
res = await runner.invoke(emeter, ["--year", "1900"], obj=dev)
|
||||
assert "For year" in res.output
|
||||
monthly.assert_called()
|
||||
|
||||
daily = mocker.patch.object(dev, "get_emeter_daily")
|
||||
res = runner.invoke(emeter, ["--month", "1900-12"], obj=dev)
|
||||
res = await runner.invoke(emeter, ["--month", "1900-12"], obj=dev)
|
||||
assert "For month" in res.output
|
||||
daily.assert_called()
|
||||
|
||||
|
||||
def test_brightness(dev):
|
||||
async def test_brightness(dev):
|
||||
runner = CliRunner()
|
||||
res = runner.invoke(brightness, obj=dev)
|
||||
res = await runner.invoke(brightness, obj=dev)
|
||||
if not dev.is_dimmable:
|
||||
assert "This device does not support brightness." in res.output
|
||||
return
|
||||
|
||||
res = runner.invoke(brightness, obj=dev)
|
||||
res = await runner.invoke(brightness, obj=dev)
|
||||
assert f"Brightness: {dev.brightness}" in res.output
|
||||
|
||||
res = runner.invoke(brightness, ["12"], obj=dev)
|
||||
res = await runner.invoke(brightness, ["12"], obj=dev)
|
||||
assert "Setting brightness" in res.output
|
||||
|
||||
res = runner.invoke(brightness, obj=dev)
|
||||
res = await runner.invoke(brightness, obj=dev)
|
||||
assert f"Brightness: 12" in res.output
|
||||
|
||||
|
||||
def test_temperature(dev):
|
||||
async def test_temperature(dev):
|
||||
pass
|
||||
|
||||
|
||||
def test_hsv(dev):
|
||||
async def test_hsv(dev):
|
||||
pass
|
||||
|
||||
|
||||
def test_led(dev):
|
||||
async def test_led(dev):
|
||||
pass
|
||||
|
||||
|
||||
def test_on(dev):
|
||||
async def test_on(dev):
|
||||
pass
|
||||
|
||||
|
||||
def test_off(dev):
|
||||
async def test_off(dev):
|
||||
pass
|
||||
|
||||
|
||||
def test_reboot(dev):
|
||||
async def test_reboot(dev):
|
||||
pass
|
||||
|
@ -425,6 +425,26 @@ async def test_children_on_since(dev):
|
||||
assert plug.on_since
|
||||
|
||||
|
||||
@strip
|
||||
async def test_get_plug_by_name(dev: SmartStrip):
|
||||
name = dev.plugs[0].alias
|
||||
assert dev.get_plug_by_name(name) == dev.plugs[0]
|
||||
|
||||
with pytest.raises(SmartDeviceException):
|
||||
dev.get_plug_by_name("NONEXISTING NAME")
|
||||
|
||||
|
||||
@strip
|
||||
async def test_get_plug_by_index(dev: SmartStrip):
|
||||
assert dev.get_plug_by_index(0) == dev.plugs[0]
|
||||
|
||||
with pytest.raises(SmartDeviceException):
|
||||
dev.get_plug_by_index(-1)
|
||||
|
||||
with pytest.raises(SmartDeviceException):
|
||||
dev.get_plug_by_index(len(dev.plugs))
|
||||
|
||||
|
||||
@pytest.mark.skip("this test will wear out your relays")
|
||||
async def test_all_binary_states(dev):
|
||||
# test every binary state
|
||||
|
@ -1,4 +1,3 @@
|
||||
click
|
||||
click-datetime
|
||||
asyncclick
|
||||
pre-commit
|
||||
voluptuous
|
||||
|
@ -3,6 +3,6 @@ pytest-azurepipelines
|
||||
pytest-cov
|
||||
pytest-asyncio
|
||||
pytest-mock
|
||||
click # needed for test_cli
|
||||
asyncclick
|
||||
voluptuous
|
||||
codecov
|
||||
|
2
setup.py
2
setup.py
@ -12,7 +12,7 @@ setup(
|
||||
author_email="",
|
||||
license="GPLv3",
|
||||
packages=["kasa"],
|
||||
install_requires=["click"],
|
||||
install_requires=["asyncclick"],
|
||||
python_requires=">=3.7",
|
||||
entry_points={"console_scripts": ["kasa=kasa.cli:cli"]},
|
||||
zip_safe=False,
|
||||
|
Loading…
Reference in New Issue
Block a user