diff --git a/README.md b/README.md index 9846362..2be59ca 100644 --- a/README.md +++ b/README.md @@ -40,10 +40,10 @@ Unofficial Tapo API Client. Works with TP-Link Tapo smart devices. Tested with l | get_device_usage | | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | | | get_energy_data | | | | | | | ✅ | | | | get_energy_usage | | | | | | | ✅ | | | -| get_supported_ringtone_list | | | | | | | | | ✓ | +| get_supported_ringtone_list | | | | | | | | | ✅ | | off | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | | | on | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | | -| play_alarm | | | | | | | | | ✓ | +| play_alarm | | | | | | | | | ✅ | | refresh_session | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | set_brightness | | ✅ | ✅ | ✅ | ✅ | | | | | | set_color | | | ✅ | ✅ | ✅ | | | | | @@ -51,7 +51,7 @@ Unofficial Tapo API Client. Works with TP-Link Tapo smart devices. Tested with l | set_hue_saturation | | | ✅ | ✅ | ✅ | | | | | | set_lighting_effect | | | | | ✅ | | | | | | set() API \* | | | ✅ | ✅ | ✅ | | | | | -| stop_alarm | | | | | | | | | ✓ | +| stop_alarm | | | | | | | | | ✅ | \* The `set()` API allows multiple properties to be set in a single request. diff --git a/tapo-py/examples/tapo_h100.py b/tapo-py/examples/tapo_h100.py index 3729fae..96d8260 100644 --- a/tapo-py/examples/tapo_h100.py +++ b/tapo-py/examples/tapo_h100.py @@ -4,6 +4,7 @@ import os from tapo import ApiClient +from tapo.requests import AlarmRingtone, AlarmVolume, AlarmDuration from tapo.responses import KE100Result, S200BResult, T100Result, T110Result, T300Result, T31XResult @@ -101,6 +102,19 @@ async def main(): ) ) + print(f"Triggering the alarm ringtone 'Alarm 1' at a 'Low' volume for '3 Seconds'...") + await hub.play_alarm(AlarmRingtone.Alarm1, AlarmVolume.Low, AlarmDuration.Seconds, seconds=3) + + device_info = await hub.get_device_info() + print(f"Is device ringing?: {device_info.in_alarm}") + + print("Stopping the alarm after 1 Second...") + await asyncio.sleep(1) + await hub.stop_alarm() + + device_info = await hub.get_device_info() + print(f"Is device ringing?: {device_info.in_alarm}") + if __name__ == "__main__": asyncio.run(main()) diff --git a/tapo-py/src/api/hub_handler.rs b/tapo-py/src/api/hub_handler.rs index 08aa2f3..2b134a7 100644 --- a/tapo-py/src/api/hub_handler.rs +++ b/tapo-py/src/api/hub_handler.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use pyo3::prelude::*; use pyo3::types::{PyDict, PyList}; +use tapo::requests::{AlarmDuration, AlarmRingtone, AlarmVolume}; use tapo::responses::{ChildDeviceHubResult, DeviceInfoHubResult}; use tapo::{Error, HubDevice, HubHandler}; use tokio::sync::RwLock; @@ -12,6 +13,7 @@ use crate::api::{ }; use crate::call_handler_method; use crate::errors::ErrorWrapper; +use crate::requests::PyAlarmDuration; #[derive(Clone)] #[pyclass(name = "HubHandler")] @@ -131,6 +133,57 @@ impl PyHubHandler { Python::with_gil(|py| tapo::python::serde_object_to_py_dict(py, &result)) } + pub async fn get_supported_ringtone_list(&self) -> PyResult> { + let handler = self.inner.clone(); + call_handler_method!( + handler.read().await.deref(), + HubHandler::get_supported_ringtone_list + ) + } + + #[pyo3(signature = (ringtone, volume, duration, seconds=None))] + pub async fn play_alarm( + &self, + ringtone: AlarmRingtone, + volume: AlarmVolume, + duration: PyAlarmDuration, + seconds: Option, + ) -> PyResult<()> { + let handler = self.inner.clone(); + + let duration = match duration { + PyAlarmDuration::Continuous => AlarmDuration::Continuous, + PyAlarmDuration::Once => AlarmDuration::Once, + PyAlarmDuration::Seconds => { + if let Some(seconds) = seconds { + AlarmDuration::Seconds(seconds) + } else { + return Err(Into::::into(Error::Validation { + field: "seconds".to_string(), + message: + "A value must be provided for seconds when duration = AlarmDuration.Seconds" + .to_string(), + }) + .into()); + } + } + }; + + call_handler_method!( + handler.read().await.deref(), + HubHandler::play_alarm, + ringtone, + volume, + duration.into() + ) + } + + pub async fn stop_alarm(&self) -> PyResult<()> { + let handler = self.inner.clone(); + + call_handler_method!(handler.read().await.deref(), HubHandler::stop_alarm) + } + #[pyo3(signature = (device_id=None, nickname=None))] pub async fn ke100( &self, diff --git a/tapo-py/src/lib.rs b/tapo-py/src/lib.rs index 852dbbf..dd2fbaa 100644 --- a/tapo-py/src/lib.rs +++ b/tapo-py/src/lib.rs @@ -8,7 +8,7 @@ use log::LevelFilter; use pyo3::prelude::*; use pyo3_log::{Caching, Logger}; -use tapo::requests::{Color, LightingEffectPreset, LightingEffectType}; +use tapo::requests::{AlarmRingtone, AlarmVolume, Color, LightingEffectPreset, LightingEffectType}; use tapo::responses::{ AutoOffStatus, ColorLightState, CurrentPowerResult, DefaultBrightnessState, DefaultColorLightState, DefaultLightState, DefaultPlugState, DefaultPowerType, @@ -30,7 +30,9 @@ use api::{ PyPowerStripPlugHandler, PyRgbLightStripHandler, PyRgbicLightStripHandler, PyT100Handler, PyT110Handler, PyT300Handler, PyT31XHandler, }; -use requests::{PyColorLightSetDeviceInfoParams, PyEnergyDataInterval, PyLightingEffect}; +use requests::{ + PyAlarmDuration, PyColorLightSetDeviceInfoParams, PyEnergyDataInterval, PyLightingEffect, +}; use responses::{ TriggerLogsS200BResult, TriggerLogsT100Result, TriggerLogsT110Result, TriggerLogsT300Result, }; @@ -70,6 +72,11 @@ fn register_requests(module: &Bound<'_, PyModule>) -> Result<(), PyErr> { module.add_class::()?; module.add_class::()?; module.add_class::()?; + + // hub requests + module.add_class::()?; + module.add_class::()?; + module.add_class::()?; module.add_class::()?; Ok(()) diff --git a/tapo-py/src/requests.rs b/tapo-py/src/requests.rs index 50eb687..13e48ac 100644 --- a/tapo-py/src/requests.rs +++ b/tapo-py/src/requests.rs @@ -1,5 +1,7 @@ mod energy_data_interval; +mod play_alarm; mod set_device_info; pub use energy_data_interval::*; +pub use play_alarm::*; pub use set_device_info::*; diff --git a/tapo-py/src/requests/play_alarm.rs b/tapo-py/src/requests/play_alarm.rs new file mode 100644 index 0000000..a5c6a4c --- /dev/null +++ b/tapo-py/src/requests/play_alarm.rs @@ -0,0 +1,9 @@ +use pyo3::prelude::*; + +#[derive(Debug, Clone, PartialEq)] +#[pyclass(name = "AlarmDuration", eq)] +pub enum PyAlarmDuration { + Continuous, + Once, + Seconds, +} diff --git a/tapo-py/tapo-py/tapo/hub_handler.pyi b/tapo-py/tapo-py/tapo/hub_handler.pyi index 1b34674..507ccaa 100644 --- a/tapo-py/tapo-py/tapo/hub_handler.pyi +++ b/tapo-py/tapo-py/tapo/hub_handler.pyi @@ -1,5 +1,7 @@ from typing import List, Optional, Union +from tapo import KE100Handler, S200BHandler, T100Handler, T110Handler, T300Handler, T31XHandler +from tapo.requests.play_alarm import AlarmDuration, AlarmRingtone, AlarmVolume from tapo.responses import ( DeviceInfoHubResult, KE100Result, @@ -9,7 +11,6 @@ from tapo.responses import ( T300Result, T31XResult, ) -from tapo import KE100Handler, S200BHandler, T100Handler, T110Handler, T300Handler, T31XHandler class HubHandler: """Handler for the [H100](https://www.tapo.com/en/search/?q=H100) hubs.""" @@ -76,6 +77,33 @@ class HubHandler: dict: Device info as a dictionary. """ + async def get_supported_ringtone_list() -> List[str]: + """Returns a list of ringtones (alarm types) supported by the hub. + Used for debugging only. + + Returns: + List[str]: List of the ringtones supported by the hub. + """ + + async def play_alarm( + self, + ringtone: AlarmRingtone, + volume: AlarmVolume, + duration: AlarmDuration, + seconds: Optional[int] = None, + ) -> None: + """Start playing the hub alarm. + + Args: + ringtone (AlarmRingtone): The ringtone of a H100 alarm. + volume (AlarmVolume): The volume of the alarm. + duration (AlarmDuration): Controls how long the alarm plays for. + seconds (Optional[int]): Play the alarm a number of seconds. Required if `duration` is `AlarmDuration.Seconds`. + """ + + async def stop_alarm(self) -> None: + """Stop playing the hub alarm, if it's currently playing.""" + async def ke100( self, device_id: Optional[str] = None, nickname: Optional[str] = None ) -> KE100Handler: diff --git a/tapo-py/tapo-py/tapo/requests/__init__.pyi b/tapo-py/tapo-py/tapo/requests/__init__.pyi index 37391d9..27f18d0 100644 --- a/tapo-py/tapo-py/tapo/requests/__init__.pyi +++ b/tapo-py/tapo-py/tapo/requests/__init__.pyi @@ -1,3 +1,5 @@ from .energy_data_interval import * from .set_device_info import * +from .play_alarm import * + from tapo.responses import TemperatureUnitKE100 as TemperatureUnitKE100 diff --git a/tapo-py/tapo-py/tapo/requests/play_alarm.pyi b/tapo-py/tapo-py/tapo/requests/play_alarm.pyi new file mode 100644 index 0000000..768ba87 --- /dev/null +++ b/tapo-py/tapo-py/tapo/requests/play_alarm.pyi @@ -0,0 +1,103 @@ +from enum import Enum + +class AlarmVolume(str, Enum): + """The volume of the alarm. + For the H100, this is a fixed list of volume levels.""" + + Default = "Default" + """Use the default volume for the hub.""" + + Mute = "Mute" + """Mute the audio output from the alarm. + This causes the alarm to be shown as triggered in the Tapo App + without an audible sound, and makes the `in_alarm` property + in `DeviceInfoHubResult` return as `True`.""" + + Low = "Low" + """Lowest volume.""" + + Normal = "Normal" + """Normal volume. This is the default.""" + + High = "High" + """Highest volume.""" + +class AlarmRingtone(str, Enum): + """The ringtone of a H100 alarm.""" + + Alarm1 = "Alarm1" + """Alarm 1""" + + Alarm2 = "Alarm2" + """Alarm 2""" + + Alarm3 = "Alarm3" + """Alarm 3""" + + Alarm4 = "Alarm4" + """Alarm 4""" + + Alarm5 = "Alarm5" + """Alarm 5""" + + Connection1 = "Connection1" + """Connection 1""" + + Connection2 = "Connection2" + """Connection 2""" + + DoorbellRing1 = "DoorbellRing1" + """Doorbell Ring 1""" + + DoorbellRing2 = "DoorbellRing2" + """Doorbell Ring 2""" + + DoorbellRing3 = "DoorbellRing3" + """Doorbell Ring 3""" + + DoorbellRing4 = "DoorbellRing4" + """Doorbell Ring 4""" + + DoorbellRing5 = "DoorbellRing5" + """Doorbell Ring 5""" + + DoorbellRing6 = "DoorbellRing6" + """Doorbell Ring 6""" + + DoorbellRing7 = "DoorbellRing7" + """Doorbell Ring 7""" + + DoorbellRing8 = "DoorbellRing8" + """Doorbell Ring 8""" + + DoorbellRing9 = "DoorbellRing9" + """Doorbell Ring 9""" + + DoorbellRing10 = "DoorbellRing10" + """Doorbell Ring 10""" + + DrippingTap = "DrippingTap" + """Dripping Tap""" + + PhoneRing = "PhoneRing" + """Phone Ring""" + +class AlarmDuration(str, Enum): + """Controls how long the alarm plays for.""" + + Continuous = "Continuous" + """Play the alarm continuously until stopped.""" + + Once = "Once" + """Play the alarm once. + This is useful for previewing the audio. + + Limitations: + + The `in_alarm` field of `DeviceInfoHubResult` will not remain `True` for the + duration of the audio track. Each audio track has a different runtime. + + Has no observable affect when used in conjunction with `AlarmVolume.Mute`.""" + + Seconds = "Seconds" + """Play the alarm a number of seconds.""" diff --git a/tapo/examples/tapo_h100.rs b/tapo/examples/tapo_h100.rs index 208548c..bdbbc77 100644 --- a/tapo/examples/tapo_h100.rs +++ b/tapo/examples/tapo_h100.rs @@ -115,20 +115,19 @@ async fn main() -> Result<(), Box> { } } - let ringtone = AlarmRingtone::Alarm1; - let volume = AlarmVolume::Low; - let duration = AlarmDuration::Seconds(1); - - info!("Triggering the alarm ringtone {ringtone:?} for {duration:?} at a {volume:?} volume"); - hub.play_alarm(Some(ringtone), Some(volume), duration) - .await?; + info!("Triggering the alarm ringtone 'Alarm 1' at a 'Low' volume for '3 Seconds'..."); + hub.play_alarm( + AlarmRingtone::Alarm1, + AlarmVolume::Low, + AlarmDuration::Seconds(3), + ) + .await?; let device_info = hub.get_device_info().await?; info!("Is device ringing?: {:?}", device_info.in_alarm); + info!("Stopping the alarm after 1 Second..."); tokio::time::sleep(Duration::from_secs(1)).await; - - info!("Stopping the alarm"); hub.stop_alarm().await?; let device_info = hub.get_device_info().await?; diff --git a/tapo/src/api/hub_handler.rs b/tapo/src/api/hub_handler.rs index d25579d..ac93151 100644 --- a/tapo/src/api/hub_handler.rs +++ b/tapo/src/api/hub_handler.rs @@ -58,38 +58,6 @@ impl HubHandler { self.client.read().await.get_device_info().await } - /// Returns a list of ringtones (alarm types) supported by the hub. - /// Used for debugging only. - pub async fn get_supported_ringtone_list(&self) -> Result, Error> { - self.client - .read() - .await - .get_supported_alarm_type_list() - .await - .map(|response| response.alarm_type_list) - } - - /// Start playing the hub alarm. - /// By default, this uses the configured alarm settings on the hub. - /// Each of the settings can be overridden by passing `Some` as a parameter. - pub async fn play_alarm( - &self, - ringtone: Option, - volume: Option, - duration: AlarmDuration, - ) -> Result<(), Error> { - self.client - .read() - .await - .play_alarm(PlayAlarmParams::new(ringtone, volume, duration)?) - .await - } - - /// Stop playing the hub alarm if currently playing - pub async fn stop_alarm(&self) -> Result<(), Error> { - self.client.read().await.stop_alarm().await - } - /// Returns *device info* as [`serde_json::Value`]. /// It contains all the properties returned from the Tapo API. pub async fn get_device_info_json(&self) -> Result { @@ -149,6 +117,36 @@ impl HubHandler { .get_child_device_component_list() .await } + + /// Returns a list of ringtones (alarm types) supported by the hub. + /// Used for debugging only. + pub async fn get_supported_ringtone_list(&self) -> Result, Error> { + self.client + .read() + .await + .get_supported_alarm_type_list() + .await + .map(|response| response.alarm_type_list) + } + + /// Start playing the hub alarm. + pub async fn play_alarm( + &self, + ringtone: AlarmRingtone, + volume: AlarmVolume, + duration: AlarmDuration, + ) -> Result<(), Error> { + self.client + .read() + .await + .play_alarm(PlayAlarmParams::new(ringtone, volume, duration)?) + .await + } + + /// Stop playing the hub alarm, if it's currently playing. + pub async fn stop_alarm(&self) -> Result<(), Error> { + self.client.read().await.stop_alarm().await + } } /// Child device handler builders. diff --git a/tapo/src/requests/play_alarm.rs b/tapo/src/requests/play_alarm.rs index d295fbe..f4616f7 100644 --- a/tapo/src/requests/play_alarm.rs +++ b/tapo/src/requests/play_alarm.rs @@ -6,7 +6,15 @@ use serde::{Serialize, Serializer}; #[derive(Debug, Default, Serialize)] #[cfg_attr(test, derive(Clone, Copy))] #[serde(rename_all = "lowercase")] +#[cfg_attr( + feature = "python", + derive(Clone, PartialEq), + pyo3::prelude::pyclass(get_all, eq, eq_int) +)] pub enum AlarmVolume { + /// Use the default volume for the hub. + #[default] + Default, /// Mute the audio output from the alarm. /// This causes the alarm to be shown as triggered in the Tapo App /// without an audible sound, and makes the `in_alarm` property @@ -15,16 +23,29 @@ pub enum AlarmVolume { /// Lowest volume. Low, /// Normal volume. This is the default. - #[default] Normal, /// Highest volume. High, } +impl AlarmVolume { + fn is_default(&self) -> bool { + matches!(self, Self::Default) + } +} + /// The ringtone of a H100 alarm. -#[derive(Debug, Serialize)] +#[derive(Debug, Default, Serialize)] #[cfg_attr(test, derive(Clone, Copy))] +#[cfg_attr( + feature = "python", + derive(Clone, PartialEq), + pyo3::prelude::pyclass(get_all, eq, eq_int) +)] pub enum AlarmRingtone { + /// Use the default ringtone for the hub. + #[default] + Default, /// Alarm 1 #[serde(rename = "Alarm 1")] Alarm1, @@ -84,11 +105,16 @@ pub enum AlarmRingtone { PhoneRing, } +impl AlarmRingtone { + fn is_default(&self) -> bool { + matches!(self, Self::Default) + } +} + /// Controls how long the alarm plays for. -#[derive(Debug, Default)] +#[derive(Debug)] pub enum AlarmDuration { - /// Play the alarm continuously until stopped - #[default] + /// Play the alarm continuously until stopped. Continuous, /// Play the alarm once. /// This is useful for previewing the audio. @@ -97,9 +123,9 @@ pub enum AlarmDuration { /// The `in_alarm` field of [`crate::responses::DeviceInfoHubResult`] will not remain `true` for the /// duration of the audio track. Each audio track has a different runtime. /// - /// Has no observable affect if the [`AlarmVolume::Mute`]. + /// Has no observable affect when used in conjunction with [`AlarmVolume::Mute`]. Once, - /// Play the alarm a number of seconds + /// Play the alarm a number of seconds. Seconds(u32), } impl AlarmDuration { @@ -119,19 +145,19 @@ impl Serialize for AlarmDuration { } /// Parameters for playing the alarm on a H100 hub. -#[derive(Debug, Default, Serialize)] +#[derive(Debug, Serialize)] pub(crate) struct PlayAlarmParams { - #[serde(skip_serializing_if = "Option::is_none")] - alarm_type: Option, - #[serde(skip_serializing_if = "Option::is_none")] - alarm_volume: Option, + #[serde(skip_serializing_if = "AlarmRingtone::is_default")] + alarm_type: AlarmRingtone, + #[serde(skip_serializing_if = "AlarmVolume::is_default")] + alarm_volume: AlarmVolume, #[serde(skip_serializing_if = "AlarmDuration::is_continuous")] alarm_duration: AlarmDuration, } impl PlayAlarmParams { pub(crate) fn new( - ringtone: Option, - volume: Option, + ringtone: AlarmRingtone, + volume: AlarmVolume, duration: AlarmDuration, ) -> Result { let params = Self { @@ -146,8 +172,8 @@ impl PlayAlarmParams { fn validate(&self) -> Result<(), Error> { match self.alarm_duration { AlarmDuration::Seconds(0) => Err(Error::Validation { - field: "alarm_duration".to_string(), - message: "seconds must be greater than zero".to_string(), + field: "duration".to_string(), + message: "The seconds value must be greater than zero".to_string(), }), _ => Ok(()), } @@ -160,8 +186,8 @@ mod tests { #[test] fn test_valid_inputs() { - for valid_ringtone in [None, Some(AlarmRingtone::Alarm1)] { - for valid_volume in [None, Some(AlarmVolume::Normal)] { + for valid_ringtone in [AlarmRingtone::Default, AlarmRingtone::Alarm1] { + for valid_volume in [AlarmVolume::Default, AlarmVolume::Normal] { for valid_duration in [ AlarmDuration::Continuous, AlarmDuration::Once, @@ -176,16 +202,20 @@ mod tests { #[test] fn test_invalid_inputs() { - let result = PlayAlarmParams::new(None, None, AlarmDuration::Seconds(0)); + let result = PlayAlarmParams::new( + AlarmRingtone::Default, + AlarmVolume::Default, + AlarmDuration::Seconds(0), + ); assert!(matches!( result.err(), - Some(Error::Validation { field, message }) if field == "alarm_duration" && message == "seconds must be greater than zero" + Some(Error::Validation { field, message }) if field == "duration" && message == "The seconds value must be greater than zero" )); } fn params_to_json( - ringtone: Option, - volume: Option, + ringtone: AlarmRingtone, + volume: AlarmVolume, duration: AlarmDuration, ) -> String { let params = PlayAlarmParams::new(ringtone, volume, duration).unwrap(); @@ -196,7 +226,11 @@ mod tests { fn test_serialize_params_where_ringtone_is_some() { assert_eq!( r#"{"alarm_type":"Alarm 1"}"#, - params_to_json(Some(AlarmRingtone::Alarm1), None, AlarmDuration::Continuous) + params_to_json( + AlarmRingtone::Alarm1, + AlarmVolume::Default, + AlarmDuration::Continuous + ) ); } @@ -204,19 +238,35 @@ mod tests { fn test_serialize_params_where_volume_is_some() { assert_eq!( r#"{"alarm_volume":"mute"}"#, - params_to_json(None, Some(AlarmVolume::Mute), AlarmDuration::Continuous) + params_to_json( + AlarmRingtone::Default, + AlarmVolume::Mute, + AlarmDuration::Continuous + ) ); assert_eq!( r#"{"alarm_volume":"low"}"#, - params_to_json(None, Some(AlarmVolume::Low), AlarmDuration::Continuous) + params_to_json( + AlarmRingtone::Default, + AlarmVolume::Low, + AlarmDuration::Continuous + ) ); assert_eq!( r#"{"alarm_volume":"normal"}"#, - params_to_json(None, Some(AlarmVolume::Normal), AlarmDuration::Continuous) + params_to_json( + AlarmRingtone::Default, + AlarmVolume::Normal, + AlarmDuration::Continuous + ) ); assert_eq!( r#"{"alarm_volume":"high"}"#, - params_to_json(None, Some(AlarmVolume::High), AlarmDuration::Continuous) + params_to_json( + AlarmRingtone::Default, + AlarmVolume::High, + AlarmDuration::Continuous + ) ); } @@ -224,7 +274,11 @@ mod tests { fn test_serialize_params_where_duration_is_continuous() { assert_eq!( r#"{}"#, - params_to_json(None, None, AlarmDuration::Continuous) + params_to_json( + AlarmRingtone::Default, + AlarmVolume::Default, + AlarmDuration::Continuous + ) ); } @@ -232,7 +286,11 @@ mod tests { fn test_serialize_params_where_duration_is_once() { assert_eq!( r#"{"alarm_duration":0}"#, - params_to_json(None, None, AlarmDuration::Once) + params_to_json( + AlarmRingtone::Default, + AlarmVolume::Default, + AlarmDuration::Once + ) ); } @@ -240,7 +298,11 @@ mod tests { fn test_serialize_params_where_duration_is_1second() { assert_eq!( r#"{"alarm_duration":1}"#, - params_to_json(None, None, AlarmDuration::Seconds(1)) + params_to_json( + AlarmRingtone::Default, + AlarmVolume::Default, + AlarmDuration::Seconds(1) + ) ); } @@ -249,8 +311,8 @@ mod tests { assert_eq!( r#"{"alarm_type":"Doorbell Ring 1","alarm_volume":"normal","alarm_duration":1}"#, params_to_json( - Some(AlarmRingtone::DoorbellRing1), - Some(AlarmVolume::Normal), + AlarmRingtone::DoorbellRing1, + AlarmVolume::Normal, AlarmDuration::Seconds(1) ) );