feat(tapo): add countdown/schedule support and CLI tool

This commit is contained in:
seb
2025-12-24 06:53:49 +01:00
parent 028763bdb2
commit 853a67c73a
12 changed files with 513 additions and 124 deletions

View File

@@ -10,14 +10,14 @@ use tokio::sync::RwLock;
use crate::error::{Error, TapoResponseError};
use crate::requests::{
ControlChildParams, DeviceRebootParams, EmptyParams, EnergyDataInterval,
GetChildDeviceListParams, GetEnergyDataParams, GetPowerDataParams, GetRulesParams,
LightingEffect, MultipleRequestParams, PlayAlarmParams, PowerDataInterval, TapoParams,
TapoRequest,
AddCountdownRuleParams, ControlChildParams, DeviceRebootParams, EditCountdownRuleParams,
EmptyParams, EnergyDataInterval, GetChildDeviceListParams, GetEnergyDataParams,
GetPowerDataParams, GetRulesParams, LightingEffect, MultipleRequestParams, PlayAlarmParams,
PowerDataInterval, TapoParams, TapoRequest,
};
use crate::responses::{
ControlChildResult, CountdownRulesResult, CurrentPowerResult, DecodableResultExt,
EnergyDataResult, EnergyDataResultRaw, EnergyUsageResult, NextEventResult, PowerDataResult,
EnergyDataResult, EnergyDataResultRaw, EnergyUsageResult, PowerDataResult,
PowerDataResultRaw, ScheduleRulesResult, SupportedAlarmTypeListResult, TapoMultipleResponse,
TapoResponseExt, TapoResult, validate_response,
};
@@ -876,16 +876,35 @@ impl ApiClient {
.await?
.ok_or_else(|| Error::Tapo(TapoResponseError::EmptyResult))
}
/// Gets next scheduled event.
pub(crate) async fn get_next_event(&self) -> Result<NextEventResult, Error> {
debug!("Get Next event...");
let request = TapoRequest::GetNextEvent(TapoParams::new(EmptyParams));
/// Adds or updates a countdown rule.
pub(crate) async fn add_countdown_rule(&self, delay: u64, turn_on: bool) -> Result<(), Error> {
// Check if a countdown rule already exists
let existing = self.get_countdown_rules().await.ok();
if let Some(countdown) = existing {
if let Some(rule) = countdown.rules.first() {
// Edit existing rule
debug!("Edit Countdown rule: id={}, delay={}, turn_on={}", rule.id, delay, turn_on);
let request = TapoRequest::EditCountdownRule(TapoParams::new(
EditCountdownRuleParams::new(rule.id.clone(), delay, turn_on),
));
self.get_protocol()?
.execute_request::<serde_json::Value>(request, true)
.await?;
return Ok(());
}
}
// No existing rule, add new one
debug!("Add Countdown rule: delay={}, turn_on={}", delay, turn_on);
let request = TapoRequest::AddCountdownRule(TapoParams::new(
AddCountdownRuleParams::new(delay, turn_on),
));
self.get_protocol()?
.execute_request(request, true)
.await?
.ok_or_else(|| Error::Tapo(TapoResponseError::EmptyResult))
.execute_request::<serde_json::Value>(request, true)
.await?;
Ok(())
}
fn get_protocol_mut(&mut self) -> Result<&mut TapoProtocol, Error> {

View File

@@ -7,7 +7,7 @@ use crate::error::Error;
use crate::requests::{EnergyDataInterval, GenericSetDeviceInfoParams, PowerDataInterval};
use crate::responses::{
CountdownRulesResult, CurrentPowerResult, DeviceInfoPlugEnergyMonitoringResult,
DeviceUsageEnergyMonitoringResult, EnergyDataResult, EnergyUsageResult, NextEventResult,
DeviceUsageEnergyMonitoringResult, EnergyDataResult, EnergyUsageResult,
PowerDataResult, ScheduleRulesResult,
};
@@ -98,9 +98,13 @@ impl PlugEnergyMonitoringHandler {
self.client.read().await.get_schedule_rules().await
}
/// Returns *next scheduled event* as [`NextEventResult`].
pub async fn get_next_event(&self) -> Result<NextEventResult, Error> {
self.client.read().await.get_next_event().await
/// Sets a countdown rule.
///
/// # Arguments
/// * `delay` - Seconds until action
/// * `turn_on` - true to turn on, false to turn off when countdown completes
pub async fn set_countdown(&self, delay: u64, turn_on: bool) -> Result<(), Error> {
self.client.read().await.add_countdown_rule(delay, turn_on).await
}
}

View File

@@ -5,7 +5,9 @@ use tokio::sync::{RwLock, RwLockReadGuard};
use crate::error::Error;
use crate::requests::GenericSetDeviceInfoParams;
use crate::responses::{DeviceInfoPlugResult, DeviceUsageResult};
use crate::responses::{
CountdownRulesResult, DeviceInfoPlugResult, DeviceUsageResult, ScheduleRulesResult,
};
use super::{ApiClient, ApiClientExt, DeviceManagementExt, HandlerExt};
@@ -56,6 +58,25 @@ impl PlugHandler {
pub async fn get_device_usage(&self) -> Result<DeviceUsageResult, Error> {
self.client.read().await.get_device_usage().await
}
/// Returns *countdown rules* as [`CountdownRulesResult`].
pub async fn get_countdown_rules(&self) -> Result<CountdownRulesResult, Error> {
self.client.read().await.get_countdown_rules().await
}
/// Returns *schedule rules* as [`ScheduleRulesResult`].
pub async fn get_schedule_rules(&self) -> Result<ScheduleRulesResult, Error> {
self.client.read().await.get_schedule_rules().await
}
/// Sets a countdown rule.
///
/// # Arguments
/// * `delay` - Seconds until action
/// * `turn_on` - true to turn on, false to turn off when countdown completes
pub async fn set_countdown(&self, delay: u64, turn_on: bool) -> Result<(), Error> {
self.client.read().await.add_countdown_rule(delay, turn_on).await
}
}
#[async_trait]

View File

@@ -1,5 +1,6 @@
//! Tapo request objects.
mod add_countdown_rule;
mod control_child;
mod device_reboot;
mod energy_data_interval;
@@ -23,6 +24,7 @@ pub use play_alarm::*;
pub use power_data_interval::*;
pub use set_device_info::*;
pub(crate) use add_countdown_rule::*;
pub(crate) use control_child::*;
pub(crate) use device_reboot::*;
pub(crate) use get_child_device_list::*;
@@ -35,3 +37,4 @@ pub(crate) use login_device::*;
pub(crate) use multiple_request::*;
pub(crate) use secure_passthrough::*;
pub(crate) use tapo_request::*;

View File

@@ -0,0 +1,55 @@
//! Parameters for editing countdown rules
use serde::Serialize;
/// Parameters for editing a countdown rule
#[derive(Debug, Serialize)]
pub(crate) struct EditCountdownRuleParams {
/// Rule ID to edit
pub id: String,
/// Delay in seconds
pub delay: u64,
/// Desired states when countdown completes
pub desired_states: CountdownDesiredStates,
/// Whether to enable the rule
pub enable: bool,
}
/// Desired states for countdown
#[derive(Debug, Clone, Serialize)]
pub(crate) struct CountdownDesiredStates {
/// Whether device should be on
pub on: bool,
}
impl EditCountdownRuleParams {
pub fn new(id: String, delay: u64, turn_on: bool) -> Self {
Self {
id,
delay,
desired_states: CountdownDesiredStates { on: turn_on },
enable: true,
}
}
}
/// Parameters for adding a countdown rule
#[derive(Debug, Serialize)]
pub(crate) struct AddCountdownRuleParams {
/// Delay in seconds
pub delay: u64,
/// Desired states when countdown completes
pub desired_states: CountdownDesiredStates,
/// Whether to enable the rule
pub enable: bool,
}
impl AddCountdownRuleParams {
pub fn new(delay: u64, turn_on: bool) -> Self {
Self {
delay,
desired_states: CountdownDesiredStates { on: turn_on },
enable: true,
}
}
}

View File

@@ -7,6 +7,7 @@ pub(crate) struct GetRulesParams {
}
impl GetRulesParams {
#[allow(dead_code)]
pub fn new(start_index: u32) -> Self {
Self { start_index }
}

View File

@@ -3,9 +3,10 @@ use std::time::{SystemTime, UNIX_EPOCH};
use serde::Serialize;
use super::{
ControlChildParams, DeviceRebootParams, GetChildDeviceListParams, GetEnergyDataParams,
GetPowerDataParams, GetRulesParams, GetTriggerLogsParams, HandshakeParams, LightingEffect,
LoginDeviceParams, MultipleRequestParams, PlayAlarmParams, SecurePassthroughParams,
AddCountdownRuleParams, ControlChildParams, DeviceRebootParams, EditCountdownRuleParams,
GetChildDeviceListParams, GetEnergyDataParams, GetPowerDataParams, GetRulesParams,
GetTriggerLogsParams, HandshakeParams, LightingEffect, LoginDeviceParams,
MultipleRequestParams, PlayAlarmParams, SecurePassthroughParams,
};
#[derive(Debug, Serialize)]
@@ -46,9 +47,15 @@ pub(crate) enum TapoRequest {
GetCountdownRules(TapoParams<GetRulesParams>),
#[serde(rename = "get_schedule_rules")]
GetScheduleRules(TapoParams<GetRulesParams>),
#[serde(rename = "add_countdown_rule")]
AddCountdownRule(TapoParams<AddCountdownRuleParams>),
#[serde(rename = "edit_countdown_rule")]
EditCountdownRule(TapoParams<EditCountdownRuleParams>),
#[serde(rename = "get_next_event")]
#[allow(dead_code)]
GetNextEvent(TapoParams<EmptyParams>),
#[serde(rename = "get_antitheft_rules")]
#[allow(dead_code)]
GetAntitheftRules(TapoParams<GetRulesParams>),
}

View File

@@ -15,15 +15,14 @@ pub struct CountdownRule {
pub delay: u64,
/// Seconds remaining (if timer is active)
pub remain: u64,
/// Action when countdown completes: true = turn on, false = turn off
#[serde(rename = "desired_states")]
pub desired_states: Option<CountdownDesiredState>,
/// Action when countdown completes
pub desired_states: Option<DesiredState>,
}
/// Desired state for countdown
/// Desired state for countdown/schedule
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct CountdownDesiredState {
/// Whether device should be on after countdown
pub struct DesiredState {
/// Whether device should be on
pub on: Option<bool>,
}
@@ -34,47 +33,39 @@ pub struct ScheduleRule {
pub id: String,
/// Whether the rule is enabled
pub enable: bool,
/// Weekday mask (bits 0-6 for Sun-Sat)
/// Weekday mask (bits for days, 127 = all days)
#[serde(default)]
pub wday: Vec<u8>,
pub week_day: u8,
/// Start minute of day (0-1439)
#[serde(rename = "s_min", default)]
pub start_min: u16,
/// End minute of day (for duration schedules)
#[serde(rename = "e_min", default)]
pub end_min: u16,
/// Action: true = turn on, false = turn off
#[serde(rename = "desired_states")]
pub desired_states: Option<ScheduleDesiredState>,
}
/// Desired state for schedule
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ScheduleDesiredState {
/// Whether device should be on
pub on: Option<bool>,
}
/// Next scheduled event
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct NextEventResult {
/// Schedule type
#[serde(rename = "schd_type")]
pub schedule_type: Option<String>,
/// Timestamp of next event (seconds since epoch)
pub timestamp: Option<u64>,
/// Action for the event
pub action: Option<i32>,
#[serde(default)]
pub s_min: u16,
/// End minute of day
#[serde(default)]
pub e_min: u16,
/// Mode (e.g., "repeat")
pub mode: Option<String>,
/// Day of month
pub day: Option<u8>,
/// Month
pub month: Option<u8>,
/// Year
pub year: Option<u16>,
/// Action
pub desired_states: Option<DesiredState>,
}
/// Result wrapper for countdown rules
#[derive(Debug, Clone, Deserialize)]
pub struct CountdownRulesResult {
/// Whether countdown is enabled globally
#[serde(default)]
pub enable: bool,
/// Max countdown rules
#[serde(default)]
pub countdown_rule_max_count: u32,
/// List of countdown rules
#[serde(rename = "countdown_rules")]
#[serde(rename = "rule_list", default)]
pub rules: Vec<CountdownRule>,
/// Sum of rules (for pagination)
pub sum: Option<u32>,
}
impl TapoResponseExt for CountdownRulesResult {}
@@ -82,13 +73,21 @@ impl TapoResponseExt for CountdownRulesResult {}
/// Result wrapper for schedule rules
#[derive(Debug, Clone, Deserialize)]
pub struct ScheduleRulesResult {
/// Whether schedule is enabled globally
#[serde(default)]
pub enable: bool,
/// Max schedule rules
#[serde(default)]
pub schedule_rule_max_count: u32,
/// List of schedule rules
#[serde(rename = "schedule_rules")]
#[serde(rename = "rule_list", default)]
pub rules: Vec<ScheduleRule>,
/// Sum of rules (for pagination)
pub sum: Option<u32>,
/// Total count (for pagination)
#[serde(default)]
pub sum: u32,
/// Start index (for pagination)
#[serde(default)]
pub start_index: u32,
}
impl TapoResponseExt for ScheduleRulesResult {}
impl TapoResponseExt for NextEventResult {}