feat(tapo): add countdown/schedule support and CLI tool
This commit is contained in:
@@ -14,7 +14,7 @@ serde_json = "1"
|
|||||||
toml = "0.8"
|
toml = "0.8"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
env_logger = "0.11"
|
env_logger = "0.11"
|
||||||
clap = { version = "4", features = ["derive"] }
|
clap = { version = "4", features = ["derive", "env"] }
|
||||||
# Add reqwest with rustls to override tapo's default
|
# Add reqwest with rustls to override tapo's default
|
||||||
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] }
|
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] }
|
||||||
|
|
||||||
|
|||||||
@@ -124,14 +124,18 @@ echo -e "${GREEN}Building for local/native target...${NC}"
|
|||||||
HOST_TARGET=$(rustc -vV | grep host | cut -d' ' -f2)
|
HOST_TARGET=$(rustc -vV | grep host | cut -d' ' -f2)
|
||||||
|
|
||||||
# Use separate target dir for local builds to avoid GLIBC conflicts with cross builds
|
# Use separate target dir for local builds to avoid GLIBC conflicts with cross builds
|
||||||
CARGO_TARGET_DIR=target-local cargo build --release -j $(nproc)
|
# Build both tapo-agent and tapo-countdown
|
||||||
|
CARGO_TARGET_DIR=target-local cargo build --release --bin tapo-agent --bin tapo-countdown -j $(nproc)
|
||||||
|
|
||||||
# Copy binary to dist folder
|
# Copy binaries to dist folder
|
||||||
cp "target-local/release/tapo-agent" "dist/tapo-agent-local-${HOST_TARGET}"
|
cp "target-local/release/tapo-agent" "dist/tapo-agent-local-${HOST_TARGET}"
|
||||||
|
cp "target-local/release/tapo-countdown" "dist/tapo-countdown-local-${HOST_TARGET}"
|
||||||
|
|
||||||
# Get binary size
|
# Get binary sizes
|
||||||
size=$(du -h "dist/tapo-agent-local-${HOST_TARGET}" | cut -f1)
|
size_agent=$(du -h "dist/tapo-agent-local-${HOST_TARGET}" | cut -f1)
|
||||||
echo -e " → ${GREEN}dist/tapo-agent-local-${HOST_TARGET}${NC} ($size)"
|
size_cnt=$(du -h "dist/tapo-countdown-local-${HOST_TARGET}" | cut -f1)
|
||||||
|
echo -e " → ${GREEN}dist/tapo-agent-local-${HOST_TARGET}${NC} ($size_agent)"
|
||||||
|
echo -e " → ${GREEN}dist/tapo-countdown-local-${HOST_TARGET}${NC} ($size_cnt)"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
@@ -145,14 +149,18 @@ for target in "${!TARGETS[@]}"; do
|
|||||||
name="${TARGETS[$target]}"
|
name="${TARGETS[$target]}"
|
||||||
echo -e "${GREEN}Building for $target ($name)...${NC}"
|
echo -e "${GREEN}Building for $target ($name)...${NC}"
|
||||||
|
|
||||||
cross build --release --target "$target" -j $(nproc)
|
# Build both binaries
|
||||||
|
cross build --release --target "$target" --bin tapo-agent --bin tapo-countdown -j $(nproc)
|
||||||
|
|
||||||
# Copy binary to dist folder with descriptive name
|
# Copy binaries to dist folder with descriptive name
|
||||||
cp "target/$target/release/tapo-agent" "dist/tapo-agent-$name"
|
cp "target/$target/release/tapo-agent" "dist/tapo-agent-$name"
|
||||||
|
cp "target/$target/release/tapo-countdown" "dist/tapo-countdown-$name"
|
||||||
|
|
||||||
# Get binary size
|
# Get binary sizes
|
||||||
size=$(du -h "dist/tapo-agent-$name" | cut -f1)
|
size_agent=$(du -h "dist/tapo-agent-$name" | cut -f1)
|
||||||
echo -e " → ${GREEN}dist/tapo-agent-$name${NC} ($size)"
|
size_cnt=$(du -h "dist/tapo-countdown-$name" | cut -f1)
|
||||||
|
echo -e " → ${GREEN}dist/tapo-agent-$name${NC} ($size_agent)"
|
||||||
|
echo -e " → ${GREEN}dist/tapo-countdown-$name${NC} ($size_cnt)"
|
||||||
echo ""
|
echo ""
|
||||||
done
|
done
|
||||||
|
|
||||||
@@ -163,13 +171,13 @@ ls -lh dist/
|
|||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "To deploy to Raspberry Pi:"
|
echo "To deploy to Raspberry Pi:"
|
||||||
echo -e " ${YELLOW}scp dist/tapo-agent-pi3_pi4_64bit pi@raspberrypi:~/tapo-agent${NC}"
|
echo -e " ${YELLOW}scp dist/tapo-agent-pi3_pi4_64bit dist/tapo-countdown-pi3_pi4_64bit pi@raspberrypi:~/${NC}"
|
||||||
echo -e " ${YELLOW}ssh pi@raspberrypi 'chmod +x ~/tapo-agent && ./tapo-agent'${NC}"
|
echo -e " ${YELLOW}ssh pi@raspberrypi 'chmod +x ~/tapo-agent-* ~/tapo-countdown-*'${NC}"
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${BLUE}Upload to bashupload.com for web console deploy (3 days, 1 download):${NC}"
|
echo -e "${BLUE}Upload to bashupload.com for web console deploy (3 days, 1 download):${NC}"
|
||||||
echo -e " ${YELLOW}curl https://bashupload.com -F=@dist/tapo-agent-pi3_pi4_64bit${NC}"
|
echo -e " ${YELLOW}curl https://bashupload.com -F=@dist/tapo-agent-pi3_pi4_64bit${NC}"
|
||||||
echo -e " ${YELLOW}curl https://bashupload.com -F=@dist/tapo-agent-pi2_pi3_pi4_32bit${NC}"
|
echo -e " ${YELLOW}curl https://bashupload.com -F=@dist/tapo-countdown-pi3_pi4_64bit${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Then on Pi, download and run:"
|
echo "Then on Pi, download and run:"
|
||||||
echo -e " ${YELLOW}curl -sSL https://bashupload.com/XXXXX -o tapo-agent && chmod +x tapo-agent${NC}"
|
echo -e " ${YELLOW}curl -sSL https://bashupload.com/XXXXX -o tapo-agent && chmod +x tapo-agent${NC}"
|
||||||
|
|||||||
184
agents/tapo/src/bin/tapo-countdown.rs
Normal file
184
agents/tapo/src/bin/tapo-countdown.rs
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
use clap::Parser;
|
||||||
|
use tapo::ApiClient;
|
||||||
|
use tapo::responses::CountdownRulesResult;
|
||||||
|
use tapo::{PlugEnergyMonitoringHandler, PlugHandler};
|
||||||
|
use tokio::time::{sleep, Duration};
|
||||||
|
|
||||||
|
// Enum to wrap different device handlers
|
||||||
|
enum DeviceHandler {
|
||||||
|
P100(PlugHandler),
|
||||||
|
P110(PlugEnergyMonitoringHandler),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DeviceHandler {
|
||||||
|
async fn set_countdown(&self, delay: u64, turn_on: bool) -> Result<(), tapo::Error> {
|
||||||
|
match self {
|
||||||
|
Self::P100(h) => h.set_countdown(delay, turn_on).await,
|
||||||
|
Self::P110(h) => h.set_countdown(delay, turn_on).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_countdown_rules(&self) -> Result<CountdownRulesResult, tapo::Error> {
|
||||||
|
match self {
|
||||||
|
Self::P100(h) => h.get_countdown_rules().await,
|
||||||
|
Self::P110(h) => h.get_countdown_rules().await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn on(&self) -> Result<(), tapo::Error> {
|
||||||
|
match self {
|
||||||
|
Self::P100(h) => h.on().await,
|
||||||
|
Self::P110(h) => h.on().await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn off(&self) -> Result<(), tapo::Error> {
|
||||||
|
match self {
|
||||||
|
Self::P100(h) => h.off().await,
|
||||||
|
Self::P110(h) => h.off().await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[command(name = "tapo-countdown")]
|
||||||
|
#[command(about = "Set or cancel countdown timer on Tapo smart plug")]
|
||||||
|
struct Cli {
|
||||||
|
/// Device IP address
|
||||||
|
#[arg(short, long)]
|
||||||
|
ip: String,
|
||||||
|
|
||||||
|
/// Tapo account email
|
||||||
|
#[arg(short, long, env = "TAPO_EMAIL")]
|
||||||
|
email: String,
|
||||||
|
|
||||||
|
/// Tapo account password
|
||||||
|
#[arg(short = 'P', long, env = "TAPO_PASSWORD")]
|
||||||
|
password: String,
|
||||||
|
|
||||||
|
/// Device type: P100 or P110 (default: P110)
|
||||||
|
#[arg(short = 't', long, default_value = "P110")]
|
||||||
|
device_type: String,
|
||||||
|
|
||||||
|
/// Delay in seconds (required unless --cancel is used)
|
||||||
|
#[arg(short, long, required_unless_present = "cancel")]
|
||||||
|
delay: Option<u64>,
|
||||||
|
|
||||||
|
/// Action when countdown completes: "on" or "off"
|
||||||
|
#[arg(short, long, default_value = "off")]
|
||||||
|
action: String,
|
||||||
|
|
||||||
|
/// Set immediate state after verifying countdown (safety feature)
|
||||||
|
/// Only works if delay is set. "on" or "off"
|
||||||
|
#[arg(short = 's', long)]
|
||||||
|
set_state: Option<String>,
|
||||||
|
|
||||||
|
/// Cancel any active countdown
|
||||||
|
#[arg(short, long)]
|
||||||
|
cancel: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
env_logger::init();
|
||||||
|
let cli = Cli::parse();
|
||||||
|
|
||||||
|
println!("Connecting to {} device at {}...", cli.device_type, cli.ip);
|
||||||
|
|
||||||
|
let client = ApiClient::new(&cli.email, &cli.password);
|
||||||
|
|
||||||
|
// Create the appropriate handler based on device type
|
||||||
|
let plug = match cli.device_type.to_uppercase().as_str() {
|
||||||
|
"P100" | "P105" => DeviceHandler::P100(client.p100(&cli.ip).await?),
|
||||||
|
"P110" | "P115" => DeviceHandler::P110(client.p110(&cli.ip).await?),
|
||||||
|
_ => {
|
||||||
|
eprintln!("Error: device-type must be P100 or P110 (or similar)");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if cli.cancel {
|
||||||
|
println!("Canceling countdown...");
|
||||||
|
// Set countdown to disabled by using delay 0
|
||||||
|
plug.set_countdown(0, false).await?;
|
||||||
|
println!("Countdown canceled!");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let delay = cli.delay.unwrap();
|
||||||
|
let turn_on = match cli.action.to_lowercase().as_str() {
|
||||||
|
"on" => true,
|
||||||
|
"off" => false,
|
||||||
|
_ => {
|
||||||
|
eprintln!("Error: action must be 'on' or 'off'");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"Setting countdown: turn {} in {} seconds",
|
||||||
|
if turn_on { "ON" } else { "OFF" },
|
||||||
|
delay
|
||||||
|
);
|
||||||
|
|
||||||
|
plug.set_countdown(delay, turn_on).await?;
|
||||||
|
println!("Countdown set successfully!");
|
||||||
|
|
||||||
|
// Verify countdown status
|
||||||
|
let mut verified = false;
|
||||||
|
// Retry a few times to ensure device has updated state
|
||||||
|
for _ in 0..3 {
|
||||||
|
match plug.get_countdown_rules().await {
|
||||||
|
Ok(countdown) => {
|
||||||
|
if let Some(rule) = countdown.rules.iter().find(|r| r.enable && r.remain > 0) {
|
||||||
|
let will_turn_on = rule.desired_states.as_ref().and_then(|s| s.on).unwrap_or(false);
|
||||||
|
println!(
|
||||||
|
"Active countdown verified: {} seconds remaining, will turn {}",
|
||||||
|
rule.remain,
|
||||||
|
if will_turn_on { "ON" } else { "OFF" }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify that the set rule matches our intention
|
||||||
|
if will_turn_on == turn_on {
|
||||||
|
verified = true;
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
eprintln!("Warning: Active countdown action doesn't match requested action!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Warning: Could not verify countdown: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sleep(Duration::from_millis(500)).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
if verified {
|
||||||
|
if let Some(target_state) = cli.set_state {
|
||||||
|
let set_on = match target_state.to_lowercase().as_str() {
|
||||||
|
"on" => true,
|
||||||
|
"off" => false,
|
||||||
|
_ => {
|
||||||
|
eprintln!("Error: set-state must be 'on' or 'off'");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
println!("Safely setting device state to {}...", if set_on { "ON" } else { "OFF" });
|
||||||
|
if set_on {
|
||||||
|
plug.on().await?;
|
||||||
|
} else {
|
||||||
|
plug.off().await?;
|
||||||
|
}
|
||||||
|
println!("Device state updated.");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
eprintln!("Verification FAILED or timed out. NOT changing device state for safety.");
|
||||||
|
if cli.set_state.is_some() {
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
use futures_util::{SinkExt, StreamExt};
|
use futures_util::{SinkExt, StreamExt};
|
||||||
use log::{error, info, warn};
|
use log::{debug, error, info, warn};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tapo::{ApiClient, DiscoveryResult};
|
use tapo::{ApiClient, DiscoveryResult};
|
||||||
@@ -91,8 +91,13 @@ struct DataMessage {
|
|||||||
#[derive(Debug, Serialize, Clone)]
|
#[derive(Debug, Serialize, Clone)]
|
||||||
struct Reading {
|
struct Reading {
|
||||||
device: String,
|
device: String,
|
||||||
|
#[serde(skip)]
|
||||||
|
device_type: String,
|
||||||
channel: String,
|
channel: String,
|
||||||
value: f64,
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
value: Option<f64>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
data: Option<serde_json::Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
@@ -194,26 +199,34 @@ async fn collect_device_data(device: &DeviceConfig) -> Vec<Reading> {
|
|||||||
if let Ok(info) = plug.get_device_info().await {
|
if let Ok(info) = plug.get_device_info().await {
|
||||||
readings.push(Reading {
|
readings.push(Reading {
|
||||||
device: device.name.clone(),
|
device: device.name.clone(),
|
||||||
|
device_type: device.device_type.clone(),
|
||||||
channel: "state".to_string(),
|
channel: "state".to_string(),
|
||||||
value: if info.device_on { 1.0 } else { 0.0 },
|
value: Some(if info.device_on { 1.0 } else { 0.0 }),
|
||||||
|
data: None,
|
||||||
});
|
});
|
||||||
// Time device has been ON since last state change (seconds)
|
// Time device has been ON since last state change (seconds)
|
||||||
readings.push(Reading {
|
readings.push(Reading {
|
||||||
device: device.name.clone(),
|
device: device.name.clone(),
|
||||||
|
device_type: device.device_type.clone(),
|
||||||
channel: "on_time".to_string(),
|
channel: "on_time".to_string(),
|
||||||
value: info.on_time as f64,
|
value: Some(info.on_time as f64),
|
||||||
|
data: None,
|
||||||
});
|
});
|
||||||
// WiFi signal level (0-3)
|
// WiFi signal level (0-3)
|
||||||
readings.push(Reading {
|
readings.push(Reading {
|
||||||
device: device.name.clone(),
|
device: device.name.clone(),
|
||||||
|
device_type: device.device_type.clone(),
|
||||||
channel: "signal_level".to_string(),
|
channel: "signal_level".to_string(),
|
||||||
value: info.signal_level as f64,
|
value: Some(info.signal_level as f64),
|
||||||
|
data: None,
|
||||||
});
|
});
|
||||||
// WiFi RSSI (dBm, negative value)
|
// WiFi RSSI (dBm, negative value)
|
||||||
readings.push(Reading {
|
readings.push(Reading {
|
||||||
device: device.name.clone(),
|
device: device.name.clone(),
|
||||||
|
device_type: device.device_type.clone(),
|
||||||
channel: "rssi".to_string(),
|
channel: "rssi".to_string(),
|
||||||
value: info.rssi as f64,
|
value: Some(info.rssi as f64),
|
||||||
|
data: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,8 +234,10 @@ async fn collect_device_data(device: &DeviceConfig) -> Vec<Reading> {
|
|||||||
if let Ok(energy) = plug.get_current_power().await {
|
if let Ok(energy) = plug.get_current_power().await {
|
||||||
readings.push(Reading {
|
readings.push(Reading {
|
||||||
device: device.name.clone(),
|
device: device.name.clone(),
|
||||||
|
device_type: device.device_type.clone(),
|
||||||
channel: "power".to_string(),
|
channel: "power".to_string(),
|
||||||
value: energy.current_power as f64 / 1000.0,
|
value: Some(energy.current_power as f64 / 1000.0),
|
||||||
|
data: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,71 +245,73 @@ async fn collect_device_data(device: &DeviceConfig) -> Vec<Reading> {
|
|||||||
// Today's energy in Wh
|
// Today's energy in Wh
|
||||||
readings.push(Reading {
|
readings.push(Reading {
|
||||||
device: device.name.clone(),
|
device: device.name.clone(),
|
||||||
|
device_type: device.device_type.clone(),
|
||||||
channel: "energy_today".to_string(),
|
channel: "energy_today".to_string(),
|
||||||
value: usage.today_energy as f64,
|
value: Some(usage.today_energy as f64),
|
||||||
|
data: None,
|
||||||
});
|
});
|
||||||
// Today's runtime in minutes
|
// Today's runtime in minutes
|
||||||
readings.push(Reading {
|
readings.push(Reading {
|
||||||
device: device.name.clone(),
|
device: device.name.clone(),
|
||||||
|
device_type: device.device_type.clone(),
|
||||||
channel: "runtime_today".to_string(),
|
channel: "runtime_today".to_string(),
|
||||||
value: usage.today_runtime as f64,
|
value: Some(usage.today_runtime as f64),
|
||||||
|
data: None,
|
||||||
});
|
});
|
||||||
// This month's energy in Wh
|
// This month's energy in Wh
|
||||||
readings.push(Reading {
|
readings.push(Reading {
|
||||||
device: device.name.clone(),
|
device: device.name.clone(),
|
||||||
|
device_type: device.device_type.clone(),
|
||||||
channel: "energy_month".to_string(),
|
channel: "energy_month".to_string(),
|
||||||
value: usage.month_energy as f64,
|
value: Some(usage.month_energy as f64),
|
||||||
|
data: None,
|
||||||
});
|
});
|
||||||
// This month's runtime in minutes
|
// This month's runtime in minutes
|
||||||
readings.push(Reading {
|
readings.push(Reading {
|
||||||
device: device.name.clone(),
|
device: device.name.clone(),
|
||||||
|
device_type: device.device_type.clone(),
|
||||||
channel: "runtime_month".to_string(),
|
channel: "runtime_month".to_string(),
|
||||||
value: usage.month_runtime as f64,
|
value: Some(usage.month_runtime as f64),
|
||||||
|
data: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Countdown timer status
|
// Countdown timer - return full data or null if none
|
||||||
if let Ok(countdown) = plug.get_countdown_rules().await {
|
match plug.get_countdown_rules().await {
|
||||||
let active_countdown = countdown.rules.iter().find(|r| r.enable);
|
Ok(countdown) => {
|
||||||
|
let active = countdown.rules.iter().find(|r| r.enable);
|
||||||
readings.push(Reading {
|
readings.push(Reading {
|
||||||
device: device.name.clone(),
|
device: device.name.clone(),
|
||||||
channel: "countdown_active".to_string(),
|
device_type: device.device_type.clone(),
|
||||||
value: if active_countdown.is_some() { 1.0 } else { 0.0 },
|
channel: "countdown".to_string(),
|
||||||
});
|
value: None,
|
||||||
if let Some(rule) = active_countdown {
|
data: Some(if let Some(rule) = active {
|
||||||
readings.push(Reading {
|
serde_json::json!({
|
||||||
device: device.name.clone(),
|
"remain": rule.remain,
|
||||||
channel: "countdown_remain".to_string(),
|
"action": rule.desired_states.as_ref()
|
||||||
value: rule.remain as f64,
|
.and_then(|s| s.on)
|
||||||
|
.map(|on| if on { "on" } else { "off" })
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
serde_json::Value::Null
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Err(e) => debug!("get_countdown_rules failed for {}: {}", device.name, e),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Schedule rules count
|
// Schedule rules - return full schedule list
|
||||||
if let Ok(schedules) = plug.get_schedule_rules().await {
|
match plug.get_schedule_rules().await {
|
||||||
|
Ok(schedules) => {
|
||||||
readings.push(Reading {
|
readings.push(Reading {
|
||||||
device: device.name.clone(),
|
device: device.name.clone(),
|
||||||
channel: "schedule_count".to_string(),
|
device_type: device.device_type.clone(),
|
||||||
value: schedules.rules.len() as f64,
|
channel: "schedules".to_string(),
|
||||||
});
|
value: None,
|
||||||
// Count active schedules
|
data: Some(serde_json::to_value(&schedules.rules).unwrap_or_default()),
|
||||||
let active_count = schedules.rules.iter().filter(|r| r.enable).count();
|
|
||||||
readings.push(Reading {
|
|
||||||
device: device.name.clone(),
|
|
||||||
channel: "schedule_active_count".to_string(),
|
|
||||||
value: active_count as f64,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Next scheduled event
|
|
||||||
if let Ok(next) = plug.get_next_event().await {
|
|
||||||
if let Some(ts) = next.timestamp {
|
|
||||||
readings.push(Reading {
|
|
||||||
device: device.name.clone(),
|
|
||||||
channel: "next_event_time".to_string(),
|
|
||||||
value: ts as f64,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Err(e) => debug!("get_schedule_rules failed for {}: {}", device.name, e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => error!("Failed to connect to P110 {}: {}", device.name, e),
|
Err(e) => error!("Failed to connect to P110 {}: {}", device.name, e),
|
||||||
@@ -306,9 +323,71 @@ async fn collect_device_data(device: &DeviceConfig) -> Vec<Reading> {
|
|||||||
if let Ok(info) = plug.get_device_info().await {
|
if let Ok(info) = plug.get_device_info().await {
|
||||||
readings.push(Reading {
|
readings.push(Reading {
|
||||||
device: device.name.clone(),
|
device: device.name.clone(),
|
||||||
|
device_type: device.device_type.clone(),
|
||||||
channel: "state".to_string(),
|
channel: "state".to_string(),
|
||||||
value: if info.device_on { 1.0 } else { 0.0 },
|
value: Some(if info.device_on { 1.0 } else { 0.0 }),
|
||||||
|
data: None,
|
||||||
});
|
});
|
||||||
|
// Time device has been ON since last state change (seconds)
|
||||||
|
readings.push(Reading {
|
||||||
|
device: device.name.clone(),
|
||||||
|
device_type: device.device_type.clone(),
|
||||||
|
channel: "on_time".to_string(),
|
||||||
|
value: Some(info.on_time as f64),
|
||||||
|
data: None,
|
||||||
|
});
|
||||||
|
// WiFi signal level (0-3)
|
||||||
|
readings.push(Reading {
|
||||||
|
device: device.name.clone(),
|
||||||
|
device_type: device.device_type.clone(),
|
||||||
|
channel: "signal_level".to_string(),
|
||||||
|
value: Some(info.signal_level as f64),
|
||||||
|
data: None,
|
||||||
|
});
|
||||||
|
// WiFi RSSI (dBm, negative value)
|
||||||
|
readings.push(Reading {
|
||||||
|
device: device.name.clone(),
|
||||||
|
device_type: device.device_type.clone(),
|
||||||
|
channel: "rssi".to_string(),
|
||||||
|
value: Some(info.rssi as f64),
|
||||||
|
data: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Countdown rules
|
||||||
|
match plug.get_countdown_rules().await {
|
||||||
|
Ok(countdown) => {
|
||||||
|
let active = countdown.rules.iter().find(|r| r.enable);
|
||||||
|
readings.push(Reading {
|
||||||
|
device: device.name.clone(),
|
||||||
|
device_type: device.device_type.clone(),
|
||||||
|
channel: "countdown".to_string(),
|
||||||
|
value: None,
|
||||||
|
data: Some(if let Some(rule) = active {
|
||||||
|
serde_json::json!({
|
||||||
|
"remain": rule.remain,
|
||||||
|
"action": if rule.desired_states.as_ref().and_then(|s| s.on).unwrap_or(false) { "on" } else { "off" }
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
serde_json::Value::Null
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(e) => debug!("get_countdown_rules failed for {}: {}", device.name, e),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schedule rules
|
||||||
|
match plug.get_schedule_rules().await {
|
||||||
|
Ok(schedules) => {
|
||||||
|
readings.push(Reading {
|
||||||
|
device: device.name.clone(),
|
||||||
|
device_type: device.device_type.clone(),
|
||||||
|
channel: "schedules".to_string(),
|
||||||
|
value: None,
|
||||||
|
data: Some(serde_json::to_value(&schedules.rules).unwrap_or_default()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(e) => debug!("get_schedule_rules failed for {}: {}", device.name, e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => error!("Failed to connect to P100 {}: {}", device.name, e),
|
Err(e) => error!("Failed to connect to P100 {}: {}", device.name, e),
|
||||||
@@ -344,9 +423,18 @@ async fn run_agent(config: Config) -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
|
|
||||||
if !all_readings.is_empty() {
|
if !all_readings.is_empty() {
|
||||||
info!("Collected {} readings from devices", all_readings.len());
|
info!("Collected {} readings from devices", all_readings.len());
|
||||||
// Log readings even if not connected
|
// Group readings by device for cleaner output
|
||||||
|
let mut current_device = String::new();
|
||||||
for reading in &all_readings {
|
for reading in &all_readings {
|
||||||
info!(" {} {} = {}", reading.device, reading.channel, reading.value);
|
if reading.device != current_device {
|
||||||
|
current_device = reading.device.clone();
|
||||||
|
info!("Device: {} (name: {})", reading.device_type, current_device);
|
||||||
|
}
|
||||||
|
if let Some(val) = reading.value {
|
||||||
|
info!(" {} = {}", reading.channel, val);
|
||||||
|
} else if let Some(ref data) = reading.data {
|
||||||
|
info!(" {} = {}", reading.channel, data);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Try to send to connection task, drop if channel full
|
// Try to send to connection task, drop if channel full
|
||||||
let _ = tx.try_send(all_readings);
|
let _ = tx.try_send(all_readings);
|
||||||
|
|||||||
@@ -10,14 +10,14 @@ use tokio::sync::RwLock;
|
|||||||
|
|
||||||
use crate::error::{Error, TapoResponseError};
|
use crate::error::{Error, TapoResponseError};
|
||||||
use crate::requests::{
|
use crate::requests::{
|
||||||
ControlChildParams, DeviceRebootParams, EmptyParams, EnergyDataInterval,
|
AddCountdownRuleParams, ControlChildParams, DeviceRebootParams, EditCountdownRuleParams,
|
||||||
GetChildDeviceListParams, GetEnergyDataParams, GetPowerDataParams, GetRulesParams,
|
EmptyParams, EnergyDataInterval, GetChildDeviceListParams, GetEnergyDataParams,
|
||||||
LightingEffect, MultipleRequestParams, PlayAlarmParams, PowerDataInterval, TapoParams,
|
GetPowerDataParams, GetRulesParams, LightingEffect, MultipleRequestParams, PlayAlarmParams,
|
||||||
TapoRequest,
|
PowerDataInterval, TapoParams, TapoRequest,
|
||||||
};
|
};
|
||||||
use crate::responses::{
|
use crate::responses::{
|
||||||
ControlChildResult, CountdownRulesResult, CurrentPowerResult, DecodableResultExt,
|
ControlChildResult, CountdownRulesResult, CurrentPowerResult, DecodableResultExt,
|
||||||
EnergyDataResult, EnergyDataResultRaw, EnergyUsageResult, NextEventResult, PowerDataResult,
|
EnergyDataResult, EnergyDataResultRaw, EnergyUsageResult, PowerDataResult,
|
||||||
PowerDataResultRaw, ScheduleRulesResult, SupportedAlarmTypeListResult, TapoMultipleResponse,
|
PowerDataResultRaw, ScheduleRulesResult, SupportedAlarmTypeListResult, TapoMultipleResponse,
|
||||||
TapoResponseExt, TapoResult, validate_response,
|
TapoResponseExt, TapoResult, validate_response,
|
||||||
};
|
};
|
||||||
@@ -876,16 +876,35 @@ impl ApiClient {
|
|||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| Error::Tapo(TapoResponseError::EmptyResult))
|
.ok_or_else(|| Error::Tapo(TapoResponseError::EmptyResult))
|
||||||
}
|
}
|
||||||
|
/// 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();
|
||||||
|
|
||||||
/// Gets next scheduled event.
|
if let Some(countdown) = existing {
|
||||||
pub(crate) async fn get_next_event(&self) -> Result<NextEventResult, Error> {
|
if let Some(rule) = countdown.rules.first() {
|
||||||
debug!("Get Next event...");
|
// Edit existing rule
|
||||||
let request = TapoRequest::GetNextEvent(TapoParams::new(EmptyParams));
|
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()?
|
self.get_protocol()?
|
||||||
.execute_request(request, true)
|
.execute_request::<serde_json::Value>(request, true)
|
||||||
.await?
|
.await?;
|
||||||
.ok_or_else(|| Error::Tapo(TapoResponseError::EmptyResult))
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_protocol_mut(&mut self) -> Result<&mut TapoProtocol, Error> {
|
fn get_protocol_mut(&mut self) -> Result<&mut TapoProtocol, Error> {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use crate::error::Error;
|
|||||||
use crate::requests::{EnergyDataInterval, GenericSetDeviceInfoParams, PowerDataInterval};
|
use crate::requests::{EnergyDataInterval, GenericSetDeviceInfoParams, PowerDataInterval};
|
||||||
use crate::responses::{
|
use crate::responses::{
|
||||||
CountdownRulesResult, CurrentPowerResult, DeviceInfoPlugEnergyMonitoringResult,
|
CountdownRulesResult, CurrentPowerResult, DeviceInfoPlugEnergyMonitoringResult,
|
||||||
DeviceUsageEnergyMonitoringResult, EnergyDataResult, EnergyUsageResult, NextEventResult,
|
DeviceUsageEnergyMonitoringResult, EnergyDataResult, EnergyUsageResult,
|
||||||
PowerDataResult, ScheduleRulesResult,
|
PowerDataResult, ScheduleRulesResult,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -98,9 +98,13 @@ impl PlugEnergyMonitoringHandler {
|
|||||||
self.client.read().await.get_schedule_rules().await
|
self.client.read().await.get_schedule_rules().await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns *next scheduled event* as [`NextEventResult`].
|
/// Sets a countdown rule.
|
||||||
pub async fn get_next_event(&self) -> Result<NextEventResult, Error> {
|
///
|
||||||
self.client.read().await.get_next_event().await
|
/// # 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ use tokio::sync::{RwLock, RwLockReadGuard};
|
|||||||
|
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
use crate::requests::GenericSetDeviceInfoParams;
|
use crate::requests::GenericSetDeviceInfoParams;
|
||||||
use crate::responses::{DeviceInfoPlugResult, DeviceUsageResult};
|
use crate::responses::{
|
||||||
|
CountdownRulesResult, DeviceInfoPlugResult, DeviceUsageResult, ScheduleRulesResult,
|
||||||
|
};
|
||||||
|
|
||||||
use super::{ApiClient, ApiClientExt, DeviceManagementExt, HandlerExt};
|
use super::{ApiClient, ApiClientExt, DeviceManagementExt, HandlerExt};
|
||||||
|
|
||||||
@@ -56,6 +58,25 @@ impl PlugHandler {
|
|||||||
pub async fn get_device_usage(&self) -> Result<DeviceUsageResult, Error> {
|
pub async fn get_device_usage(&self) -> Result<DeviceUsageResult, Error> {
|
||||||
self.client.read().await.get_device_usage().await
|
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]
|
#[async_trait]
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
//! Tapo request objects.
|
//! Tapo request objects.
|
||||||
|
|
||||||
|
mod add_countdown_rule;
|
||||||
mod control_child;
|
mod control_child;
|
||||||
mod device_reboot;
|
mod device_reboot;
|
||||||
mod energy_data_interval;
|
mod energy_data_interval;
|
||||||
@@ -23,6 +24,7 @@ pub use play_alarm::*;
|
|||||||
pub use power_data_interval::*;
|
pub use power_data_interval::*;
|
||||||
pub use set_device_info::*;
|
pub use set_device_info::*;
|
||||||
|
|
||||||
|
pub(crate) use add_countdown_rule::*;
|
||||||
pub(crate) use control_child::*;
|
pub(crate) use control_child::*;
|
||||||
pub(crate) use device_reboot::*;
|
pub(crate) use device_reboot::*;
|
||||||
pub(crate) use get_child_device_list::*;
|
pub(crate) use get_child_device_list::*;
|
||||||
@@ -35,3 +37,4 @@ pub(crate) use login_device::*;
|
|||||||
pub(crate) use multiple_request::*;
|
pub(crate) use multiple_request::*;
|
||||||
pub(crate) use secure_passthrough::*;
|
pub(crate) use secure_passthrough::*;
|
||||||
pub(crate) use tapo_request::*;
|
pub(crate) use tapo_request::*;
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ pub(crate) struct GetRulesParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl GetRulesParams {
|
impl GetRulesParams {
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn new(start_index: u32) -> Self {
|
pub fn new(start_index: u32) -> Self {
|
||||||
Self { start_index }
|
Self { start_index }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,10 @@ use std::time::{SystemTime, UNIX_EPOCH};
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
ControlChildParams, DeviceRebootParams, GetChildDeviceListParams, GetEnergyDataParams,
|
AddCountdownRuleParams, ControlChildParams, DeviceRebootParams, EditCountdownRuleParams,
|
||||||
GetPowerDataParams, GetRulesParams, GetTriggerLogsParams, HandshakeParams, LightingEffect,
|
GetChildDeviceListParams, GetEnergyDataParams, GetPowerDataParams, GetRulesParams,
|
||||||
LoginDeviceParams, MultipleRequestParams, PlayAlarmParams, SecurePassthroughParams,
|
GetTriggerLogsParams, HandshakeParams, LightingEffect, LoginDeviceParams,
|
||||||
|
MultipleRequestParams, PlayAlarmParams, SecurePassthroughParams,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
@@ -46,9 +47,15 @@ pub(crate) enum TapoRequest {
|
|||||||
GetCountdownRules(TapoParams<GetRulesParams>),
|
GetCountdownRules(TapoParams<GetRulesParams>),
|
||||||
#[serde(rename = "get_schedule_rules")]
|
#[serde(rename = "get_schedule_rules")]
|
||||||
GetScheduleRules(TapoParams<GetRulesParams>),
|
GetScheduleRules(TapoParams<GetRulesParams>),
|
||||||
|
#[serde(rename = "add_countdown_rule")]
|
||||||
|
AddCountdownRule(TapoParams<AddCountdownRuleParams>),
|
||||||
|
#[serde(rename = "edit_countdown_rule")]
|
||||||
|
EditCountdownRule(TapoParams<EditCountdownRuleParams>),
|
||||||
#[serde(rename = "get_next_event")]
|
#[serde(rename = "get_next_event")]
|
||||||
|
#[allow(dead_code)]
|
||||||
GetNextEvent(TapoParams<EmptyParams>),
|
GetNextEvent(TapoParams<EmptyParams>),
|
||||||
#[serde(rename = "get_antitheft_rules")]
|
#[serde(rename = "get_antitheft_rules")]
|
||||||
|
#[allow(dead_code)]
|
||||||
GetAntitheftRules(TapoParams<GetRulesParams>),
|
GetAntitheftRules(TapoParams<GetRulesParams>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,15 +15,14 @@ pub struct CountdownRule {
|
|||||||
pub delay: u64,
|
pub delay: u64,
|
||||||
/// Seconds remaining (if timer is active)
|
/// Seconds remaining (if timer is active)
|
||||||
pub remain: u64,
|
pub remain: u64,
|
||||||
/// Action when countdown completes: true = turn on, false = turn off
|
/// Action when countdown completes
|
||||||
#[serde(rename = "desired_states")]
|
pub desired_states: Option<DesiredState>,
|
||||||
pub desired_states: Option<CountdownDesiredState>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Desired state for countdown
|
/// Desired state for countdown/schedule
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
pub struct CountdownDesiredState {
|
pub struct DesiredState {
|
||||||
/// Whether device should be on after countdown
|
/// Whether device should be on
|
||||||
pub on: Option<bool>,
|
pub on: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,47 +33,39 @@ pub struct ScheduleRule {
|
|||||||
pub id: String,
|
pub id: String,
|
||||||
/// Whether the rule is enabled
|
/// Whether the rule is enabled
|
||||||
pub enable: bool,
|
pub enable: bool,
|
||||||
/// Weekday mask (bits 0-6 for Sun-Sat)
|
/// Weekday mask (bits for days, 127 = all days)
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub wday: Vec<u8>,
|
pub week_day: u8,
|
||||||
/// Start minute of day (0-1439)
|
/// Start minute of day (0-1439)
|
||||||
#[serde(rename = "s_min", default)]
|
#[serde(default)]
|
||||||
pub start_min: u16,
|
pub s_min: u16,
|
||||||
/// End minute of day (for duration schedules)
|
/// End minute of day
|
||||||
#[serde(rename = "e_min", default)]
|
#[serde(default)]
|
||||||
pub end_min: u16,
|
pub e_min: u16,
|
||||||
/// Action: true = turn on, false = turn off
|
/// Mode (e.g., "repeat")
|
||||||
#[serde(rename = "desired_states")]
|
pub mode: Option<String>,
|
||||||
pub desired_states: Option<ScheduleDesiredState>,
|
/// Day of month
|
||||||
}
|
pub day: Option<u8>,
|
||||||
|
/// Month
|
||||||
/// Desired state for schedule
|
pub month: Option<u8>,
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
/// Year
|
||||||
pub struct ScheduleDesiredState {
|
pub year: Option<u16>,
|
||||||
/// Whether device should be on
|
/// Action
|
||||||
pub on: Option<bool>,
|
pub desired_states: Option<DesiredState>,
|
||||||
}
|
|
||||||
|
|
||||||
/// 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>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Result wrapper for countdown rules
|
/// Result wrapper for countdown rules
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
pub struct CountdownRulesResult {
|
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
|
/// List of countdown rules
|
||||||
#[serde(rename = "countdown_rules")]
|
#[serde(rename = "rule_list", default)]
|
||||||
pub rules: Vec<CountdownRule>,
|
pub rules: Vec<CountdownRule>,
|
||||||
/// Sum of rules (for pagination)
|
|
||||||
pub sum: Option<u32>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TapoResponseExt for CountdownRulesResult {}
|
impl TapoResponseExt for CountdownRulesResult {}
|
||||||
@@ -82,13 +73,21 @@ impl TapoResponseExt for CountdownRulesResult {}
|
|||||||
/// Result wrapper for schedule rules
|
/// Result wrapper for schedule rules
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
pub struct ScheduleRulesResult {
|
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
|
/// List of schedule rules
|
||||||
#[serde(rename = "schedule_rules")]
|
#[serde(rename = "rule_list", default)]
|
||||||
pub rules: Vec<ScheduleRule>,
|
pub rules: Vec<ScheduleRule>,
|
||||||
/// Sum of rules (for pagination)
|
/// Total count (for pagination)
|
||||||
pub sum: Option<u32>,
|
#[serde(default)]
|
||||||
|
pub sum: u32,
|
||||||
|
/// Start index (for pagination)
|
||||||
|
#[serde(default)]
|
||||||
|
pub start_index: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TapoResponseExt for ScheduleRulesResult {}
|
impl TapoResponseExt for ScheduleRulesResult {}
|
||||||
|
|
||||||
impl TapoResponseExt for NextEventResult {}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user