From 54bb53899e4300b57847c59b8cd715e416912c3e Mon Sep 17 00:00:00 2001 From: steveredden <35814432+steveredden@users.noreply.github.com> Date: Thu, 23 Jan 2025 03:22:41 -0600 Subject: [PATCH] Add support for doorbells and chimes (#1435) Add support for `smart` chimes and `smartcam` doorbells that are not hub child devices. Co-authored-by: Steven B <51370195+sdb9696@users.noreply.github.com> --- README.md | 5 +++-- SUPPORTED.md | 21 ++++++++++++--------- devtools/generate_supported.py | 4 +++- kasa/device_factory.py | 6 +++++- kasa/device_type.py | 2 ++ kasa/deviceconfig.py | 2 ++ kasa/smart/smartdevice.py | 2 ++ kasa/smartcam/modules/camera.py | 7 ++----- kasa/smartcam/smartcamchild.py | 7 +++++++ kasa/smartcam/smartcamdevice.py | 20 +++++++++----------- tests/device_fixtures.py | 13 +++++++++++++ tests/test_device.py | 16 +++------------- tests/test_device_factory.py | 12 ++++++++++++ 13 files changed, 75 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index 8c7ac09a..9761684f 100644 --- a/README.md +++ b/README.md @@ -201,10 +201,11 @@ The following devices have been tested and confirmed as working. If your device - **Wall Switches**: S210, S220, S500D, S505, S505D - **Bulbs**: L510B, L510E, L530E, L630 - **Light Strips**: L900-10, L900-5, L920-5, L930-5 -- **Cameras**: C100, C210, C220, C225, C325WB, C520WS, C720, D230, TC65, TC70 +- **Cameras**: C100, C210, C220, C225, C325WB, C520WS, C720, TC65, TC70 +- **Doorbells and chimes**: D230 +- **Vacuums**: RV20 Max Plus, RV30 Max - **Hubs**: H100, H200 - **Hub-Connected Devices[^3]**: S200B, S200D, T100, T110, T300, T310, T315 -- **Vacuums**: RV20 Max Plus, RV30 Max [^1]: Model requires authentication diff --git a/SUPPORTED.md b/SUPPORTED.md index 905f7ab3..01d2d63e 100644 --- a/SUPPORTED.md +++ b/SUPPORTED.md @@ -285,13 +285,23 @@ All Tapo devices require authentication.
Hub-Connected Devices may work acros - Hardware: 1.0 (US) / Firmware: 1.2.8 - **C720** - Hardware: 1.0 (US) / Firmware: 1.2.3 -- **D230** - - Hardware: 1.20 (EU) / Firmware: 1.1.19 - **TC65** - Hardware: 1.0 / Firmware: 1.3.9 - **TC70** - Hardware: 3.0 / Firmware: 1.3.11 +### Doorbells and chimes + +- **D230** + - Hardware: 1.20 (EU) / Firmware: 1.1.19 + +### Vacuums + +- **RV20 Max Plus** + - Hardware: 1.0 (EU) / Firmware: 1.0.7 +- **RV30 Max** + - Hardware: 1.0 (US) / Firmware: 1.2.0 + ### Hubs - **H100** @@ -326,13 +336,6 @@ All Tapo devices require authentication.
Hub-Connected Devices may work acros - Hardware: 1.0 (EU) / Firmware: 1.7.0 - Hardware: 1.0 (US) / Firmware: 1.8.0 -### Vacuums - -- **RV20 Max Plus** - - Hardware: 1.0 (EU) / Firmware: 1.0.7 -- **RV30 Max** - - Hardware: 1.0 (US) / Firmware: 1.2.0 - [^1]: Model requires authentication diff --git a/devtools/generate_supported.py b/devtools/generate_supported.py index 8aba9b21..669a2de2 100755 --- a/devtools/generate_supported.py +++ b/devtools/generate_supported.py @@ -36,10 +36,12 @@ DEVICE_TYPE_TO_PRODUCT_GROUP = { DeviceType.Bulb: "Bulbs", DeviceType.LightStrip: "Light Strips", DeviceType.Camera: "Cameras", + DeviceType.Doorbell: "Doorbells and chimes", + DeviceType.Chime: "Doorbells and chimes", + DeviceType.Vacuum: "Vacuums", DeviceType.Hub: "Hubs", DeviceType.Sensor: "Hub-Connected Devices", DeviceType.Thermostat: "Hub-Connected Devices", - DeviceType.Vacuum: "Vacuums", } diff --git a/kasa/device_factory.py b/kasa/device_factory.py index 83661038..53ceba17 100644 --- a/kasa/device_factory.py +++ b/kasa/device_factory.py @@ -159,6 +159,7 @@ def get_device_class_from_family( "SMART.KASAHUB": SmartDevice, "SMART.KASASWITCH": SmartDevice, "SMART.IPCAMERA.HTTPS": SmartCamDevice, + "SMART.TAPODOORBELL.HTTPS": SmartCamDevice, "SMART.TAPOROBOVAC.HTTPS": SmartDevice, "IOT.SMARTPLUGSWITCH": IotPlug, "IOT.SMARTBULB": IotBulb, @@ -194,7 +195,10 @@ def get_protocol(config: DeviceConfig, *, strict: bool = False) -> BaseProtocol protocol_name = ctype.device_family.value.split(".")[0] _LOGGER.debug("Finding protocol for %s", ctype.device_family) - if ctype.device_family is DeviceFamily.SmartIpCamera: + if ctype.device_family in { + DeviceFamily.SmartIpCamera, + DeviceFamily.SmartTapoDoorbell, + }: if strict and ctype.encryption_type is not DeviceEncryptionType.Aes: return None return SmartCamProtocol(transport=SslAesTransport(config=config)) diff --git a/kasa/device_type.py b/kasa/device_type.py index 7fe485d3..d3996217 100755 --- a/kasa/device_type.py +++ b/kasa/device_type.py @@ -22,6 +22,8 @@ class DeviceType(Enum): Fan = "fan" Thermostat = "thermostat" Vacuum = "vacuum" + Chime = "chime" + Doorbell = "doorbell" Unknown = "unknown" @staticmethod diff --git a/kasa/deviceconfig.py b/kasa/deviceconfig.py index b6325570..2b669f80 100644 --- a/kasa/deviceconfig.py +++ b/kasa/deviceconfig.py @@ -79,6 +79,8 @@ class DeviceFamily(Enum): SmartKasaHub = "SMART.KASAHUB" SmartIpCamera = "SMART.IPCAMERA" SmartTapoRobovac = "SMART.TAPOROBOVAC" + SmartTapoChime = "SMART.TAPOCHIME" + SmartTapoDoorbell = "SMART.TAPODOORBELL" class _DeviceConfigBaseMixin(DataClassJSONMixin): diff --git a/kasa/smart/smartdevice.py b/kasa/smart/smartdevice.py index ee86b0e2..c668a208 100644 --- a/kasa/smart/smartdevice.py +++ b/kasa/smart/smartdevice.py @@ -885,6 +885,8 @@ class SmartDevice(Device): return DeviceType.Thermostat if "ROBOVAC" in device_type: return DeviceType.Vacuum + if "TAPOCHIME" in device_type: + return DeviceType.Chime _LOGGER.warning("Unknown device type, falling back to plug") return DeviceType.Plug diff --git a/kasa/smartcam/modules/camera.py b/kasa/smartcam/modules/camera.py index 9a339120..bd4b2808 100644 --- a/kasa/smartcam/modules/camera.py +++ b/kasa/smartcam/modules/camera.py @@ -9,7 +9,6 @@ from typing import Annotated from urllib.parse import quote_plus from ...credentials import Credentials -from ...device_type import DeviceType from ...feature import Feature from ...json import loads as json_loads from ...module import FeatureAttribute, Module @@ -31,6 +30,8 @@ class StreamResolution(StrEnum): class Camera(SmartCamModule): """Implementation of device module.""" + REQUIRED_COMPONENT = "video" + def _initialize_features(self) -> None: """Initialize features after the initial update.""" if Module.LensMask in self._device.modules: @@ -126,7 +127,3 @@ class Camera(SmartCamModule): return None return f"http://{self._device.host}:{ONVIF_PORT}/onvif/device_service" - - async def _check_supported(self) -> bool: - """Additional check to see if the module is supported by the device.""" - return self._device.device_type is DeviceType.Camera diff --git a/kasa/smartcam/smartcamchild.py b/kasa/smartcam/smartcamchild.py index d1b263b4..d2614464 100644 --- a/kasa/smartcam/smartcamchild.py +++ b/kasa/smartcam/smartcamchild.py @@ -85,6 +85,13 @@ class SmartCamChild(SmartChildDevice, SmartCamDevice): # devices self._info = self._map_child_info_from_parent(info) + @property + def device_type(self) -> DeviceType: + """Return the device type.""" + if self._device_type == DeviceType.Unknown and self._info: + self._device_type = self._get_device_type_from_sysinfo(self._info) + return self._device_type + @staticmethod def _get_device_info( info: dict[str, Any], discovery_info: dict[str, Any] | None diff --git a/kasa/smartcam/smartcamdevice.py b/kasa/smartcam/smartcamdevice.py index d096fb5b..fc9d0b92 100644 --- a/kasa/smartcam/smartcamdevice.py +++ b/kasa/smartcam/smartcamdevice.py @@ -26,12 +26,15 @@ class SmartCamDevice(SmartDevice): @staticmethod def _get_device_type_from_sysinfo(sysinfo: dict[str, Any]) -> DeviceType: """Find type to be displayed as a supported device category.""" - if ( - sysinfo - and (device_type := sysinfo.get("device_type")) - and device_type.endswith("HUB") - ): + if not (device_type := sysinfo.get("device_type")): + return DeviceType.Unknown + + if device_type.endswith("HUB"): return DeviceType.Hub + + if "DOORBELL" in device_type: + return DeviceType.Doorbell + return DeviceType.Camera @staticmethod @@ -165,11 +168,6 @@ class SmartCamDevice(SmartDevice): if ( mod.REQUIRED_COMPONENT and mod.REQUIRED_COMPONENT not in self._components - # Always add Camera module to cameras - and ( - mod._module_name() != Module.Camera - or self._device_type is not DeviceType.Camera - ) ): continue module = mod(self, mod._module_name()) @@ -258,7 +256,7 @@ class SmartCamDevice(SmartDevice): @property def device_type(self) -> DeviceType: """Return the device type.""" - if self._device_type == DeviceType.Unknown: + if self._device_type == DeviceType.Unknown and self._info: self._device_type = self._get_device_type_from_sysinfo(self._info) return self._device_type diff --git a/tests/device_fixtures.py b/tests/device_fixtures.py index f28b17e3..f6a2dfe4 100644 --- a/tests/device_fixtures.py +++ b/tests/device_fixtures.py @@ -131,6 +131,7 @@ SENSORS_SMART = { "S200D", "S210", "S220", + "D100C", # needs a home category? } THERMOSTATS_SMART = {"KE100"} @@ -345,6 +346,16 @@ hub_smartcam = parametrize( device_type_filter=[DeviceType.Hub], protocol_filter={"SMARTCAM"}, ) +doobell_smartcam = parametrize( + "doorbell smartcam", + device_type_filter=[DeviceType.Doorbell], + protocol_filter={"SMARTCAM", "SMARTCAM.CHILD"}, +) +chime_smart = parametrize( + "chime smart", + device_type_filter=[DeviceType.Chime], + protocol_filter={"SMART"}, +) vacuum = parametrize("vacuums", device_type_filter=[DeviceType.Vacuum]) @@ -362,7 +373,9 @@ def check_categories(): + hubs_smart.args[1] + sensors_smart.args[1] + thermostats_smart.args[1] + + chime_smart.args[1] + camera_smartcam.args[1] + + doobell_smartcam.args[1] + hub_smartcam.args[1] + vacuum.args[1] ) diff --git a/tests/test_device.py b/tests/test_device.py index 4f74e89c..2c001bc6 100644 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -121,19 +121,9 @@ async def test_device_class_repr(device_class_name_obj): klass = device_class_name_obj[1] if issubclass(klass, SmartChildDevice | SmartCamChild): parent = SmartDevice(host, config=config) - smartcam_required = { - "device_model": "foo", - "device_type": "SMART.TAPODOORBELL", - "alias": "Foo", - "sw_ver": "1.1", - "hw_ver": "1.0", - "mac": "1.2.3.4", - "hwId": "hw_id", - "oem_id": "oem_id", - } dev = klass( parent, - {"dummy": "info", "device_id": "dummy", **smartcam_required}, + {"dummy": "info", "device_id": "dummy"}, { "component_list": [{"id": "device", "ver_code": 1}], "app_component_list": [{"name": "device", "version": 1}], @@ -153,8 +143,8 @@ async def test_device_class_repr(device_class_name_obj): IotCamera: DeviceType.Camera, SmartChildDevice: DeviceType.Unknown, SmartDevice: DeviceType.Unknown, - SmartCamDevice: DeviceType.Camera, - SmartCamChild: DeviceType.Camera, + SmartCamDevice: DeviceType.Unknown, + SmartCamChild: DeviceType.Unknown, } type_ = CLASS_TO_DEFAULT_TYPE[klass] child_repr = ">" diff --git a/tests/test_device_factory.py b/tests/test_device_factory.py index d6bdaedf..539609c3 100644 --- a/tests/test_device_factory.py +++ b/tests/test_device_factory.py @@ -245,6 +245,12 @@ ET = DeviceEncryptionType SslAesTransport, id="smartcam-hub", ), + pytest.param( + CP(DF.SmartTapoDoorbell, ET.Aes, https=True), + SmartCamProtocol, + SslAesTransport, + id="smartcam-doorbell", + ), pytest.param( CP(DF.IotIpCamera, ET.Aes, https=True), IotProtocol, @@ -281,6 +287,12 @@ ET = DeviceEncryptionType KlapTransportV2, id="smart-klap", ), + pytest.param( + CP(DF.SmartTapoChime, ET.Klap, https=False), + SmartProtocol, + KlapTransportV2, + id="smart-chime", + ), ], ) async def test_get_protocol(