mirror of
https://github.com/python-kasa/python-kasa.git
synced 2025-10-14 03:18:26 +00:00
Merge remote-tracking branch 'upstream/master' into feat/device_update
This commit is contained in:
@@ -1,49 +1,53 @@
|
||||
"""Modules for SMART devices."""
|
||||
|
||||
from .alarmmodule import AlarmModule
|
||||
from .autooffmodule import AutoOffModule
|
||||
from .battery import BatterySensor
|
||||
from .alarm import Alarm
|
||||
from .autooff import AutoOff
|
||||
from .batterysensor import BatterySensor
|
||||
from .brightness import Brightness
|
||||
from .childdevicemodule import ChildDeviceModule
|
||||
from .cloudmodule import CloudModule
|
||||
from .colormodule import ColorModule
|
||||
from .colortemp import ColorTemperatureModule
|
||||
from .childdevice import ChildDevice
|
||||
from .cloud import Cloud
|
||||
from .color import Color
|
||||
from .colortemperature import ColorTemperature
|
||||
from .contactsensor import ContactSensor
|
||||
from .devicemodule import DeviceModule
|
||||
from .energymodule import EnergyModule
|
||||
from .fanmodule import FanModule
|
||||
from .energy import Energy
|
||||
from .fan import Fan
|
||||
from .firmware import Firmware
|
||||
from .frostprotection import FrostProtectionModule
|
||||
from .humidity import HumiditySensor
|
||||
from .ledmodule import LedModule
|
||||
from .lighteffectmodule import LightEffectModule
|
||||
from .lighttransitionmodule import LightTransitionModule
|
||||
from .reportmodule import ReportModule
|
||||
from .temperature import TemperatureSensor
|
||||
from .frostprotection import FrostProtection
|
||||
from .humiditysensor import HumiditySensor
|
||||
from .led import Led
|
||||
from .light import Light
|
||||
from .lighteffect import LightEffect
|
||||
from .lighttransition import LightTransition
|
||||
from .reportmode import ReportMode
|
||||
from .temperaturecontrol import TemperatureControl
|
||||
from .timemodule import TimeModule
|
||||
from .waterleak import WaterleakSensor
|
||||
from .temperaturesensor import TemperatureSensor
|
||||
from .time import Time
|
||||
from .waterleaksensor import WaterleakSensor
|
||||
|
||||
__all__ = [
|
||||
"AlarmModule",
|
||||
"TimeModule",
|
||||
"EnergyModule",
|
||||
"Alarm",
|
||||
"Time",
|
||||
"Energy",
|
||||
"DeviceModule",
|
||||
"ChildDeviceModule",
|
||||
"ChildDevice",
|
||||
"BatterySensor",
|
||||
"HumiditySensor",
|
||||
"TemperatureSensor",
|
||||
"TemperatureControl",
|
||||
"ReportModule",
|
||||
"AutoOffModule",
|
||||
"LedModule",
|
||||
"ReportMode",
|
||||
"AutoOff",
|
||||
"Led",
|
||||
"Brightness",
|
||||
"FanModule",
|
||||
"Fan",
|
||||
"Firmware",
|
||||
"CloudModule",
|
||||
"LightEffectModule",
|
||||
"LightTransitionModule",
|
||||
"ColorTemperatureModule",
|
||||
"ColorModule",
|
||||
"Cloud",
|
||||
"Light",
|
||||
"LightEffect",
|
||||
"LightTransition",
|
||||
"ColorTemperature",
|
||||
"Color",
|
||||
"WaterleakSensor",
|
||||
"FrostProtectionModule",
|
||||
"ContactSensor",
|
||||
"FrostProtection",
|
||||
]
|
||||
|
@@ -6,7 +6,7 @@ from ...feature import Feature
|
||||
from ..smartmodule import SmartModule
|
||||
|
||||
|
||||
class AlarmModule(SmartModule):
|
||||
class Alarm(SmartModule):
|
||||
"""Implementation of alarm module."""
|
||||
|
||||
REQUIRED_COMPONENT = "alarm"
|
@@ -12,7 +12,7 @@ if TYPE_CHECKING:
|
||||
from ..smartdevice import SmartDevice
|
||||
|
||||
|
||||
class AutoOffModule(SmartModule):
|
||||
class AutoOff(SmartModule):
|
||||
"""Implementation of auto off module."""
|
||||
|
||||
REQUIRED_COMPONENT = "auto_off"
|
@@ -2,14 +2,9 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from ...feature import Feature
|
||||
from ..smartmodule import SmartModule
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..smartdevice import SmartDevice
|
||||
|
||||
|
||||
class BatterySensor(SmartModule):
|
||||
"""Implementation of battery module."""
|
||||
@@ -17,23 +12,11 @@ class BatterySensor(SmartModule):
|
||||
REQUIRED_COMPONENT = "battery_detect"
|
||||
QUERY_GETTER_NAME = "get_battery_detect_info"
|
||||
|
||||
def __init__(self, device: SmartDevice, module: str):
|
||||
super().__init__(device, module)
|
||||
def _initialize_features(self):
|
||||
"""Initialize features."""
|
||||
self._add_feature(
|
||||
Feature(
|
||||
device,
|
||||
"battery_level",
|
||||
"Battery level",
|
||||
container=self,
|
||||
attribute_getter="battery",
|
||||
icon="mdi:battery",
|
||||
unit="%",
|
||||
category=Feature.Category.Info,
|
||||
)
|
||||
)
|
||||
self._add_feature(
|
||||
Feature(
|
||||
device,
|
||||
self._device,
|
||||
"battery_low",
|
||||
"Battery low",
|
||||
container=self,
|
||||
@@ -44,6 +27,22 @@ class BatterySensor(SmartModule):
|
||||
)
|
||||
)
|
||||
|
||||
# Some devices, like T110 contact sensor do not report the battery percentage
|
||||
if "battery_percentage" in self._device.sys_info:
|
||||
self._add_feature(
|
||||
Feature(
|
||||
self._device,
|
||||
"battery_level",
|
||||
"Battery level",
|
||||
container=self,
|
||||
attribute_getter="battery",
|
||||
icon="mdi:battery",
|
||||
unit="%",
|
||||
category=Feature.Category.Info,
|
||||
type=Feature.Type.Sensor,
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
def battery(self):
|
||||
"""Return battery level."""
|
@@ -2,16 +2,10 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from ...feature import Feature
|
||||
from ..smartmodule import SmartModule
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..smartdevice import SmartDevice
|
||||
|
||||
|
||||
BRIGHTNESS_MIN = 1
|
||||
BRIGHTNESS_MIN = 0
|
||||
BRIGHTNESS_MAX = 100
|
||||
|
||||
|
||||
@@ -20,8 +14,11 @@ class Brightness(SmartModule):
|
||||
|
||||
REQUIRED_COMPONENT = "brightness"
|
||||
|
||||
def __init__(self, device: SmartDevice, module: str):
|
||||
super().__init__(device, module)
|
||||
def _initialize_features(self):
|
||||
"""Initialize features."""
|
||||
super()._initialize_features()
|
||||
|
||||
device = self._device
|
||||
self._add_feature(
|
||||
Feature(
|
||||
device,
|
||||
@@ -47,8 +44,11 @@ class Brightness(SmartModule):
|
||||
"""Return current brightness."""
|
||||
return self.data["brightness"]
|
||||
|
||||
async def set_brightness(self, brightness: int):
|
||||
"""Set the brightness."""
|
||||
async def set_brightness(self, brightness: int, *, transition: int | None = None):
|
||||
"""Set the brightness. A brightness value of 0 will turn off the light.
|
||||
|
||||
Note, transition is not supported and will be ignored.
|
||||
"""
|
||||
if not isinstance(brightness, int) or not (
|
||||
BRIGHTNESS_MIN <= brightness <= BRIGHTNESS_MAX
|
||||
):
|
||||
@@ -57,6 +57,8 @@ class Brightness(SmartModule):
|
||||
f"(valid range: {BRIGHTNESS_MIN}-{BRIGHTNESS_MAX}%)"
|
||||
)
|
||||
|
||||
if brightness == 0:
|
||||
return await self._device.turn_off()
|
||||
return await self.call("set_device_info", {"brightness": brightness})
|
||||
|
||||
async def _check_supported(self):
|
||||
|
@@ -3,7 +3,7 @@
|
||||
from ..smartmodule import SmartModule
|
||||
|
||||
|
||||
class ChildDeviceModule(SmartModule):
|
||||
class ChildDevice(SmartModule):
|
||||
"""Implementation for child devices."""
|
||||
|
||||
REQUIRED_COMPONENT = "child_device"
|
@@ -12,7 +12,7 @@ if TYPE_CHECKING:
|
||||
from ..smartdevice import SmartDevice
|
||||
|
||||
|
||||
class CloudModule(SmartModule):
|
||||
class Cloud(SmartModule):
|
||||
"""Implementation of cloud module."""
|
||||
|
||||
QUERY_GETTER_NAME = "get_connect_cloud_state"
|
@@ -4,15 +4,15 @@ from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from ...bulb import HSV
|
||||
from ...feature import Feature
|
||||
from ...interfaces.light import HSV
|
||||
from ..smartmodule import SmartModule
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..smartdevice import SmartDevice
|
||||
|
||||
|
||||
class ColorModule(SmartModule):
|
||||
class Color(SmartModule):
|
||||
"""Implementation of color module."""
|
||||
|
||||
REQUIRED_COMPONENT = "color"
|
@@ -5,8 +5,8 @@ from __future__ import annotations
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from ...bulb import ColorTempRange
|
||||
from ...feature import Feature
|
||||
from ...interfaces.light import ColorTempRange
|
||||
from ..smartmodule import SmartModule
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -18,7 +18,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
DEFAULT_TEMP_RANGE = [2500, 6500]
|
||||
|
||||
|
||||
class ColorTemperatureModule(SmartModule):
|
||||
class ColorTemperature(SmartModule):
|
||||
"""Implementation of color temp module."""
|
||||
|
||||
REQUIRED_COMPONENT = "color_temperature"
|
42
kasa/smart/modules/contactsensor.py
Normal file
42
kasa/smart/modules/contactsensor.py
Normal file
@@ -0,0 +1,42 @@
|
||||
"""Implementation of contact sensor module."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from ...feature import Feature
|
||||
from ..smartmodule import SmartModule
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..smartdevice import SmartDevice
|
||||
|
||||
|
||||
class ContactSensor(SmartModule):
|
||||
"""Implementation of contact sensor module."""
|
||||
|
||||
REQUIRED_COMPONENT = None # we depend on availability of key
|
||||
REQUIRED_KEY_ON_PARENT = "open"
|
||||
|
||||
def __init__(self, device: SmartDevice, module: str):
|
||||
super().__init__(device, module)
|
||||
self._add_feature(
|
||||
Feature(
|
||||
device,
|
||||
id="is_open",
|
||||
name="Open",
|
||||
container=self,
|
||||
attribute_getter="is_open",
|
||||
icon="mdi:door",
|
||||
category=Feature.Category.Primary,
|
||||
type=Feature.Type.BinarySensor,
|
||||
)
|
||||
)
|
||||
|
||||
def query(self) -> dict:
|
||||
"""Query to execute during the update cycle."""
|
||||
return {}
|
||||
|
||||
@property
|
||||
def is_open(self):
|
||||
"""Return True if the contact sensor is open."""
|
||||
return self._device.sys_info["open"]
|
@@ -12,7 +12,7 @@ if TYPE_CHECKING:
|
||||
from ..smartdevice import SmartDevice
|
||||
|
||||
|
||||
class EnergyModule(SmartModule):
|
||||
class Energy(SmartModule):
|
||||
"""Implementation of energy monitoring module."""
|
||||
|
||||
REQUIRED_COMPONENT = "energy_monitoring"
|
@@ -11,7 +11,7 @@ if TYPE_CHECKING:
|
||||
from ..smartdevice import SmartDevice
|
||||
|
||||
|
||||
class FanModule(SmartModule):
|
||||
class Fan(SmartModule):
|
||||
"""Implementation of fan_control module."""
|
||||
|
||||
REQUIRED_COMPONENT = "fan_control"
|
@@ -5,10 +5,8 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import date
|
||||
from typing import TYPE_CHECKING, Any, Optional
|
||||
from typing import TYPE_CHECKING, Any, Callable, Coroutine, Optional
|
||||
|
||||
# When support for cpython older than 3.11 is dropped
|
||||
# async_timeout can be replaced with asyncio.timeout
|
||||
# When support for cpython older than 3.11 is dropped
|
||||
# async_timeout can be replaced with asyncio.timeout
|
||||
from async_timeout import timeout as asyncio_timeout
|
||||
@@ -16,9 +14,12 @@ from pydantic.v1 import BaseModel, Field, validator
|
||||
|
||||
from ...exceptions import SmartErrorCode
|
||||
from ...feature import Feature
|
||||
from ...firmware import Firmware as FirmwareInterface
|
||||
from ...firmware import FirmwareUpdate as FirmwareUpdateInterface
|
||||
from ...firmware import UpdateResult
|
||||
from ...interfaces import Firmware as FirmwareInterface
|
||||
from ...interfaces.firmware import (
|
||||
FirmwareDownloadState as FirmwareDownloadStateInterface,
|
||||
)
|
||||
from ...interfaces.firmware import FirmwareUpdateInfo as FirmwareUpdateInfoInterface
|
||||
from ...interfaces.firmware import UpdateResult
|
||||
from ..smartmodule import SmartModule
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -28,7 +29,20 @@ if TYPE_CHECKING:
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FirmwareUpdate(BaseModel):
|
||||
class DownloadState(BaseModel):
|
||||
"""Download state."""
|
||||
|
||||
# Example:
|
||||
# {'status': 0, 'download_progress': 0, 'reboot_time': 5,
|
||||
# 'upgrade_time': 5, 'auto_upgrade': False}
|
||||
status: int
|
||||
progress: int = Field(alias="download_progress")
|
||||
reboot_time: int
|
||||
upgrade_time: int
|
||||
auto_upgrade: bool
|
||||
|
||||
|
||||
class FirmwareUpdateInfo(BaseModel):
|
||||
"""Update info status object."""
|
||||
|
||||
status: int = Field(alias="type")
|
||||
@@ -91,7 +105,7 @@ class Firmware(SmartModule, FirmwareInterface):
|
||||
name="Current firmware version",
|
||||
container=self,
|
||||
attribute_getter="current_firmware",
|
||||
category=Feature.Category.Info,
|
||||
category=Feature.Category.Debug,
|
||||
)
|
||||
)
|
||||
self._add_feature(
|
||||
@@ -101,7 +115,7 @@ class Firmware(SmartModule, FirmwareInterface):
|
||||
name="Available firmware version",
|
||||
container=self,
|
||||
attribute_getter="latest_firmware",
|
||||
category=Feature.Category.Info,
|
||||
category=Feature.Category.Debug,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -128,9 +142,9 @@ class Firmware(SmartModule, FirmwareInterface):
|
||||
fw = self.data.get("get_latest_fw") or self.data
|
||||
if not self._device.is_cloud_connected or isinstance(fw, SmartErrorCode):
|
||||
# Error in response, probably disconnected from the cloud.
|
||||
return FirmwareUpdate(type=0, need_to_upgrade=False)
|
||||
return FirmwareUpdateInfo(type=0, need_to_upgrade=False)
|
||||
|
||||
return FirmwareUpdate.parse_obj(fw)
|
||||
return FirmwareUpdateInfo.parse_obj(fw)
|
||||
|
||||
@property
|
||||
def update_available(self) -> bool | None:
|
||||
@@ -139,31 +153,60 @@ class Firmware(SmartModule, FirmwareInterface):
|
||||
return None
|
||||
return self.firmware_update_info.update_available
|
||||
|
||||
async def get_update_state(self):
|
||||
async def get_update_state(self) -> DownloadState:
|
||||
"""Return update state."""
|
||||
return await self.call("get_fw_download_state")
|
||||
resp = await self.call("get_fw_download_state")
|
||||
state = resp["get_fw_download_state"]
|
||||
return DownloadState(**state)
|
||||
|
||||
async def update(self):
|
||||
async def update(
|
||||
self, progress_cb: Callable[[DownloadState], Coroutine] | None = None
|
||||
):
|
||||
"""Update the device firmware."""
|
||||
current_fw = self.current_firmware
|
||||
_LOGGER.debug(
|
||||
_LOGGER.info(
|
||||
"Going to upgrade from %s to %s",
|
||||
current_fw,
|
||||
self.firmware_update_info.version,
|
||||
)
|
||||
resp = await self.call("fw_download")
|
||||
_LOGGER.debug("Update request response: %s", resp)
|
||||
await self.call("fw_download")
|
||||
|
||||
# TODO: read timeout from get_auto_update_info or from get_fw_download_state?
|
||||
async with asyncio_timeout(60 * 5):
|
||||
while True:
|
||||
await asyncio.sleep(0.5)
|
||||
state = await self.get_update_state()
|
||||
_LOGGER.debug("Update state: %s" % state)
|
||||
# TODO: this could await a given callable for progress
|
||||
try:
|
||||
state = await self.get_update_state()
|
||||
except Exception as ex:
|
||||
_LOGGER.warning(
|
||||
"Got exception, maybe the device is rebooting? %s", ex
|
||||
)
|
||||
continue
|
||||
|
||||
if self.firmware_update_info.version != current_fw:
|
||||
_LOGGER.info("Updated to %s", self.firmware_update_info.version)
|
||||
_LOGGER.debug("Update state: %s" % state)
|
||||
if progress_cb is not None:
|
||||
asyncio.create_task(progress_cb(state))
|
||||
|
||||
if state.status == 0:
|
||||
_LOGGER.info(
|
||||
"Update idle, hopefully updated to %s",
|
||||
self.firmware_update_info.version,
|
||||
)
|
||||
break
|
||||
elif state.status == 2:
|
||||
_LOGGER.info("Downloading firmware, progress: %s", state.progress)
|
||||
elif state.status == 3:
|
||||
upgrade_sleep = state.upgrade_time
|
||||
_LOGGER.info(
|
||||
"Flashing firmware, sleeping for %s before checking status",
|
||||
upgrade_sleep,
|
||||
)
|
||||
await asyncio.sleep(upgrade_sleep)
|
||||
elif state.status < 0:
|
||||
_LOGGER.error("Got error: %s", state.status)
|
||||
break
|
||||
else:
|
||||
_LOGGER.warning("Unhandled state code: %s", state)
|
||||
|
||||
@property
|
||||
def auto_update_enabled(self):
|
||||
@@ -178,16 +221,21 @@ class Firmware(SmartModule, FirmwareInterface):
|
||||
data = {**self.data["get_auto_update_info"], "enable": enabled}
|
||||
await self.call("set_auto_update_info", data)
|
||||
|
||||
async def update_firmware(self, *, progress_cb) -> UpdateResult:
|
||||
async def update_firmware(
|
||||
self,
|
||||
*,
|
||||
progress_cb: Callable[[FirmwareDownloadStateInterface], Coroutine]
|
||||
| None = None,
|
||||
) -> UpdateResult:
|
||||
"""Update the firmware."""
|
||||
# TODO: implement, this is part of the common firmware API
|
||||
raise NotImplementedError
|
||||
|
||||
async def check_for_updates(self) -> FirmwareUpdateInterface:
|
||||
async def check_for_updates(self) -> FirmwareUpdateInfoInterface:
|
||||
"""Return firmware update information."""
|
||||
# TODO: naming of the common firmware API methods
|
||||
info = self.firmware_update_info
|
||||
return FirmwareUpdateInterface(
|
||||
return FirmwareUpdateInfoInterface(
|
||||
current_version=self.current_firmware,
|
||||
update_available=info.update_available,
|
||||
available_version=info.version,
|
||||
|
@@ -12,7 +12,7 @@ if TYPE_CHECKING:
|
||||
from ..smartdevice import SmartDevice
|
||||
|
||||
|
||||
class FrostProtectionModule(SmartModule):
|
||||
class FrostProtection(SmartModule):
|
||||
"""Implementation for frost protection module.
|
||||
|
||||
This basically turns the thermostat on and off.
|
||||
|
@@ -2,37 +2,16 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from ...feature import Feature
|
||||
from ...interfaces.led import Led as LedInterface
|
||||
from ..smartmodule import SmartModule
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..smartdevice import SmartDevice
|
||||
|
||||
|
||||
class LedModule(SmartModule):
|
||||
class Led(SmartModule, LedInterface):
|
||||
"""Implementation of led controls."""
|
||||
|
||||
REQUIRED_COMPONENT = "led"
|
||||
QUERY_GETTER_NAME = "get_led_info"
|
||||
|
||||
def __init__(self, device: SmartDevice, module: str):
|
||||
super().__init__(device, module)
|
||||
self._add_feature(
|
||||
Feature(
|
||||
device=device,
|
||||
container=self,
|
||||
id="led",
|
||||
name="LED",
|
||||
icon="mdi:led-{state}",
|
||||
attribute_getter="led",
|
||||
attribute_setter="set_led",
|
||||
type=Feature.Type.Switch,
|
||||
category=Feature.Category.Config,
|
||||
)
|
||||
)
|
||||
|
||||
def query(self) -> dict:
|
||||
"""Query to execute during the update cycle."""
|
||||
return {self.QUERY_GETTER_NAME: {"led_rule": None}}
|
||||
@@ -56,7 +35,7 @@ class LedModule(SmartModule):
|
||||
This should probably be a select with always/never/nightmode.
|
||||
"""
|
||||
rule = "always" if enable else "never"
|
||||
return await self.call("set_led_info", self.data | {"led_rule": rule})
|
||||
return await self.call("set_led_info", dict(self.data, **{"led_rule": rule}))
|
||||
|
||||
@property
|
||||
def night_mode_settings(self):
|
126
kasa/smart/modules/light.py
Normal file
126
kasa/smart/modules/light.py
Normal file
@@ -0,0 +1,126 @@
|
||||
"""Module for led controls."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from ...exceptions import KasaException
|
||||
from ...interfaces.light import HSV, ColorTempRange
|
||||
from ...interfaces.light import Light as LightInterface
|
||||
from ...module import Module
|
||||
from ..smartmodule import SmartModule
|
||||
|
||||
|
||||
class Light(SmartModule, LightInterface):
|
||||
"""Implementation of a light."""
|
||||
|
||||
def query(self) -> dict:
|
||||
"""Query to execute during the update cycle."""
|
||||
return {}
|
||||
|
||||
@property
|
||||
def is_color(self) -> bool:
|
||||
"""Whether the bulb supports color changes."""
|
||||
return Module.Color in self._device.modules
|
||||
|
||||
@property
|
||||
def is_dimmable(self) -> bool:
|
||||
"""Whether the bulb supports brightness changes."""
|
||||
return Module.Brightness in self._device.modules
|
||||
|
||||
@property
|
||||
def is_variable_color_temp(self) -> bool:
|
||||
"""Whether the bulb supports color temperature changes."""
|
||||
return Module.ColorTemperature in self._device.modules
|
||||
|
||||
@property
|
||||
def valid_temperature_range(self) -> ColorTempRange:
|
||||
"""Return the device-specific white temperature range (in Kelvin).
|
||||
|
||||
:return: White temperature range in Kelvin (minimum, maximum)
|
||||
"""
|
||||
if not self.is_variable_color_temp:
|
||||
raise KasaException("Color temperature not supported")
|
||||
|
||||
return self._device.modules[Module.ColorTemperature].valid_temperature_range
|
||||
|
||||
@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 KasaException("Bulb does not support color.")
|
||||
|
||||
return self._device.modules[Module.Color].hsv
|
||||
|
||||
@property
|
||||
def color_temp(self) -> int:
|
||||
"""Whether the bulb supports color temperature changes."""
|
||||
if not self.is_variable_color_temp:
|
||||
raise KasaException("Bulb does not support colortemp.")
|
||||
|
||||
return self._device.modules[Module.ColorTemperature].color_temp
|
||||
|
||||
@property
|
||||
def brightness(self) -> int:
|
||||
"""Return the current brightness in percentage."""
|
||||
if not self.is_dimmable: # pragma: no cover
|
||||
raise KasaException("Bulb is not dimmable.")
|
||||
|
||||
return self._device.modules[Module.Brightness].brightness
|
||||
|
||||
async def set_hsv(
|
||||
self,
|
||||
hue: int,
|
||||
saturation: int,
|
||||
value: int | None = None,
|
||||
*,
|
||||
transition: int | None = 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 between 1 and 100
|
||||
:param int transition: transition in milliseconds.
|
||||
"""
|
||||
if not self.is_color:
|
||||
raise KasaException("Bulb does not support color.")
|
||||
|
||||
return await self._device.modules[Module.Color].set_hsv(hue, saturation, value)
|
||||
|
||||
async def set_color_temp(
|
||||
self, temp: int, *, brightness=None, transition: int | None = 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.
|
||||
"""
|
||||
if not self.is_variable_color_temp:
|
||||
raise KasaException("Bulb does not support colortemp.")
|
||||
return await self._device.modules[Module.ColorTemperature].set_color_temp(temp)
|
||||
|
||||
async def set_brightness(
|
||||
self, brightness: int, *, transition: int | None = 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 KasaException("Bulb is not dimmable.")
|
||||
|
||||
return await self._device.modules[Module.Brightness].set_brightness(brightness)
|
||||
|
||||
@property
|
||||
def has_effects(self) -> bool:
|
||||
"""Return True if the device supports effects."""
|
||||
return Module.LightEffect in self._device.modules
|
@@ -6,14 +6,14 @@ import base64
|
||||
import copy
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from ...feature import Feature
|
||||
from ...interfaces.lighteffect import LightEffect as LightEffectInterface
|
||||
from ..smartmodule import SmartModule
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..smartdevice import SmartDevice
|
||||
|
||||
|
||||
class LightEffectModule(SmartModule):
|
||||
class LightEffect(SmartModule, LightEffectInterface):
|
||||
"""Implementation of dynamic light effects."""
|
||||
|
||||
REQUIRED_COMPONENT = "light_effect"
|
||||
@@ -22,29 +22,11 @@ class LightEffectModule(SmartModule):
|
||||
"L1": "Party",
|
||||
"L2": "Relax",
|
||||
}
|
||||
LIGHT_EFFECTS_OFF = "Off"
|
||||
|
||||
def __init__(self, device: SmartDevice, module: str):
|
||||
super().__init__(device, module)
|
||||
self._scenes_names_to_id: dict[str, str] = {}
|
||||
|
||||
def _initialize_features(self):
|
||||
"""Initialize features."""
|
||||
device = self._device
|
||||
self._add_feature(
|
||||
Feature(
|
||||
device,
|
||||
id="light_effect",
|
||||
name="Light effect",
|
||||
container=self,
|
||||
attribute_getter="effect",
|
||||
attribute_setter="set_effect",
|
||||
category=Feature.Category.Config,
|
||||
type=Feature.Type.Choice,
|
||||
choices_getter="effect_list",
|
||||
)
|
||||
)
|
||||
|
||||
def _initialize_effects(self) -> dict[str, dict[str, Any]]:
|
||||
"""Return built-in effects."""
|
||||
# Copy the effects so scene name updates do not update the underlying dict.
|
||||
@@ -64,7 +46,7 @@ class LightEffectModule(SmartModule):
|
||||
return effects
|
||||
|
||||
@property
|
||||
def effect_list(self) -> list[str] | None:
|
||||
def effect_list(self) -> list[str]:
|
||||
"""Return built-in effects list.
|
||||
|
||||
Example:
|
||||
@@ -90,6 +72,9 @@ class LightEffectModule(SmartModule):
|
||||
async def set_effect(
|
||||
self,
|
||||
effect: str,
|
||||
*,
|
||||
brightness: int | None = None,
|
||||
transition: int | None = None,
|
||||
) -> None:
|
||||
"""Set an effect for the device.
|
||||
|
||||
@@ -108,6 +93,24 @@ class LightEffectModule(SmartModule):
|
||||
params["id"] = effect_id
|
||||
return await self.call("set_dynamic_light_effect_rule_enable", params)
|
||||
|
||||
async def set_custom_effect(
|
||||
self,
|
||||
effect_dict: dict,
|
||||
) -> None:
|
||||
"""Set a custom effect on the device.
|
||||
|
||||
:param str effect_dict: The custom effect dict to set
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"Device does not support setting custom effects. "
|
||||
"Use has_custom_effects to check for support."
|
||||
)
|
||||
|
||||
@property
|
||||
def has_custom_effects(self) -> bool:
|
||||
"""Return True if the device supports setting custom effects."""
|
||||
return False
|
||||
|
||||
def query(self) -> dict:
|
||||
"""Query to execute during the update cycle."""
|
||||
return {self.QUERY_GETTER_NAME: {"start_index": 0}}
|
@@ -12,7 +12,7 @@ if TYPE_CHECKING:
|
||||
from ..smartdevice import SmartDevice
|
||||
|
||||
|
||||
class LightTransitionModule(SmartModule):
|
||||
class LightTransition(SmartModule):
|
||||
"""Implementation of gradual on/off."""
|
||||
|
||||
REQUIRED_COMPONENT = "on_off_gradually"
|
@@ -11,7 +11,7 @@ if TYPE_CHECKING:
|
||||
from ..smartdevice import SmartDevice
|
||||
|
||||
|
||||
class ReportModule(SmartModule):
|
||||
class ReportMode(SmartModule):
|
||||
"""Implementation of report module."""
|
||||
|
||||
REQUIRED_COMPONENT = "report_mode"
|
@@ -13,7 +13,7 @@ if TYPE_CHECKING:
|
||||
from ..smartdevice import SmartDevice
|
||||
|
||||
|
||||
class TimeModule(SmartModule):
|
||||
class Time(SmartModule):
|
||||
"""Implementation of device_local_time."""
|
||||
|
||||
REQUIRED_COMPONENT = "time"
|
Reference in New Issue
Block a user