Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions SUPPORTED.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,7 @@ All Tapo devices require authentication.<br>Hub-Connected Devices may work acros
- Hardware: 2.0 (EU) / Firmware: 1.4.3
- **C210**
- Hardware: 2.0 / Firmware: 1.3.11
- Hardware: 1.0 (EU) / Firmware: 1.4.7
- Hardware: 2.0 (EU) / Firmware: 1.4.2
- Hardware: 2.0 (EU) / Firmware: 1.4.3
- **C220**
Expand Down
1 change: 1 addition & 0 deletions kasa/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ class Module(ABC):
# SMARTCAM only modules
Camera: Final[ModuleName[smartcam.Camera]] = ModuleName("Camera")
LensMask: Final[ModuleName[smartcam.LensMask]] = ModuleName("LensMask")
PanTilt: Final[ModuleName[smartcam.PanTilt]] = ModuleName("PanTilt")

# Vacuum modules
Clean: Final[ModuleName[smart.Clean]] = ModuleName("Clean")
Expand Down
76 changes: 72 additions & 4 deletions kasa/smartcam/modules/pantilt.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Implementation of time module."""
"""Implementation of pan/tilt module."""

from __future__ import annotations

Expand All @@ -10,9 +10,13 @@


class PanTilt(SmartCamModule):
"""Implementation of device_local_time."""
"""Implementation of pan/tilt module for PTZ cameras."""

REQUIRED_COMPONENT = "ptz"
QUERY_GETTER_NAME = "getPresetConfig"
QUERY_MODULE_NAME = "preset"
QUERY_SECTION_NAMES = ["preset"]

_pan_step = DEFAULT_PAN_STEP
_tilt_step = DEFAULT_TILT_STEP

Expand Down Expand Up @@ -88,10 +92,52 @@ async def set_tilt_step(value: int) -> None:
)
)

def query(self) -> dict:
"""Query to execute during the update cycle."""
if self._presets:
self._add_feature(
Feature(
self._device,
"ptz_preset",
"PTZ Preset",
container=self,
attribute_getter="preset",
attribute_setter="set_preset",
choices_getter=lambda: list(self._presets.keys()),
type=Feature.Type.Choice,
)
)

@property
def _presets(self) -> dict[str, str]:
"""Return presets from device data."""
if "preset" not in self.data:
return {}
preset_info = self.data["preset"]
return {
name: preset_id
for preset_id, name in zip(
preset_info.get("id", []), preset_info.get("name", []), strict=False
)
}

@property
def preset(self) -> str | None:
"""Return first preset name as current value."""
return next(iter(self._presets.keys()), None)

async def set_preset(self, preset: str) -> dict:
"""Set preset by name or ID."""
preset_id = self._presets.get(preset)
if preset_id:
return await self.goto_preset(preset_id)
if preset in self._presets.values():
return await self.goto_preset(preset)
return {}

@property
def presets(self) -> dict[str, str]:
"""Return available presets as dict of name -> id."""
return self._presets

async def pan(self, pan: int) -> dict:
"""Pan horizontally."""
return await self.move(pan=pan, tilt=0)
Expand All @@ -105,3 +151,25 @@ async def move(self, *, pan: int, tilt: int) -> dict:
return await self._device._raw_query(
{"do": {"motor": {"move": {"x_coord": str(pan), "y_coord": str(tilt)}}}}
)

async def get_presets(self) -> dict:
"""Get presets."""
return await self._device._raw_query(
{"getPresetConfig": {"preset": {"name": ["preset"]}}}
)

async def goto_preset(self, preset_id: str) -> dict:
"""Go to preset."""
return await self._device._raw_query(
{"motorMoveToPreset": {"preset": {"goto_preset": {"id": preset_id}}}}
)

async def save_preset(self, name: str) -> dict:
"""Save preset."""
return await self._device._raw_query(
{
"addMotorPostion": { # Note: API has typo in method name
"preset": {"set_preset": {"name": name, "save_ptz": "1"}}
}
}
)
2 changes: 2 additions & 0 deletions tests/fakeprotocol_smartcam.py
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,8 @@ async def _send_request(self, request_dict: dict):
elif method in [
"addScanChildDeviceList",
"startScanChildDevice",
"motorMoveToPreset",
"addMotorPostion", # Note: API has typo in method name
]:
return {"result": {}, "error_code": 0}

Expand Down
Loading
Loading