mirror of
https://github.com/python-kasa/python-kasa.git
synced 2024-12-23 03:33:35 +00:00
Add support for tapo bulbs (#558)
* Add support for tapo light bulbs * Use TapoDevice for on/off * Add tapobulbs to discovery * Add partial support for effects Activating the effect does not work as I thought it would, but this implements rest of the interface from SmartLightStrip. * Add missing __init__ for tapo package * Make mypy happy * Add docstrings to make ruff happy * Implement state_information and has_emeter * Import tapoplug from kasa.tapo package * Add tapo L530 fixture * Enable tests for L530 fixture * Make ruff happy * Update fixture filename * Raise exceptions on invalid parameters * Return results in a wrapped dict * Implement set_* * Reorganize bulbs to iot&smart, fix tests for smarts * Fix linting * Fix BULBS_LIGHT_STRIP back to LIGHT_STRIPS
This commit is contained in:
parent
5febd300ca
commit
f9b5003da2
@ -1,5 +1,12 @@
|
|||||||
"""Script that checks if README.md is missing devices that have fixtures."""
|
"""Script that checks if README.md is missing devices that have fixtures."""
|
||||||
from kasa.tests.conftest import ALL_DEVICES, BULBS, DIMMERS, LIGHT_STRIPS, PLUGS, STRIPS
|
from kasa.tests.conftest import (
|
||||||
|
ALL_DEVICES,
|
||||||
|
BULBS,
|
||||||
|
DIMMERS,
|
||||||
|
LIGHT_STRIPS,
|
||||||
|
PLUGS,
|
||||||
|
STRIPS,
|
||||||
|
)
|
||||||
|
|
||||||
with open("README.md") as f:
|
with open("README.md") as f:
|
||||||
readme = f.read()
|
readme = f.read()
|
||||||
|
@ -18,7 +18,7 @@ from .smartlightstrip import SmartLightStrip
|
|||||||
from .smartplug import SmartPlug
|
from .smartplug import SmartPlug
|
||||||
from .smartprotocol import SmartProtocol
|
from .smartprotocol import SmartProtocol
|
||||||
from .smartstrip import SmartStrip
|
from .smartstrip import SmartStrip
|
||||||
from .tapo.tapoplug import TapoPlug
|
from .tapo import TapoBulb, TapoPlug
|
||||||
|
|
||||||
DEVICE_TYPE_TO_CLASS = {
|
DEVICE_TYPE_TO_CLASS = {
|
||||||
DeviceType.Plug: SmartPlug,
|
DeviceType.Plug: SmartPlug,
|
||||||
@ -27,6 +27,7 @@ DEVICE_TYPE_TO_CLASS = {
|
|||||||
DeviceType.Dimmer: SmartDimmer,
|
DeviceType.Dimmer: SmartDimmer,
|
||||||
DeviceType.LightStrip: SmartLightStrip,
|
DeviceType.LightStrip: SmartLightStrip,
|
||||||
DeviceType.TapoPlug: TapoPlug,
|
DeviceType.TapoPlug: TapoPlug,
|
||||||
|
DeviceType.TapoBulb: TapoBulb,
|
||||||
}
|
}
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -139,6 +140,7 @@ def get_device_class_from_type_name(device_type: str) -> Optional[Type[SmartDevi
|
|||||||
"""Return the device class from the type name."""
|
"""Return the device class from the type name."""
|
||||||
supported_device_types: dict[str, Type[SmartDevice]] = {
|
supported_device_types: dict[str, Type[SmartDevice]] = {
|
||||||
"SMART.TAPOPLUG": TapoPlug,
|
"SMART.TAPOPLUG": TapoPlug,
|
||||||
|
"SMART.TAPOBULB": TapoBulb,
|
||||||
"SMART.KASAPLUG": TapoPlug,
|
"SMART.KASAPLUG": TapoPlug,
|
||||||
"IOT.SMARTPLUGSWITCH": SmartPlug,
|
"IOT.SMARTPLUGSWITCH": SmartPlug,
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,7 @@ class DeviceType(Enum):
|
|||||||
Dimmer = "dimmer"
|
Dimmer = "dimmer"
|
||||||
LightStrip = "lightstrip"
|
LightStrip = "lightstrip"
|
||||||
TapoPlug = "tapoplug"
|
TapoPlug = "tapoplug"
|
||||||
|
TapoBulb = "tapobulb"
|
||||||
Unknown = "unknown"
|
Unknown = "unknown"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
"""Package for supporting tapo-branded and newer kasa devices."""
|
"""Package for supporting tapo-branded and newer kasa devices."""
|
||||||
|
from .tapobulb import TapoBulb
|
||||||
from .tapodevice import TapoDevice
|
from .tapodevice import TapoDevice
|
||||||
from .tapoplug import TapoPlug
|
from .tapoplug import TapoPlug
|
||||||
|
|
||||||
__all__ = ["TapoDevice", "TapoPlug"]
|
__all__ = ["TapoDevice", "TapoPlug", "TapoBulb"]
|
||||||
|
267
kasa/tapo/tapobulb.py
Normal file
267
kasa/tapo/tapobulb.py
Normal file
@ -0,0 +1,267 @@
|
|||||||
|
"""Module for tapo-branded smart bulbs (L5**)."""
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
from ..exceptions import SmartDeviceException
|
||||||
|
from ..smartbulb import HSV, ColorTempRange, SmartBulb, SmartBulbPreset
|
||||||
|
from .tapodevice import TapoDevice
|
||||||
|
|
||||||
|
AVAILABLE_EFFECTS = {
|
||||||
|
"L1": "Party",
|
||||||
|
"L2": "Relax",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TapoBulb(TapoDevice, SmartBulb):
|
||||||
|
"""Representation of a TP-Link Tapo Bulb.
|
||||||
|
|
||||||
|
Documentation TBD. See :class:`~kasa.smartbulb.SmartBulb` for now.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_emeter(self) -> bool:
|
||||||
|
"""Bulbs have only historical emeter.
|
||||||
|
|
||||||
|
{'usage':
|
||||||
|
'power_usage': {'today': 6, 'past7': 106, 'past30': 106},
|
||||||
|
'saved_power': {'today': 35, 'past7': 529, 'past30': 529},
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_color(self) -> bool:
|
||||||
|
"""Whether the bulb supports color changes."""
|
||||||
|
# TODO: this makes an assumption that only color bulbs report this
|
||||||
|
return "hue" in self._info
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_dimmable(self) -> bool:
|
||||||
|
"""Whether the bulb supports brightness changes."""
|
||||||
|
# TODO: this makes an assumption that only dimmables report this
|
||||||
|
return "brightness" in self._info
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_variable_color_temp(self) -> bool:
|
||||||
|
"""Whether the bulb supports color temperature changes."""
|
||||||
|
# TODO: this makes an assumption, that only ct bulbs report this
|
||||||
|
return bool(self._info.get("color_temp_range", False))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def valid_temperature_range(self) -> ColorTempRange:
|
||||||
|
"""Return the device-specific white temperature range (in Kelvin).
|
||||||
|
|
||||||
|
:return: White temperature range in Kelvin (minimum, maximum)
|
||||||
|
"""
|
||||||
|
ct_range = self._info.get("color_temp_range", [0, 0])
|
||||||
|
return ColorTempRange(min=ct_range[0], max=ct_range[1])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_effects(self) -> bool:
|
||||||
|
"""Return True if the device supports effects."""
|
||||||
|
return "dynamic_light_effect_enable" in self._info
|
||||||
|
|
||||||
|
@property
|
||||||
|
def effect(self) -> Dict:
|
||||||
|
"""Return effect state.
|
||||||
|
|
||||||
|
This follows the format used by SmartLightStrip.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
{'brightness': 50,
|
||||||
|
'custom': 0,
|
||||||
|
'enable': 0,
|
||||||
|
'id': '',
|
||||||
|
'name': ''}
|
||||||
|
"""
|
||||||
|
# If no effect is active, dynamic_light_effect_id does not appear in info
|
||||||
|
current_effect = self._info.get("dynamic_light_effect_id", "")
|
||||||
|
data = {
|
||||||
|
"brightness": self.brightness,
|
||||||
|
"enable": current_effect != "",
|
||||||
|
"id": current_effect,
|
||||||
|
"name": AVAILABLE_EFFECTS.get(current_effect, ""),
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
@property
|
||||||
|
def effect_list(self) -> Optional[List[str]]:
|
||||||
|
"""Return built-in effects list.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
['Party', 'Relax', ...]
|
||||||
|
"""
|
||||||
|
return list(AVAILABLE_EFFECTS.keys()) if self.has_effects else None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def hsv(self) -> HSV:
|
||||||
|
"""Return the current HSV state of the bulb.
|
||||||
|
|
||||||
|
:return: hue, saturation and value (degrees, %, %)
|
||||||
|
"""
|
||||||
|
if not self.is_color:
|
||||||
|
raise SmartDeviceException("Bulb does not support color.")
|
||||||
|
|
||||||
|
h, s, v = (
|
||||||
|
self._info.get("hue", 0),
|
||||||
|
self._info.get("saturation", 0),
|
||||||
|
self._info.get("brightness", 0),
|
||||||
|
)
|
||||||
|
|
||||||
|
return HSV(hue=h, saturation=s, value=v)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def color_temp(self) -> int:
|
||||||
|
"""Whether the bulb supports color temperature changes."""
|
||||||
|
if not self.is_variable_color_temp:
|
||||||
|
raise SmartDeviceException("Bulb does not support colortemp.")
|
||||||
|
|
||||||
|
return self._info.get("color_temp", -1)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def brightness(self) -> int:
|
||||||
|
"""Return the current brightness in percentage."""
|
||||||
|
if not self.is_dimmable: # pragma: no cover
|
||||||
|
raise SmartDeviceException("Bulb is not dimmable.")
|
||||||
|
|
||||||
|
return self._info.get("brightness", -1)
|
||||||
|
|
||||||
|
async def set_hsv(
|
||||||
|
self,
|
||||||
|
hue: int,
|
||||||
|
saturation: int,
|
||||||
|
value: Optional[int] = None,
|
||||||
|
*,
|
||||||
|
transition: Optional[int] = None,
|
||||||
|
) -> Dict:
|
||||||
|
"""Set new HSV.
|
||||||
|
|
||||||
|
Note, transition is not supported and will be ignored.
|
||||||
|
|
||||||
|
:param int hue: hue in degrees
|
||||||
|
:param int saturation: saturation in percentage [0,100]
|
||||||
|
:param int value: value in percentage [0, 100]
|
||||||
|
:param int transition: transition in milliseconds.
|
||||||
|
"""
|
||||||
|
if not self.is_color:
|
||||||
|
raise SmartDeviceException("Bulb does not support color.")
|
||||||
|
|
||||||
|
if not isinstance(hue, int) or not (0 <= hue <= 360):
|
||||||
|
raise ValueError(f"Invalid hue value: {hue} (valid range: 0-360)")
|
||||||
|
|
||||||
|
if not isinstance(saturation, int) or not (0 <= saturation <= 100):
|
||||||
|
raise ValueError(
|
||||||
|
f"Invalid saturation value: {saturation} (valid range: 0-100%)"
|
||||||
|
)
|
||||||
|
|
||||||
|
if value is not None:
|
||||||
|
self._raise_for_invalid_brightness(value)
|
||||||
|
|
||||||
|
return await self.protocol.query(
|
||||||
|
{
|
||||||
|
"set_device_info": {
|
||||||
|
"hue": hue,
|
||||||
|
"saturation": saturation,
|
||||||
|
"brightness": value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
async def set_color_temp(
|
||||||
|
self, temp: int, *, brightness=None, transition: Optional[int] = None
|
||||||
|
) -> Dict:
|
||||||
|
"""Set the color temperature of the device in kelvin.
|
||||||
|
|
||||||
|
Note, transition is not supported and will be ignored.
|
||||||
|
|
||||||
|
:param int temp: The new color temperature, in Kelvin
|
||||||
|
:param int transition: transition in milliseconds.
|
||||||
|
"""
|
||||||
|
# TODO: Note, trying to set brightness at the same time
|
||||||
|
# with color_temp causes error -1008
|
||||||
|
if not self.is_variable_color_temp:
|
||||||
|
raise SmartDeviceException("Bulb does not support colortemp.")
|
||||||
|
|
||||||
|
valid_temperature_range = self.valid_temperature_range
|
||||||
|
if temp < valid_temperature_range[0] or temp > valid_temperature_range[1]:
|
||||||
|
raise ValueError(
|
||||||
|
"Temperature should be between {} and {}, was {}".format(
|
||||||
|
*valid_temperature_range, temp
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return await self.protocol.query({"set_device_info": {"color_temp": temp}})
|
||||||
|
|
||||||
|
async def set_brightness(
|
||||||
|
self, brightness: int, *, transition: Optional[int] = None
|
||||||
|
) -> Dict:
|
||||||
|
"""Set the brightness in percentage.
|
||||||
|
|
||||||
|
Note, transition is not supported and will be ignored.
|
||||||
|
|
||||||
|
:param int brightness: brightness in percent
|
||||||
|
:param int transition: transition in milliseconds.
|
||||||
|
"""
|
||||||
|
if not self.is_dimmable: # pragma: no cover
|
||||||
|
raise SmartDeviceException("Bulb is not dimmable.")
|
||||||
|
|
||||||
|
return await self.protocol.query(
|
||||||
|
{"set_device_info": {"brightness": brightness}}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Default state information, should be made to settings
|
||||||
|
"""
|
||||||
|
"info": {
|
||||||
|
"default_states": {
|
||||||
|
"re_power_type": "always_on",
|
||||||
|
"type": "last_states",
|
||||||
|
"state": {
|
||||||
|
"brightness": 36,
|
||||||
|
"hue": 0,
|
||||||
|
"saturation": 0,
|
||||||
|
"color_temp": 2700,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def set_effect(
|
||||||
|
self,
|
||||||
|
effect: str,
|
||||||
|
*,
|
||||||
|
brightness: Optional[int] = None,
|
||||||
|
transition: Optional[int] = None,
|
||||||
|
) -> None:
|
||||||
|
"""Set an effect on the device."""
|
||||||
|
raise NotImplementedError()
|
||||||
|
# TODO: the code below does to activate the effect but gives no error
|
||||||
|
return await self.protocol.query(
|
||||||
|
{
|
||||||
|
"set_device_info": {
|
||||||
|
"dynamic_light_effect_enable": 1,
|
||||||
|
"dynamic_light_effect_id": effect,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
@property # type: ignore
|
||||||
|
def state_information(self) -> Dict[str, Any]:
|
||||||
|
"""Return bulb-specific state information."""
|
||||||
|
info: Dict[str, Any] = {
|
||||||
|
# TODO: re-enable after we don't inherit from smartbulb
|
||||||
|
# **super().state_information
|
||||||
|
"Brightness": self.brightness,
|
||||||
|
"Is dimmable": self.is_dimmable,
|
||||||
|
}
|
||||||
|
if self.is_variable_color_temp:
|
||||||
|
info["Color temperature"] = self.color_temp
|
||||||
|
info["Valid temperature range"] = self.valid_temperature_range
|
||||||
|
if self.is_color:
|
||||||
|
info["HSV"] = self.hsv
|
||||||
|
info["Presets"] = self.presets
|
||||||
|
|
||||||
|
return info
|
||||||
|
|
||||||
|
@property
|
||||||
|
def presets(self) -> List[SmartBulbPreset]:
|
||||||
|
"""Return a list of available bulb setting presets."""
|
||||||
|
return []
|
@ -6,7 +6,7 @@ from dataclasses import dataclass
|
|||||||
from json import dumps as json_dumps
|
from json import dumps as json_dumps
|
||||||
from os.path import basename
|
from os.path import basename
|
||||||
from pathlib import Path, PurePath
|
from pathlib import Path, PurePath
|
||||||
from typing import Dict, Optional
|
from typing import Dict, Optional, Set
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import pytest # type: ignore # see https://github.com/pytest-dev/pytest/issues/3342
|
import pytest # type: ignore # see https://github.com/pytest-dev/pytest/issues/3342
|
||||||
@ -21,7 +21,7 @@ from kasa import (
|
|||||||
SmartStrip,
|
SmartStrip,
|
||||||
TPLinkSmartHomeProtocol,
|
TPLinkSmartHomeProtocol,
|
||||||
)
|
)
|
||||||
from kasa.tapo import TapoDevice, TapoPlug
|
from kasa.tapo import TapoBulb, TapoDevice, TapoPlug
|
||||||
|
|
||||||
from .newfakes import FakeSmartProtocol, FakeTransportProtocol
|
from .newfakes import FakeSmartProtocol, FakeTransportProtocol
|
||||||
|
|
||||||
@ -42,19 +42,44 @@ SUPPORTED_SMART_DEVICES = [
|
|||||||
|
|
||||||
SUPPORTED_DEVICES = SUPPORTED_IOT_DEVICES + SUPPORTED_SMART_DEVICES
|
SUPPORTED_DEVICES = SUPPORTED_IOT_DEVICES + SUPPORTED_SMART_DEVICES
|
||||||
|
|
||||||
|
# Tapo bulbs
|
||||||
|
BULBS_SMART_VARIABLE_TEMP = {"L530"}
|
||||||
|
BULBS_SMART_COLOR = {"L530"}
|
||||||
|
BULBS_SMART_LIGHT_STRIP: Set[str] = set()
|
||||||
|
BULBS_SMART_DIMMABLE: Set[str] = set()
|
||||||
|
BULBS_SMART = (
|
||||||
|
BULBS_SMART_VARIABLE_TEMP.union(BULBS_SMART_COLOR)
|
||||||
|
.union(BULBS_SMART_DIMMABLE)
|
||||||
|
.union(BULBS_SMART_LIGHT_STRIP)
|
||||||
|
)
|
||||||
|
|
||||||
LIGHT_STRIPS = {"KL400", "KL430", "KL420"}
|
# Kasa (IOT-prefixed) bulbs
|
||||||
VARIABLE_TEMP = {"LB120", "LB130", "KL120", "KL125", "KL130", "KL135", "KL430"}
|
BULBS_IOT_LIGHT_STRIP = {"KL400", "KL430", "KL420"}
|
||||||
COLOR_BULBS = {"LB130", "KL125", "KL130", "KL135", *LIGHT_STRIPS}
|
BULBS_IOT_VARIABLE_TEMP = {
|
||||||
|
"LB120",
|
||||||
|
"LB130",
|
||||||
|
"KL120",
|
||||||
|
"KL125",
|
||||||
|
"KL130",
|
||||||
|
"KL135",
|
||||||
|
"KL430",
|
||||||
|
}
|
||||||
|
BULBS_IOT_COLOR = {"LB130", "KL125", "KL130", "KL135", *BULBS_IOT_LIGHT_STRIP}
|
||||||
|
BULBS_IOT_DIMMABLE = {"KL50", "KL60", "LB100", "LB110", "KL110"}
|
||||||
|
BULBS_IOT = (
|
||||||
|
BULBS_IOT_VARIABLE_TEMP.union(BULBS_IOT_COLOR)
|
||||||
|
.union(BULBS_IOT_DIMMABLE)
|
||||||
|
.union(BULBS_IOT_LIGHT_STRIP)
|
||||||
|
)
|
||||||
|
|
||||||
|
BULBS_VARIABLE_TEMP = {*BULBS_SMART_VARIABLE_TEMP, *BULBS_IOT_VARIABLE_TEMP}
|
||||||
|
BULBS_COLOR = {*BULBS_SMART_COLOR, *BULBS_IOT_COLOR}
|
||||||
|
|
||||||
|
|
||||||
|
LIGHT_STRIPS = {*BULBS_SMART_LIGHT_STRIP, *BULBS_IOT_LIGHT_STRIP}
|
||||||
BULBS = {
|
BULBS = {
|
||||||
"KL50",
|
*BULBS_IOT,
|
||||||
"KL60",
|
*BULBS_SMART,
|
||||||
"LB100",
|
|
||||||
"LB110",
|
|
||||||
"KL110",
|
|
||||||
*VARIABLE_TEMP,
|
|
||||||
*COLOR_BULBS,
|
|
||||||
*LIGHT_STRIPS,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -83,7 +108,7 @@ WITH_EMETER = {"HS110", "HS300", "KP115", "KP125", *BULBS}
|
|||||||
ALL_DEVICES_IOT = BULBS.union(PLUGS).union(STRIPS).union(DIMMERS)
|
ALL_DEVICES_IOT = BULBS.union(PLUGS).union(STRIPS).union(DIMMERS)
|
||||||
|
|
||||||
PLUGS_SMART = {"P110"}
|
PLUGS_SMART = {"P110"}
|
||||||
ALL_DEVICES_SMART = PLUGS_SMART
|
ALL_DEVICES_SMART = BULBS_SMART.union(PLUGS_SMART)
|
||||||
|
|
||||||
ALL_DEVICES = ALL_DEVICES_IOT.union(ALL_DEVICES_SMART)
|
ALL_DEVICES = ALL_DEVICES_IOT.union(ALL_DEVICES_SMART)
|
||||||
|
|
||||||
@ -91,9 +116,12 @@ IP_MODEL_CACHE: Dict[str, str] = {}
|
|||||||
|
|
||||||
|
|
||||||
def idgenerator(paramtuple):
|
def idgenerator(paramtuple):
|
||||||
|
try:
|
||||||
return basename(paramtuple[0]) + (
|
return basename(paramtuple[0]) + (
|
||||||
"" if paramtuple[1] == "IOT" else "-" + paramtuple[1]
|
"" if paramtuple[1] == "IOT" else "-" + paramtuple[1]
|
||||||
)
|
)
|
||||||
|
except: # TODO: HACK as idgenerator is now used by default # noqa: E722
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def filter_model(desc, model_filter, protocol_filter=None):
|
def filter_model(desc, model_filter, protocol_filter=None):
|
||||||
@ -108,11 +136,15 @@ def filter_model(desc, model_filter, protocol_filter=None):
|
|||||||
filtered.append((file, protocol))
|
filtered.append((file, protocol))
|
||||||
|
|
||||||
filtered_basenames = [basename(f) + "-" + p for f, p in filtered]
|
filtered_basenames = [basename(f) + "-" + p for f, p in filtered]
|
||||||
print(f"{desc}: {filtered_basenames}")
|
print(f"# {desc}")
|
||||||
|
for file in filtered_basenames:
|
||||||
|
print(f"\t{file}")
|
||||||
return filtered
|
return filtered
|
||||||
|
|
||||||
|
|
||||||
def parametrize(desc, devices, protocol_filter=None, ids=None):
|
def parametrize(desc, devices, protocol_filter=None, ids=None):
|
||||||
|
if ids is None:
|
||||||
|
ids = idgenerator
|
||||||
return pytest.mark.parametrize(
|
return pytest.mark.parametrize(
|
||||||
"dev", filter_model(desc, devices, protocol_filter), indirect=True, ids=ids
|
"dev", filter_model(desc, devices, protocol_filter), indirect=True, ids=ids
|
||||||
)
|
)
|
||||||
@ -121,29 +153,34 @@ def parametrize(desc, devices, protocol_filter=None, ids=None):
|
|||||||
has_emeter = parametrize("has emeter", WITH_EMETER)
|
has_emeter = parametrize("has emeter", WITH_EMETER)
|
||||||
no_emeter = parametrize("no emeter", ALL_DEVICES_IOT - WITH_EMETER)
|
no_emeter = parametrize("no emeter", ALL_DEVICES_IOT - WITH_EMETER)
|
||||||
|
|
||||||
bulb = parametrize("bulbs", BULBS, ids=idgenerator)
|
bulb = parametrize("bulbs", BULBS, protocol_filter={"SMART", "IOT"})
|
||||||
plug = parametrize("plugs", PLUGS, ids=idgenerator)
|
plug = parametrize("plugs", PLUGS)
|
||||||
strip = parametrize("strips", STRIPS, ids=idgenerator)
|
strip = parametrize("strips", STRIPS)
|
||||||
dimmer = parametrize("dimmers", DIMMERS, ids=idgenerator)
|
dimmer = parametrize("dimmers", DIMMERS)
|
||||||
lightstrip = parametrize("lightstrips", LIGHT_STRIPS, ids=idgenerator)
|
lightstrip = parametrize("lightstrips", LIGHT_STRIPS)
|
||||||
|
|
||||||
# bulb types
|
# bulb types
|
||||||
dimmable = parametrize("dimmable", DIMMABLE)
|
dimmable = parametrize("dimmable", DIMMABLE)
|
||||||
non_dimmable = parametrize("non-dimmable", BULBS - DIMMABLE)
|
non_dimmable = parametrize("non-dimmable", BULBS - DIMMABLE)
|
||||||
variable_temp = parametrize("variable color temp", VARIABLE_TEMP)
|
variable_temp = parametrize(
|
||||||
non_variable_temp = parametrize("non-variable color temp", BULBS - VARIABLE_TEMP)
|
"variable color temp", BULBS_VARIABLE_TEMP, {"SMART", "IOT"}
|
||||||
color_bulb = parametrize("color bulbs", COLOR_BULBS)
|
)
|
||||||
non_color_bulb = parametrize("non-color bulbs", BULBS - COLOR_BULBS)
|
non_variable_temp = parametrize(
|
||||||
|
"non-variable color temp", BULBS - BULBS_VARIABLE_TEMP, {"SMART", "IOT"}
|
||||||
|
)
|
||||||
|
color_bulb = parametrize("color bulbs", BULBS_COLOR, {"SMART", "IOT"})
|
||||||
|
non_color_bulb = parametrize("non-color bulbs", BULBS - BULBS_COLOR, {"SMART", "IOT"})
|
||||||
|
|
||||||
plug_smart = parametrize(
|
color_bulb_iot = parametrize("color bulbs iot", BULBS_COLOR, {"IOT"})
|
||||||
"plug devices smart", PLUGS_SMART, protocol_filter={"SMART"}, ids=idgenerator
|
variable_temp_iot = parametrize("variable color temp iot", BULBS_VARIABLE_TEMP, {"IOT"})
|
||||||
)
|
bulb_iot = parametrize("bulb devices iot", BULBS_IOT)
|
||||||
|
|
||||||
|
plug_smart = parametrize("plug devices smart", PLUGS_SMART, protocol_filter={"SMART"})
|
||||||
|
bulb_smart = parametrize("bulb devices smart", BULBS_SMART, protocol_filter={"SMART"})
|
||||||
device_smart = parametrize(
|
device_smart = parametrize(
|
||||||
"devices smart", ALL_DEVICES_SMART, protocol_filter={"SMART"}, ids=idgenerator
|
"devices smart", ALL_DEVICES_SMART, protocol_filter={"SMART"}
|
||||||
)
|
|
||||||
device_iot = parametrize(
|
|
||||||
"devices iot", ALL_DEVICES_IOT, protocol_filter={"IOT"}, ids=idgenerator
|
|
||||||
)
|
)
|
||||||
|
device_iot = parametrize("devices iot", ALL_DEVICES_IOT, protocol_filter={"IOT"})
|
||||||
|
|
||||||
|
|
||||||
def get_fixture_data():
|
def get_fixture_data():
|
||||||
@ -197,6 +234,7 @@ def check_categories():
|
|||||||
+ bulb.args[1]
|
+ bulb.args[1]
|
||||||
+ lightstrip.args[1]
|
+ lightstrip.args[1]
|
||||||
+ plug_smart.args[1]
|
+ plug_smart.args[1]
|
||||||
|
+ bulb_smart.args[1]
|
||||||
)
|
)
|
||||||
diff = set(SUPPORTED_DEVICES) - set(categorized_fixtures)
|
diff = set(SUPPORTED_DEVICES) - set(categorized_fixtures)
|
||||||
if diff:
|
if diff:
|
||||||
@ -225,6 +263,9 @@ def device_for_file(model, protocol):
|
|||||||
for d in PLUGS_SMART:
|
for d in PLUGS_SMART:
|
||||||
if d in model:
|
if d in model:
|
||||||
return TapoPlug
|
return TapoPlug
|
||||||
|
for d in BULBS_SMART:
|
||||||
|
if d in model:
|
||||||
|
return TapoBulb
|
||||||
else:
|
else:
|
||||||
for d in STRIPS:
|
for d in STRIPS:
|
||||||
if d in model:
|
if d in model:
|
||||||
@ -235,7 +276,7 @@ def device_for_file(model, protocol):
|
|||||||
return SmartPlug
|
return SmartPlug
|
||||||
|
|
||||||
# Light strips are recognized also as bulbs, so this has to go first
|
# Light strips are recognized also as bulbs, so this has to go first
|
||||||
for d in LIGHT_STRIPS:
|
for d in BULBS_IOT_LIGHT_STRIP:
|
||||||
if d in model:
|
if d in model:
|
||||||
return SmartLightStrip
|
return SmartLightStrip
|
||||||
|
|
||||||
|
186
kasa/tests/fixtures/smart/L530E(EU)_3.0_1.0.6.json
vendored
Normal file
186
kasa/tests/fixtures/smart/L530E(EU)_3.0_1.0.6.json
vendored
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
{
|
||||||
|
"component_nego": {
|
||||||
|
"component_list": [
|
||||||
|
{
|
||||||
|
"id": "device",
|
||||||
|
"ver_code": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "firmware",
|
||||||
|
"ver_code": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "quick_setup",
|
||||||
|
"ver_code": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "inherit",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "time",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wireless",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "schedule",
|
||||||
|
"ver_code": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "countdown",
|
||||||
|
"ver_code": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "antitheft",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "account",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "synchronize",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "sunrise_sunset",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cloud_connect",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "default_states",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "preset",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "brightness",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "color",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "color_temperature",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "auto_light",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "on_off_gradually",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "device_local_time",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "light_effect",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "iot_cloud",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "bulb_quick_control",
|
||||||
|
"ver_code": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"discovery_result": {
|
||||||
|
"device_id": "00000000000000000000000000000000",
|
||||||
|
"device_model": "L530E(EU)",
|
||||||
|
"device_type": "SMART.TAPOBULB",
|
||||||
|
"factory_default": false,
|
||||||
|
"ip": "127.0.0.123",
|
||||||
|
"is_support_iot_cloud": true,
|
||||||
|
"mac": "00-00-00-00-00-00",
|
||||||
|
"mgt_encrypt_schm": {
|
||||||
|
"encrypt_type": "AES",
|
||||||
|
"http_port": 80,
|
||||||
|
"is_support_https": false,
|
||||||
|
"lv": 2
|
||||||
|
},
|
||||||
|
"obd_src": "tplink",
|
||||||
|
"owner": "00000000000000000000000000000000"
|
||||||
|
},
|
||||||
|
"get_device_info": {
|
||||||
|
"avatar": "bulb",
|
||||||
|
"brightness": 100,
|
||||||
|
"color_temp": 2500,
|
||||||
|
"color_temp_range": [
|
||||||
|
2500,
|
||||||
|
6500
|
||||||
|
],
|
||||||
|
"default_states": {
|
||||||
|
"re_power_type": "always_on",
|
||||||
|
"state": {
|
||||||
|
"brightness": 100,
|
||||||
|
"color_temp": 2500,
|
||||||
|
"hue": 0,
|
||||||
|
"saturation": 100
|
||||||
|
},
|
||||||
|
"type": "last_states"
|
||||||
|
},
|
||||||
|
"device_id": "0000000000000000000000000000000000000000",
|
||||||
|
"device_on": true,
|
||||||
|
"dynamic_light_effect_enable": false,
|
||||||
|
"fw_id": "00000000000000000000000000000000",
|
||||||
|
"fw_ver": "1.0.6 Build 230509 Rel.195312",
|
||||||
|
"has_set_location_info": true,
|
||||||
|
"hue": 0,
|
||||||
|
"hw_id": "00000000000000000000000000000000",
|
||||||
|
"hw_ver": "3.0",
|
||||||
|
"ip": "127.0.0.123",
|
||||||
|
"lang": "de_DE",
|
||||||
|
"latitude": 0,
|
||||||
|
"longitude": 0,
|
||||||
|
"mac": "00-00-00-00-00-00",
|
||||||
|
"model": "L530",
|
||||||
|
"nickname": "c21hcnRlIFdMQU4tR2zDvGhiaXJuZQ==",
|
||||||
|
"oem_id": "00000000000000000000000000000000",
|
||||||
|
"overheated": false,
|
||||||
|
"region": "Europe/Berlin",
|
||||||
|
"rssi": -38,
|
||||||
|
"saturation": 100,
|
||||||
|
"signal_level": 3,
|
||||||
|
"specs": "",
|
||||||
|
"ssid": "IyNNQVNLRUROQU1FIyM=",
|
||||||
|
"time_diff": 60,
|
||||||
|
"type": "SMART.TAPOBULB"
|
||||||
|
},
|
||||||
|
"get_device_time": {
|
||||||
|
"region": "Europe/Berlin",
|
||||||
|
"time_diff": 60,
|
||||||
|
"timestamp": 1701618972
|
||||||
|
},
|
||||||
|
"get_device_usage": {
|
||||||
|
"power_usage": {
|
||||||
|
"past30": 107,
|
||||||
|
"past7": 107,
|
||||||
|
"today": 7
|
||||||
|
},
|
||||||
|
"saved_power": {
|
||||||
|
"past30": 535,
|
||||||
|
"past7": 535,
|
||||||
|
"today": 41
|
||||||
|
},
|
||||||
|
"time_usage": {
|
||||||
|
"past30": 642,
|
||||||
|
"past7": 642,
|
||||||
|
"today": 48
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -313,10 +313,13 @@ class FakeSmartTransport(BaseTransport):
|
|||||||
async def send(self, request: str):
|
async def send(self, request: str):
|
||||||
request_dict = json_loads(request)
|
request_dict = json_loads(request)
|
||||||
method = request_dict["method"]
|
method = request_dict["method"]
|
||||||
|
params = request_dict["params"]
|
||||||
if method == "component_nego" or method[:4] == "get_":
|
if method == "component_nego" or method[:4] == "get_":
|
||||||
return self.info[method]
|
return {"result": self.info[method]}
|
||||||
elif method[:4] == "set_":
|
elif method[:4] == "set_":
|
||||||
_LOGGER.debug("Call %s not implemented, doing nothing", method)
|
target_method = f"get_{method[4:]}"
|
||||||
|
self.info[target_method].update(params)
|
||||||
|
return {"result": ""}
|
||||||
|
|
||||||
async def close(self) -> None:
|
async def close(self) -> None:
|
||||||
pass
|
pass
|
||||||
|
@ -4,7 +4,9 @@ from kasa import DeviceType, SmartBulb, SmartBulbPreset, SmartDeviceException
|
|||||||
|
|
||||||
from .conftest import (
|
from .conftest import (
|
||||||
bulb,
|
bulb,
|
||||||
|
bulb_iot,
|
||||||
color_bulb,
|
color_bulb,
|
||||||
|
color_bulb_iot,
|
||||||
dimmable,
|
dimmable,
|
||||||
handle_turn_on,
|
handle_turn_on,
|
||||||
non_color_bulb,
|
non_color_bulb,
|
||||||
@ -12,6 +14,7 @@ from .conftest import (
|
|||||||
non_variable_temp,
|
non_variable_temp,
|
||||||
turn_on,
|
turn_on,
|
||||||
variable_temp,
|
variable_temp,
|
||||||
|
variable_temp_iot,
|
||||||
)
|
)
|
||||||
from .newfakes import BULB_SCHEMA, LIGHT_STATE_SCHEMA
|
from .newfakes import BULB_SCHEMA, LIGHT_STATE_SCHEMA
|
||||||
|
|
||||||
@ -38,7 +41,7 @@ async def test_state_attributes(dev: SmartBulb):
|
|||||||
assert dev.state_information["Is dimmable"] == dev.is_dimmable
|
assert dev.state_information["Is dimmable"] == dev.is_dimmable
|
||||||
|
|
||||||
|
|
||||||
@bulb
|
@bulb_iot
|
||||||
async def test_light_state_without_update(dev: SmartBulb, monkeypatch):
|
async def test_light_state_without_update(dev: SmartBulb, monkeypatch):
|
||||||
with pytest.raises(SmartDeviceException):
|
with pytest.raises(SmartDeviceException):
|
||||||
monkeypatch.setitem(
|
monkeypatch.setitem(
|
||||||
@ -47,7 +50,7 @@ async def test_light_state_without_update(dev: SmartBulb, monkeypatch):
|
|||||||
print(dev.light_state)
|
print(dev.light_state)
|
||||||
|
|
||||||
|
|
||||||
@bulb
|
@bulb_iot
|
||||||
async def test_get_light_state(dev: SmartBulb):
|
async def test_get_light_state(dev: SmartBulb):
|
||||||
LIGHT_STATE_SCHEMA(await dev.get_light_state())
|
LIGHT_STATE_SCHEMA(await dev.get_light_state())
|
||||||
|
|
||||||
@ -72,7 +75,7 @@ async def test_hsv(dev: SmartBulb, turn_on):
|
|||||||
assert brightness == 1
|
assert brightness == 1
|
||||||
|
|
||||||
|
|
||||||
@color_bulb
|
@color_bulb_iot
|
||||||
async def test_set_hsv_transition(dev: SmartBulb, mocker):
|
async def test_set_hsv_transition(dev: SmartBulb, mocker):
|
||||||
set_light_state = mocker.patch("kasa.SmartBulb.set_light_state")
|
set_light_state = mocker.patch("kasa.SmartBulb.set_light_state")
|
||||||
await dev.set_hsv(10, 10, 100, transition=1000)
|
await dev.set_hsv(10, 10, 100, transition=1000)
|
||||||
@ -138,7 +141,7 @@ async def test_try_set_colortemp(dev: SmartBulb, turn_on):
|
|||||||
assert dev.color_temp == 2700
|
assert dev.color_temp == 2700
|
||||||
|
|
||||||
|
|
||||||
@variable_temp
|
@variable_temp_iot
|
||||||
async def test_set_color_temp_transition(dev: SmartBulb, mocker):
|
async def test_set_color_temp_transition(dev: SmartBulb, mocker):
|
||||||
set_light_state = mocker.patch("kasa.SmartBulb.set_light_state")
|
set_light_state = mocker.patch("kasa.SmartBulb.set_light_state")
|
||||||
await dev.set_color_temp(2700, transition=100)
|
await dev.set_color_temp(2700, transition=100)
|
||||||
@ -146,7 +149,7 @@ async def test_set_color_temp_transition(dev: SmartBulb, mocker):
|
|||||||
set_light_state.assert_called_with({"color_temp": 2700}, transition=100)
|
set_light_state.assert_called_with({"color_temp": 2700}, transition=100)
|
||||||
|
|
||||||
|
|
||||||
@variable_temp
|
@variable_temp_iot
|
||||||
async def test_unknown_temp_range(dev: SmartBulb, monkeypatch, caplog):
|
async def test_unknown_temp_range(dev: SmartBulb, monkeypatch, caplog):
|
||||||
monkeypatch.setitem(dev._sys_info, "model", "unknown bulb")
|
monkeypatch.setitem(dev._sys_info, "model", "unknown bulb")
|
||||||
|
|
||||||
@ -192,7 +195,7 @@ async def test_dimmable_brightness(dev: SmartBulb, turn_on):
|
|||||||
await dev.set_brightness("foo")
|
await dev.set_brightness("foo")
|
||||||
|
|
||||||
|
|
||||||
@bulb
|
@bulb_iot
|
||||||
async def test_turn_on_transition(dev: SmartBulb, mocker):
|
async def test_turn_on_transition(dev: SmartBulb, mocker):
|
||||||
set_light_state = mocker.patch("kasa.SmartBulb.set_light_state")
|
set_light_state = mocker.patch("kasa.SmartBulb.set_light_state")
|
||||||
await dev.turn_on(transition=1000)
|
await dev.turn_on(transition=1000)
|
||||||
@ -204,7 +207,7 @@ async def test_turn_on_transition(dev: SmartBulb, mocker):
|
|||||||
set_light_state.assert_called_with({"on_off": 0}, transition=100)
|
set_light_state.assert_called_with({"on_off": 0}, transition=100)
|
||||||
|
|
||||||
|
|
||||||
@bulb
|
@bulb_iot
|
||||||
async def test_dimmable_brightness_transition(dev: SmartBulb, mocker):
|
async def test_dimmable_brightness_transition(dev: SmartBulb, mocker):
|
||||||
set_light_state = mocker.patch("kasa.SmartBulb.set_light_state")
|
set_light_state = mocker.patch("kasa.SmartBulb.set_light_state")
|
||||||
await dev.set_brightness(10, transition=1000)
|
await dev.set_brightness(10, transition=1000)
|
||||||
@ -233,7 +236,7 @@ async def test_non_dimmable(dev: SmartBulb):
|
|||||||
await dev.set_brightness(100)
|
await dev.set_brightness(100)
|
||||||
|
|
||||||
|
|
||||||
@bulb
|
@bulb_iot
|
||||||
async def test_ignore_default_not_set_without_color_mode_change_turn_on(
|
async def test_ignore_default_not_set_without_color_mode_change_turn_on(
|
||||||
dev: SmartBulb, mocker
|
dev: SmartBulb, mocker
|
||||||
):
|
):
|
||||||
@ -248,7 +251,7 @@ async def test_ignore_default_not_set_without_color_mode_change_turn_on(
|
|||||||
assert args[2] == {"on_off": 0, "ignore_default": 1}
|
assert args[2] == {"on_off": 0, "ignore_default": 1}
|
||||||
|
|
||||||
|
|
||||||
@bulb
|
@bulb_iot
|
||||||
async def test_list_presets(dev: SmartBulb):
|
async def test_list_presets(dev: SmartBulb):
|
||||||
presets = dev.presets
|
presets = dev.presets
|
||||||
assert len(presets) == len(dev.sys_info["preferred_state"])
|
assert len(presets) == len(dev.sys_info["preferred_state"])
|
||||||
@ -261,7 +264,7 @@ async def test_list_presets(dev: SmartBulb):
|
|||||||
assert preset.color_temp == raw["color_temp"]
|
assert preset.color_temp == raw["color_temp"]
|
||||||
|
|
||||||
|
|
||||||
@bulb
|
@bulb_iot
|
||||||
async def test_modify_preset(dev: SmartBulb, mocker):
|
async def test_modify_preset(dev: SmartBulb, mocker):
|
||||||
"""Verify that modifying preset calls the and exceptions are raised properly."""
|
"""Verify that modifying preset calls the and exceptions are raised properly."""
|
||||||
if not dev.presets:
|
if not dev.presets:
|
||||||
@ -291,7 +294,7 @@ async def test_modify_preset(dev: SmartBulb, mocker):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@bulb
|
@bulb_iot
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("preset", "payload"),
|
("preset", "payload"),
|
||||||
[
|
[
|
||||||
|
@ -15,7 +15,7 @@ from kasa import (
|
|||||||
from kasa.discover import DiscoveryResult, _DiscoverProtocol, json_dumps
|
from kasa.discover import DiscoveryResult, _DiscoverProtocol, json_dumps
|
||||||
from kasa.exceptions import AuthenticationException, UnsupportedDeviceException
|
from kasa.exceptions import AuthenticationException, UnsupportedDeviceException
|
||||||
|
|
||||||
from .conftest import bulb, dimmer, lightstrip, plug, strip
|
from .conftest import bulb, bulb_iot, dimmer, lightstrip, plug, strip
|
||||||
|
|
||||||
UNSUPPORTED = {
|
UNSUPPORTED = {
|
||||||
"result": {
|
"result": {
|
||||||
@ -46,7 +46,7 @@ async def test_type_detection_plug(dev: SmartDevice):
|
|||||||
assert d.device_type == DeviceType.Plug
|
assert d.device_type == DeviceType.Plug
|
||||||
|
|
||||||
|
|
||||||
@bulb
|
@bulb_iot
|
||||||
async def test_type_detection_bulb(dev: SmartDevice):
|
async def test_type_detection_bulb(dev: SmartDevice):
|
||||||
d = Discover._get_device_class(dev._last_update)("localhost")
|
d = Discover._get_device_class(dev._last_update)("localhost")
|
||||||
# TODO: light_strip is a special case for now to force bulb tests on it
|
# TODO: light_strip is a special case for now to force bulb tests on it
|
||||||
|
Loading…
Reference in New Issue
Block a user