mirror of
				https://github.com/python-kasa/python-kasa.git
				synced 2025-11-03 22:22:06 +00:00 
			
		
		
		
	General cleanups all around (janitoring) (#63)
* Move tests to device-type specific test files to make improvements more approachable
* protocol: remove the port parameter from query, as there are no other known ports, fix docstrings
* Revise docstrings, remove superfluous information and remove unused methods ({set,get_icon} and set_time)
* cli: indent device output to make it more easily readable when having multiple devices
* remove adjust flake8 ignores (we have no setup.py anymore)
* pyproject: include cli tool to coverage, add config for interrogate (docstring coverage)
* bulb: raise exception on color_temp error cases instead of returning zero values
* improve bulb tests, simplify conftest
* strip: rename plugs property to children and move it to smartdevice
			
			
This commit is contained in:
		
							
								
								
									
										1
									
								
								.flake8
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								.flake8
									
									
									
									
									
								
							@@ -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
									
								
								kasa/cli.py
									
									
									
									
									
								
							
							
						
						
									
										37
									
								
								kasa/cli.py
									
									
									
									
									
								
							@@ -42,7 +42,7 @@ pass_dev = click.make_pass_decorator(SmartDevice)
 | 
			
		||||
@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:
 | 
			
		||||
@@ -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(f"Host/IP: {dev.host}")
 | 
			
		||||
    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(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()
 | 
			
		||||
@@ -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}")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -62,6 +62,7 @@ class _DiscoverProtocol(asyncio.DatagramProtocol):
 | 
			
		||||
            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
 | 
			
		||||
@@ -82,10 +83,11 @@ class _DiscoverProtocol(asyncio.DatagramProtocol):
 | 
			
		||||
            _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:
 | 
			
		||||
 
 | 
			
		||||
@@ -14,7 +14,7 @@ import json
 | 
			
		||||
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__)
 | 
			
		||||
 | 
			
		||||
@@ -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
 | 
			
		||||
@@ -44,7 +41,7 @@ class TPLinkSmartHomeProtocol:
 | 
			
		||||
        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))
 | 
			
		||||
@@ -75,11 +72,10 @@ class TPLinkSmartHomeProtocol:
 | 
			
		||||
 | 
			
		||||
    @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
 | 
			
		||||
 | 
			
		||||
@@ -95,8 +91,7 @@ class TPLinkSmartHomeProtocol:
 | 
			
		||||
 | 
			
		||||
    @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
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
"""Module for bulbs."""
 | 
			
		||||
"""Module for bulbs (LB*, KL*, KB*)."""
 | 
			
		||||
import re
 | 
			
		||||
from typing import Any, Dict, Tuple, cast
 | 
			
		||||
 | 
			
		||||
@@ -82,34 +82,21 @@ class SmartBulb(SmartDevice):
 | 
			
		||||
    @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"])
 | 
			
		||||
 | 
			
		||||
@@ -118,16 +105,18 @@ class SmartBulb(SmartDevice):
 | 
			
		||||
    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
 | 
			
		||||
@@ -166,7 +155,6 @@ class SmartBulb(SmartDevice):
 | 
			
		||||
        """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.")
 | 
			
		||||
@@ -220,11 +208,7 @@ class SmartBulb(SmartDevice):
 | 
			
		||||
    @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.")
 | 
			
		||||
 | 
			
		||||
@@ -233,10 +217,7 @@ class SmartBulb(SmartDevice):
 | 
			
		||||
 | 
			
		||||
    @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.")
 | 
			
		||||
 | 
			
		||||
@@ -253,11 +234,7 @@ class SmartBulb(SmartDevice):
 | 
			
		||||
    @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.")
 | 
			
		||||
 | 
			
		||||
@@ -266,10 +243,7 @@ class SmartBulb(SmartDevice):
 | 
			
		||||
 | 
			
		||||
    @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.")
 | 
			
		||||
 | 
			
		||||
@@ -281,11 +255,7 @@ class SmartBulb(SmartDevice):
 | 
			
		||||
    @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,
 | 
			
		||||
 
 | 
			
		||||
@@ -127,7 +127,6 @@ class SmartDevice:
 | 
			
		||||
        """Create a new SmartDevice instance.
 | 
			
		||||
 | 
			
		||||
        :param str host: host name or ip address on which the device listens
 | 
			
		||||
        :param child_id: optional child ID for context in a parent device
 | 
			
		||||
        """
 | 
			
		||||
        self.host = host
 | 
			
		||||
 | 
			
		||||
@@ -141,6 +140,8 @@ class SmartDevice:
 | 
			
		||||
        self._last_update: Any = None
 | 
			
		||||
        self._sys_info: Any = None  # TODO: this is here to avoid changing tests
 | 
			
		||||
 | 
			
		||||
        self.children: List["SmartDevice"] = []
 | 
			
		||||
 | 
			
		||||
    def _create_request(
 | 
			
		||||
        self, target: str, cmd: str, arg: Optional[Dict] = None, child_ids=None
 | 
			
		||||
    ):
 | 
			
		||||
@@ -153,15 +154,13 @@ class SmartDevice:
 | 
			
		||||
    async def _query_helper(
 | 
			
		||||
        self, target: str, cmd: str, arg: Optional[Dict] = None, child_ids=None
 | 
			
		||||
    ) -> Any:
 | 
			
		||||
        """Handle result unwrapping and error handling.
 | 
			
		||||
        """Query device, return results or raise an exception.
 | 
			
		||||
 | 
			
		||||
        :param target: Target system {system, time, emeter, ..}
 | 
			
		||||
        :param cmd: Command to execute
 | 
			
		||||
        :param arg: JSON object passed as parameter to the command
 | 
			
		||||
        :param arg: payload dict to be send to the device
 | 
			
		||||
        :param child_ids: ids of child devices
 | 
			
		||||
        :return: Unwrapped result for the call.
 | 
			
		||||
        :rtype: dict
 | 
			
		||||
        :raises SmartDeviceException: if command was not executed correctly
 | 
			
		||||
        """
 | 
			
		||||
        request = self._create_request(target, cmd, arg, child_ids)
 | 
			
		||||
 | 
			
		||||
@@ -191,22 +190,13 @@ class SmartDevice:
 | 
			
		||||
    @property  # type: ignore
 | 
			
		||||
    @requires_update
 | 
			
		||||
    def has_emeter(self) -> bool:
 | 
			
		||||
        """Return whether device has an energy meter.
 | 
			
		||||
 | 
			
		||||
        :return: True if energy meter is available
 | 
			
		||||
                 False otherwise
 | 
			
		||||
        """
 | 
			
		||||
        """Return True if device has an energy meter."""
 | 
			
		||||
        sys_info = self.sys_info
 | 
			
		||||
        features = sys_info["feature"].split(":")
 | 
			
		||||
        return "ENE" in features
 | 
			
		||||
 | 
			
		||||
    async def get_sys_info(self) -> Dict[str, Any]:
 | 
			
		||||
        """Retrieve system information.
 | 
			
		||||
 | 
			
		||||
        :return: sysinfo
 | 
			
		||||
        :rtype dict
 | 
			
		||||
        :raises SmartDeviceException: on error
 | 
			
		||||
        """
 | 
			
		||||
        """Retrieve system information."""
 | 
			
		||||
        return await self._query_helper("system", "get_sysinfo")
 | 
			
		||||
 | 
			
		||||
    async def update(self):
 | 
			
		||||
