2020-04-18 21:35:39 +00:00
|
|
|
"""Module for dimmers (currently only HS220)."""
|
2024-04-16 18:21:20 +00:00
|
|
|
|
2024-04-17 13:39:24 +00:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2023-05-17 18:10:39 +00:00
|
|
|
from enum import Enum
|
2024-04-17 13:39:24 +00:00
|
|
|
from typing import Any
|
2020-04-18 21:35:39 +00:00
|
|
|
|
2024-02-04 15:20:08 +00:00
|
|
|
from ..device_type import DeviceType
|
|
|
|
from ..deviceconfig import DeviceConfig
|
2024-04-24 16:38:52 +00:00
|
|
|
from ..feature import Feature
|
2024-02-04 15:20:08 +00:00
|
|
|
from ..protocol import BaseProtocol
|
2024-02-21 15:52:55 +00:00
|
|
|
from .iotdevice import KasaException, requires_update
|
2024-02-04 15:20:08 +00:00
|
|
|
from .iotplug import IotPlug
|
|
|
|
from .modules import AmbientLight, Motion
|
2020-04-18 21:35:39 +00:00
|
|
|
|
|
|
|
|
2023-05-17 18:10:39 +00:00
|
|
|
class ButtonAction(Enum):
|
|
|
|
"""Button action."""
|
|
|
|
|
|
|
|
NoAction = "none"
|
|
|
|
Instant = "instant_on_off"
|
|
|
|
Gentle = "gentle_on_off"
|
|
|
|
Preset = "customize_preset"
|
|
|
|
|
|
|
|
|
|
|
|
class ActionType(Enum):
|
|
|
|
"""Button action."""
|
|
|
|
|
|
|
|
DoubleClick = "double_click_action"
|
|
|
|
LongPress = "long_press_action"
|
|
|
|
|
|
|
|
|
|
|
|
class FadeType(Enum):
|
|
|
|
"""Fade on/off setting."""
|
|
|
|
|
|
|
|
FadeOn = "fade_on"
|
|
|
|
FadeOff = "fade_off"
|
|
|
|
|
|
|
|
|
2024-02-04 15:20:08 +00:00
|
|
|
class IotDimmer(IotPlug):
|
2022-04-05 23:41:08 +00:00
|
|
|
r"""Representation of a TP-Link Smart Dimmer.
|
2020-04-18 21:35:39 +00:00
|
|
|
|
|
|
|
Dimmers work similarly to plugs, but provide also support for
|
2020-06-30 00:29:52 +00:00
|
|
|
adjusting the brightness. This class extends :class:`SmartPlug` interface.
|
2020-04-18 21:35:39 +00:00
|
|
|
|
2020-06-30 00:29:52 +00:00
|
|
|
To initialize, you have to await :func:`update()` at least once.
|
|
|
|
This will allow accessing the properties using the exposed properties.
|
2020-04-18 21:35:39 +00:00
|
|
|
|
2020-06-30 00:29:52 +00:00
|
|
|
All changes to the device are done using awaitable methods,
|
2023-10-29 22:15:42 +00:00
|
|
|
which will not change the cached values,
|
|
|
|
but you must await :func:`update()` separately.
|
2020-04-18 21:35:39 +00:00
|
|
|
|
2024-02-21 15:52:55 +00:00
|
|
|
Errors reported by the device are raised as :class:`KasaException`\s,
|
2020-06-30 00:29:52 +00:00
|
|
|
and should be handled by the user of the library.
|
|
|
|
|
|
|
|
Examples:
|
|
|
|
>>> import asyncio
|
2024-02-04 15:20:08 +00:00
|
|
|
>>> dimmer = IotDimmer("192.168.1.105")
|
2020-06-30 00:29:52 +00:00
|
|
|
>>> asyncio.run(dimmer.turn_on())
|
|
|
|
>>> dimmer.brightness
|
|
|
|
25
|
|
|
|
|
|
|
|
>>> asyncio.run(dimmer.set_brightness(50))
|
|
|
|
>>> asyncio.run(dimmer.update())
|
|
|
|
>>> dimmer.brightness
|
|
|
|
50
|
|
|
|
|
|
|
|
Refer to :class:`SmartPlug` for the full API.
|
2020-04-18 21:35:39 +00:00
|
|
|
"""
|
|
|
|
|
2020-06-14 16:09:28 +00:00
|
|
|
DIMMER_SERVICE = "smartlife.iot.dimmer"
|
|
|
|
|
2023-09-13 13:46:38 +00:00
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
host: str,
|
|
|
|
*,
|
2024-04-17 13:39:24 +00:00
|
|
|
config: DeviceConfig | None = None,
|
|
|
|
protocol: BaseProtocol | None = None,
|
2023-09-13 13:46:38 +00:00
|
|
|
) -> None:
|
2023-12-29 19:17:15 +00:00
|
|
|
super().__init__(host=host, config=config, protocol=protocol)
|
2020-04-18 21:35:39 +00:00
|
|
|
self._device_type = DeviceType.Dimmer
|
2022-01-29 16:53:18 +00:00
|
|
|
# TODO: need to be verified if it's okay to call these on HS220 w/o these
|
2023-10-29 22:15:42 +00:00
|
|
|
# TODO: need to be figured out what's the best approach to detect support
|
2022-01-29 16:53:18 +00:00
|
|
|
self.add_module("motion", Motion(self, "smartlife.iot.PIR"))
|
|
|
|
self.add_module("ambient", AmbientLight(self, "smartlife.iot.LAS"))
|
2020-04-18 21:35:39 +00:00
|
|
|
|
2024-03-06 16:45:08 +00:00
|
|
|
async def _initialize_features(self):
|
|
|
|
await super()._initialize_features()
|
|
|
|
|
|
|
|
if "brightness" in self.sys_info: # pragma: no branch
|
|
|
|
self._add_feature(
|
|
|
|
Feature(
|
|
|
|
device=self,
|
|
|
|
name="Brightness",
|
|
|
|
attribute_getter="brightness",
|
|
|
|
attribute_setter="set_brightness",
|
|
|
|
minimum_value=1,
|
|
|
|
maximum_value=100,
|
2024-04-24 16:38:52 +00:00
|
|
|
type=Feature.Type.Number,
|
2024-03-06 16:45:08 +00:00
|
|
|
)
|
|
|
|
)
|
|
|
|
|
2020-04-18 21:35:39 +00:00
|
|
|
@property # type: ignore
|
|
|
|
@requires_update
|
|
|
|
def brightness(self) -> int:
|
|
|
|
"""Return current brightness on dimmers.
|
|
|
|
|
|
|
|
Will return a range between 0 - 100.
|
|
|
|
"""
|
|
|
|
if not self.is_dimmable:
|
2024-02-21 15:52:55 +00:00
|
|
|
raise KasaException("Device is not dimmable.")
|
2020-04-18 21:35:39 +00:00
|
|
|
|
|
|
|
sys_info = self.sys_info
|
|
|
|
return int(sys_info["brightness"])
|
|
|
|
|
|
|
|
@requires_update
|
2024-04-17 13:39:24 +00:00
|
|
|
async def set_brightness(self, brightness: int, *, transition: int | None = None):
|
2020-06-14 16:09:28 +00:00
|
|
|
"""Set the new dimmer brightness level in percentage.
|
|
|
|
|
|
|
|
:param int transition: transition duration in milliseconds.
|
|
|
|
Using a transition will cause the dimmer to turn on.
|
|
|
|
"""
|
2020-04-18 21:35:39 +00:00
|
|
|
if not self.is_dimmable:
|
2024-02-21 15:52:55 +00:00
|
|
|
raise KasaException("Device is not dimmable.")
|
2020-04-18 21:35:39 +00:00
|
|
|
|
2020-06-14 16:09:28 +00:00
|
|
|
if not isinstance(brightness, int):
|
|
|
|
raise ValueError(
|
|
|
|
"Brightness must be integer, " "not of %s.", type(brightness)
|
|
|
|
)
|
|
|
|
|
|
|
|
if not 0 <= brightness <= 100:
|
|
|
|
raise ValueError("Brightness value %s is not valid." % brightness)
|
|
|
|
|
|
|
|
# Dimmers do not support a brightness of 0, but bulbs do.
|
|
|
|
# Coerce 0 to 1 to maintain the same interface between dimmers and bulbs.
|
|
|
|
if brightness == 0:
|
|
|
|
brightness = 1
|
|
|
|
|
|
|
|
if transition is not None:
|
|
|
|
return await self.set_dimmer_transition(brightness, transition)
|
|
|
|
|
|
|
|
return await self._query_helper(
|
|
|
|
self.DIMMER_SERVICE, "set_brightness", {"brightness": brightness}
|
|
|
|
)
|
|
|
|
|
2024-04-17 13:39:24 +00:00
|
|
|
async def turn_off(self, *, transition: int | None = None, **kwargs):
|
2020-06-14 16:09:28 +00:00
|
|
|
"""Turn the bulb off.
|
|
|
|
|
|
|
|
:param int transition: transition duration in milliseconds.
|
|
|
|
"""
|
|
|
|
if transition is not None:
|
|
|
|
return await self.set_dimmer_transition(brightness=0, transition=transition)
|
|
|
|
|
|
|
|
return await super().turn_off()
|
|
|
|
|
|
|
|
@requires_update
|
2024-04-17 13:39:24 +00:00
|
|
|
async def turn_on(self, *, transition: int | None = None, **kwargs):
|
2020-06-14 16:09:28 +00:00
|
|
|
"""Turn the bulb on.
|
|
|
|
|
|
|
|
:param int transition: transition duration in milliseconds.
|
|
|
|
"""
|
|
|
|
if transition is not None:
|
|
|
|
return await self.set_dimmer_transition(
|
|
|
|
brightness=self.brightness, transition=transition
|
|
|
|
)
|
|
|
|
|
|
|
|
return await super().turn_on()
|
|
|
|
|
|
|
|
async def set_dimmer_transition(self, brightness: int, transition: int):
|
|
|
|
"""Turn the bulb on to brightness percentage over transition milliseconds.
|
|
|
|
|
|
|
|
A brightness value of 0 will turn off the dimmer.
|
|
|
|
"""
|
|
|
|
if not isinstance(brightness, int):
|
|
|
|
raise ValueError(
|
|
|
|
"Brightness must be integer, " "not of %s.", type(brightness)
|
|
|
|
)
|
|
|
|
|
|
|
|
if not 0 <= brightness <= 100:
|
|
|
|
raise ValueError("Brightness value %s is not valid." % brightness)
|
|
|
|
|
|
|
|
if not isinstance(transition, int):
|
|
|
|
raise ValueError(
|
|
|
|
"Transition must be integer, " "not of %s.", type(transition)
|
2020-04-18 21:35:39 +00:00
|
|
|
)
|
2020-06-14 16:09:28 +00:00
|
|
|
if transition <= 0:
|
|
|
|
raise ValueError("Transition value %s is not valid." % transition)
|
|
|
|
|
|
|
|
return await self._query_helper(
|
|
|
|
self.DIMMER_SERVICE,
|
|
|
|
"set_dimmer_transition",
|
|
|
|
{"brightness": brightness, "duration": transition},
|
|
|
|
)
|
2020-04-18 21:35:39 +00:00
|
|
|
|
2023-05-17 18:10:39 +00:00
|
|
|
@requires_update
|
|
|
|
async def get_behaviors(self):
|
|
|
|
"""Return button behavior settings."""
|
|
|
|
behaviors = await self._query_helper(
|
|
|
|
self.DIMMER_SERVICE, "get_default_behavior", {}
|
|
|
|
)
|
|
|
|
return behaviors
|
|
|
|
|
|
|
|
@requires_update
|
|
|
|
async def set_button_action(
|
2024-04-17 13:39:24 +00:00
|
|
|
self, action_type: ActionType, action: ButtonAction, index: int | None = None
|
2023-05-17 18:10:39 +00:00
|
|
|
):
|
|
|
|
"""Set action to perform on button click/hold.
|
|
|
|
|
|
|
|
:param action_type ActionType: whether to control double click or hold action.
|
2023-10-29 22:15:42 +00:00
|
|
|
:param action ButtonAction: what should the button do
|
|
|
|
(nothing, instant, gentle, change preset)
|
2023-05-17 18:10:39 +00:00
|
|
|
:param index int: in case of preset change, the preset to select
|
|
|
|
"""
|
|
|
|
action_type_setter = f"set_{action_type}"
|
|
|
|
|
2024-04-17 13:39:24 +00:00
|
|
|
payload: dict[str, Any] = {"mode": str(action)}
|
2023-05-17 18:10:39 +00:00
|
|
|
if index is not None:
|
|
|
|
payload["index"] = index
|
|
|
|
|
|
|
|
await self._query_helper(self.DIMMER_SERVICE, action_type_setter, payload)
|
|
|
|
|
|
|
|
@requires_update
|
|
|
|
async def set_fade_time(self, fade_type: FadeType, time: int):
|
|
|
|
"""Set time for fade in / fade out."""
|
|
|
|
fade_type_setter = f"set_{fade_type}_time"
|
|
|
|
payload = {"fadeTime": time}
|
|
|
|
|
|
|
|
await self._query_helper(self.DIMMER_SERVICE, fade_type_setter, payload)
|
|
|
|
|
2020-04-18 21:35:39 +00:00
|
|
|
@property # type: ignore
|
|
|
|
@requires_update
|
2020-05-27 14:55:18 +00:00
|
|
|
def is_dimmable(self) -> bool:
|
|
|
|
"""Whether the switch supports brightness changes."""
|
2020-04-18 21:35:39 +00:00
|
|
|
sys_info = self.sys_info
|
|
|
|
return "brightness" in sys_info
|