From 5b5a148f9a5f70452fc4478cb36c8cd986216164 Mon Sep 17 00:00:00 2001 From: "Steven B." <51370195+sdb9696@users.noreply.github.com> Date: Tue, 19 Nov 2024 10:11:51 +0000 Subject: [PATCH] Add pan tilt camera module (#1261) Add ptz controls for smartcameras. --------- Co-authored-by: Teemu R. --- kasa/smartcamera/modules/__init__.py | 2 + kasa/smartcamera/modules/pantilt.py | 107 +++++++++++++++++++++++++++ tests/test_feature.py | 4 +- 3 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 kasa/smartcamera/modules/pantilt.py diff --git a/kasa/smartcamera/modules/__init__.py b/kasa/smartcamera/modules/__init__.py index f3e36cc3..462241e8 100644 --- a/kasa/smartcamera/modules/__init__.py +++ b/kasa/smartcamera/modules/__init__.py @@ -5,6 +5,7 @@ from .camera import Camera from .childdevice import ChildDevice from .device import DeviceModule from .led import Led +from .pantilt import PanTilt from .time import Time __all__ = [ @@ -13,5 +14,6 @@ __all__ = [ "ChildDevice", "DeviceModule", "Led", + "PanTilt", "Time", ] diff --git a/kasa/smartcamera/modules/pantilt.py b/kasa/smartcamera/modules/pantilt.py new file mode 100644 index 00000000..d1882927 --- /dev/null +++ b/kasa/smartcamera/modules/pantilt.py @@ -0,0 +1,107 @@ +"""Implementation of time module.""" + +from __future__ import annotations + +from ...feature import Feature +from ..smartcameramodule import SmartCameraModule + +DEFAULT_PAN_STEP = 30 +DEFAULT_TILT_STEP = 10 + + +class PanTilt(SmartCameraModule): + """Implementation of device_local_time.""" + + REQUIRED_COMPONENT = "ptz" + _pan_step = DEFAULT_PAN_STEP + _tilt_step = DEFAULT_TILT_STEP + + def _initialize_features(self) -> None: + """Initialize features after the initial update.""" + + async def set_pan_step(value: int) -> None: + self._pan_step = value + + async def set_tilt_step(value: int) -> None: + self._tilt_step = value + + self._add_feature( + Feature( + self._device, + "pan_right", + "Pan right", + container=self, + attribute_setter=lambda: self.pan(self._pan_step * -1), + type=Feature.Type.Action, + ) + ) + self._add_feature( + Feature( + self._device, + "pan_left", + "Pan left", + container=self, + attribute_setter=lambda: self.pan(self._pan_step), + type=Feature.Type.Action, + ) + ) + self._add_feature( + Feature( + self._device, + "pan_step", + "Pan step", + container=self, + attribute_getter="_pan_step", + attribute_setter=set_pan_step, + type=Feature.Type.Number, + ) + ) + self._add_feature( + Feature( + self._device, + "tilt_up", + "Tilt up", + container=self, + attribute_setter=lambda: self.tilt(self._tilt_step), + type=Feature.Type.Action, + ) + ) + self._add_feature( + Feature( + self._device, + "tilt_down", + "Tilt down", + container=self, + attribute_setter=lambda: self.tilt(self._tilt_step * -1), + type=Feature.Type.Action, + ) + ) + self._add_feature( + Feature( + self._device, + "tilt_step", + "Tilt step", + container=self, + attribute_getter="_tilt_step", + attribute_setter=set_tilt_step, + type=Feature.Type.Number, + ) + ) + + def query(self) -> dict: + """Query to execute during the update cycle.""" + return {} + + async def pan(self, pan: int) -> dict: + """Pan horizontally.""" + return await self.move(pan=pan, tilt=0) + + async def tilt(self, tilt: int) -> dict: + """Tilt vertically.""" + return await self.move(pan=0, tilt=tilt) + + async def move(self, *, pan: int, tilt: int) -> dict: + """Pan and tilt camera.""" + return await self._device._raw_query( + {"do": {"motor": {"move": {"x_coord": str(pan), "y_coord": str(tilt)}}}} + ) diff --git a/tests/test_feature.py b/tests/test_feature.py index 0ff6e1be..79560b1a 100644 --- a/tests/test_feature.py +++ b/tests/test_feature.py @@ -160,12 +160,14 @@ async def test_precision_hint(dummy_feature, precision_hint): async def test_feature_setters(dev: Device, mocker: MockerFixture): """Test that all feature setters query something.""" + # setters that do not call set on the device itself. + internal_setters = {"pan_step", "tilt_step"} async def _test_feature(feat, query_mock): if feat.attribute_setter is None: return - expecting_call = True + expecting_call = feat.id not in internal_setters if feat.type == Feature.Type.Number: await feat.set_value(feat.minimum_value)