@@ -227,78 +217,29 @@ class SmartDevice:
 | 
			
		||||
    @property  # type: ignore
 | 
			
		||||
    @requires_update
 | 
			
		||||
    def sys_info(self) -> Dict[str, Any]:
 | 
			
		||||
        """Retrieve system information.
 | 
			
		||||
 | 
			
		||||
        :return: sysinfo
 | 
			
		||||
        :rtype dict
 | 
			
		||||
        :raises SmartDeviceException: on error
 | 
			
		||||
        """
 | 
			
		||||
        """Return system information."""
 | 
			
		||||
        return self._sys_info  # type: ignore
 | 
			
		||||
 | 
			
		||||
    @property  # type: ignore
 | 
			
		||||
    @requires_update
 | 
			
		||||
    def model(self) -> str:
 | 
			
		||||
        """Return device model.
 | 
			
		||||
 | 
			
		||||
        :return: device model
 | 
			
		||||
        :rtype: str
 | 
			
		||||
        :raises SmartDeviceException: on error
 | 
			
		||||
        """
 | 
			
		||||
        """Return device model."""
 | 
			
		||||
        sys_info = self.sys_info
 | 
			
		||||
        return str(sys_info["model"])
 | 
			
		||||
 | 
			
		||||
    @property  # type: ignore
 | 
			
		||||
    @requires_update
 | 
			
		||||
    def alias(self) -> str:
 | 
			
		||||
        """Return device name (alias).
 | 
			
		||||
 | 
			
		||||
        :return: Device name aka alias.
 | 
			
		||||
        :rtype: str
 | 
			
		||||
        """
 | 
			
		||||
        """Return device name (alias)."""
 | 
			
		||||
        sys_info = self.sys_info
 | 
			
		||||
        return str(sys_info["alias"])
 | 
			
		||||
 | 
			
		||||
    async def set_alias(self, alias: str) -> None:
 | 
			
		||||
        """Set the device name (alias).
 | 
			
		||||
 | 
			
		||||
        :param alias: New alias (name)
 | 
			
		||||
        :raises SmartDeviceException: on error
 | 
			
		||||
        """
 | 
			
		||||
        """Set the device name (alias)."""
 | 
			
		||||
        return await self._query_helper("system", "set_dev_alias", {"alias": alias})
 | 
			
		||||
 | 
			
		||||
    async def get_icon(self) -> Dict:
 | 
			
		||||
        """Return device icon.
 | 
			
		||||
 | 
			
		||||
        Note: not working on HS110, but is always empty.
 | 
			
		||||
 | 
			
		||||
        :return: icon and its hash
 | 
			
		||||
        :rtype: dict
 | 
			
		||||
        :raises SmartDeviceException: on error
 | 
			
		||||
        """
 | 
			
		||||
        return await self._query_helper("system", "get_dev_icon")
 | 
			
		||||
 | 
			
		||||
    def set_icon(self, icon: str) -> None:
 | 
			
		||||
        """Set device icon.
 | 
			
		||||
 | 
			
		||||
        Content for hash and icon are unknown.
 | 
			
		||||
 | 
			
		||||
        :param str icon: Icon path(?)
 | 
			
		||||
        :raises NotImplementedError: when not implemented
 | 
			
		||||
        :raises SmartPlugError: on error
 | 
			
		||||
        """
 | 
			
		||||
        raise NotImplementedError()
 | 
			
		||||
        # here just for the sake of completeness
 | 
			
		||||
        # await self._query_helper("system",
 | 
			
		||||
        #                    "set_dev_icon", {"icon": "", "hash": ""})
 | 
			
		||||
        # self.initialize()
 | 
			
		||||
 | 
			
		||||
    async def get_time(self) -> Optional[datetime]:
 | 
			
		||||
        """Return current time from the device.
 | 
			
		||||
 | 
			
		||||
        :return: datetime for device's time
 | 
			
		||||
        :rtype: datetime or None when not available
 | 
			
		||||
        :raises SmartDeviceException: on error
 | 
			
		||||
        """
 | 
			
		||||
        """Return current time from the device, if available."""
 | 
			
		||||
        try:
 | 
			
		||||
            res = await self._query_helper("time", "get_time")
 | 
			
		||||
            return datetime(
 | 
			
		||||
@@ -312,46 +253,8 @@ class SmartDevice:
 | 
			
		||||
        except SmartDeviceException:
 | 
			
		||||
            return None
 | 
			
		||||
 | 
			
		||||
    async def set_time(self, ts: datetime) -> None:
 | 
			
		||||
        """Set the device time.
 | 
			
		||||
 | 
			
		||||
        Note: this calls set_timezone() for setting.
 | 
			
		||||
 | 
			
		||||
        :param datetime ts: New date and time
 | 
			
		||||
        :return: result
 | 
			
		||||
        :type: dict
 | 
			
		||||
        :raises NotImplemented: when not implemented.
 | 
			
		||||
        :raises SmartDeviceException: on error
 | 
			
		||||
        """
 | 
			
		||||
        raise NotImplementedError("Fails with err_code == 0 with HS110.")
 | 
			
		||||
        """
 | 
			
		||||
        here just for the sake of completeness.
 | 
			
		||||
        if someone figures out why it doesn't work,
 | 
			
		||||
        please create a PR :-)
 | 
			
		||||
        ts_obj = {
 | 
			
		||||
            "index": self.timezone["index"],
 | 
			
		||||
            "hour": ts.hour,
 | 
			
		||||
            "min": ts.minute,
 | 
			
		||||
            "sec": ts.second,
 | 
			
		||||
            "year": ts.year,
 | 
			
		||||
            "month": ts.month,
 | 
			
		||||
            "mday": ts.day,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        response = await self._query_helper("time", "set_timezone", ts_obj)
 | 
			
		||||
        self.initialize()
 | 
			
		||||
 | 
			
		||||
        return response
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
    async def get_timezone(self) -> Dict:
 | 
			
		||||
        """Return timezone information.
 | 
			
		||||
 | 
			
		||||
        :return: Timezone information
 | 
			
		||||
        :rtype: dict
 | 
			
		||||
        :raises SmartDeviceException: on error
 | 
			
		||||
        """
 | 
			
		||||
        """Return timezone information."""
 | 
			
		||||
        return await self._query_helper("time", "get_timezone")
 | 
			
		||||
 | 
			
		||||
    @property  # type: ignore
 | 
			
		||||
@@ -359,8 +262,7 @@ class SmartDevice:
 | 
			
		||||
    def hw_info(self) -> Dict:
 | 
			
		||||
        """Return hardware information.
 | 
			
		||||
 | 
			
		||||
        :return: Information about hardware
 | 
			
		||||
        :rtype: dict
 | 
			
		||||
        This returns just a selection of sysinfo keys that are related to hardware.
 | 
			
		||||
        """
 | 
			
		||||
        keys = [
 | 
			
		||||
            "sw_ver",
 | 
			
		||||
@@ -380,11 +282,7 @@ class SmartDevice:
 | 
			
		||||
    @property  # type: ignore
 | 
			
		||||
    @requires_update
 | 
			
		||||
    def location(self) -> Dict:
 | 
			
		||||
        """Return geographical location.
 | 
			
		||||
 | 
			
		||||
        :return: latitude and longitude
 | 
			
		||||
        :rtype: dict
 | 
			
		||||
        """
 | 
			
		||||
        """Return geographical location."""
 | 
			
		||||
        sys_info = self.sys_info
 | 
			
		||||
        loc = {"latitude": None, "longitude": None}
 | 
			
		||||
 | 
			
		||||
@@ -402,11 +300,7 @@ class SmartDevice:
 | 
			
		||||
    @property  # type: ignore
 | 
			
		||||
    @requires_update
 | 
			
		||||
    def rssi(self) -> Optional[int]:
 | 
			
		||||
        """Return WiFi signal strenth (rssi).
 | 
			
		||||
 | 
			
		||||
        :return: rssi
 | 
			
		||||
        :rtype: int
 | 
			
		||||
        """
 | 
			
		||||
        """Return WiFi signal strenth (rssi)."""
 | 
			
		||||
        sys_info = self.sys_info
 | 
			
		||||
        if "rssi" in sys_info:
 | 
			
		||||
            return int(sys_info["rssi"])
 | 
			
		||||
@@ -418,7 +312,6 @@ class SmartDevice:
 | 
			
		||||
        """Return mac address.
 | 
			
		||||
 | 
			
		||||
        :return: mac address in hexadecimal with colons, e.g. 01:23:45:67:89:ab
 | 
			
		||||
        :rtype: str
 | 
			
		||||
        """
 | 
			
		||||
        sys_info = self.sys_info
 | 
			
		||||
 | 
			
		||||
@@ -437,26 +330,20 @@ class SmartDevice:
 | 
			
		||||
        """Set the mac address.
 | 
			
		||||
 | 
			
		||||
        :param str mac: mac in hexadecimal with colons, e.g. 01:23:45:67:89:ab
 | 
			
		||||
        :raises SmartDeviceException: on error
 | 
			
		||||
        """
 | 
			
		||||
        return await self._query_helper("system", "set_mac_addr", {"mac": mac})
 | 
			
		||||
 | 
			
		||||
    @property  # type: ignore
 | 
			
		||||
    @requires_update
 | 
			
		||||
    def emeter_realtime(self) -> EmeterStatus:
 | 
			
		||||
        """Return current emeter status."""
 | 
			
		||||
        """Return current energy readings."""
 | 
			
		||||
        if not self.has_emeter:
 | 
			
		||||
            raise SmartDeviceException("Device has no emeter")
 | 
			
		||||
 | 
			
		||||
        return EmeterStatus(self._last_update[self.emeter_type]["get_realtime"])
 | 
			
		||||
 | 
			
		||||
    async def get_emeter_realtime(self) -> EmeterStatus:
 | 
			
		||||
        """Retrieve current energy readings.
 | 
			
		||||
 | 
			
		||||
        :returns: current readings or False
 | 
			
		||||
        :rtype: dict, None
 | 
			
		||||
        :raises SmartDeviceException: on error
 | 
			
		||||
        """
 | 
			
		||||
        """Retrieve current energy readings."""
 | 
			
		||||
        if not self.has_emeter:
 | 
			
		||||
            raise SmartDeviceException("Device has no emeter")
 | 
			
		||||
 | 
			
		||||
@@ -549,8 +436,6 @@ class SmartDevice:
 | 
			
		||||
                      month)
 | 
			
		||||
        :param kwh: return usage in kWh (default: True)
 | 
			
		||||
        :return: mapping of day of month to value
 | 
			
		||||
        :rtype: dict
 | 
			
		||||
        :raises SmartDeviceException: on error
 | 
			
		||||
        """
 | 
			
		||||
        if not self.has_emeter:
 | 
			
		||||
            raise SmartDeviceException("Device has no emeter")
 | 
			
		||||
@@ -573,8 +458,6 @@ class SmartDevice:
 | 
			
		||||
        :param year: year for which to retrieve statistics (default: this year)
 | 
			
		||||
        :param kwh: return usage in kWh (default: True)
 | 
			
		||||
        :return: dict: mapping of month to value
 | 
			
		||||
        :rtype: dict
 | 
			
		||||
        :raises SmartDeviceException: on error
 | 
			
		||||
        """
 | 
			
		||||
        if not self.has_emeter:
 | 
			
		||||
            raise SmartDeviceException("Device has no emeter")
 | 
			
		||||
@@ -589,12 +472,8 @@ class SmartDevice:
 | 
			
		||||
        return self._emeter_convert_emeter_data(response["month_list"], kwh)
 | 
			
		||||
 | 
			
		||||
    @requires_update
 | 
			
		||||
    async def erase_emeter_stats(self):
 | 
			
		||||
        """Erase energy meter statistics.
 | 
			
		||||
 | 
			
		||||
        :return: True if statistics were deleted
 | 
			
		||||
        :raises SmartDeviceException: on error
 | 
			
		||||
        """
 | 
			
		||||
    async def erase_emeter_stats(self) -> Dict:
 | 
			
		||||
        """Erase energy meter statistics."""
 | 
			
		||||
        if not self.has_emeter:
 | 
			
		||||
            raise SmartDeviceException("Device has no emeter")
 | 
			
		||||
 | 
			
		||||
@@ -602,11 +481,7 @@ class SmartDevice:
 | 
			
		||||
 | 
			
		||||
    @requires_update
 | 
			
		||||
    async def current_consumption(self) -> float:
 | 
			
		||||
        """Get the current power consumption in Watt.
 | 
			
		||||
 | 
			
		||||
        :return: the current power consumption in Watts.
 | 
			
		||||
        :raises SmartDeviceException: on error
 | 
			
		||||
        """
 | 
			
		||||
        """Get the current power consumption in Watt."""
 | 
			
		||||
        if not self.has_emeter:
 | 
			
		||||
            raise SmartDeviceException("Device has no emeter")
 | 
			
		||||
 | 
			
		||||
@@ -618,9 +493,6 @@ class SmartDevice:
 | 
			
		||||
 | 
			
		||||
        Note that giving a delay of zero causes this to block,
 | 
			
		||||
        as the device reboots immediately without responding to the call.
 | 
			
		||||
 | 
			
		||||
        :param delay: Delay the reboot for `delay` seconds.
 | 
			
		||||
        :return: None
 | 
			
		||||
        """
 | 
			
		||||
        await self._query_helper("system", "reboot", {"delay": delay})
 | 
			
		||||
 | 
			
		||||
@@ -631,11 +503,7 @@ class SmartDevice:
 | 
			
		||||
    @property  # type: ignore
 | 
			
		||||
    @requires_update
 | 
			
		||||
    def is_off(self) -> bool:
 | 
			
		||||
        """Return True if device is off.
 | 
			
		||||
 | 
			
		||||
        :return: True if device is off, False otherwise.
 | 
			
		||||
        :rtype: bool
 | 
			
		||||
        """
 | 
			
		||||
        """Return True if device is off."""
 | 
			
		||||
        return not self.is_on
 | 
			
		||||
 | 
			
		||||
    async def turn_on(self) -> None:
 | 
			
		||||
@@ -645,21 +513,13 @@ class SmartDevice:
 | 
			
		||||
    @property  # type: ignore
 | 
			
		||||
    @requires_update
 | 
			
		||||
    def is_on(self) -> bool:
 | 
			
		||||
        """Return if the device is on.
 | 
			
		||||
 | 
			
		||||
        :return: True if the device is on, False otherwise.
 | 
			
		||||
        :rtype: bool
 | 
			
		||||
        :return:
 | 
			
		||||
        """
 | 
			
		||||
        """Return True if the device is on."""
 | 
			
		||||
        raise NotImplementedError("Device subclass needs to implement this.")
 | 
			
		||||
 | 
			
		||||
    @property  # type: ignore
 | 
			
		||||
    @requires_update
 | 
			
		||||
    def on_since(self) -> Optional[datetime]:
 | 
			
		||||
        """Return pretty-printed on-time, if available.
 | 
			
		||||
 | 
			
		||||
        Returns None if the device is turned off or does not report it.
 | 
			
		||||
        """
 | 
			
		||||
        """Return pretty-printed on-time, or None if not available."""
 | 
			
		||||
        if "on_time" not in self.sys_info:
 | 
			
		||||
            return None
 | 
			
		||||
 | 
			
		||||
@@ -673,11 +533,7 @@ class SmartDevice:
 | 
			
		||||
    @property  # type: ignore
 | 
			
		||||
    @requires_update
 | 
			
		||||
    def state_information(self) -> Dict[str, Any]:
 | 
			
		||||
        """Return device-type specific, end-user friendly state information.
 | 
			
		||||
 | 
			
		||||
        :return: dict with state information.
 | 
			
		||||
        :rtype: dict
 | 
			
		||||
        """
 | 
			
		||||
        """Return device-type specific, end-user friendly state information."""
 | 
			
		||||
        raise NotImplementedError("Device subclass needs to implement this.")
 | 
			
		||||
 | 
			
		||||
    @property  # type: ignore
 | 
			
		||||
@@ -726,6 +582,22 @@ class SmartDevice:
 | 
			
		||||
            )
 | 
			
		||||
            return await _join("smartlife.iot.common.softaponboarding", payload)
 | 
			
		||||
 | 
			
		||||
    def get_plug_by_name(self, name: str) -> "SmartDevice":
 | 
			
		||||
        """Return child device for the given name."""
 | 
			
		||||
        for p in self.children:
 | 
			
		||||
            if p.alias == name:
 | 
			
		||||
                return p
 | 
			
		||||
 | 
			
		||||
        raise SmartDeviceException(f"Device has no child with {name}")
 | 
			
		||||
 | 
			
		||||
    def get_plug_by_index(self, index: int) -> "SmartDevice":
 | 
			
		||||
        """Return child device for the given index."""
 | 
			
		||||
        if index + 1 > len(self.children) or index < 0:
 | 
			
		||||
            raise SmartDeviceException(
 | 
			
		||||
                f"Invalid index {index}, device has {len(self.children)} plugs"
 | 
			
		||||
            )
 | 
			
		||||
        return self.children[index]
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def device_type(self) -> DeviceType:
 | 
			
		||||
        """Return the device type."""
 | 
			
		||||
 
 | 
			
		||||
@@ -33,9 +33,6 @@ class SmartDimmer(SmartPlug):
 | 
			
		||||
        """Return current brightness on dimmers.
 | 
			
		||||
 | 
			
		||||
        Will return a range between 0 - 100.
 | 
			
		||||
 | 
			
		||||
        :returns: integer
 | 
			
		||||
        :rtype: int
 | 
			
		||||
        """
 | 
			
		||||
        if not self.is_dimmable:
 | 
			
		||||
            raise SmartDeviceException("Device is not dimmable.")
 | 
			
		||||
