Add initial support for H100 and T315 (#776)

Adds initial support for H100 and its alarmmodule.

Also implements the following modules for T315:
* reportmodule (reporting interval)
* battery
* humidity
* temperature
This commit is contained in:
Teemu R 2024-02-22 23:09:38 +01:00 committed by GitHub
parent 951d41a628
commit bc65f96f85
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 292 additions and 21 deletions

View File

@ -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})")

View File

@ -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,

View File

@ -15,6 +15,7 @@ class DeviceType(Enum):
Dimmer = "dimmer"
LightStrip = "lightstrip"
Sensor = "sensor"
Hub = "hub"
Unknown = "unknown"
@staticmethod

View File

@ -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):

View File

@ -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",

View File

@ -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")

View File

@ -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"]

View File

@ -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

View File

@ -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"]

View File

@ -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})

View File

@ -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."""

View File

@ -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:

View File

@ -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,

View File

@ -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)