From 3407b568015ef61ce651307361e073b7f069884d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Szczepaniak?= Date: Tue, 23 Apr 2024 21:37:32 +0200 Subject: [PATCH] Add P300 support (#192) Co-authored-by: Mihai Dinculescu --- CHANGELOG.md | 8 + README.md | 40 ++--- tapo/Cargo.toml | 2 +- tapo/examples/tapo_h100.rs | 14 +- tapo/examples/tapo_p300.rs | 55 +++++++ tapo/src/api.rs | 2 + tapo/src/api/api_client.rs | 28 +++- tapo/src/api/child_devices.rs | 2 + .../child_devices/plug_power_strip_handler.rs | 66 ++++++++ tapo/src/api/hub_handler.rs | 8 +- tapo/src/api/power_strip_handler.rs | 142 ++++++++++++++++++ tapo/src/error.rs | 3 + tapo/src/lib.rs | 2 +- tapo/src/responses.rs | 6 +- ...ult.rs => child_device_list_hub_result.rs} | 50 +++--- .../ke100_result.rs | 0 .../s200b_result.rs | 0 .../t100_result.rs | 0 .../t110_result.rs | 0 .../t300_result.rs | 0 .../t31x_result.rs | 0 .../child_device_list_power_strip_result.rs | 88 +++++++++++ tapo/src/responses/device_info_result.rs | 2 + .../device_info_result/power_strip.rs | 46 ++++++ 24 files changed, 505 insertions(+), 59 deletions(-) create mode 100644 tapo/examples/tapo_p300.rs create mode 100644 tapo/src/api/child_devices/plug_power_strip_handler.rs create mode 100644 tapo/src/api/power_strip_handler.rs rename tapo/src/responses/{child_device_list_result.rs => child_device_list_hub_result.rs} (55%) rename tapo/src/responses/{child_device_list_result => child_device_list_hub_result}/ke100_result.rs (100%) rename tapo/src/responses/{child_device_list_result => child_device_list_hub_result}/s200b_result.rs (100%) rename tapo/src/responses/{child_device_list_result => child_device_list_hub_result}/t100_result.rs (100%) rename tapo/src/responses/{child_device_list_result => child_device_list_hub_result}/t110_result.rs (100%) rename tapo/src/responses/{child_device_list_result => child_device_list_hub_result}/t300_result.rs (100%) rename tapo/src/responses/{child_device_list_result => child_device_list_hub_result}/t31x_result.rs (100%) create mode 100644 tapo/src/responses/child_device_list_power_strip_result.rs create mode 100644 tapo/src/responses/device_info_result/power_strip.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bdd8b4..83a42fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ file. This change log follows the conventions of ## [Rust Unreleased][Unreleased] +### Added + +- Added support for the P300 power strip (thanks to @Michal-Szczepaniak). + +### Changed + +- `ChildDeviceResult` has been renamed to `ChildDeviceHubResult` to facilitate adding support for other devices with children. + ## [Python Unreleased][Unreleased] ## [Rust v0.7.10][v0.7.10] - 2024-04-05 diff --git a/README.md b/README.md index 4c61be4..6b4c72b 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ [![PyPI][pypi_badge]][pypi] [![Python][pypi_versions_badge]][pypi] [![PyPI][pypi_downloads_badge]][pypi]\ -Unofficial Tapo API Client. Works with TP-Link Tapo smart devices. Tested with light bulbs (L510, L520, L530, L610, L630), light strips (L900, L920, L930), plugs (P100, P105, P110, P115), hubs (H100), switches (S200B) and sensors (KE100, T100, T110, T300, T310, T315). +Unofficial Tapo API Client. Works with TP-Link Tapo smart devices. Tested with light bulbs (L510, L520, L530, L610, L630), light strips (L900, L920, L930), plugs (P100, P105, P110, P115, P300), hubs (H100), switches (S200B) and sensors (KE100, T100, T110, T300, T310, T315). [license_badge]: https://img.shields.io/crates/l/tapo.svg [license]: https://github.com/mihai-dinculescu/tapo/blob/main/LICENSE @@ -28,23 +28,27 @@ Unofficial Tapo API Client. Works with TP-Link Tapo smart devices. Tested with l ✓ - Rust only\ ✅ - Rust and Python -| Feature | GenericDevice | L510, L520, L610 | L530, L630, L900 | L920, L930 | P100, P105 | P110, P115 | -| --------------------- | ------------: | ---------------: | ---------------: | ---------: | ---------: | ---------: | -| device_reset | | ✅ | ✅ | ✓ | ✅ | ✅ | -| get_current_power | | | | | | ✅ | -| get_device_info | ✅ | ✅ | ✅ | ✓ | ✅ | ✅ | -| get_device_info_json | ✅ | ✅ | ✅ | ✓ | ✅ | ✅ | -| get_device_usage | | ✅ | ✅ | ✓ | ✅ | ✅ | -| get_energy_data | | | | | | ✅ | -| get_energy_usage | | | | | | ✅ | -| off | ✅ | ✅ | ✅ | ✓ | ✅ | ✅ | -| on | ✅ | ✅ | ✅ | ✓ | ✅ | ✅ | -| set_brightness | | ✅ | ✅ | ✓ | | | -| set_color | | | ✅ | ✓ | | | -| set_color_temperature | | | ✅ | ✓ | | | -| set_hue_saturation | | | ✅ | ✓ | | | -| set_lighting_effect | | | | ✓ | | | -| set() API \* | | | ✅ | ✓ | | | +| Feature


| GenericDevice


| L510
L520
L610 | L530
L630
L900
| L920
L930

| P100
P105

| P110
P115

| P300


| H100


| +| ------------------------------------ | ---------------------------: | ---------------------: | --------------------------: | ----------------------: | ----------------------: | ----------------------: | ------------------: | ------------------: | +| device_reset | | ✅ | ✅ | ✓ | ✅ | ✅ | | | +| get_child_device_component_list_json | | | | | | | ✓ | ✓ | +| get_child_device_list | | | | | | | ✓ | ✓ | +| get_child_device_list_json | | | | | | | ✓ | ✓ | +| get_current_power | | | | | | ✅ | | | +| get_device_info | ✅ | ✅ | ✅ | ✓ | ✅ | ✅ | ✓ | ✓ | +| get_device_info_json | ✅ | ✅ | ✅ | ✓ | ✅ | ✅ | ✓ | ✓ | +| get_device_usage | | ✅ | ✅ | ✓ | ✅ | ✅ | | | +| get_energy_data | | | | | | ✅ | | | +| get_energy_usage | | | | | | ✅ | | | +| off | ✅ | ✅ | ✅ | ✓ | ✅ | ✅ | | | +| on | ✅ | ✅ | ✅ | ✓ | ✅ | ✅ | | | +| refresh_session | ✅ | ✅ | ✅ | ✓ | ✅ | ✅ | ✓ | ✓ | +| set_brightness | | ✅ | ✅ | ✓ | | | | | +| set_color | | | ✅ | ✓ | | | | | +| set_color_temperature | | | ✅ | ✓ | | | | | +| set_hue_saturation | | | ✅ | ✓ | | | | | +| set_lighting_effect | | | | ✓ | | | | | +| set() API \* | | | ✅ | ✓ | | | | | \* The `set()` API allows multiple properties to be set in a single request. diff --git a/tapo/Cargo.toml b/tapo/Cargo.toml index 5c96b5b..47d71ec 100644 --- a/tapo/Cargo.toml +++ b/tapo/Cargo.toml @@ -4,7 +4,7 @@ version = "0.7.10" edition = "2021" license = "MIT" authors = ["Mihai Dinculescu "] -description = "Unofficial Tapo API Client. Works with TP-Link Tapo smart devices. Tested with light bulbs (L510, L520, L530, L610, L630), light strips (L900, L920, L930), plugs (P100, P105, P110, P115), hubs (H100), switches (S200B) and sensors (KE100, T100, T110, T300, T310, T315)." +description = "Unofficial Tapo API Client. Works with TP-Link Tapo smart devices. Tested with light bulbs (L510, L520, L530, L610, L630), light strips (L900, L920, L930), plugs (P100, P105, P110, P115, P300), hubs (H100), switches (S200B) and sensors (KE100, T100, T110, T300, T310, T315)." keywords = ["IOT", "tapo", "smart-home", "smart-bulb", "smart-plug"] categories = ["hardware-support", "embedded", "development-tools"] readme = "README.md" diff --git a/tapo/examples/tapo_h100.rs b/tapo/examples/tapo_h100.rs index 49f9321..b78217f 100644 --- a/tapo/examples/tapo_h100.rs +++ b/tapo/examples/tapo_h100.rs @@ -2,7 +2,7 @@ use std::env; use log::{info, LevelFilter}; -use tapo::{responses::ChildDeviceResult, ApiClient}; +use tapo::{responses::ChildDeviceHubResult, ApiClient}; #[tokio::main] async fn main() -> Result<(), Box> { @@ -31,7 +31,7 @@ async fn main() -> Result<(), Box> { for child in child_device_list { match child { - ChildDeviceResult::KE100(device) => { + ChildDeviceHubResult::KE100(device) => { info!( "Found KE100 child device with nickname: {}, id: {}, current temperature: {} {:?} and target temperature: {} {:?}.", device.nickname, @@ -42,7 +42,7 @@ async fn main() -> Result<(), Box> { device.temperature_unit, ); } - ChildDeviceResult::S200B(device) => { + ChildDeviceHubResult::S200B(device) => { let s200b = hub.s200b(&device.device_id); let trigger_logs = s200b.get_trigger_logs(5, 0).await?; @@ -51,7 +51,7 @@ async fn main() -> Result<(), Box> { device.nickname, device.device_id, trigger_logs ); } - ChildDeviceResult::T100(device) => { + ChildDeviceHubResult::T100(device) => { let t100 = hub.t100(&device.device_id); let trigger_logs = t100.get_trigger_logs(5, 0).await?; @@ -60,7 +60,7 @@ async fn main() -> Result<(), Box> { device.nickname, device.device_id, device.detected, trigger_logs ); } - ChildDeviceResult::T110(device) => { + ChildDeviceHubResult::T110(device) => { let t110 = hub.t110(&device.device_id); let trigger_logs = t110.get_trigger_logs(5, 0).await?; @@ -69,7 +69,7 @@ async fn main() -> Result<(), Box> { device.nickname, device.device_id, device.open, trigger_logs ); } - ChildDeviceResult::T300(device) => { + ChildDeviceHubResult::T300(device) => { let t300 = hub.t300(&device.device_id); let trigger_logs = t300.get_trigger_logs(5, 0).await?; @@ -82,7 +82,7 @@ async fn main() -> Result<(), Box> { trigger_logs ); } - ChildDeviceResult::T310(device) | ChildDeviceResult::T315(device) => { + ChildDeviceHubResult::T310(device) | ChildDeviceHubResult::T315(device) => { let t31x = hub.t315(&device.device_id); let temperature_humidity_records = t31x.get_temperature_humidity_records().await?; diff --git a/tapo/examples/tapo_p300.rs b/tapo/examples/tapo_p300.rs new file mode 100644 index 0000000..165a7cb --- /dev/null +++ b/tapo/examples/tapo_p300.rs @@ -0,0 +1,55 @@ +/// P300 Example +use std::{env, thread, time::Duration}; + +use log::{info, LevelFilter}; +use tapo::{ApiClient, PlugIdentifier}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let log_level = env::var("RUST_LOG") + .unwrap_or_else(|_| "info".to_string()) + .parse() + .unwrap_or(LevelFilter::Info); + + pretty_env_logger::formatted_timed_builder() + .filter(Some("tapo"), log_level) + .init(); + + let tapo_username = env::var("TAPO_USERNAME")?; + let tapo_password = env::var("TAPO_PASSWORD")?; + let ip_address = env::var("IP_ADDRESS")?; + + let power_strip = ApiClient::new(tapo_username, tapo_password) + .p300(ip_address) + .await?; + + let device_info = power_strip.get_device_info().await?; + info!("Device info: {device_info:?}"); + + info!("Getting child devices..."); + let child_device_list = power_strip.get_child_device_list().await?; + + for child in child_device_list { + info!( + "Found plug with nickname: {}, id: {}, state: {}.", + child.nickname, child.device_id, child.device_on, + ); + + let plug = power_strip + .plug(PlugIdentifier::ByDeviceId(&child.device_id)) + .await?; + + info!("Turning device on..."); + plug.on().await?; + + info!("Waiting 2 seconds..."); + thread::sleep(Duration::from_secs(2)); + + info!("Turning device off..."); + plug.off().await?; + + info!("Waiting 2 seconds..."); + } + + Ok(()) +} diff --git a/tapo/src/api.rs b/tapo/src/api.rs index 4d5ac51..53b38ba 100644 --- a/tapo/src/api.rs +++ b/tapo/src/api.rs @@ -8,6 +8,7 @@ mod hub_handler; mod light_handler; mod plug_energy_monitoring_handler; mod plug_handler; +mod power_strip_handler; mod protocol; pub use api_client::*; @@ -20,3 +21,4 @@ pub use hub_handler::*; pub use light_handler::*; pub use plug_energy_monitoring_handler::*; pub use plug_handler::*; +pub use power_strip_handler::*; diff --git a/tapo/src/api/api_client.rs b/tapo/src/api/api_client.rs index 46fff77..6191d1d 100644 --- a/tapo/src/api/api_client.rs +++ b/tapo/src/api/api_client.rs @@ -10,7 +10,7 @@ use serde::de::DeserializeOwned; use crate::api::protocol::{TapoProtocol, TapoProtocolExt}; use crate::api::{ ColorLightHandler, ColorLightStripHandler, GenericDeviceHandler, HubHandler, LightHandler, - PlugEnergyMonitoringHandler, PlugHandler, + PlugEnergyMonitoringHandler, PlugHandler, PowerStripHandler, }; use crate::error::{Error, TapoResponseError}; use crate::requests::{ @@ -404,6 +404,32 @@ impl ApiClient { Ok(PlugEnergyMonitoringHandler::new(self)) } + /// Specializes the given [`ApiClient`] into an authenticated [`PowerStripHandler`]. + /// + /// # Arguments + /// + /// * `ip_address` - the IP address of the device + /// + /// # Example + /// + /// ```rust,no_run + /// # use tapo::ApiClient; + /// # #[tokio::main] + /// # async fn main() -> Result<(), Box> { + /// let device = ApiClient::new("tapo-username@example.com", "tapo-password") + /// .p300("192.168.1.100") + /// .await?; + /// let child_device_list = device.get_child_device_list().await?; + /// println!("Child device list: {child_device_list:?}"); + /// # Ok(()) + /// # } + /// ``` + pub async fn p300(mut self, ip_address: impl Into) -> Result { + self.login(ip_address).await?; + + Ok(PowerStripHandler::new(self)) + } + /// Specializes the given [`ApiClient`] into an authenticated [`PlugEnergyMonitoringHandler`]. /// /// # Arguments diff --git a/tapo/src/api/child_devices.rs b/tapo/src/api/child_devices.rs index fc0c29d..35b3d72 100644 --- a/tapo/src/api/child_devices.rs +++ b/tapo/src/api/child_devices.rs @@ -1,4 +1,5 @@ mod ke100_handler; +mod plug_power_strip_handler; mod s200b_handler; mod t100_handler; mod t110_handler; @@ -6,6 +7,7 @@ mod t300_handler; mod t31x_handler; pub use ke100_handler::*; +pub use plug_power_strip_handler::*; pub use s200b_handler::*; pub use t100_handler::*; pub use t110_handler::*; diff --git a/tapo/src/api/child_devices/plug_power_strip_handler.rs b/tapo/src/api/child_devices/plug_power_strip_handler.rs new file mode 100644 index 0000000..0e575e4 --- /dev/null +++ b/tapo/src/api/child_devices/plug_power_strip_handler.rs @@ -0,0 +1,66 @@ +use crate::api::PowerStripHandler; +use crate::error::{Error, TapoResponseError}; +use crate::requests::{EmptyParams, GenericSetDeviceInfoParams, TapoParams, TapoRequest}; +use crate::responses::{DecodableResultExt, PlugPowerStripResult}; + +/// Handler for the [P300](https://www.tapo.com/en/search/?q=P300) child plugs. +pub struct PlugPowerStripHandler<'h> { + power_strip_handler: &'h PowerStripHandler, + device_id: String, +} + +impl<'h> PlugPowerStripHandler<'h> { + pub(crate) fn new(power_strip_handler: &'h PowerStripHandler, device_id: String) -> Self { + Self { + power_strip_handler, + device_id, + } + } + + /// Returns *device info* as [`PlugPowerStripResult`]. + /// It is not guaranteed to contain all the properties returned from the Tapo API. + pub async fn get_device_info(&self) -> Result { + let request = TapoRequest::GetDeviceInfo(TapoParams::new(EmptyParams)); + + self.power_strip_handler + .control_child::(self.device_id.clone(), request) + .await? + .ok_or_else(|| Error::Tapo(TapoResponseError::EmptyResult)) + .map(|result| result.decode())? + } + + /// Returns *device info* as [`serde_json::Value`]. + /// It is not guaranteed to contain all the properties returned from the Tapo API. + pub async fn get_device_info_json(&self) -> Result { + let request = TapoRequest::GetDeviceInfo(TapoParams::new(EmptyParams)); + + self.power_strip_handler + .control_child::(self.device_id.clone(), request) + .await? + .ok_or_else(|| Error::Tapo(TapoResponseError::EmptyResult)) + } + + /// Turns *on* the device. + pub async fn on(&self) -> Result<(), Error> { + let json = serde_json::to_value(GenericSetDeviceInfoParams::device_on(true)?)?; + let request = TapoRequest::SetDeviceInfo(Box::new(TapoParams::new(json))); + + self.power_strip_handler + .control_child::(self.device_id.clone(), request) + .await?; + + Ok(()) + } + + /// Turns *off* the device. + pub async fn off(&self) -> Result<(), Error> { + let json = serde_json::to_value(GenericSetDeviceInfoParams::device_on(false)?)?; + let request = TapoRequest::SetDeviceInfo(Box::new(TapoParams::new(json))); + + self.power_strip_handler + .control_child::(self.device_id.clone(), request) + .await?; + + Ok(()) + } +} diff --git a/tapo/src/api/hub_handler.rs b/tapo/src/api/hub_handler.rs index 57903b7..8caaca6 100644 --- a/tapo/src/api/hub_handler.rs +++ b/tapo/src/api/hub_handler.rs @@ -7,7 +7,7 @@ use crate::api::{KE100Handler, S200BHandler, T100Handler, T110Handler, T300Handl use crate::error::Error; use crate::requests::TapoRequest; use crate::responses::{ - ChildDeviceListResult, ChildDeviceResult, DeviceInfoHubResult, TapoResponseExt, + ChildDeviceHubResult, ChildDeviceListHubResult, DeviceInfoHubResult, TapoResponseExt, }; /// Handler for the [H100](https://www.tapo.com/en/search/?q=H100) hubs. @@ -40,12 +40,12 @@ impl HubHandler { self.client.get_device_info().await } - /// Returns *child device list* as [`ChildDeviceListResult`]. + /// Returns *child device list* as [`ChildDeviceHubResult`]. /// It is not guaranteed to contain all the properties returned from the Tapo API /// or to support all the possible devices connected to the hub. - pub async fn get_child_device_list(&self) -> Result, Error> { + pub async fn get_child_device_list(&self) -> Result, Error> { self.client - .get_child_device_list::() + .get_child_device_list::() .await .map(|r| r.devices) } diff --git a/tapo/src/api/power_strip_handler.rs b/tapo/src/api/power_strip_handler.rs new file mode 100644 index 0000000..8b8bde9 --- /dev/null +++ b/tapo/src/api/power_strip_handler.rs @@ -0,0 +1,142 @@ +use std::fmt; + +use serde::de::DeserializeOwned; + +use crate::api::ApiClient; +use crate::api::PlugPowerStripHandler; +use crate::error::Error; +use crate::requests::TapoRequest; +use crate::responses::{ + ChildDeviceListPowerStripResult, DeviceInfoPowerStripResult, PlugPowerStripResult, + TapoResponseExt, +}; + +/// Handler for the [P300](https://www.tapo.com/en/search/?q=P300) devices. +pub struct PowerStripHandler { + client: ApiClient, +} + +impl PowerStripHandler { + pub(crate) fn new(client: ApiClient) -> Self { + Self { client } + } + + /// Refreshes the authentication session. + pub async fn refresh_session(&mut self) -> Result<&mut Self, Error> { + self.client.refresh_session().await?; + Ok(self) + } + + /// Returns *device info* as [`DeviceInfoPowerStripResult`]. + /// It is not guaranteed to contain all the properties returned from the Tapo API. + /// If the deserialization fails, or if a property that you care about it's not present, + /// try [`PowerStripHandler::get_device_info_json`]. + pub async fn get_device_info(&self) -> Result { + self.client.get_device_info().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 { + self.client.get_device_info().await + } + + /// Returns *child device list* as [`PlugPowerStripResult`]. + /// It is not guaranteed to contain all the properties returned from the Tapo API + /// or to support all the possible devices connected to the hub. + pub async fn get_child_device_list(&self) -> Result, Error> { + self.client + .get_child_device_list::() + .await + .map(|r| r.sub_plugs) + } + + /// Returns *child device list* as [`serde_json::Value`]. + /// It contains all the properties returned from the Tapo API. + pub async fn get_child_device_list_json(&self) -> Result { + self.client.get_child_device_list().await + } + + /// Returns *child device component list* as [`serde_json::Value`]. + /// This information is useful in debugging or when investigating new functionality to add. + pub async fn get_child_device_component_list_json(&self) -> Result { + self.client.get_child_device_component_list().await + } + + /// Internal method that's called by functions of the child devices. + pub(crate) async fn control_child( + &self, + device_id: String, + request_data: TapoRequest, + ) -> Result, Error> + where + R: fmt::Debug + DeserializeOwned + TapoResponseExt, + { + self.client.control_child(device_id, request_data).await + } +} + +/// Child device handler builders. +impl PowerStripHandler { + /// Returns a [`PlugPowerStripHandler`] for the given [`PlugIdentifier`]. + /// + /// # Arguments + /// + /// * `identifier` - a PowerStrip plug identifier. + /// + /// # Example + /// + /// ```rust,no_run + /// # use tapo::{ApiClient, PlugIdentifier}; + /// # #[tokio::main] + /// # async fn main() -> Result<(), Box> { + /// // Connect to the hub + /// let power_strip = ApiClient::new("tapo-username@example.com", "tapo-password") + /// .p300("192.168.1.100") + /// .await?; + /// // Get a handler for the child device + /// let device = power_strip + /// .plug(PlugIdentifier::ByDeviceId("0000000000000000000000000000000000000000")) + /// .await?; + /// // Get the device info of the child device + /// let device_info = device.get_device_info().await?; + /// # Ok(()) + /// # } + /// ``` + pub async fn plug<'a>( + &self, + identifier: PlugIdentifier<'a>, + ) -> Result { + let device_id = match identifier { + PlugIdentifier::ByDeviceId(device_id) => device_id.to_string(), + PlugIdentifier::ByNickname(nickname) => self + .get_child_device_list() + .await? + .iter() + .find(|child| child.nickname == nickname) + .ok_or_else(|| Error::DeviceNotFound)? + .device_id + .clone(), + PlugIdentifier::ByPosition(position) => self + .get_child_device_list() + .await? + .iter() + .find(|child| child.position == position) + .ok_or_else(|| Error::DeviceNotFound)? + .device_id + .clone(), + }; + + Ok(PlugPowerStripHandler::new(self, device_id)) + } +} + +/// Plug Identifier. +pub enum PlugIdentifier<'a> { + /// By Device ID. + ByDeviceId(&'a str), + /// By Nickname. + ByNickname(&'a str), + /// By Position. + ByPosition(u8), +} diff --git a/tapo/src/error.rs b/tapo/src/error.rs index d16ba31..531d0c4 100644 --- a/tapo/src/error.rs +++ b/tapo/src/error.rs @@ -42,6 +42,9 @@ pub enum Error { /// HTTP Error. #[error("Http: {0}")] Http(#[from] isahc::Error), + /// Device not found + #[error("Device not found")] + DeviceNotFound, /// Other Error. This is a catch-all for errors that don't fit into the other categories. #[error(transparent)] Other(#[from] anyhow::Error), diff --git a/tapo/src/lib.rs b/tapo/src/lib.rs index 9099ffe..646f6e1 100644 --- a/tapo/src/lib.rs +++ b/tapo/src/lib.rs @@ -3,7 +3,7 @@ //! Tapo API Client. //! //! Tested with light bulbs (L510, L520, L530, L610, L630), light strips (L900, L920, L930), -//! plugs (P100, P105, P110, P115), hubs (H100), switches (S200B) and sensors (KE100, T100, T110, T300, T310, T315). +//! plugs (P100, P105, P110, P115, P300), hubs (H100), switches (S200B) and sensors (KE100, T100, T110, T300, T310, T315). //! //! # Example with L530 //! ```rust,no_run diff --git a/tapo/src/responses.rs b/tapo/src/responses.rs index df68dc7..954e637 100644 --- a/tapo/src/responses.rs +++ b/tapo/src/responses.rs @@ -1,6 +1,7 @@ //! Tapo response objects. -mod child_device_list_result; +mod child_device_list_hub_result; +mod child_device_list_power_strip_result; mod control_child_result; mod current_power_result; mod decodable_result_ext; @@ -15,7 +16,8 @@ mod tapo_result; mod token_result; mod trigger_logs_result; -pub use child_device_list_result::*; +pub use child_device_list_hub_result::*; +pub use child_device_list_power_strip_result::*; pub use current_power_result::*; pub use device_info_result::*; pub use device_usage_energy_monitoring_result::*; diff --git a/tapo/src/responses/child_device_list_result.rs b/tapo/src/responses/child_device_list_hub_result.rs similarity index 55% rename from tapo/src/responses/child_device_list_result.rs rename to tapo/src/responses/child_device_list_hub_result.rs index 1380b4d..1b35e0a 100644 --- a/tapo/src/responses/child_device_list_result.rs +++ b/tapo/src/responses/child_device_list_hub_result.rs @@ -17,17 +17,17 @@ use serde::{Deserialize, Serialize}; use crate::error::Error; use crate::responses::{DecodableResultExt, TapoResponseExt}; -/// Child device list result. +/// Hub child device list result. #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ChildDeviceListResult { - /// Child devices +pub(crate) struct ChildDeviceListHubResult { + /// Hub child devices #[serde(rename = "child_device_list")] - pub devices: Vec, + pub devices: Vec, } -impl DecodableResultExt for ChildDeviceListResult { +impl DecodableResultExt for ChildDeviceListHubResult { fn decode(self) -> Result { - Ok(ChildDeviceListResult { + Ok(ChildDeviceListHubResult { devices: self .devices .into_iter() @@ -37,7 +37,7 @@ impl DecodableResultExt for ChildDeviceListResult { } } -impl TapoResponseExt for ChildDeviceListResult {} +impl TapoResponseExt for ChildDeviceListHubResult {} /// Device status. #[derive(Debug, Clone, Serialize, Deserialize)] @@ -48,10 +48,10 @@ pub enum Status { Offline, } -/// Child device result. +/// Hub child device result. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "model")] -pub enum ChildDeviceResult { +pub enum ChildDeviceHubResult { /// KE100 thermostatic radiator valve (TRV). KE100(Box), /// S200B button switch. @@ -72,31 +72,31 @@ pub enum ChildDeviceResult { Other, } -impl DecodableResultExt for ChildDeviceResult { +impl DecodableResultExt for ChildDeviceHubResult { fn decode(self) -> Result { match self { - ChildDeviceResult::KE100(device) => { - Ok(ChildDeviceResult::KE100(Box::new(device.decode()?))) + ChildDeviceHubResult::KE100(device) => { + Ok(ChildDeviceHubResult::KE100(Box::new(device.decode()?))) } - ChildDeviceResult::S200B(device) => { - Ok(ChildDeviceResult::S200B(Box::new(device.decode()?))) + ChildDeviceHubResult::S200B(device) => { + Ok(ChildDeviceHubResult::S200B(Box::new(device.decode()?))) } - ChildDeviceResult::T100(device) => { - Ok(ChildDeviceResult::T100(Box::new(device.decode()?))) + ChildDeviceHubResult::T100(device) => { + Ok(ChildDeviceHubResult::T100(Box::new(device.decode()?))) } - ChildDeviceResult::T110(device) => { - Ok(ChildDeviceResult::T110(Box::new(device.decode()?))) + ChildDeviceHubResult::T110(device) => { + Ok(ChildDeviceHubResult::T110(Box::new(device.decode()?))) } - ChildDeviceResult::T300(device) => { - Ok(ChildDeviceResult::T300(Box::new(device.decode()?))) + ChildDeviceHubResult::T300(device) => { + Ok(ChildDeviceHubResult::T300(Box::new(device.decode()?))) } - ChildDeviceResult::T310(device) => { - Ok(ChildDeviceResult::T310(Box::new(device.decode()?))) + ChildDeviceHubResult::T310(device) => { + Ok(ChildDeviceHubResult::T310(Box::new(device.decode()?))) } - ChildDeviceResult::T315(device) => { - Ok(ChildDeviceResult::T315(Box::new(device.decode()?))) + ChildDeviceHubResult::T315(device) => { + Ok(ChildDeviceHubResult::T315(Box::new(device.decode()?))) } - ChildDeviceResult::Other => Ok(ChildDeviceResult::Other), + ChildDeviceHubResult::Other => Ok(ChildDeviceHubResult::Other), } } } diff --git a/tapo/src/responses/child_device_list_result/ke100_result.rs b/tapo/src/responses/child_device_list_hub_result/ke100_result.rs similarity index 100% rename from tapo/src/responses/child_device_list_result/ke100_result.rs rename to tapo/src/responses/child_device_list_hub_result/ke100_result.rs diff --git a/tapo/src/responses/child_device_list_result/s200b_result.rs b/tapo/src/responses/child_device_list_hub_result/s200b_result.rs similarity index 100% rename from tapo/src/responses/child_device_list_result/s200b_result.rs rename to tapo/src/responses/child_device_list_hub_result/s200b_result.rs diff --git a/tapo/src/responses/child_device_list_result/t100_result.rs b/tapo/src/responses/child_device_list_hub_result/t100_result.rs similarity index 100% rename from tapo/src/responses/child_device_list_result/t100_result.rs rename to tapo/src/responses/child_device_list_hub_result/t100_result.rs diff --git a/tapo/src/responses/child_device_list_result/t110_result.rs b/tapo/src/responses/child_device_list_hub_result/t110_result.rs similarity index 100% rename from tapo/src/responses/child_device_list_result/t110_result.rs rename to tapo/src/responses/child_device_list_hub_result/t110_result.rs diff --git a/tapo/src/responses/child_device_list_result/t300_result.rs b/tapo/src/responses/child_device_list_hub_result/t300_result.rs similarity index 100% rename from tapo/src/responses/child_device_list_result/t300_result.rs rename to tapo/src/responses/child_device_list_hub_result/t300_result.rs diff --git a/tapo/src/responses/child_device_list_result/t31x_result.rs b/tapo/src/responses/child_device_list_hub_result/t31x_result.rs similarity index 100% rename from tapo/src/responses/child_device_list_result/t31x_result.rs rename to tapo/src/responses/child_device_list_hub_result/t31x_result.rs diff --git a/tapo/src/responses/child_device_list_power_strip_result.rs b/tapo/src/responses/child_device_list_power_strip_result.rs new file mode 100644 index 0000000..b16de07 --- /dev/null +++ b/tapo/src/responses/child_device_list_power_strip_result.rs @@ -0,0 +1,88 @@ +use serde::{Deserialize, Serialize}; + +use crate::error::Error; +use crate::responses::{decode_value, DecodableResultExt, TapoResponseExt}; + +/// Power Strip child device list result. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct ChildDeviceListPowerStripResult { + /// Power Strip child devices + #[serde(rename = "child_device_list")] + pub sub_plugs: Vec, +} + +impl DecodableResultExt for ChildDeviceListPowerStripResult { + fn decode(self) -> Result { + Ok(ChildDeviceListPowerStripResult { + sub_plugs: self + .sub_plugs + .into_iter() + .map(|d| d.decode()) + .collect::, _>>()?, + }) + } +} + +impl TapoResponseExt for ChildDeviceListPowerStripResult {} + +/// P300 power strip child plug. +/// +/// Specific properties: `auto_off_remain_time`, `auto_off_status`, +/// `bind_count`, `overheat_status`, `position`, `slot_number`. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[allow(missing_docs)] +pub struct PlugPowerStripResult { + pub auto_off_remain_time: u64, + pub auto_off_status: AutoOffStatus, + pub avatar: String, + pub bind_count: u8, + pub category: String, + pub device_id: String, + pub device_on: bool, + pub fw_id: String, + pub fw_ver: String, + pub has_set_location_info: bool, + pub hw_id: String, + pub hw_ver: String, + pub latitude: Option, + pub longitude: Option, + pub mac: String, + pub model: String, + pub nickname: String, + pub oem_id: String, + /// The time in seconds this device has been ON since the last state change (ON/OFF). + pub on_time: u64, + pub original_device_id: String, + pub overheat_status: OverheatStatus, + pub position: u8, + pub region: Option, + pub slot_number: u8, + pub status_follow_edge: bool, + pub r#type: String, +} + +impl TapoResponseExt for PlugPowerStripResult {} + +impl DecodableResultExt for PlugPowerStripResult { + fn decode(mut self) -> Result { + self.nickname = decode_value(&self.nickname)?; + Ok(self) + } +} + +/// Auto Off Status. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[allow(missing_docs)] +pub enum AutoOffStatus { + On, + Off, +} + +/// Overheat Status. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[allow(missing_docs)] +pub enum OverheatStatus { + Normal, +} diff --git a/tapo/src/responses/device_info_result.rs b/tapo/src/responses/device_info_result.rs index 3ca11e0..678e4f0 100644 --- a/tapo/src/responses/device_info_result.rs +++ b/tapo/src/responses/device_info_result.rs @@ -5,6 +5,7 @@ mod generic; mod hub; mod light; mod plug; +mod power_strip; pub use color_light::*; pub use color_light_strip::*; @@ -13,3 +14,4 @@ pub use generic::*; pub use hub::*; pub use light::*; pub use plug::*; +pub use power_strip::*; diff --git a/tapo/src/responses/device_info_result/power_strip.rs b/tapo/src/responses/device_info_result/power_strip.rs new file mode 100644 index 0000000..238e4ca --- /dev/null +++ b/tapo/src/responses/device_info_result/power_strip.rs @@ -0,0 +1,46 @@ +use serde::{Deserialize, Serialize}; + +use crate::error::Error; +use crate::responses::{decode_value, DecodableResultExt, TapoResponseExt}; + +/// Device info of Tapo H100. Superset of [`crate::responses::DeviceInfoGenericResult`]. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[allow(missing_docs)] +pub struct DeviceInfoPowerStripResult { + // + // Inherited from DeviceInfoGenericResult + // + pub device_id: String, + pub r#type: String, + pub model: String, + pub hw_id: String, + pub hw_ver: String, + pub fw_id: String, + pub fw_ver: String, + pub oem_id: String, + pub mac: String, + pub ip: String, + pub ssid: String, + pub signal_level: u8, + pub rssi: i16, + pub specs: String, + pub lang: String, + pub nickname: String, + pub avatar: String, + pub has_set_location_info: bool, + pub region: Option, + pub latitude: Option, + pub longitude: Option, + pub time_diff: Option, +} + +impl TapoResponseExt for DeviceInfoPowerStripResult {} + +impl DecodableResultExt for DeviceInfoPowerStripResult { + fn decode(mut self) -> Result { + self.ssid = decode_value(&self.ssid)?; + self.nickname = decode_value(&self.nickname)?; + + Ok(self) + } +}