Skip to content

Commit

Permalink
tapo-py: Support for playing the alarm on the H100 hub
Browse files Browse the repository at this point in the history
  • Loading branch information
mihai-dinculescu committed Feb 10, 2025
1 parent e6ce474 commit e590ad2
Show file tree
Hide file tree
Showing 12 changed files with 356 additions and 79 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,18 +40,18 @@ 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 | | | ✅ | ✅ | ✅ | | | | |
| set_color_temperature | | | ✅ | ✅ | ✅ | | | | |
| 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.
Expand Down
14 changes: 14 additions & 0 deletions tapo-py/examples/tapo_h100.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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())
53 changes: 53 additions & 0 deletions tapo-py/src/api/hub_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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")]
Expand Down Expand Up @@ -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<Vec<String>> {
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<u32>,
) -> 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::<ErrorWrapper>::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,
Expand Down
11 changes: 9 additions & 2 deletions tapo-py/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
};
Expand Down Expand Up @@ -70,6 +72,11 @@ fn register_requests(module: &Bound<'_, PyModule>) -> Result<(), PyErr> {
module.add_class::<LightingEffectType>()?;
module.add_class::<PyColorLightSetDeviceInfoParams>()?;
module.add_class::<PyEnergyDataInterval>()?;

// hub requests
module.add_class::<AlarmRingtone>()?;
module.add_class::<AlarmVolume>()?;
module.add_class::<PyAlarmDuration>()?;
module.add_class::<TemperatureUnitKE100>()?;

Ok(())
Expand Down
2 changes: 2 additions & 0 deletions tapo-py/src/requests.rs
Original file line number Diff line number Diff line change
@@ -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::*;
9 changes: 9 additions & 0 deletions tapo-py/src/requests/play_alarm.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
use pyo3::prelude::*;

#[derive(Debug, Clone, PartialEq)]
#[pyclass(name = "AlarmDuration", eq)]
pub enum PyAlarmDuration {
Continuous,
Once,
Seconds,
}
30 changes: 29 additions & 1 deletion tapo-py/tapo-py/tapo/hub_handler.pyi
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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."""
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions tapo-py/tapo-py/tapo/requests/__init__.pyi
Original file line number Diff line number Diff line change
@@ -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
103 changes: 103 additions & 0 deletions tapo-py/tapo-py/tapo/requests/play_alarm.pyi
Original file line number Diff line number Diff line change
@@ -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."""
17 changes: 8 additions & 9 deletions tapo/examples/tapo_h100.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,20 +115,19 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
}
}

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?;
Expand Down
Loading

0 comments on commit e590ad2

Please sign in to comment.