diff --git a/kasa/module.py b/kasa/module.py index 754814ec..2870b661 100644 --- a/kasa/module.py +++ b/kasa/module.py @@ -159,6 +159,7 @@ class Module(ABC): # SMARTCAM only modules Camera: Final[ModuleName[smartcam.Camera]] = ModuleName("Camera") + LensMask: Final[ModuleName[smartcam.LensMask]] = ModuleName("LensMask") def __init__(self, device: Device, module: str) -> None: self._device = device diff --git a/kasa/smartcam/modules/__init__.py b/kasa/smartcam/modules/__init__.py index a3f51c87..fae5923f 100644 --- a/kasa/smartcam/modules/__init__.py +++ b/kasa/smartcam/modules/__init__.py @@ -6,6 +6,7 @@ from .childdevice import ChildDevice from .device import DeviceModule from .homekit import HomeKit from .led import Led +from .lensmask import LensMask from .matter import Matter from .pantilt import PanTilt from .time import Time @@ -20,4 +21,5 @@ __all__ = [ "Time", "HomeKit", "Matter", + "LensMask", ] diff --git a/kasa/smartcam/modules/camera.py b/kasa/smartcam/modules/camera.py index e96794c2..1e1f4570 100644 --- a/kasa/smartcam/modules/camera.py +++ b/kasa/smartcam/modules/camera.py @@ -1,16 +1,18 @@ -"""Implementation of device module.""" +"""Implementation of camera module.""" from __future__ import annotations import base64 import logging from enum import StrEnum +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 from ..smartcammodule import SmartCamModule _LOGGER = logging.getLogger(__name__) @@ -29,28 +31,37 @@ class StreamResolution(StrEnum): class Camera(SmartCamModule): """Implementation of device module.""" - QUERY_GETTER_NAME = "getLensMaskConfig" - QUERY_MODULE_NAME = "lens_mask" - QUERY_SECTION_NAMES = "lens_mask_info" - def _initialize_features(self) -> None: """Initialize features after the initial update.""" - self._add_feature( - Feature( - self._device, - id="state", - name="State", - attribute_getter="is_on", - attribute_setter="set_state", - type=Feature.Type.Switch, - category=Feature.Category.Primary, + if Module.LensMask in self._device.modules: + self._add_feature( + Feature( + self._device, + id="state", + name="State", + attribute_getter="is_on", + attribute_setter="set_state", + type=Feature.Type.Switch, + category=Feature.Category.Primary, + ) ) - ) @property def is_on(self) -> bool: - """Return the device id.""" - return self.data["lens_mask_info"]["enabled"] == "off" + """Return the device on state.""" + if lens_mask := self._device.modules.get(Module.LensMask): + return lens_mask.state + return True + + async def set_state(self, on: bool) -> Annotated[dict, FeatureAttribute()]: + """Set the device on state. + + If the device does not support setting state will do nothing. + """ + if lens_mask := self._device.modules.get(Module.LensMask): + # Turning off enables the privacy mask which is why value is reversed. + return await lens_mask.set_state(not on) + return {} def _get_credentials(self) -> Credentials | None: """Get credentials from .""" @@ -109,14 +120,6 @@ class Camera(SmartCamModule): """Return the onvif url.""" return f"http://{self._device.host}:{ONVIF_PORT}/onvif/device_service" - async def set_state(self, on: bool) -> dict: - """Set the device state.""" - # Turning off enables the privacy mask which is why value is reversed. - params = {"enabled": "off" if on else "on"} - return await self._device._query_setter_helper( - "setLensMaskConfig", self.QUERY_MODULE_NAME, "lens_mask_info", params - ) - 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/modules/lensmask.py b/kasa/smartcam/modules/lensmask.py new file mode 100644 index 00000000..7a54beb1 --- /dev/null +++ b/kasa/smartcam/modules/lensmask.py @@ -0,0 +1,29 @@ +"""Implementation of lens mask privacy module.""" + +from __future__ import annotations + +import logging + +from ..smartcammodule import SmartCamModule + +_LOGGER = logging.getLogger(__name__) + + +class LensMask(SmartCamModule): + """Implementation of lens mask module.""" + + QUERY_GETTER_NAME = "getLensMaskConfig" + QUERY_MODULE_NAME = "lens_mask" + QUERY_SECTION_NAMES = "lens_mask_info" + + @property + def state(self) -> bool: + """Return the lens mask state.""" + return self.data["lens_mask_info"]["enabled"] == "off" + + async def set_state(self, state: bool) -> dict: + """Set the lens mask state.""" + params = {"enabled": "on" if state else "off"} + return await self._device._query_setter_helper( + "setLensMaskConfig", self.QUERY_MODULE_NAME, "lens_mask_info", params + ) diff --git a/kasa/smartcam/smartcamdevice.py b/kasa/smartcam/smartcamdevice.py index b3058ab3..6bc4963a 100644 --- a/kasa/smartcam/smartcamdevice.py +++ b/kasa/smartcam/smartcamdevice.py @@ -134,6 +134,11 @@ 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()) diff --git a/tests/smartcam/test_smartcamdevice.py b/tests/smartcam/test_smartcamdevice.py index 438737eb..3355d2f0 100644 --- a/tests/smartcam/test_smartcamdevice.py +++ b/tests/smartcam/test_smartcamdevice.py @@ -17,10 +17,20 @@ async def test_state(dev: Device): if dev.device_type is DeviceType.Hub: pytest.skip("Hubs cannot be switched on and off") - state = dev.is_on - await dev.set_state(not state) + if Module.LensMask in dev.modules: + state = dev.is_on + await dev.set_state(not state) + await dev.update() + assert dev.is_on is not state + + dev.modules.pop(Module.LensMask) # type: ignore[attr-defined] + + # Test with no lens mask module. Device is always on. + assert dev.is_on is True + res = await dev.set_state(False) + assert res == {} await dev.update() - assert dev.is_on is not state + assert dev.is_on is True @device_smartcam