@@ -45,15 +42,7 @@ class SmartDimmer(SmartPlug):
 | 
			
		||||
 | 
			
		||||
    @requires_update
 | 
			
		||||
    async def set_brightness(self, value: int):
 | 
			
		||||
        """Set the new dimmer brightness level.
 | 
			
		||||
 | 
			
		||||
        Note:
 | 
			
		||||
        When setting brightness, if the light is not
 | 
			
		||||
        already on, it will be turned on automatically.
 | 
			
		||||
 | 
			
		||||
        :param value: integer between 0 and 100
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        """Set the new dimmer brightness level in percentage."""
 | 
			
		||||
        if not self.is_dimmable:
 | 
			
		||||
            raise SmartDeviceException("Device is not dimmable.")
 | 
			
		||||
 | 
			
		||||
@@ -68,23 +57,15 @@ class SmartDimmer(SmartPlug):
 | 
			
		||||
 | 
			
		||||
    @property  # type: ignore
 | 
			
		||||
    @requires_update
 | 
			
		||||
    def is_dimmable(self):
 | 
			
		||||
        """Whether the switch supports brightness changes.
 | 
			
		||||
 | 
			
		||||
        :return: True if switch supports brightness changes, False otherwise
 | 
			
		||||
        :rtype: bool
 | 
			
		||||
        """
 | 
			
		||||
    def is_dimmable(self) -> bool:
 | 
			
		||||
        """Whether the switch supports brightness changes."""
 | 
			
		||||
        sys_info = self.sys_info
 | 
			
		||||
        return "brightness" in sys_info
 | 
			
		||||
 | 
			
		||||
    @property  # type: ignore
 | 
			
		||||
    @requires_update
 | 
			
		||||
    def state_information(self) -> Dict[str, Any]:
 | 
			
		||||
        """Return switch-specific state information.
 | 
			
		||||
 | 
			
		||||
        :return: Switch information dict, keys in user-presentable form.
 | 
			
		||||
        :rtype: dict
 | 
			
		||||
        """
 | 
			
		||||
        """Return switch-specific state information."""
 | 
			
		||||
        info = super().state_information
 | 
			
		||||
        info["Brightness"] = self.brightness
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
"""Module for plugs."""
 | 
			
		||||
"""Module for smart plugs (HS100, HS110, ..)."""
 | 
			
		||||
import logging
 | 
			
		||||
from typing import Any, Dict
 | 
			
		||||
 | 
			
		||||
@@ -38,44 +38,27 @@ class SmartPlug(SmartDevice):
 | 
			
		||||
    @property  # type: ignore
 | 
			
		||||
    @requires_update
 | 
			
		||||
    def is_on(self) -> bool:
 | 
			
		||||
        """Return whether device is on.
 | 
			
		||||
 | 
			
		||||
        :return: True if device is on, False otherwise
 | 
			
		||||
        """
 | 
			
		||||
        """Return whether device is on."""
 | 
			
		||||
        sys_info = self.sys_info
 | 
			
		||||
        return bool(sys_info["relay_state"])
 | 
			
		||||
 | 
			
		||||
    async def turn_on(self):
 | 
			
		||||
        """Turn the switch on.
 | 
			
		||||
 | 
			
		||||
        :raises SmartDeviceException: on error
 | 
			
		||||
        """
 | 
			
		||||
        """Turn the switch on."""
 | 
			
		||||
        return await self._query_helper("system", "set_relay_state", {"state": 1})
 | 
			
		||||
 | 
			
		||||
    async def turn_off(self):
 | 
			
		||||
        """Turn the switch off.
 | 
			
		||||
 | 
			
		||||
        :raises SmartDeviceException: on error
 | 
			
		||||
        """
 | 
			
		||||
        """Turn the switch off."""
 | 
			
		||||
        return await self._query_helper("system", "set_relay_state", {"state": 0})
 | 
			
		||||
 | 
			
		||||
    @property  # type: ignore
 | 
			
		||||
    @requires_update
 | 
			
		||||
    def led(self) -> bool:
 | 
			
		||||
        """Return the state of the led.
 | 
			
		||||
 | 
			
		||||
        :return: True if led is on, False otherwise
 | 
			
		||||
        :rtype: bool
 | 
			
		||||
        """
 | 
			
		||||
        """Return the state of the led."""
 | 
			
		||||
        sys_info = self.sys_info
 | 
			
		||||
        return bool(1 - sys_info["led_off"])
 | 
			
		||||
 | 
			
		||||
    async def set_led(self, state: bool):
 | 
			
		||||
        """Set the state of the led (night mode).
 | 
			
		||||
 | 
			
		||||
        :param bool state: True to set led on, False to set led off
 | 
			
		||||
        :raises SmartDeviceException: on error
 | 
			
		||||
        """
 | 
			
		||||
        """Set the state of the led (night mode)."""
 | 
			
		||||
        return await self._query_helper(
 | 
			
		||||
            "system", "set_led_off", {"off": int(not state)}
 | 
			
		||||
        )
 | 
			
		||||
@@ -83,10 +66,6 @@ class SmartPlug(SmartDevice):
 | 
			
		||||
    @property  # type: ignore
 | 
			
		||||
    @requires_update
 | 
			
		||||
    def state_information(self) -> Dict[str, Any]:
 | 
			
		||||
        """Return switch-specific state information.
 | 
			
		||||
 | 
			
		||||
        :return: Switch information dict, keys in user-presentable form.
 | 
			
		||||
        :rtype: dict
 | 
			
		||||
        """
 | 
			
		||||
        """Return switch-specific state information."""
 | 
			
		||||
        info = {"LED state": self.led, "On since": self.on_since}
 | 
			
		||||
        return info
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,8 @@
 | 
			
		||||
"""Module for multi-socket devices (HS300, HS107).
 | 
			
		||||
 | 
			
		||||
.. todo:: describe how this interfaces with single plugs.
 | 
			
		||||
"""
 | 
			
		||||
"""Module for multi-socket devices (HS300, HS107, KP303, ..)."""
 | 
			
		||||
import logging
 | 
			
		||||
from collections import defaultdict
 | 
			
		||||
from datetime import datetime, timedelta
 | 
			
		||||
from typing import Any, DefaultDict, Dict, List, Optional
 | 
			
		||||
from typing import Any, DefaultDict, Dict, Optional
 | 
			
		||||
 | 
			
		||||
from kasa.smartdevice import (
 | 
			
		||||
    DeviceType,
 | 
			
		||||
@@ -49,13 +46,12 @@ class SmartStrip(SmartDevice):
 | 
			
		||||
        super().__init__(host=host)
 | 
			
		||||
        self.emeter_type = "emeter"
 | 
			
		||||
        self._device_type = DeviceType.Strip
 | 
			
		||||
        self.plugs: List[SmartStripPlug] = []
 | 
			
		||||
 | 
			
		||||
    @property  # type: ignore
 | 
			
		||||
    @requires_update
 | 
			
		||||
    def is_on(self) -> bool:
 | 
			
		||||
        """Return if any of the outlets are on."""
 | 
			
		||||
        for plug in self.plugs:
 | 
			
		||||
        for plug in self.children:
 | 
			
		||||
            is_on = plug.is_on
 | 
			
		||||
            if is_on:
 | 
			
		||||
                return True
 | 
			
		||||
@@ -69,46 +65,24 @@ class SmartStrip(SmartDevice):
 | 
			
		||||
        await super().update()
 | 
			
		||||
 | 
			
		||||
        # Initialize the child devices during the first update.
 | 
			
		||||
        if not self.plugs:
 | 
			
		||||
        if not self.children:
 | 
			
		||||
            children = self.sys_info["children"]
 | 
			
		||||
            _LOGGER.debug("Initializing %s child sockets", len(children))
 | 
			
		||||
            for child in children:
 | 
			
		||||
                self.plugs.append(
 | 
			
		||||
                self.children.append(
 | 
			
		||||
                    SmartStripPlug(self.host, parent=self, child_id=child["id"])
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
    async def turn_on(self):
 | 
			
		||||
        """Turn the strip on.
 | 
			
		||||
 | 
			
		||||
        :raises SmartDeviceException: on error
 | 
			
		||||
        """
 | 
			
		||||
        """Turn the strip on."""
 | 
			
		||||
        await self._query_helper("system", "set_relay_state", {"state": 1})
 | 
			
		||||
        await self.update()
 | 
			
		||||
 | 
			
		||||
    async def turn_off(self):
 | 
			
		||||
        """Turn the strip off.
 | 
			
		||||
 | 
			
		||||
        :raises SmartDeviceException: on error
 | 
			
		||||
        """
 | 
			
		||||
        """Turn the strip off."""
 | 
			
		||||
        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) -> Optional[datetime]:
 | 
			
		||||
@@ -116,25 +90,17 @@ class SmartStrip(SmartDevice):
 | 
			
		||||
        if self.is_off:
 | 
			
		||||
            return None
 | 
			
		||||
 | 
			
		||||
        return max(plug.on_since for plug in self.plugs if plug.on_since is not None)
 | 
			
		||||
        return max(plug.on_since for plug in self.children if plug.on_since is not None)
 | 
			
		||||
 | 
			
		||||
    @property  # type: ignore
 | 
			
		||||
    @requires_update
 | 
			
		||||
    def led(self) -> bool:
 | 
			
		||||
        """Return the state of the led.
 | 
			
		||||
 | 
			
		||||
        :return: True if led is on, False otherwise
 | 
			
		||||
        :rtype: bool
 | 
			
		||||
        """
 | 
			
		||||
        """Return the state of the led."""
 | 
			
		||||
        sys_info = self.sys_info
 | 
			
		||||
        return bool(1 - sys_info["led_off"])
 | 
			
		||||
 | 
			
		||||
    async def set_led(self, state: bool):
 | 
			
		||||
        """Set the state of the led (night mode).
 | 
			
		||||
 | 
			
		||||
        :param bool state: True to set led on, False to set led off
 | 
			
		||||
        :raises SmartDeviceException: on error
 | 
			
		||||
        """
 | 
			
		||||
        """Set the state of the led (night mode)."""
 | 
			
		||||
        await self._query_helper("system", "set_led_off", {"off": int(not state)})
 | 
			
		||||
        await self.update()
 | 
			
		||||
 | 
			
		||||
@@ -144,38 +110,23 @@ class SmartStrip(SmartDevice):
 | 
			
		||||
        """Return strip-specific state information.
 | 
			
		||||
 | 
			
		||||
        :return: Strip information dict, keys in user-presentable form.
 | 
			
		||||
        :rtype: dict
 | 
			
		||||
        """
 | 
			
		||||
        return {
 | 
			
		||||
            "LED state": self.led,
 | 
			
		||||
            "Childs count": len(self.plugs),
 | 
			
		||||
            "Childs count": len(self.children),
 | 
			
		||||
            "On since": self.on_since,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    async def current_consumption(self) -> float:
 | 
			
		||||
        """Get the current power consumption in watts.
 | 
			
		||||
 | 
			
		||||
        :return: the current power consumption in watts.
 | 
			
		||||
        :rtype: float
 | 
			
		||||
        :raises SmartDeviceException: on error
 | 
			
		||||
        """
 | 
			
		||||
        consumption = sum([await plug.current_consumption() for plug in self.plugs])
 | 
			
		||||
        """Get the current power consumption in watts."""
 | 
			
		||||
        consumption = sum([await plug.current_consumption() for plug in self.children])
 | 
			
		||||
 | 
			
		||||
        return consumption
 | 
			
		||||
 | 
			
		||||
    async def get_icon(self) -> Dict:
 | 
			
		||||
        """Icon for the device.
 | 
			
		||||
 | 
			
		||||
        Overriden to keep the API, as the SmartStrip and children do not
 | 
			
		||||
        have icons, we just return dummy strings.
 | 
			
		||||
        """
 | 
			
		||||
        return {"icon": "SMARTSTRIP-DUMMY", "hash": "SMARTSTRIP-DUMMY"}
 | 
			
		||||
 | 
			
		||||
    async def set_alias(self, alias: str) -> None:
 | 
			
		||||
        """Set the alias for the strip.
 | 
			
		||||
 | 
			
		||||
        :param alias: new alias
 | 
			
		||||
        :raises SmartDeviceException: on error
 | 
			
		||||
        """
 | 
			
		||||
        return await super().set_alias(alias)
 | 
			
		||||
 | 
			
		||||
@@ -190,11 +141,9 @@ class SmartStrip(SmartDevice):
 | 
			
		||||
                      month)
 | 
			
		||||
        :param kwh: return usage in kWh (default: True)
 | 
			
		||||
        :return: mapping of day of month to value
 | 
			
		||||
        :rtype: dict
 | 
			
		||||
        :raises SmartDeviceException: on error
 | 
			
		||||
        """
 | 
			
		||||
        emeter_daily: DefaultDict[int, float] = defaultdict(lambda: 0.0)
 | 
			
		||||
        for plug in self.plugs:
 | 
			
		||||
        for plug in self.children:
 | 
			
		||||
            plug_emeter_daily = await plug.get_emeter_daily(
 | 
			
		||||
                year=year, month=month, kwh=kwh
 | 
			
		||||
            )
 | 
			
		||||
