Skip to content
Merged
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
2 changes: 2 additions & 0 deletions kasa/smartcamera/modules/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
"""Modules for SMARTCAMERA devices."""

from .alarm import Alarm
from .camera import Camera
from .childdevice import ChildDevice
from .device import DeviceModule
from .led import Led
from .time import Time

__all__ = [
"Alarm",
"Camera",
"ChildDevice",
"DeviceModule",
Expand Down
166 changes: 166 additions & 0 deletions kasa/smartcamera/modules/alarm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
"""Implementation of alarm module."""

from __future__ import annotations

from ...feature import Feature
from ..smartcameramodule import SmartCameraModule

DURATION_MIN = 0
DURATION_MAX = 6000

VOLUME_MIN = 0
VOLUME_MAX = 10


class Alarm(SmartCameraModule):
"""Implementation of alarm module."""

# Needs a different name to avoid clashing with SmartAlarm
NAME = "SmartCameraAlarm"

REQUIRED_COMPONENT = "siren"
QUERY_GETTER_NAME = "getSirenStatus"
QUERY_MODULE_NAME = "siren"

def query(self) -> dict:
"""Query to execute during the update cycle."""
q = super().query()
q["getSirenConfig"] = {self.QUERY_MODULE_NAME: {}}
q["getSirenTypeList"] = {self.QUERY_MODULE_NAME: {}}

return q

def _initialize_features(self) -> None:
"""Initialize features."""
device = self._device
self._add_feature(
Feature(
device,
id="alarm",
name="Alarm",
container=self,
attribute_getter="active",
icon="mdi:bell",
category=Feature.Category.Debug,
type=Feature.Type.BinarySensor,
)
)
self._add_feature(
Feature(
device,
id="alarm_sound",
name="Alarm sound",
container=self,
attribute_getter="alarm_sound",
attribute_setter="set_alarm_sound",
category=Feature.Category.Config,
type=Feature.Type.Choice,
choices_getter="alarm_sounds",
)
)
self._add_feature(
Feature(
device,
id="alarm_volume",
name="Alarm volume",
container=self,
attribute_getter="alarm_volume",
attribute_setter="set_alarm_volume",
category=Feature.Category.Config,
type=Feature.Type.Number,
range_getter=lambda: (VOLUME_MIN, VOLUME_MAX),
)
)
self._add_feature(
Feature(
device,
id="alarm_duration",
name="Alarm duration",
container=self,
attribute_getter="alarm_duration",
attribute_setter="set_alarm_duration",
category=Feature.Category.Config,
type=Feature.Type.Number,
range_getter=lambda: (DURATION_MIN, DURATION_MAX),
)
)
self._add_feature(
Feature(
device,
id="test_alarm",
name="Test alarm",
container=self,
attribute_setter="play",
type=Feature.Type.Action,
)
)
self._add_feature(
Feature(
device,
id="stop_alarm",
name="Stop alarm",
container=self,
attribute_setter="stop",
type=Feature.Type.Action,
)
)

@property
def alarm_sound(self) -> str:
"""Return current alarm sound."""
return self.data["getSirenConfig"]["siren_type"]

async def set_alarm_sound(self, sound: str) -> dict:
"""Set alarm sound.

See *alarm_sounds* for list of available sounds.
"""
if sound not in self.alarm_sounds:
raise ValueError(
f"sound must be one of {', '.join(self.alarm_sounds)}: {sound}"
)
return await self.call("setSirenConfig", {"siren": {"siren_type": sound}})

@property
def alarm_sounds(self) -> list[str]:
"""Return list of available alarm sounds."""
return self.data["getSirenTypeList"]["siren_type_list"]

@property
def alarm_volume(self) -> int:
"""Return alarm volume.

Unlike duration the device expects/returns a string for volume.
"""
return int(self.data["getSirenConfig"]["volume"])

async def set_alarm_volume(self, volume: int) -> dict:
"""Set alarm volume."""
if volume < VOLUME_MIN or volume > VOLUME_MAX:
raise ValueError(f"volume must be between {VOLUME_MIN} and {VOLUME_MAX}")
return await self.call("setSirenConfig", {"siren": {"volume": str(volume)}})

@property
def alarm_duration(self) -> int:
"""Return alarm duration."""
return self.data["getSirenConfig"]["duration"]

async def set_alarm_duration(self, duration: int) -> dict:
"""Set alarm volume."""
if duration < DURATION_MIN or duration > DURATION_MAX:
msg = f"duration must be between {DURATION_MIN} and {DURATION_MAX}"
raise ValueError(msg)
return await self.call("setSirenConfig", {"siren": {"duration": duration}})

@property
def active(self) -> bool:
"""Return true if alarm is active."""
return self.data["getSirenStatus"]["status"] != "off"

async def play(self) -> dict:
"""Play alarm."""
return await self.call("setSirenStatus", {"siren": {"status": "on"}})

async def stop(self) -> dict:
"""Stop alarm."""
return await self.call("setSirenStatus", {"siren": {"status": "off"}})
17 changes: 10 additions & 7 deletions kasa/smartcamera/smartcameramodule.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
from __future__ import annotations

import logging
from typing import TYPE_CHECKING, Any, cast
from typing import TYPE_CHECKING, Any, Final, cast

from ..exceptions import DeviceError, KasaException, SmartErrorCode
from ..modulemapping import ModuleName
from ..smart.smartmodule import SmartModule

if TYPE_CHECKING:
from . import modules
from .smartcamera import SmartCamera

_LOGGER = logging.getLogger(__name__)
Expand All @@ -17,12 +19,14 @@
class SmartCameraModule(SmartModule):
"""Base class for SMARTCAMERA modules."""

SmartCameraAlarm: Final[ModuleName[modules.Alarm]] = ModuleName("SmartCameraAlarm")

#: Query to execute during the main update cycle
QUERY_GETTER_NAME: str
#: Module name to be queried
QUERY_MODULE_NAME: str
#: Section name or names to be queried
QUERY_SECTION_NAMES: str | list[str]
QUERY_SECTION_NAMES: str | list[str] | None = None

REGISTERED_MODULES = {}

Expand All @@ -33,11 +37,10 @@ def query(self) -> dict:

Default implementation uses the raw query getter w/o parameters.
"""
return {
self.QUERY_GETTER_NAME: {
self.QUERY_MODULE_NAME: {"name": self.QUERY_SECTION_NAMES}
}
}
section_names = (
{"name": self.QUERY_SECTION_NAMES} if self.QUERY_SECTION_NAMES else {}
)
return {self.QUERY_GETTER_NAME: {self.QUERY_MODULE_NAME: section_names}}

async def call(self, method: str, params: dict | None = None) -> dict:
"""Call a method.
Expand Down
71 changes: 35 additions & 36 deletions tests/fakeprotocol_smartcamera.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,43 +105,28 @@ def _get_param_set_value(info: dict, set_keys: list[str], value):
info = info[key]
info[set_keys[-1]] = value

# Setters for when there's not a simple mapping of setters to getters
SETTERS = {
("system", "sys", "dev_alias"): [
"getDeviceInfo",
"device_info",
"basic_info",
"device_alias",
],
("lens_mask", "lens_mask_info", "enabled"): [
"getLensMaskConfig",
"lens_mask",
"lens_mask_info",
"enabled",
],
# setTimezone maps to getClockStatus
("system", "clock_status", "seconds_from_1970"): [
"getClockStatus",
"system",
"clock_status",
"seconds_from_1970",
],
# setTimezone maps to getClockStatus
("system", "clock_status", "local_time"): [
"getClockStatus",
"system",
"clock_status",
"local_time",
],
("system", "basic", "zone_id"): [
"getTimezone",
"system",
"basic",
"zone_id",
],
("led", "config", "enabled"): [
"getLedStatus",
"led",
"config",
"enabled",
],
}

async def _send_request(self, request_dict: dict):
Expand All @@ -154,27 +139,41 @@ async def _send_request(self, request_dict: dict):
)

