Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .flake8
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,5 @@ exclude = .git,.tox,__pycache__,kasa/tests/newfakes.py,kasa/tests/test_fixtures.
max-line-length = 88
per-file-ignores =
kasa/tests/*.py:D100,D101,D102,D103,D104
setup.py:D100
ignore = D105, D107, E203, E501, W503
max-complexity = 18
37 changes: 22 additions & 15 deletions kasa/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
@click.version_option()
@click.pass_context
async def cli(ctx, host, alias, target, debug, bulb, plug, strip):
"""A cli tool for controlling TP-Link smart home plugs.""" # noqa
"""A tool for controlling TP-Link smart home devices.""" # noqa
if debug:
logging.basicConfig(level=logging.DEBUG)
else:
Expand Down Expand Up @@ -214,37 +214,44 @@ async def state(ctx, dev: SmartDevice):
"""Print out device state and versions."""
await dev.update()
click.echo(click.style(f"== {dev.alias} - {dev.model} ==", bold=True))

click.echo(f"\tHost: {dev.host}")
click.echo(
click.style(
"Device state: {}".format("ON" if dev.is_on else "OFF"),
"\tDevice state: {}\n".format("ON" if dev.is_on else "OFF"),
fg="green" if dev.is_on else "red",
)
)
if dev.is_strip:
for plug in dev.plugs: # type: ignore
click.echo(click.style("\t== Plugs ==", bold=True))
for plug in dev.children: # type: ignore
is_on = plug.is_on
alias = plug.alias
click.echo(
click.style(
" * Socket '{}' state: {} on_since: {}".format(
"\t* Socket '{}' state: {} on_since: {}".format(
alias, ("ON" if is_on else "OFF"), plug.on_since
),
fg="green" if is_on else "red",
)
)
click.echo()

click.echo(click.style("\t== Generic information ==", bold=True))
click.echo(f"\tTime: {await dev.get_time()}")
click.echo(f"\tHardware: {dev.hw_info['hw_ver']}")
click.echo(f"\tSoftware: {dev.hw_info['sw_ver']}")
click.echo(f"\tMAC (rssi): {dev.mac} ({dev.rssi})")
click.echo(f"\tLocation: {dev.location}")

click.echo(f"Host/IP: {dev.host}")
click.echo(click.style("\n\t== Device specific information ==", bold=True))
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: {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}")
click.echo(f"\t{k}: {v}")
click.echo()

await ctx.invoke(emeter)
if dev.has_emeter:
click.echo(click.style("\n\t== Current State ==", bold=True))
emeter_status = dev.emeter_realtime
click.echo(f"\t{emeter_status}")


@cli.command()
Expand All @@ -267,7 +274,7 @@ async def alias(dev, new_alias, index):

click.echo(f"Alias: {dev.alias}")
if dev.is_strip:
for plug in dev.plugs:
for plug in dev.children:
click.echo(f" * {plug.alias}")


Expand Down
4 changes: 3 additions & 1 deletion kasa/discover.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ def do_discover(self) -> None:
self.transport.sendto(encrypted_req[4:], self.target) # type: ignore

def datagram_received(self, data, addr) -> None:
"""Handle discovery responses."""
ip, port = addr
if ip in self.discovered_devices:
return
Expand All @@ -82,10 +83,11 @@ def datagram_received(self, data, addr) -> None:
_LOGGER.error("Received invalid response: %s", info)

def error_received(self, ex):
"""Handle asyncio.Protocol errors."""
_LOGGER.error("Got error: %s", ex)

def connection_lost(self, ex):
pass
"""NOP implementation of connection lost."""


class Discover:
Expand Down
17 changes: 6 additions & 11 deletions kasa/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import logging
import struct
from pprint import pformat as pf
from typing import Any, Dict, Union
from typing import Dict, Union

_LOGGER = logging.getLogger(__name__)

Expand All @@ -27,13 +27,10 @@ class TPLinkSmartHomeProtocol:
DEFAULT_TIMEOUT = 5

@staticmethod
async def query(
host: str, request: Union[str, Dict], port: int = DEFAULT_PORT
) -> Any:
async def query(host: str, request: Union[str, Dict]) -> Dict:
"""Request information from a TP-Link SmartHome Device.

:param str host: host name or ip address of the device
:param int port: port on the device (default: 9999)
:param request: command to send to the device (can be either dict or
json string)
:return: response dict
Expand All @@ -44,7 +41,7 @@ async def query(
timeout = TPLinkSmartHomeProtocol.DEFAULT_TIMEOUT
writer = None
try:
task = asyncio.open_connection(host, port)
task = asyncio.open_connection(host, TPLinkSmartHomeProtocol.DEFAULT_PORT)
reader, writer = await asyncio.wait_for(task, timeout=timeout)
_LOGGER.debug("> (%i) %s", len(request), request)
writer.write(TPLinkSmartHomeProtocol.encrypt(request))
Expand Down Expand Up @@ -75,11 +72,10 @@ async def query(

@staticmethod
def encrypt(request: str) -> bytes:
"""
Encrypt a request for a TP-Link Smart Home Device.
"""Encrypt a request for a TP-Link Smart Home Device.

:param request: plaintext request data
:return: ciphertext request
:return: ciphertext to be send over wire, in bytes
"""
key = TPLinkSmartHomeProtocol.INITIALIZATION_VECTOR

Expand All @@ -95,8 +91,7 @@ def encrypt(request: str) -> bytes:

@staticmethod
def decrypt(ciphertext: bytes) -> str:
"""
Decrypt a response of a TP-Link Smart Home Device.
"""Decrypt a response of a TP-Link Smart Home Device.

:param ciphertext: encrypted response data
:return: plaintext response
Expand Down
60 changes: 15 additions & 45 deletions kasa/smartbulb.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Module for bulbs."""
"""Module for bulbs (LB*, KL*, KB*)."""
import re
from typing import Any, Dict, Tuple, cast

Expand Down Expand Up @@ -82,34 +82,21 @@ def __init__(self, host: str) -> None:
@property # type: ignore
@requires_update
def is_color(self) -> bool:
"""Whether the bulb supports color changes.

:return: True if the bulb supports color changes, False otherwise
:rtype: bool
"""
"""Whether the bulb supports color changes."""
sys_info = self.sys_info
return bool(sys_info["is_color"])

@property # type: ignore
@requires_update
def is_dimmable(self) -> bool:
"""Whether the bulb supports brightness changes.

:return: True if the bulb supports brightness changes, False otherwise
:rtype: bool
"""
"""Whether the bulb supports brightness changes."""
sys_info = self.sys_info
return bool(sys_info["is_dimmable"])

@property # type: ignore
@requires_update
def is_variable_color_temp(self) -> bool:
"""Whether the bulb supports color temperature changes.

:return: True if the bulb supports color temperature changes, False
otherwise
:rtype: bool
"""
"""Whether the bulb supports color temperature changes."""
sys_info = self.sys_info
return bool(sys_info["is_variable_color_temp"])

Expand All @@ -118,16 +105,18 @@ def is_variable_color_temp(self) -> bool:
def valid_temperature_range(self) -> Tuple[int, int]:
"""Return the device-specific white temperature range (in Kelvin).

:return: White temperature range in Kelvin (minimun, maximum)
:rtype: tuple
:return: White temperature range in Kelvin (minimum, maximum)
"""
if not self.is_variable_color_temp:
return (0, 0)
raise SmartDeviceException("Color temperature not supported")
for model, temp_range in TPLINK_KELVIN.items():
sys_info = self.sys_info
if re.match(model, sys_info["model"]):
return temp_range
return (0, 0)

raise SmartDeviceException(
"Unknown color temperature range, please open an issue on github"
)

@property # type: ignore
@requires_update
Expand Down Expand Up @@ -166,7 +155,6 @@ def hsv(self) -> Tuple[int, int, int]:
"""Return the current HSV state of the bulb.

:return: hue, saturation and value (degrees, %, %)
:rtype: tuple
"""
if not self.is_color:
raise SmartDeviceException("Bulb does not support color.")
Expand Down Expand Up @@ -220,11 +208,7 @@ async def set_hsv(self, hue: int, saturation: int, value: int):
@property # type: ignore
@requires_update
def color_temp(self) -> int:
"""Return color temperature of the device.

:return: Color temperature in Kelvin
:rtype: int
"""
"""Return color temperature of the device in kelvin."""
if not self.is_variable_color_temp:
raise SmartDeviceException("Bulb does not support colortemp.")

Expand All @@ -233,10 +217,7 @@ def color_temp(self) -> int:

@requires_update
async def set_color_temp(self, temp: int) -> None:
"""Set the color temperature of the device.

:param int temp: The new color temperature, in Kelvin
"""
"""Set the color temperature of the device in kelvin."""
if not self.is_variable_color_temp:
raise SmartDeviceException("Bulb does not support colortemp.")

Expand All @@ -253,11 +234,7 @@ async def set_color_temp(self, temp: int) -> None:
@property # type: ignore
@requires_update
def brightness(self) -> int:
"""Return the current brightness.

:return: brightness in percent
:rtype: int
"""
"""Return the current brightness in percentage."""
if not self.is_dimmable: # pragma: no cover
raise SmartDeviceException("Bulb is not dimmable.")

Expand All @@ -266,10 +243,7 @@ def brightness(self) -> int:

@requires_update
async def set_brightness(self, brightness: int) -> None:
"""Set the brightness.

:param int brightness: brightness in percent
"""
"""Set the brightness in percentage."""
if not self.is_dimmable: # pragma: no cover
raise SmartDeviceException("Bulb is not dimmable.")

Expand All @@ -281,11 +255,7 @@ async def set_brightness(self, brightness: int) -> None:
@property # type: ignore
@requires_update
def state_information(self) -> Dict[str, Any]:
"""Return bulb-specific state information.

:return: Bulb information dict, keys in user-presentable form.
:rtype: dict
"""
"""Return bulb-specific state information."""
info: Dict[str, Any] = {
"Brightness": self.brightness,
"Is dimmable": self.is_dimmable,
Expand Down
Loading