diff --git a/kasa/cli.py b/kasa/cli.py index 16e64a05..c8624966 100755 --- a/kasa/cli.py +++ b/kasa/cli.py @@ -581,7 +581,7 @@ async def state(ctx, dev: Device): echo(f"\tHost: {dev.host}") echo(f"\tPort: {dev.port}") echo(f"\tDevice state: {dev.is_on}") - if dev.is_strip: + if dev.children: echo("\t[bold]== Children ==[/bold]") for child in dev.children: echo(f"\t* {child.alias} ({child.model}, {child.device_type})") diff --git a/kasa/device_factory.py b/kasa/device_factory.py index 66903468..2e8ba0c9 100755 --- a/kasa/device_factory.py +++ b/kasa/device_factory.py @@ -139,6 +139,7 @@ def get_device_class_from_family(device_type: str) -> Optional[Type[Device]]: "SMART.TAPOBULB": SmartBulb, "SMART.TAPOSWITCH": SmartBulb, "SMART.KASAPLUG": SmartDevice, + "SMART.TAPOHUB": SmartDevice, "SMART.KASASWITCH": SmartBulb, "IOT.SMARTPLUGSWITCH": IotPlug, "IOT.SMARTBULB": IotBulb, diff --git a/kasa/device_type.py b/kasa/device_type.py index 41dd6e36..a44efffa 100755 --- a/kasa/device_type.py +++ b/kasa/device_type.py @@ -15,6 +15,7 @@ class DeviceType(Enum): Dimmer = "dimmer" LightStrip = "lightstrip" Sensor = "sensor" + Hub = "hub" Unknown = "unknown" @staticmethod diff --git a/kasa/deviceconfig.py b/kasa/deviceconfig.py index af809ac2..c55265b4 100644 --- a/kasa/deviceconfig.py +++ b/kasa/deviceconfig.py @@ -31,6 +31,7 @@ class DeviceFamilyType(Enum): SmartTapoPlug = "SMART.TAPOPLUG" SmartTapoBulb = "SMART.TAPOBULB" SmartTapoSwitch = "SMART.TAPOSWITCH" + SmartTapoHub = "SMART.TAPOHUB" def _dataclass_from_dict(klass, in_val): diff --git a/kasa/smart/modules/__init__.py b/kasa/smart/modules/__init__.py index 02c3b86a..3e95dfe7 100644 --- a/kasa/smart/modules/__init__.py +++ b/kasa/smart/modules/__init__.py @@ -1,19 +1,29 @@ """Modules for SMART devices.""" +from .alarmmodule import AlarmModule from .autooffmodule import AutoOffModule +from .battery import BatterySensor from .childdevicemodule import ChildDeviceModule from .cloudmodule import CloudModule from .devicemodule import DeviceModule from .energymodule import EnergyModule from .firmware import Firmware +from .humidity import HumiditySensor from .ledmodule import LedModule from .lighttransitionmodule import LightTransitionModule +from .reportmodule import ReportModule +from .temperature import TemperatureSensor from .timemodule import TimeModule __all__ = [ + "AlarmModule", "TimeModule", "EnergyModule", "DeviceModule", "ChildDeviceModule", + "BatterySensor", + "HumiditySensor", + "TemperatureSensor", + "ReportModule", "AutoOffModule", "LedModule", "Firmware", diff --git a/kasa/smart/modules/alarmmodule.py b/kasa/smart/modules/alarmmodule.py new file mode 100644 index 00000000..637c4497 --- /dev/null +++ b/kasa/smart/modules/alarmmodule.py @@ -0,0 +1,87 @@ +"""Implementation of alarm module.""" +from typing import TYPE_CHECKING, Dict, List, Optional + +from ...feature import Feature, FeatureType +from ..smartmodule import SmartModule + +if TYPE_CHECKING: + from ..smartdevice import SmartDevice + + +class AlarmModule(SmartModule): + """Implementation of alarm module.""" + + REQUIRED_COMPONENT = "alarm" + + def query(self) -> Dict: + """Query to execute during the update cycle.""" + return { + "get_alarm_configure": None, + "get_support_alarm_type_list": None, # This should be needed only once + } + + def __init__(self, device: "SmartDevice", module: str): + super().__init__(device, module) + self._add_feature( + Feature( + device, + "Alarm", + container=self, + attribute_getter="active", + icon="mdi:bell", + type=FeatureType.BinarySensor, + ) + ) + self._add_feature( + Feature( + device, + "Alarm source", + container=self, + attribute_getter="source", + icon="mdi:bell", + ) + ) + self._add_feature( + Feature( + device, "Alarm sound", container=self, attribute_getter="alarm_sound" + ) + ) + self._add_feature( + Feature( + device, "Alarm volume", container=self, attribute_getter="alarm_volume" + ) + ) + + @property + def alarm_sound(self): + """Return current alarm sound.""" + return self.data["get_alarm_configure"]["type"] + + @property + def alarm_sounds(self) -> List[str]: + """Return list of available alarm sounds.""" + return self.data["get_support_alarm_type_list"]["alarm_type_list"] + + @property + def alarm_volume(self): + """Return alarm volume.""" + return self.data["get_alarm_configure"]["volume"] + + @property + def active(self) -> bool: + """Return true if alarm is active.""" + return self._device.sys_info["in_alarm"] + + @property + def source(self) -> Optional[str]: + """Return the alarm cause.""" + src = self._device.sys_info["in_alarm_source"] + return src if src else None + + async def play(self): + """Play alarm.""" + return self.call("play_alarm") + + async def stop(self): + """Stop alarm.""" + return self.call("stop_alarm") diff --git a/kasa/smart/modules/battery.py b/kasa/smart/modules/battery.py new file mode 100644 index 00000000..accf875b --- /dev/null +++ b/kasa/smart/modules/battery.py @@ -0,0 +1,47 @@ +"""Implementation of battery module.""" +from typing import TYPE_CHECKING + +from ...feature import Feature, FeatureType +from ..smartmodule import SmartModule + +if TYPE_CHECKING: + from ..smartdevice import SmartDevice + + +class BatterySensor(SmartModule): + """Implementation of battery module.""" + + REQUIRED_COMPONENT = "battery_detect" + QUERY_GETTER_NAME = "get_battery_detect_info" + + def __init__(self, device: "SmartDevice", module: str): + super().__init__(device, module) + self._add_feature( + Feature( + device, + "Battery level", + container=self, + attribute_getter="battery", + icon="mdi:battery", + ) + ) + self._add_feature( + Feature( + device, + "Battery low", + container=self, + attribute_getter="battery_low", + icon="mdi:alert", + type=FeatureType.BinarySensor, + ) + ) + + @property + def battery(self): + """Return battery level.""" + return self._device.sys_info["battery_percentage"] + + @property + def battery_low(self): + """Return True if battery is low.""" + return self._device.sys_info["at_low_battery"] diff --git a/kasa/smart/modules/humidity.py b/kasa/smart/modules/humidity.py new file mode 100644 index 00000000..454bedcd --- /dev/null +++ b/kasa/smart/modules/humidity.py @@ -0,0 +1,47 @@ +"""Implementation of humidity module.""" +from typing import TYPE_CHECKING + +from ...feature import Feature, FeatureType +from ..smartmodule import SmartModule + +if TYPE_CHECKING: + from ..smartdevice import SmartDevice + + +class HumiditySensor(SmartModule): + """Implementation of humidity module.""" + + REQUIRED_COMPONENT = "humidity" + QUERY_GETTER_NAME = "get_comfort_humidity_config" + + def __init__(self, device: "SmartDevice", module: str): + super().__init__(device, module) + self._add_feature( + Feature( + device, + "Humidity", + container=self, + attribute_getter="humidity", + icon="mdi:water-percent", + ) + ) + self._add_feature( + Feature( + device, + "Humidity warning", + container=self, + attribute_getter="humidity_warning", + type=FeatureType.BinarySensor, + icon="mdi:alert", + ) + ) + + @property + def humidity(self): + """Return current humidity in percentage.""" + return self._device.sys_info["current_humidity"] + + @property + def humidity_warning(self) -> bool: + """Return true if humidity is outside of the wanted range.""" + return self._device.sys_info["current_humidity_exception"] != 0 diff --git a/kasa/smart/modules/reportmodule.py b/kasa/smart/modules/reportmodule.py new file mode 100644 index 00000000..04301bb4 --- /dev/null +++ b/kasa/smart/modules/reportmodule.py @@ -0,0 +1,31 @@ +"""Implementation of report module.""" +from typing import TYPE_CHECKING + +from ...feature import Feature +from ..smartmodule import SmartModule + +if TYPE_CHECKING: + from ..smartdevice import SmartDevice + + +class ReportModule(SmartModule): + """Implementation of report module.""" + + REQUIRED_COMPONENT = "report_mode" + QUERY_GETTER_NAME = "get_report_mode" + + def __init__(self, device: "SmartDevice", module: str): + super().__init__(device, module) + self._add_feature( + Feature( + device, + "Report interval", + container=self, + attribute_getter="report_interval", + ) + ) + + @property + def report_interval(self): + """Reporting interval of a sensor device.""" + return self._device.sys_info["report_interval"] diff --git a/kasa/smart/modules/temperature.py b/kasa/smart/modules/temperature.py new file mode 100644 index 00000000..659fb7db --- /dev/null +++ b/kasa/smart/modules/temperature.py @@ -0,0 +1,57 @@ +"""Implementation of temperature module.""" +from typing import TYPE_CHECKING, Literal + +from ...feature import Feature, FeatureType +from ..smartmodule import SmartModule + +if TYPE_CHECKING: + from ..smartdevice import SmartDevice + + +class TemperatureSensor(SmartModule): + """Implementation of temperature module.""" + + REQUIRED_COMPONENT = "humidity" + QUERY_GETTER_NAME = "get_comfort_temp_config" + + def __init__(self, device: "SmartDevice", module: str): + super().__init__(device, module) + self._add_feature( + Feature( + device, + "Temperature", + container=self, + attribute_getter="temperature", + icon="mdi:thermometer", + ) + ) + self._add_feature( + Feature( + device, + "Temperature warning", + container=self, + attribute_getter="temperature_warning", + type=FeatureType.BinarySensor, + icon="mdi:alert", + ) + ) + # TODO: use temperature_unit for feature creation + + @property + def temperature(self): + """Return current humidity in percentage.""" + return self._device.sys_info["current_temp"] + + @property + def temperature_warning(self) -> bool: + """Return True if humidity is outside of the wanted range.""" + return self._device.sys_info["current_temp_exception"] != 0 + + @property + def temperature_unit(self): + """Return current temperature unit.""" + return self._device.sys_info["temp_unit"] + + async def set_temperature_unit(self, unit: Literal["celsius", "fahrenheit"]): + """Set the device temperature unit.""" + return await self.call("set_temperature_unit", {"temp_unit": unit}) diff --git a/kasa/smart/smartbulb.py b/kasa/smart/smartbulb.py index c6295eda..eb3310e8 100644 --- a/kasa/smart/smartbulb.py +++ b/kasa/smart/smartbulb.py @@ -2,11 +2,8 @@ from typing import Any, Dict, List, Optional from ..bulb import Bulb -from ..device_type import DeviceType -from ..deviceconfig import DeviceConfig from ..exceptions import KasaException from ..iot.iotbulb import HSV, BulbPreset, ColorTempRange -from ..smartprotocol import SmartProtocol from .smartdevice import SmartDevice AVAILABLE_EFFECTS = { @@ -21,16 +18,6 @@ class SmartBulb(SmartDevice, Bulb): Documentation TBD. See :class:`~kasa.iot.Bulb` for now. """ - def __init__( - self, - host: str, - *, - config: Optional[DeviceConfig] = None, - protocol: Optional[SmartProtocol] = None, - ) -> None: - super().__init__(host=host, config=config, protocol=protocol) - self._device_type = DeviceType.Bulb - @property def is_color(self) -> bool: """Whether the bulb supports color changes.""" diff --git a/kasa/smart/smartdevice.py b/kasa/smart/smartdevice.py index c5c12fed..66db2c58 100644 --- a/kasa/smart/smartdevice.py +++ b/kasa/smart/smartdevice.py @@ -63,9 +63,12 @@ class SmartDevice(Device): ) for child_info in children } - # TODO: if all are sockets, then we are a strip, and otherwise a hub? - # doesn't work for the walldimmer with fancontrol... - self._device_type = DeviceType.Strip + # TODO: This may not be the best approach, but it allows distinguishing + # between power strips and hubs for the time being. + if all(child.is_plug for child in self._children.values()): + self._device_type = DeviceType.Strip + else: + self._device_type = DeviceType.Hub @property def children(self) -> Sequence["SmartDevice"]: @@ -518,7 +521,7 @@ class SmartDevice(Device): if self.children: if "SMART.TAPOHUB" in self.sys_info["type"]: - pass # TODO: placeholder for future hub PR + self._device_type = DeviceType.Hub else: self._device_type = DeviceType.Strip elif "light_strip" in self._components: diff --git a/kasa/tests/conftest.py b/kasa/tests/conftest.py index e69b73fa..39d5daf5 100644 --- a/kasa/tests/conftest.py +++ b/kasa/tests/conftest.py @@ -47,7 +47,7 @@ SUPPORTED_DEVICES = SUPPORTED_IOT_DEVICES + SUPPORTED_SMART_DEVICES BULBS_SMART_VARIABLE_TEMP = {"L530E", "L930-5"} BULBS_SMART_LIGHT_STRIP = {"L900-5", "L900-10", "L920-5", "L930-5"} BULBS_SMART_COLOR = {"L530E", *BULBS_SMART_LIGHT_STRIP} -BULBS_SMART_DIMMABLE = {"KS225", "L510B", "L510E"} +BULBS_SMART_DIMMABLE = {"L510B", "L510E"} BULBS_SMART = ( BULBS_SMART_VARIABLE_TEMP.union(BULBS_SMART_COLOR) .union(BULBS_SMART_DIMMABLE) @@ -120,7 +120,7 @@ STRIPS_SMART = {"P300", "TP25"} STRIPS = {*STRIPS_IOT, *STRIPS_SMART} DIMMERS_IOT = {"ES20M", "HS220", "KS220M", "KS230", "KP405"} -DIMMERS_SMART = {"S500D", "P135"} +DIMMERS_SMART = {"KS225", "S500D", "P135"} DIMMERS = { *DIMMERS_IOT, *DIMMERS_SMART, diff --git a/kasa/tests/test_childdevice.py b/kasa/tests/test_childdevice.py index 6ffd7054..07baf598 100644 --- a/kasa/tests/test_childdevice.py +++ b/kasa/tests/test_childdevice.py @@ -13,7 +13,6 @@ from .conftest import strip_smart def test_childdevice_init(dev, dummy_protocol, mocker): """Test that child devices get initialized and use protocol wrapper.""" assert len(dev.children) > 0 - assert dev.is_strip first = dev.children[0] assert isinstance(first.protocol, _ChildProtocolWrapper)