@@ -208,12 +157,9 @@ class SmartStrip(SmartDevice):
 | 
			
		||||
 | 
			
		||||
        :param year: year for which to retrieve statistics (default: this year)
 | 
			
		||||
        :param kwh: return usage in kWh (default: True)
 | 
			
		||||
        :return: dict: mapping of month to value
 | 
			
		||||
        :rtype: dict
 | 
			
		||||
        :raises SmartDeviceException: on error
 | 
			
		||||
        """
 | 
			
		||||
        emeter_monthly: DefaultDict[int, float] = defaultdict(lambda: 0.0)
 | 
			
		||||
        for plug in self.plugs:
 | 
			
		||||
        for plug in self.children:
 | 
			
		||||
            plug_emeter_monthly = await plug.get_emeter_monthly(year=year, kwh=kwh)
 | 
			
		||||
            for month, value in plug_emeter_monthly:
 | 
			
		||||
                emeter_monthly[month] += value
 | 
			
		||||
@@ -222,11 +168,8 @@ class SmartStrip(SmartDevice):
 | 
			
		||||
 | 
			
		||||
    @requires_update
 | 
			
		||||
    async def erase_emeter_stats(self):
 | 
			
		||||
        """Erase energy meter statistics for all plugs.
 | 
			
		||||
 | 
			
		||||
        :raises SmartDeviceException: on error
 | 
			
		||||
        """
 | 
			
		||||
        for plug in self.plugs:
 | 
			
		||||
        """Erase energy meter statistics for all plugs."""
 | 
			
		||||
        for plug in self.children:
 | 
			
		||||
            await plug.erase_emeter_stats()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -267,10 +210,7 @@ class SmartStripPlug(SmartPlug):
 | 
			
		||||
    @property  # type: ignore
 | 
			
		||||
    @requires_update
 | 
			
		||||
    def is_on(self) -> bool:
 | 
			
		||||
        """Return whether device is on.
 | 
			
		||||
 | 
			
		||||
        :return: True if device is on, False otherwise
 | 
			
		||||
        """
 | 
			
		||||
        """Return whether device is on."""
 | 
			
		||||
        info = self._get_child_info()
 | 
			
		||||
        return info["state"]
 | 
			
		||||
 | 
			
		||||
@@ -280,9 +220,6 @@ class SmartStripPlug(SmartPlug):
 | 
			
		||||
        """Return the state of the led.
 | 
			
		||||
 | 
			
		||||
        This is always false for subdevices.
 | 
			
		||||
 | 
			
		||||
        :return: True if led is on, False otherwise
 | 
			
		||||
        :rtype: bool
 | 
			
		||||
        """
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
@@ -304,11 +241,7 @@ class SmartStripPlug(SmartPlug):
 | 
			
		||||
    @property  # type: ignore
 | 
			
		||||
    @requires_update
 | 
			
		||||
    def alias(self) -> str:
 | 
			
		||||
        """Return device name (alias).
 | 
			
		||||
 | 
			
		||||
        :return: Device name aka alias.
 | 
			
		||||
        :rtype: str
 | 
			
		||||
        """
 | 
			
		||||
        """Return device name (alias)."""
 | 
			
		||||
        info = self._get_child_info()
 | 
			
		||||
        return info["alias"]
 | 
			
		||||
 | 
			
		||||
@@ -322,11 +255,7 @@ class SmartStripPlug(SmartPlug):
 | 
			
		||||
    @property  # type: ignore
 | 
			
		||||
    @requires_update
 | 
			
		||||
    def on_since(self) -> Optional[datetime]:
 | 
			
		||||
        """Return pretty-printed on-time.
 | 
			
		||||
 | 
			
		||||
        :return: datetime for on since
 | 
			
		||||
        :rtype: datetime
 | 
			
		||||
        """
 | 
			
		||||
        """Return on-time, if available."""
 | 
			
		||||
        if self.is_off:
 | 
			
		||||
            return None
 | 
			
		||||
 | 
			
		||||
@@ -338,21 +267,14 @@ class SmartStripPlug(SmartPlug):
 | 
			
		||||
    @property  # type: ignore
 | 
			
		||||
    @requires_update
 | 
			
		||||
    def model(self) -> str:
 | 
			
		||||
        """Return device model for a child socket.
 | 
			
		||||
 | 
			
		||||
        :return: device model
 | 
			
		||||
        :rtype: str
 | 
			
		||||
        :raises SmartDeviceException: on error
 | 
			
		||||
        """
 | 
			
		||||
        """Return device model for a child socket."""
 | 
			
		||||
        sys_info = self.parent.sys_info
 | 
			
		||||
        return f"Socket for {sys_info['model']}"
 | 
			
		||||
 | 
			
		||||
    def _get_child_info(self) -> Dict:
 | 
			
		||||
        """Return the subdevice information for this device.
 | 
			
		||||
 | 
			
		||||
        :raises SmartDeviceException: if the information is not found.
 | 
			
		||||
        """
 | 
			
		||||
        """Return the subdevice information for this device."""
 | 
			
		||||
        for plug in self.parent.sys_info["children"]:
 | 
			
		||||
            if plug["id"] == self.child_id:
 | 
			
		||||
                return plug
 | 
			
		||||
 | 
			
		||||
        raise SmartDeviceException(f"Unable to find children {self.child_id}")
 | 
			
		||||
 
 | 
			
		||||
@@ -17,12 +17,14 @@ SUPPORTED_DEVICES = glob.glob(
 | 
			
		||||
 | 
			
		||||
BULBS = {"KL60", "LB100", "LB120", "LB130", "KL120", "KL130"}
 | 
			
		||||
VARIABLE_TEMP = {"LB120", "LB130", "KL120", "KL130"}
 | 
			
		||||
COLOR_BULBS = {"LB130", "KL130"}
 | 
			
		||||
 | 
			
		||||
PLUGS = {"HS100", "HS103", "HS105", "HS110", "HS200", "HS210"}
 | 
			
		||||
STRIPS = {"HS107", "HS300", "KP303", "KP400"}
 | 
			
		||||
DIMMERS = {"HS220"}
 | 
			
		||||
COLOR_BULBS = {"LB130", "KL130"}
 | 
			
		||||
DIMMABLE = {*BULBS, "HS220"}
 | 
			
		||||
EMETER = {"HS110", "HS300", *BULBS, *STRIPS}
 | 
			
		||||
 | 
			
		||||
DIMMABLE = {*BULBS, *DIMMERS}
 | 
			
		||||
WITH_EMETER = {"HS110", "HS300", *BULBS, *STRIPS}
 | 
			
		||||
 | 
			
		||||
ALL_DEVICES = BULBS.union(PLUGS).union(STRIPS).union(DIMMERS)
 | 
			
		||||
 | 
			
		||||
@@ -39,17 +41,28 @@ def filter_model(desc, filter):
 | 
			
		||||
    return filtered
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
has_emeter = pytest.mark.parametrize(
 | 
			
		||||
    "dev", filter_model("has emeter", EMETER), indirect=True
 | 
			
		||||
)
 | 
			
		||||
no_emeter = pytest.mark.parametrize(
 | 
			
		||||
    "dev", filter_model("no emeter", ALL_DEVICES - EMETER), indirect=True
 | 
			
		||||
)
 | 
			
		||||
def parametrize(desc, devices, ids=None):
 | 
			
		||||
    # if ids is None:
 | 
			
		||||
    #    ids = ["on", "off"]
 | 
			
		||||
    return pytest.mark.parametrize(
 | 
			
		||||
        "dev", filter_model(desc, devices), indirect=True, ids=ids
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
bulb = pytest.mark.parametrize("dev", filter_model("bulbs", BULBS), indirect=True)
 | 
			
		||||
plug = pytest.mark.parametrize("dev", filter_model("plugs", PLUGS), indirect=True)
 | 
			
		||||
strip = pytest.mark.parametrize("dev", filter_model("strips", STRIPS), indirect=True)
 | 
			
		||||
dimmer = pytest.mark.parametrize("dev", filter_model("dimmers", DIMMERS), indirect=True)
 | 
			
		||||
 | 
			
		||||
has_emeter = parametrize("has emeter", WITH_EMETER)
 | 
			
		||||
no_emeter = parametrize("no emeter", ALL_DEVICES - WITH_EMETER)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def name_for_filename(x):
 | 
			
		||||
    from os.path import basename
 | 
			
		||||
 | 
			
		||||
    return basename(x)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
bulb = parametrize("bulbs", BULBS, ids=name_for_filename)
 | 
			
		||||
plug = parametrize("plugs", PLUGS, ids=name_for_filename)
 | 
			
		||||
strip = parametrize("strips", STRIPS, ids=name_for_filename)
 | 
			
		||||
dimmer = parametrize("dimmers", DIMMERS, ids=name_for_filename)
 | 
			
		||||
 | 
			
		||||
# This ensures that every single file inside fixtures/ is being placed in some category
 | 
			
		||||
categorized_fixtures = set(dimmer.args[1] + strip.args[1] + plug.args[1] + bulb.args[1])
 | 
			
		||||
@@ -62,29 +75,14 @@ if diff:
 | 
			
		||||
        )
 | 
			
		||||
    raise Exception("Missing category for %s" % diff)
 | 
			
		||||
 | 
			
		||||
dimmable = pytest.mark.parametrize(
 | 
			
		||||
    "dev", filter_model("dimmable", DIMMABLE), indirect=True
 | 
			
		||||
)
 | 
			
		||||
non_dimmable = pytest.mark.parametrize(
 | 
			
		||||
    "dev",
 | 
			
		||||
    filter_model("non-dimmable", ALL_DEVICES - DIMMABLE - STRIPS - PLUGS),
 | 
			
		||||
    indirect=True,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
variable_temp = pytest.mark.parametrize(
 | 
			
		||||
    "dev", filter_model("variable color temp", VARIABLE_TEMP), indirect=True
 | 
			
		||||
)
 | 
			
		||||
non_variable_temp = pytest.mark.parametrize(
 | 
			
		||||
    "dev", filter_model("non-variable color temp", BULBS - VARIABLE_TEMP), indirect=True
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
color_bulb = pytest.mark.parametrize(
 | 
			
		||||
    "dev", filter_model("color bulbs", COLOR_BULBS), indirect=True
 | 
			
		||||
)
 | 
			
		||||
non_color_bulb = pytest.mark.parametrize(
 | 
			
		||||
    "dev", filter_model("non-color bulbs", BULBS - COLOR_BULBS), indirect=True
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
# bulb types
 | 
			
		||||
dimmable = parametrize("dimmable", DIMMABLE)
 | 
			
		||||
non_dimmable = parametrize("non-dimmable", BULBS - DIMMABLE)
 | 
			
		||||
variable_temp = parametrize("variable color temp", VARIABLE_TEMP)
 | 
			
		||||
non_variable_temp = parametrize("non-variable color temp", BULBS - VARIABLE_TEMP)
 | 
			
		||||
color_bulb = parametrize("color bulbs", COLOR_BULBS)
 | 
			
		||||
non_color_bulb = parametrize("non-color bulbs", BULBS - COLOR_BULBS)
 | 
			
		||||
 | 
			
		||||
# Parametrize tests to run with device both on and off
 | 
			
		||||
turn_on = pytest.mark.parametrize("turn_on", [True, False])
 | 
			
		||||
@@ -97,6 +95,10 @@ async def handle_turn_on(dev, turn_on):
 | 
			
		||||
        await dev.turn_off()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# to avoid adding this for each async function separately
 | 
			
		||||
pytestmark = pytest.mark.asyncio
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture(params=SUPPORTED_DEVICES)
 | 
			
		||||
def dev(request):
 | 
			
		||||
    """Device fixture.
 | 
			
		||||
@@ -112,7 +114,7 @@ def dev(request):
 | 
			
		||||
        asyncio.run(d.update())
 | 
			
		||||
        if d.model in file:
 | 
			
		||||
            return d
 | 
			
		||||
        return
 | 
			
		||||
        raise Exception("Unable to find type for %s" % ip)
 | 
			
		||||
 | 
			
		||||
    def device_for_file(model):
 | 
			
		||||
        for d in STRIPS:
 | 
			
		||||
 
 | 
			
		||||