if method[:3] == "set":
get_method = "g" + method[1:]
for key, val in request_dict.items():
if key != "method":
# key is params for multi request and the actual params
# for single requests
if key == "params":
module = next(iter(val))
val = val[module]
if key == "method":
continue
# key is params for multi request and the actual params
# for single requests
if key == "params":
module = next(iter(val))
val = val[module]
else:
module = key
section = next(iter(val))
skey_val = val[section]
if not isinstance(skey_val, dict): # single level query
section_key = section
section_val = skey_val
if (get_info := info.get(get_method)) and section_key in get_info:
get_info[section_key] = section_val
else:
module = key
section = next(iter(val))
skey_val = val[section]
for skey, sval in skey_val.items():
section_key = skey
section_value = sval
if setter_keys := self.SETTERS.get(
(module, section, section_key)
):
self._get_param_set_value(info, setter_keys, section_value)
else:
return {"error_code": -1}
return {"error_code": -1}
break
for skey, sval in skey_val.items():
section_key = skey
section_value = sval
if setter_keys := self.SETTERS.get((module, section, section_key)):
self._get_param_set_value(info, setter_keys, section_value)
elif (
section := info.get(get_method, {})
.get(module, {})
.get(section, {})
) and section_key in section:
section[section_key] = section_value
else:
return {"error_code": -1}
break
return {"error_code": 0}
elif method[:3] == "get":
params = request_dict.get("params")
Expand Down
Empty file.
Loading
Loading