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