@@ -113,6 +113,27 @@ PLUG_SCHEMA = Schema(
 | 
			
		||||
    extra=REMOVE_EXTRA,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
LIGHT_STATE_SCHEMA = Schema(
 | 
			
		||||
    {
 | 
			
		||||
        "brightness": All(int, Range(min=0, max=100)),
 | 
			
		||||
        "color_temp": int,
 | 
			
		||||
        "hue": All(int, Range(min=0, max=255)),
 | 
			
		||||
        "mode": str,
 | 
			
		||||
        "on_off": check_int_bool,
 | 
			
		||||
        "saturation": All(int, Range(min=0, max=255)),
 | 
			
		||||
        "dft_on_state": Optional(
 | 
			
		||||
            {
 | 
			
		||||
                "brightness": All(int, Range(min=0, max=100)),
 | 
			
		||||
                "color_temp": All(int, Range(min=2000, max=9000)),
 | 
			
		||||
                "hue": All(int, Range(min=0, max=255)),
 | 
			
		||||
                "mode": str,
 | 
			
		||||
                "saturation": All(int, Range(min=0, max=255)),
 | 
			
		||||
            }
 | 
			
		||||
        ),
 | 
			
		||||
        "err_code": int,
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
BULB_SCHEMA = PLUG_SCHEMA.extend(
 | 
			
		||||
    {
 | 
			
		||||
        "ctrl_protocols": Optional(dict),
 | 
			
		||||
@@ -124,24 +145,7 @@ BULB_SCHEMA = PLUG_SCHEMA.extend(
 | 
			
		||||
        "is_dimmable": check_int_bool,
 | 
			
		||||
        "is_factory": bool,
 | 
			
		||||
        "is_variable_color_temp": check_int_bool,
 | 
			
		||||
        "light_state": {
 | 
			
		||||
            "brightness": All(int, Range(min=0, max=100)),
 | 
			
		||||
            "color_temp": int,
 | 
			
		||||
            "hue": All(int, Range(min=0, max=255)),
 | 
			
		||||
            "mode": str,
 | 
			
		||||
            "on_off": check_int_bool,
 | 
			
		||||
            "saturation": All(int, Range(min=0, max=255)),
 | 
			
		||||
            "dft_on_state": Optional(
 | 
			
		||||
                {
 | 
			
		||||
                    "brightness": All(int, Range(min=0, max=100)),
 | 
			
		||||
                    "color_temp": All(int, Range(min=2000, max=9000)),
 | 
			
		||||
                    "hue": All(int, Range(min=0, max=255)),
 | 
			
		||||
                    "mode": str,
 | 
			
		||||
                    "saturation": All(int, Range(min=0, max=255)),
 | 
			
		||||
                }
 | 
			
		||||
            ),
 | 
			
		||||
            "err_code": int,
 | 
			
		||||
        },
 | 
			
		||||
        "light_state": LIGHT_STATE_SCHEMA,
 | 
			
		||||
        "preferred_state": [
 | 
			
		||||
            {
 | 
			
		||||
                "brightness": All(int, Range(min=0, max=100)),
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										187
									
								
								kasa/tests/test_bulb.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										187
									
								
								kasa/tests/test_bulb.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,187 @@
 | 
			
		||||
import pytest
 | 
			
		||||
 | 
			
		||||
from kasa import DeviceType, SmartDeviceException
 | 
			
		||||
 | 
			
		||||
from .conftest import (
 | 
			
		||||
    bulb,
 | 
			
		||||
    color_bulb,
 | 
			
		||||
    dimmable,
 | 
			
		||||
    handle_turn_on,
 | 
			
		||||
    non_color_bulb,
 | 
			
		||||
    non_dimmable,
 | 
			
		||||
    non_variable_temp,
 | 
			
		||||
    turn_on,
 | 
			
		||||
    variable_temp,
 | 
			
		||||
)
 | 
			
		||||
from .newfakes import BULB_SCHEMA, LIGHT_STATE_SCHEMA
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@bulb
 | 
			
		||||
async def test_bulb_sysinfo(dev):
 | 
			
		||||
    assert dev.sys_info is not None
 | 
			
		||||
    BULB_SCHEMA(dev.sys_info)
 | 
			
		||||
 | 
			
		||||
    assert dev.model is not None
 | 
			
		||||
 | 
			
		||||
    assert dev.device_type == DeviceType.Bulb
 | 
			
		||||
    assert dev.is_bulb
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@bulb
 | 
			
		||||
async def test_state_attributes(dev):
 | 
			
		||||
    assert "Brightness" in dev.state_information
 | 
			
		||||
    assert dev.state_information["Brightness"] == dev.brightness
 | 
			
		||||
 | 
			
		||||
    assert "Is dimmable" in dev.state_information
 | 
			
		||||
    assert dev.state_information["Is dimmable"] == dev.is_dimmable
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@bulb
 | 
			
		||||
async def test_light_state_without_update(dev, monkeypatch):
 | 
			
		||||
    with pytest.raises(SmartDeviceException):
 | 
			
		||||
        monkeypatch.setitem(
 | 
			
		||||
            dev._last_update["system"]["get_sysinfo"], "light_state", None
 | 
			
		||||
        )
 | 
			
		||||
        print(dev.light_state)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@bulb
 | 
			
		||||
async def test_get_light_state(dev):
 | 
			
		||||
    LIGHT_STATE_SCHEMA(await dev.get_light_state())
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@color_bulb
 | 
			
		||||
@turn_on
 | 
			
		||||
async def test_hsv(dev, turn_on):
 | 
			
		||||
    await handle_turn_on(dev, turn_on)
 | 
			
		||||
    assert dev.is_color
 | 
			
		||||
 | 
			
		||||
    hue, saturation, brightness = dev.hsv
 | 
			
		||||
    assert 0 <= hue <= 255
 | 
			
		||||
    assert 0 <= saturation <= 100
 | 
			
		||||
    assert 0 <= brightness <= 100
 | 
			
		||||
 | 
			
		||||
    await dev.set_hsv(hue=1, saturation=1, value=1)
 | 
			
		||||
 | 
			
		||||
    hue, saturation, brightness = dev.hsv
 | 
			
		||||
    assert hue == 1
 | 
			
		||||
    assert saturation == 1
 | 
			
		||||
    assert brightness == 1
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@color_bulb
 | 
			
		||||
@turn_on
 | 
			
		||||
async def test_invalid_hsv(dev, 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)
 | 
			
		||||
 | 
			
		||||
    for invalid_saturation in [-1, 101, 0.5]:
 | 
			
		||||
        with pytest.raises(ValueError):
 | 
			
		||||
            await dev.set_hsv(0, invalid_saturation, 0)
 | 
			
		||||
 | 
			
		||||
    for invalid_brightness in [-1, 101, 0.5]:
 | 
			
		||||
        with pytest.raises(ValueError):
 | 
			
		||||
            await dev.set_hsv(0, 0, invalid_brightness)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@color_bulb
 | 
			
		||||
async def test_color_state_information(dev):
 | 
			
		||||
    assert "HSV" in dev.state_information
 | 
			
		||||
    assert dev.state_information["HSV"] == dev.hsv
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@non_color_bulb
 | 
			
		||||
async def test_hsv_on_non_color(dev):
 | 
			
		||||
    assert not dev.is_color
 | 
			
		||||
 | 
			
		||||
    with pytest.raises(SmartDeviceException):
 | 
			
		||||
        await dev.set_hsv(0, 0, 0)
 | 
			
		||||
    with pytest.raises(SmartDeviceException):
 | 
			
		||||
        print(dev.hsv)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@variable_temp
 | 
			
		||||
async def test_variable_temp_state_information(dev):
 | 
			
		||||
    assert "Color temperature" in dev.state_information
 | 
			
		||||
    assert dev.state_information["Color temperature"] == dev.color_temp
 | 
			
		||||
 | 
			
		||||
    assert "Valid temperature range" in dev.state_information
 | 
			
		||||
    assert (
 | 
			
		||||
        dev.state_information["Valid temperature range"] == dev.valid_temperature_range
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@variable_temp
 | 
			
		||||
@turn_on
 | 
			
		||||
async def test_try_set_colortemp(dev, turn_on):
 | 
			
		||||
    await handle_turn_on(dev, turn_on)
 | 
			
		||||
    await dev.set_color_temp(2700)
 | 
			
		||||
    assert dev.color_temp == 2700
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@variable_temp
 | 
			
		||||
async def test_unknown_temp_range(dev, monkeypatch):
 | 
			
		||||
    with pytest.raises(SmartDeviceException):
 | 
			
		||||
        monkeypatch.setitem(dev._sys_info, "model", "unknown bulb")
 | 
			
		||||
        dev.valid_temperature_range()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@variable_temp
 | 
			
		||||
async def test_out_of_range_temperature(dev):
 | 
			
		||||
    with pytest.raises(ValueError):
 | 
			
		||||
        await dev.set_color_temp(1000)
 | 
			
		||||
    with pytest.raises(ValueError):
 | 
			
		||||
        await dev.set_color_temp(10000)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@non_variable_temp
 | 
			
		||||
async def test_non_variable_temp(dev):
 | 
			
		||||
    with pytest.raises(SmartDeviceException):
 | 
			
		||||
        await dev.set_color_temp(2700)
 | 
			
		||||
 | 
			
		||||
    with pytest.raises(SmartDeviceException):
 | 
			
		||||
        dev.valid_temperature_range()
 | 
			
		||||
 | 
			
		||||
    with pytest.raises(SmartDeviceException):
 | 
			
		||||
        print(dev.color_temp)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@dimmable
 | 
			
		||||
@turn_on
 | 
			
		||||
async def test_dimmable_brightness(dev, turn_on):
 | 
			
		||||
    await handle_turn_on(dev, turn_on)
 | 
			
		||||
    assert dev.is_dimmable
 | 
			
		||||
 | 
			
		||||
    await dev.set_brightness(50)
 | 
			
		||||
    assert dev.brightness == 50
 | 
			
		||||
 | 
			
		||||
    await dev.set_brightness(10)
 | 
			
		||||
    assert dev.brightness == 10
 | 
			
		||||
 | 
			
		||||
    with pytest.raises(ValueError):
 | 
			
		||||
        await dev.set_brightness("foo")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@dimmable
 | 
			
		||||
async def test_invalid_brightness(dev):
 | 
			
		||||
    assert dev.is_dimmable
 | 
			
		||||
 | 
			
		||||
    with pytest.raises(ValueError):
 | 
			
		||||
        await dev.set_brightness(110)
 | 
			
		||||
 | 
			
		||||
    with pytest.raises(ValueError):
 | 
			
		||||
        await dev.set_brightness(-100)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@non_dimmable
 | 
			
		||||
async def test_non_dimmable(dev):
 | 
			
		||||
    assert not dev.is_dimmable
 | 
			
		||||
 | 
			
		||||
    with pytest.raises(SmartDeviceException):
 | 
			
		||||
        assert dev.brightness == 0
 | 
			
		||||
    with pytest.raises(SmartDeviceException):
 | 
			
		||||
        await dev.set_brightness(100)
 | 
			
		||||
@@ -28,9 +28,6 @@ async def test_state(dev, turn_on):
 | 
			
		||||
    else:
 | 
			
		||||
        assert "Device state: OFF" in res.output
 | 
			
		||||
 | 
			
		||||
    if not dev.has_emeter:
 | 
			
		||||
        assert "Device has no emeter" in res.output
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def test_alias(dev):
 | 
			
		||||
    runner = CliRunner()
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										117
									
								
								kasa/tests/test_emeter.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								kasa/tests/test_emeter.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,117 @@
 | 
			
		||||
import pytest
 | 
			
		||||
 | 
			
		||||
from kasa import SmartDeviceException
 | 
			
		||||
 | 
			
		||||
from .conftest import has_emeter, no_emeter
 | 
			
		||||
from .newfakes import CURRENT_CONSUMPTION_SCHEMA
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@no_emeter
 | 
			
		||||
async def test_no_emeter(dev):
 | 
			
		||||
    assert not dev.has_emeter
 | 
			
		||||
 | 
			
		||||
    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()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@has_emeter
 | 
			
		||||
async def test_get_emeter_realtime(dev):
 | 
			
		||||
    if dev.is_strip:
 | 
			
		||||
        pytest.skip("Disabled for strips temporarily")
 | 
			
		||||
 | 
			
		||||
    assert dev.has_emeter
 | 
			
		||||
 | 
			
		||||
    current_emeter = await dev.get_emeter_realtime()
 | 
			
		||||
    CURRENT_CONSUMPTION_SCHEMA(current_emeter)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@has_emeter
 | 
			
		||||
async def test_get_emeter_daily(dev):
 | 
			
		||||
    if dev.is_strip:
 | 
			
		||||
        pytest.skip("Disabled for strips temporarily")
 | 
			
		||||
 | 
			
		||||
    assert dev.has_emeter
 | 
			
		||||
 | 
			
		||||
    assert await dev.get_emeter_daily(year=1900, month=1) == {}
 | 
			
		||||
 | 
			
		||||
    d = await dev.get_emeter_daily()
 | 
			
		||||
    assert len(d) > 0
 | 
			
		||||
 | 
			
		||||
    k, v = d.popitem()
 | 
			
		||||
    assert isinstance(k, int)
 | 
			
		||||
    assert isinstance(v, float)
 | 
			
		||||
 | 
			
		||||
    # Test kwh (energy, energy_wh)
 | 
			
		||||
    d = await dev.get_emeter_daily(kwh=False)
 | 
			
		||||
    k2, v2 = d.popitem()
 | 
			
		||||
    assert v * 1000 == v2
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@has_emeter
 | 
			
		||||
async def test_get_emeter_monthly(dev):
 | 
			
		||||
    if dev.is_strip:
 | 
			
		||||
        pytest.skip("Disabled for strips temporarily")
 | 
			
		||||
 | 
			
		||||
    assert dev.has_emeter
 | 
			
		||||
 | 
			
		||||
    assert await dev.get_emeter_monthly(year=1900) == {}
 | 
			
		||||
 | 
			
		||||
    d = await dev.get_emeter_monthly()
 | 
			
		||||
    assert len(d) > 0
 | 
			
		||||
 | 
			
		||||
    k, v = d.popitem()
 | 
			
		||||
    assert isinstance(k, int)
 | 
			
		||||
    assert isinstance(v, float)
 | 
			
		||||
 | 
			
		||||
    # Test kwh (energy, energy_wh)
 | 
			
		||||
    d = await dev.get_emeter_monthly(kwh=False)
 | 
			
		||||
    k2, v2 = d.popitem()
 | 
			
		||||
    assert v * 1000 == v2
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@has_emeter
 | 
			
		||||
async def test_emeter_status(dev):
 | 
			
		||||
    if dev.is_strip:
 | 
			
		||||
        pytest.skip("Disabled for strips temporarily")
 | 
			
		||||
 | 
			
		||||
    assert dev.has_emeter
 | 
			
		||||
 | 
			
		||||
    d = await dev.get_emeter_realtime()
 | 
			
		||||
 | 
			
		||||
    with pytest.raises(KeyError):
 | 
			
		||||
        assert d["foo"]
 | 
			
		||||
 | 
			
		||||
    assert d["power_mw"] == d["power"] * 1000
 | 
			
		||||
    # bulbs have only power according to tplink simulator.
 | 
			
		||||
    if not dev.is_bulb:
 | 
			
		||||
        assert d["voltage_mv"] == d["voltage"] * 1000
 | 
			
		||||
 | 
			
		||||
        assert d["current_ma"] == d["current"] * 1000
 | 
			
		||||
        assert d["total_wh"] == d["total"] * 1000
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.skip("not clearing your stats..")
 | 
			
		||||
@has_emeter
 | 
			
		||||
async def test_erase_emeter_stats(dev):
 | 
			
		||||
    assert dev.has_emeter
 | 
			
		||||
 | 
			
		||||
    await dev.erase_emeter()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@has_emeter
 | 
			
		||||
async def test_current_consumption(dev):
 | 
			
		||||
    if dev.is_strip:
 | 
			
		||||
        pytest.skip("Disabled for strips temporarily")
 | 
			
		||||
 | 
			
		||||
    if dev.has_emeter:
 | 
			
		||||
        x = await dev.current_consumption()
 | 
			
		||||
        assert isinstance(x, float)
 | 
			
		||||
        assert x >= 0.0
 | 
			
		||||
    else:
 | 
			
		||||
        assert await dev.current_consumption() is None
 | 
			
		||||
@@ -1,518 +0,0 @@
 | 
			
		||||
from datetime import datetime
 | 
			
		||||
from unittest.mock import patch
 | 
			
		||||
 | 
			
		||||
import pytest  # type: ignore # https://github.com/pytest-dev/pytest/issues/3342
 | 
			
		||||
 | 
			
		||||
from kasa import DeviceType, SmartDeviceException, SmartStrip
 | 
			
		||||
 | 
			
		||||
from .conftest import (
 | 
			
		||||
    bulb,
 | 
			
		||||
    color_bulb,
 | 
			
		||||
    dimmable,
 | 
			
		||||
    handle_turn_on,
 | 
			
		||||
    has_emeter,
 | 
			
		||||
    no_emeter,
 | 
			
		||||
    non_color_bulb,
 | 
			
		||||
    non_dimmable,
 | 
			
		||||
    non_variable_temp,
 | 
			
		||||
    plug,
 | 
			
		||||
    strip,
 | 
			
		||||
    turn_on,
 | 
			
		||||
    variable_temp,
 | 
			
		||||
)
 | 
			
		||||
from .newfakes import (
 | 
			
		||||
    BULB_SCHEMA,
 | 
			
		||||
    CURRENT_CONSUMPTION_SCHEMA,
 | 
			
		||||
    PLUG_SCHEMA,
 | 
			
		||||
    TZ_SCHEMA,
 | 
			
		||||
    FakeTransportProtocol,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
# to avoid adding this for each async function separately
 | 
			
		||||
pytestmark = pytest.mark.asyncio
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@plug
 | 
			
		||||
async def test_plug_sysinfo(dev):
 | 
			
		||||
    assert dev.sys_info is not None
 | 
			
		||||
    PLUG_SCHEMA(dev.sys_info)
 | 
			
		||||
 | 
			
		||||
    assert dev.model is not None
 | 
			
		||||
 | 
			
		||||
    assert dev.device_type == DeviceType.Plug or dev.device_type == DeviceType.Strip
 | 
			
		||||
    assert dev.is_plug or dev.is_strip
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@bulb
 | 
			
		||||
async def test_bulb_sysinfo(dev):
 | 
			
		||||
    assert dev.sys_info is not None
 | 
			
		||||
    BULB_SCHEMA(dev.sys_info)
 | 
			
		||||
 | 
			
		||||
    assert dev.model is not None
 | 
			
		||||
 | 
			
		||||
    assert dev.device_type == DeviceType.Bulb
 | 
			
		||||
    assert dev.is_bulb
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def test_state_info(dev):
 | 
			
		||||
    assert isinstance(dev.state_information, dict)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def test_invalid_connection(dev):
 | 
			
		||||
    with patch.object(FakeTransportProtocol, "query", side_effect=SmartDeviceException):
 | 
			
		||||
        with pytest.raises(SmartDeviceException):
 | 
			
		||||
            await dev.update()
 | 
			
		||||
            dev.is_on
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def test_query_helper(dev):
 | 
			
		||||
    with pytest.raises(SmartDeviceException):
 | 
			
		||||
        await dev._query_helper("test", "testcmd", {})
 | 
			
		||||
    # TODO check for unwrapping?
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@turn_on
 | 
			
		||||
async def test_state(dev, turn_on):
 | 
			
		||||
    await handle_turn_on(dev, turn_on)
 | 
			
		||||
    orig_state = dev.is_on
 | 
			
		||||
    if orig_state:
 | 
			
		||||
        await dev.turn_off()
 | 
			
		||||
        assert not dev.is_on
 | 
			
		||||
        assert dev.is_off
 | 
			
		||||
 | 
			
		||||
        await dev.turn_on()
 | 
			
		||||
        assert dev.is_on
 | 
			
		||||
        assert not dev.is_off
 | 
			
		||||
    else:
 | 
			
		||||
        await dev.turn_on()
 | 
			
		||||
        assert dev.is_on
 | 
			
		||||
        assert not dev.is_off
 | 
			
		||||
 | 
			
		||||
        await dev.turn_off()
 | 
			
		||||
        assert not dev.is_on
 | 
			
		||||
        assert dev.is_off
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@no_emeter
 | 
			
		||||
async def test_no_emeter(dev):
 | 
			
		||||
    assert not dev.has_emeter
 | 
			
		||||
 | 
			
		||||
    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()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@has_emeter
 | 
			
		||||
async def test_get_emeter_realtime(dev):
 | 
			
		||||
    if dev.is_strip:
 | 
			
		||||
        pytest.skip("Disabled for strips temporarily")
 | 
			
		||||
 | 
			
		||||
    assert dev.has_emeter
 | 
			
		||||
 | 
			
		||||
    current_emeter = await dev.get_emeter_realtime()
 | 
			
		||||
    CURRENT_CONSUMPTION_SCHEMA(current_emeter)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@has_emeter
 | 
			
		||||
async def test_get_emeter_daily(dev):
 | 
			
		||||
    if dev.is_strip:
 | 
			
		||||
        pytest.skip("Disabled for strips temporarily")
 | 
			
		||||
 | 
			
		||||
    assert dev.has_emeter
 | 
			
		||||
 | 
			
		||||
    assert await dev.get_emeter_daily(year=1900, month=1) == {}
 | 
			
		||||
 | 
			
		||||
    d = await dev.get_emeter_daily()
 | 
			
		||||
    assert len(d) > 0
 | 
			
		||||
 | 
			
		||||
    k, v = d.popitem()
 | 
			
		||||
    assert isinstance(k, int)
 | 
			
		||||
    assert isinstance(v, float)
 | 
			
		||||
 | 
			
		||||
    # Test kwh (energy, energy_wh)
 | 
			
		||||
    d = await dev.get_emeter_daily(kwh=False)
 | 
			
		||||
    k2, v2 = d.popitem()
 | 
			
		||||
    assert v * 1000 == v2
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@has_emeter
 | 
			
		||||
async def test_get_emeter_monthly(dev):
 | 
			
		||||
    if dev.is_strip:
 | 
			
		||||
        pytest.skip("Disabled for strips temporarily")
 | 
			
		||||
 | 
			
		||||
    assert dev.has_emeter
 | 
			
		||||
 | 
			
		||||
    assert await dev.get_emeter_monthly(year=1900) == {}
 | 
			
		||||
 | 
			
		||||
    d = await dev.get_emeter_monthly()
 | 
			
		||||
    assert len(d) > 0
 | 
			
		||||
 | 
			
		||||
    k, v = d.popitem()
 | 
			
		||||
    assert isinstance(k, int)
 | 
			
		||||
    assert isinstance(v, float)
 | 
			
		||||
 | 
			
		||||
    # Test kwh (energy, energy_wh)
 | 
			
		||||
    d = await dev.get_emeter_monthly(kwh=False)
 | 
			
		||||
    k2, v2 = d.popitem()
 | 
			
		||||
    assert v * 1000 == v2
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@has_emeter
 | 
			
		||||
async def test_emeter_status(dev):
 | 
			
		||||
    if dev.is_strip:
 | 
			
		||||
        pytest.skip("Disabled for strips temporarily")
 | 
			
		||||
 | 
			
		||||
    assert dev.has_emeter
 | 
			
		||||
 | 
			
		||||
    d = await dev.get_emeter_realtime()
 | 
			
		||||
 | 
			
		||||
    with pytest.raises(KeyError):
 | 
			
		||||
        assert d["foo"]
 | 
			
		||||
 | 
			
		||||
    assert d["power_mw"] == d["power"] * 1000
 | 
			
		||||
    # bulbs have only power according to tplink simulator.
 | 
			
		||||
    if not dev.is_bulb:
 | 
			
		||||
        assert d["voltage_mv"] == d["voltage"] * 1000
 | 
			
		||||
 | 
			
		||||
        assert d["current_ma"] == d["current"] * 1000
 | 
			
		||||
        assert d["total_wh"] == d["total"] * 1000
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.skip("not clearing your stats..")
 | 
			
		||||
@has_emeter
 | 
			
		||||
async def test_erase_emeter_stats(dev):
 | 
			
		||||
    assert dev.has_emeter
 | 
			
		||||
 | 
			
		||||
    await dev.erase_emeter()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@has_emeter
 | 
			
		||||
async def test_current_consumption(dev):
 | 
			
		||||
    if dev.is_strip:
 | 
			
		||||
        pytest.skip("Disabled for strips temporarily")
 | 
			
		||||
 | 
			
		||||
    if dev.has_emeter:
 | 
			
		||||
        x = await dev.current_consumption()
 | 
			
		||||
        assert isinstance(x, float)
 | 
			
		||||
        assert x >= 0.0
 | 
			
		||||
    else:
 | 
			
		||||
        assert await dev.current_consumption() is None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def test_alias(dev):
 | 
			
		||||
    test_alias = "TEST1234"
 | 
			
		||||
    original = dev.alias
 | 
			
		||||
 | 
			
		||||
    assert isinstance(original, str)
 | 
			
		||||
    await dev.set_alias(test_alias)
 | 
			
		||||
    assert dev.alias == test_alias
 | 
			
		||||
 | 
			
		||||
    await dev.set_alias(original)
 | 
			
		||||
    assert dev.alias == original
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@plug
 | 
			
		||||
async def test_led(dev):
 | 
			
		||||
    original = dev.led
 | 
			
		||||
 | 
			
		||||
    await dev.set_led(False)
 | 
			
		||||
    assert not dev.led
 | 
			
		||||
 | 
			
		||||
    await dev.set_led(True)
 | 
			
		||||
    assert dev.led
 | 
			
		||||
 | 
			
		||||
    await dev.set_led(original)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@turn_on
 | 
			
		||||
async def test_on_since(dev, turn_on):
 | 
			
		||||
    await handle_turn_on(dev, turn_on)
 | 
			
		||||
    orig_state = dev.is_on
 | 
			
		||||
    if "on_time" not in dev.sys_info and not dev.is_strip:
 | 
			
		||||
        assert dev.on_since is None
 | 
			
		||||
    elif orig_state:
 | 
			
		||||
        assert isinstance(dev.on_since, datetime)
 | 
			
		||||
    else:
 | 
			
		||||
        assert dev.on_since is None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def test_icon(dev):
 | 
			
		||||
    assert set((await dev.get_icon()).keys()), {"icon", "hash"}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def test_time(dev):
 | 
			
		||||
    assert isinstance(await dev.get_time(), datetime)
 | 
			
		||||
    # TODO check setting?
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def test_timezone(dev):
 | 
			
		||||
    TZ_SCHEMA(await dev.get_timezone())
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def test_hw_info(dev):
 | 
			
		||||
    PLUG_SCHEMA(dev.hw_info)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def test_location(dev):
 | 
			
		||||
    PLUG_SCHEMA(dev.location)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def test_rssi(dev):
 | 
			
		||||
    PLUG_SCHEMA({"rssi": dev.rssi})  # wrapping for vol
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def test_mac(dev):
 | 
			
		||||
    PLUG_SCHEMA({"mac": dev.mac})  # wrapping for val
 | 
			
		||||
    # TODO check setting?
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@non_variable_temp
 | 
			
		||||
async def test_temperature_on_nonsupporting(dev):
 | 
			
		||||
    assert dev.valid_temperature_range == (0, 0)
 | 
			
		||||
 | 
			
		||||
    # TODO test when device does not support temperature range
 | 
			
		||||
    with pytest.raises(SmartDeviceException):
 | 
			
		||||
        await dev.set_color_temp(2700)
 | 
			
		||||
    with pytest.raises(SmartDeviceException):
 | 
			
		||||
        print(dev.color_temp)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@variable_temp
 | 
			
		||||
async def test_out_of_range_temperature(dev):
 | 
			
		||||
    with pytest.raises(ValueError):
 | 
			
		||||
        await dev.set_color_temp(1000)
 | 
			
		||||
    with pytest.raises(ValueError):
 | 
			
		||||
        await dev.set_color_temp(10000)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@non_dimmable
 | 
			
		||||
async def test_non_dimmable(dev):
 | 
			
		||||
    assert not dev.is_dimmable
 | 
			
		||||
 | 
			
		||||
    with pytest.raises(SmartDeviceException):
 | 
			
		||||
        assert dev.brightness == 0
 | 
			
		||||
    with pytest.raises(SmartDeviceException):
 | 
			
		||||
        await dev.set_brightness(100)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@dimmable
 | 
			
		||||
@turn_on
 | 
			
		||||
async def test_dimmable_brightness(dev, turn_on):
 | 
			
		||||
    await handle_turn_on(dev, turn_on)
 | 
			
		||||
    assert dev.is_dimmable
 | 
			
		||||
 | 
			
		||||
    await dev.set_brightness(50)
 | 
			
		||||
    assert dev.brightness == 50
 | 
			
		||||
 | 
			
		||||
    await dev.set_brightness(10)
 | 
			
		||||
    assert dev.brightness == 10
 | 
			
		||||
 | 
			
		||||
    with pytest.raises(ValueError):
 | 
			
		||||
        await dev.set_brightness("foo")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@dimmable
 | 
			
		||||
async def test_invalid_brightness(dev):
 | 
			
		||||
    assert dev.is_dimmable
 | 
			
		||||
 | 
			
		||||
    with pytest.raises(ValueError):
 | 
			
		||||
        await dev.set_brightness(110)
 | 
			
		||||
 | 
			
		||||
    with pytest.raises(ValueError):
 | 
			
		||||
        await dev.set_brightness(-100)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@color_bulb
 | 
			
		||||
@turn_on
 | 
			
		||||
async def test_hsv(dev, turn_on):
 | 
			
		||||
    await handle_turn_on(dev, turn_on)
 | 
			
		||||
    assert dev.is_color
 | 
			
		||||
 | 
			
		||||
    hue, saturation, brightness = dev.hsv
 | 
			
		||||
    assert 0 <= hue <= 255
 | 
			
		||||
    assert 0 <= saturation <= 100
 | 
			
		||||
    assert 0 <= brightness <= 100
 | 
			
		||||
 | 
			
		||||
    await dev.set_hsv(hue=1, saturation=1, value=1)
 | 
			
		||||
 | 
			
		||||
    hue, saturation, brightness = dev.hsv
 | 
			
		||||
    assert hue == 1
 | 
			
		||||
    assert saturation == 1
 | 
			
		||||
    assert brightness == 1
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@color_bulb
 | 
			
		||||
@turn_on
 | 
			
		||||
async def test_invalid_hsv(dev, 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)
 | 
			
		||||
 | 
			
		||||
    for invalid_saturation in [-1, 101, 0.5]:
 | 
			
		||||
        with pytest.raises(ValueError):
 | 
			
		||||
            await dev.set_hsv(0, invalid_saturation, 0)
 | 
			
		||||
 | 
			
		||||
    for invalid_brightness in [-1, 101, 0.5]:
 | 
			
		||||
        with pytest.raises(ValueError):
 | 
			
		||||
            await dev.set_hsv(0, 0, invalid_brightness)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@non_color_bulb
 | 
			
		||||
async def test_hsv_on_non_color(dev):
 | 
			
		||||
    assert not dev.is_color
 | 
			
		||||
 | 
			
		||||
    with pytest.raises(SmartDeviceException):
 | 
			
		||||
        await dev.set_hsv(0, 0, 0)
 | 
			
		||||
    with pytest.raises(SmartDeviceException):
 | 
			
		||||
        print(dev.hsv)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@variable_temp
 | 
			
		||||
@turn_on
 | 
			
		||||
async def test_try_set_colortemp(dev, turn_on):
 | 
			
		||||
    await handle_turn_on(dev, turn_on)
 | 
			
		||||
    await dev.set_color_temp(2700)
 | 
			
		||||
    assert dev.color_temp == 2700
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@non_variable_temp
 | 
			
		||||
async def test_non_variable_temp(dev):
 | 
			
		||||
    with pytest.raises(SmartDeviceException):
 | 
			
		||||
        await dev.set_color_temp(2700)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@strip
 | 
			
		||||
@turn_on
 | 
			
		||||
async def test_children_change_state(dev, turn_on):
 | 
			
		||||
    await handle_turn_on(dev, turn_on)
 | 
			
		||||
    for plug in dev.plugs:
 | 
			
		||||
        orig_state = plug.is_on
 | 
			
		||||
        if orig_state:
 | 
			
		||||
            await plug.turn_off()
 | 
			
		||||
            assert not plug.is_on
 | 
			
		||||
            assert plug.is_off
 | 
			
		||||
 | 
			
		||||
            await plug.turn_on()
 | 
			
		||||
            assert plug.is_on
 | 
			
		||||
            assert not plug.is_off
 | 
			
		||||
        else:
 | 
			
		||||
            await plug.turn_on()
 | 
			
		||||
            assert plug.is_on
 | 
			
		||||
            assert not plug.is_off
 | 
			
		||||
 | 
			
		||||
            await plug.turn_off()
 | 
			
		||||
            assert not plug.is_on
 | 
			
		||||
            assert plug.is_off
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@strip
 | 
			
		||||
async def test_children_alias(dev):
 | 
			
		||||
    test_alias = "TEST1234"
 | 
			
		||||
    for plug in dev.plugs:
 | 
			
		||||
        original = plug.alias
 | 
			
		||||
        await plug.set_alias(alias=test_alias)
 | 
			
		||||
        await dev.update()  # TODO: set_alias does not call parent's update()..
 | 
			
		||||
        assert plug.alias == test_alias
 | 
			
		||||
 | 
			
		||||
        await plug.set_alias(alias=original)
 | 
			
		||||
        await dev.update()  # TODO: set_alias does not call parent's update()..
 | 
			
		||||
        assert plug.alias == original
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@strip
 | 
			
		||||
async def test_children_on_since(dev):
 | 
			
		||||
    on_sinces = []
 | 
			
		||||
    for plug in dev.plugs:
 | 
			
		||||
        if plug.is_on:
 | 
			
		||||
            on_sinces.append(plug.on_since)
 | 
			
		||||
            assert isinstance(plug.on_since, datetime)
 | 
			
		||||
        else:
 | 
			
		||||
            assert plug.on_since is None
 | 
			
		||||
 | 
			
		||||
    if dev.is_off:
 | 
			
		||||
        assert dev.on_since is None
 | 
			
		||||
    # TODO: testing this would require some mocking utcnow which is not
 | 
			
		||||
    # very straightforward.
 | 
			
		||||
    # else:
 | 
			
		||||
    #    assert dev.on_since == max(on_sinces)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@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
 | 
			
		||||
    # TODO: this needs to be fixed, dev.plugs is not available for each device..
 | 
			
		||||
    for state in range(2 ** len(dev.plugs)):
 | 
			
		||||
        # create binary state map
 | 
			
		||||
        state_map = {}
 | 
			
		||||
        for plug_index in range(len(dev.plugs)):
 | 
			
		||||
            state_map[plug_index] = bool((state >> plug_index) & 1)
 | 
			
		||||
 | 
			
		||||
            if state_map[plug_index]:
 | 
			
		||||
                await dev.turn_on(index=plug_index)
 | 
			
		||||
            else:
 | 
			
		||||
                await dev.turn_off(index=plug_index)
 | 
			
		||||
 | 
			
		||||
        # check state map applied
 | 
			
		||||
        for index, state in dev.is_on.items():
 | 
			
		||||
            assert state_map[index] == state
 | 
			
		||||
 | 
			
		||||
        # toggle each outlet with state map applied
 | 
			
		||||
        for plug_index in range(len(dev.plugs)):
 | 
			
		||||
 | 
			
		||||
            # toggle state
 | 
			
		||||
            if state_map[plug_index]:
 | 
			
		||||
                await dev.turn_off(index=plug_index)
 | 
			
		||||
            else:
 | 
			
		||||
                await dev.turn_on(index=plug_index)
 | 
			
		||||
 | 
			
		||||
            # only target outlet should have state changed
 | 
			
		||||
            for index, state in dev.is_on.items():
 | 
			
		||||
                if index == plug_index:
 | 
			
		||||
                    assert state != state_map[index]
 | 
			
		||||
                else:
 | 
			
		||||
                    assert state == state_map[index]
 | 
			
		||||
 | 
			
		||||
            # reset state
 | 
			
		||||
            if state_map[plug_index]:
 | 
			
		||||
                await dev.turn_on(index=plug_index)
 | 
			
		||||
            else:
 | 
			
		||||
                await dev.turn_off(index=plug_index)
 | 
			
		||||
 | 
			
		||||
            # original state map should be restored
 | 
			
		||||
            for index, state in dev.is_on.items():
 | 
			
		||||
                assert state == state_map[index]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def test_representation(dev):
 | 
			
		||||
    import re
 | 
			
		||||
 | 
			
		||||
    pattern = re.compile("<.* model .* at .* (.*), is_on: .* - dev specific: .*>")
 | 
			
		||||
    assert pattern.match(str(dev))
 | 
			
		||||
							
								
								
									
										28
									
								
								kasa/tests/test_plug.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								kasa/tests/test_plug.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,28 @@
 | 
			
		||||
from kasa import DeviceType
 | 
			
		||||
 | 
			
		||||
from .conftest import plug
 | 
			
		||||
from .newfakes import PLUG_SCHEMA
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@plug
 | 
			
		||||
async def test_plug_sysinfo(dev):
 | 
			
		||||
    assert dev.sys_info is not None
 | 
			
		||||
    PLUG_SCHEMA(dev.sys_info)
 | 
			
		||||
 | 
			
		||||
    assert dev.model is not None
 | 
			
		||||
 | 
			
		||||
    assert dev.device_type == DeviceType.Plug or dev.device_type == DeviceType.Strip
 | 
			
		||||
    assert dev.is_plug or dev.is_strip
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@plug
 | 
			
		||||
async def test_led(dev):
 | 
			
		||||
    original = dev.led
 | 
			
		||||
 | 
			
		||||
    await dev.set_led(False)
 | 
			
		||||
    assert not dev.led
 | 
			
		||||
 | 
			
		||||
    await dev.set_led(True)
 | 
			
		||||
    assert dev.led
 | 
			
		||||
 | 
			
		||||
    await dev.set_led(original)
 | 
			
		||||
							
								
								
									
										111
									
								
								kasa/tests/test_smartdevice.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										111
									
								
								kasa/tests/test_smartdevice.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,111 @@
 | 
			
		||||
from datetime import datetime
 | 
			
		||||
from unittest.mock import patch
 | 
			
		||||
 | 
			
		||||
import pytest  # type: ignore # https://github.com/pytest-dev/pytest/issues/3342
 | 
			
		||||
 | 
			
		||||
from kasa import SmartDeviceException
 | 
			
		||||
 | 
			
		||||
from .conftest import handle_turn_on, turn_on
 | 
			
		||||
from .newfakes import PLUG_SCHEMA, TZ_SCHEMA, FakeTransportProtocol
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def test_state_info(dev):
 | 
			
		||||
    assert isinstance(dev.state_information, dict)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def test_invalid_connection(dev):
 | 
			
		||||
    with patch.object(FakeTransportProtocol, "query", side_effect=SmartDeviceException):
 | 
			
		||||
        with pytest.raises(SmartDeviceException):
 | 
			
		||||
            await dev.update()
 | 
			
		||||
            dev.is_on
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def test_query_helper(dev):
 | 
			
		||||
    with pytest.raises(SmartDeviceException):
 | 
			
		||||
        await dev._query_helper("test", "testcmd", {})
 | 
			
		||||
    # TODO check for unwrapping?
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@turn_on
 | 
			
		||||
async def test_state(dev, turn_on):
 | 
			
		||||
    await handle_turn_on(dev, turn_on)
 | 
			
		||||
    orig_state = dev.is_on
 | 
			
		||||
    if orig_state:
 | 
			
		||||
        await dev.turn_off()
 | 
			
		||||
        assert not dev.is_on
 | 
			
		||||
        assert dev.is_off
 | 
			
		||||
 | 
			
		||||
        await dev.turn_on()
 | 
			
		||||
        assert dev.is_on
 | 
			
		||||
        assert not dev.is_off
 | 
			
		||||
    else:
 | 
			
		||||
        await dev.turn_on()
 | 
			
		||||
        assert dev.is_on
 | 
			
		||||
        assert not dev.is_off
 | 
			
		||||
 | 
			
		||||
        await dev.turn_off()
 | 
			
		||||
        assert not dev.is_on
 | 
			
		||||
        assert dev.is_off
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def test_alias(dev):
 | 
			
		||||
    test_alias = "TEST1234"
 | 
			
		||||
    original = dev.alias
 | 
			
		||||
 | 
			
		||||
    assert isinstance(original, str)
 | 
			
		||||
    await dev.set_alias(test_alias)
 | 
			
		||||
    assert dev.alias == test_alias
 | 
			
		||||
 | 
			
		||||
    await dev.set_alias(original)
 | 
			
		||||
    assert dev.alias == original
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@turn_on
 | 
			
		||||
async def test_on_since(dev, turn_on):
 | 
			
		||||
    await handle_turn_on(dev, turn_on)
 | 
			
		||||
    orig_state = dev.is_on
 | 
			
		||||
    if "on_time" not in dev.sys_info and not dev.is_strip:
 | 
			
		||||
        assert dev.on_since is None
 | 
			
		||||
    elif orig_state:
 | 
			
		||||
        assert isinstance(dev.on_since, datetime)
 | 
			
		||||
    else:
 | 
			
		||||
        assert dev.on_since is None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def test_time(dev):
 | 
			
		||||
    assert isinstance(await dev.get_time(), datetime)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def test_timezone(dev):
 | 
			
		||||
    TZ_SCHEMA(await dev.get_timezone())
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def test_hw_info(dev):
 | 
			
		||||
    PLUG_SCHEMA(dev.hw_info)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def test_location(dev):
 | 
			
		||||
    PLUG_SCHEMA(dev.location)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def test_rssi(dev):
 | 
			
		||||
    PLUG_SCHEMA({"rssi": dev.rssi})  # wrapping for vol
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def test_mac(dev):
 | 
			
		||||
    PLUG_SCHEMA({"mac": dev.mac})  # wrapping for val
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def test_representation(dev):
 | 
			
		||||
    import re
 | 
			
		||||
 | 
			
		||||
    pattern = re.compile("<.* model .* at .* (.*), is_on: .* - dev specific: .*>")
 | 
			
		||||
    assert pattern.match(str(dev))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def test_childrens(dev):
 | 
			
		||||
    """Make sure that children property is exposed by every device."""
 | 
			
		||||
    if dev.is_strip:
 | 
			
		||||
        assert len(dev.children) > 0
 | 
			
		||||
    else:
 | 
			
		||||
        assert len(dev.children) == 0
 | 
			
		||||
							
								
								
									
										129
									
								
								kasa/tests/test_strip.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								kasa/tests/test_strip.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,129 @@
 | 
			
		||||
from datetime import datetime
 | 
			
		||||
 | 
			
		||||
import pytest
 | 
			
		||||
 | 
			
		||||
from kasa import SmartDeviceException, SmartStrip
 | 
			
		||||
 | 
			
		||||
from .conftest import handle_turn_on, strip, turn_on
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@strip
 | 
			
		||||
@turn_on
 | 
			
		||||
async def test_children_change_state(dev, turn_on):
 | 
			
		||||
    await handle_turn_on(dev, turn_on)
 | 
			
		||||
    for plug in dev.children:
 | 
			
		||||
        orig_state = plug.is_on
 | 
			
		||||
        if orig_state:
 | 
			
		||||
            await plug.turn_off()
 | 
			
		||||
            assert not plug.is_on
 | 
			
		||||
            assert plug.is_off
 | 
			
		||||
 | 
			
		||||
            await plug.turn_on()
 | 
			
		||||
            assert plug.is_on
 | 
			
		||||
            assert not plug.is_off
 | 
			
		||||
        else:
 | 
			
		||||
            await plug.turn_on()
 | 
			
		||||
            assert plug.is_on
 | 
			
		||||
            assert not plug.is_off
 | 
			
		||||
 | 
			
		||||
            await plug.turn_off()
 | 
			
		||||
            assert not plug.is_on
 | 
			
		||||
            assert plug.is_off
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@strip
 | 
			
		||||
async def test_children_alias(dev):
 | 
			
		||||
    test_alias = "TEST1234"
 | 
			
		||||
    for plug in dev.children:
 | 
			
		||||
        original = plug.alias
 | 
			
		||||
        await plug.set_alias(alias=test_alias)
 | 
			
		||||
        await dev.update()  # TODO: set_alias does not call parent's update()..
 | 
			
		||||
        assert plug.alias == test_alias
 | 
			
		||||
 | 
			
		||||
        await plug.set_alias(alias=original)
 | 
			
		||||
        await dev.update()  # TODO: set_alias does not call parent's update()..
 | 
			
		||||
        assert plug.alias == original
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@strip
 | 
			
		||||
async def test_children_on_since(dev):
 | 
			
		||||
    on_sinces = []
 | 
			
		||||
    for plug in dev.children:
 | 
			
		||||
        if plug.is_on:
 | 
			
		||||
            on_sinces.append(plug.on_since)
 | 
			
		||||
            assert isinstance(plug.on_since, datetime)
 | 
			
		||||
        else:
 | 
			
		||||
            assert plug.on_since is None
 | 
			
		||||
 | 
			
		||||
    if dev.is_off:
 | 
			
		||||
        assert dev.on_since is None
 | 
			
		||||
    # TODO: testing this would require some mocking utcnow which is not
 | 
			
		||||
    # very straightforward.
 | 
			
		||||
    # else:
 | 
			
		||||
    #    assert dev.on_since == max(on_sinces)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@strip
 | 
			
		||||
async def test_get_plug_by_name(dev: SmartStrip):
 | 
			
		||||
    name = dev.children[0].alias
 | 
			
		||||
    assert dev.get_plug_by_name(name) == dev.children[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.children[0]
 | 
			
		||||
 | 
			
		||||
    with pytest.raises(SmartDeviceException):
 | 
			
		||||
        dev.get_plug_by_index(-1)
 | 
			
		||||
 | 
			
		||||
    with pytest.raises(SmartDeviceException):
 | 
			
		||||
        dev.get_plug_by_index(len(dev.children))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.skip("this test will wear out your relays")
 | 
			
		||||
async def test_all_binary_states(dev):
 | 
			
		||||
    # test every binary state
 | 
			
		||||
    # TODO: this needs to be fixed, dev.plugs is not available for each device..
 | 
			
		||||
    for state in range(2 ** len(dev.children)):
 | 
			
		||||
        # create binary state map
 | 
			
		||||
        state_map = {}
 | 
			
		||||
        for plug_index in range(len(dev.children)):
 | 
			
		||||
            state_map[plug_index] = bool((state >> plug_index) & 1)
 | 
			
		||||
 | 
			
		||||
            if state_map[plug_index]:
 | 
			
		||||
                await dev.turn_on(index=plug_index)
 | 
			
		||||
            else:
 | 
			
		||||
                await dev.turn_off(index=plug_index)
 | 
			
		||||
 | 
			
		||||
        # check state map applied
 | 
			
		||||
        for index, state in dev.is_on.items():
 | 
			
		||||
            assert state_map[index] == state
 | 
			
		||||
 | 
			
		||||
        # toggle each outlet with state map applied
 | 
			
		||||
        for plug_index in range(len(dev.children)):
 | 
			
		||||
 | 
			
		||||
            # toggle state
 | 
			
		||||
            if state_map[plug_index]:
 | 
			
		||||
                await dev.turn_off(index=plug_index)
 | 
			
		||||
            else:
 | 
			
		||||
                await dev.turn_on(index=plug_index)
 | 
			
		||||
 | 
			
		||||
            # only target outlet should have state changed
 | 
			
		||||
            for index, state in dev.is_on.items():
 | 
			
		||||
                if index == plug_index:
 | 
			
		||||
                    assert state != state_map[index]
 | 
			
		||||
                else:
 | 
			
		||||
                    assert state == state_map[index]
 | 
			
		||||
 | 
			
		||||
            # reset state
 | 
			
		||||
            if state_map[plug_index]:
 | 
			
		||||
                await dev.turn_on(index=plug_index)
 | 
			
		||||
            else:
 | 
			
		||||
                await dev.turn_off(index=plug_index)
 | 
			
		||||
 | 
			
		||||
            # original state map should be restored
 | 
			
		||||
            for index, state in dev.is_on.items():
 | 
			
		||||
                assert state == state_map[index]
 | 
			
		||||
@@ -43,7 +43,7 @@ known_third_party = ["asyncclick", "pytest", "setuptools", "voluptuous"]
 | 
			
		||||
[tool.coverage.run]
 | 
			
		||||
source = ["kasa"]
 | 
			
		||||
branch = true
 | 
			
		||||
omit = ["kasa/cli.py", "kasa/tests/*"]
 | 
			
		||||
omit = ["kasa/tests/*"]
 | 
			
		||||
 | 
			
		||||
[tool.coverage.report]
 | 
			
		||||
exclude_lines = [
 | 
			
		||||
@@ -52,6 +52,15 @@ exclude_lines = [
 | 
			
		||||
  "def __repr__"
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[tool.interrogate]
 | 
			
		||||
ignore-init-method = true
 | 
			
		||||
ignore-magic = true
 | 
			
		||||
ignore-private = true
 | 
			
		||||
ignore-semiprivate = true
 | 
			
		||||
fail-under = 100
 | 
			
		||||
exclude = ['kasa/tests/*']
 | 
			
		||||
verbose = 2
 | 
			
		||||
 | 
			
		||||
[build-system]
 | 
			
		||||
requires = ["poetry>=0.12"]
 | 
			
		||||
build-backend = "poetry.masonry.api"
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user