Skip to content

Commit

Permalink
Add P300 support (#192)
Browse files Browse the repository at this point in the history
Co-authored-by: Mihai Dinculescu <[email protected]>
  • Loading branch information
Michal-Szczepaniak and mihai-dinculescu committed Apr 23, 2024
1 parent 22d8dce commit 3407b56
Show file tree
Hide file tree
Showing 24 changed files with 505 additions and 59 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
40 changes: 22 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -28,23 +28,27 @@ Unofficial Tapo API Client. Works with TP-Link Tapo smart devices. Tested with l
&check; - Rust only\
&#x2705; - Rust and Python

| Feature | GenericDevice | L510, L520, L610 | L530, L630, L900 | L920, L930 | P100, P105 | P110, P115 |
| --------------------- | ------------: | ---------------: | ---------------: | ---------: | ---------: | ---------: |
| device_reset | | &#x2705; | &#x2705; | &check; | &#x2705; | &#x2705; |
| get_current_power | | | | | | &#x2705; |
| get_device_info | &#x2705; | &#x2705; | &#x2705; | &check; | &#x2705; | &#x2705; |
| get_device_info_json | &#x2705; | &#x2705; | &#x2705; | &check; | &#x2705; | &#x2705; |
| get_device_usage | | &#x2705; | &#x2705; | &check; | &#x2705; | &#x2705; |
| get_energy_data | | | | | | &#x2705; |
| get_energy_usage | | | | | | &#x2705; |
| off | &#x2705; | &#x2705; | &#x2705; | &check; | &#x2705; | &#x2705; |
| on | &#x2705; | &#x2705; | &#x2705; | &check; | &#x2705; | &#x2705; |
| set_brightness | | &#x2705; | &#x2705; | &check; | | |
| set_color | | | &#x2705; | &check; | | |
| set_color_temperature | | | &#x2705; | &check; | | |
| set_hue_saturation | | | &#x2705; | &check; | | |
| set_lighting_effect | | | | &check; | | |
| set() API \* | | | &#x2705; | &check; | | |
| Feature<br/><br/><br/> | GenericDevice<br/><br/><br/> | L510<br/>L520<br/>L610 | L530<br/>L630<br/>L900<br/> | L920<br/>L930<br/><br/> | P100<br/>P105<br/><br/> | P110<br/>P115<br/><br/> | P300<br/><br/><br/> | H100<br/><br/><br/> |
| ------------------------------------ | ---------------------------: | ---------------------: | --------------------------: | ----------------------: | ----------------------: | ----------------------: | ------------------: | ------------------: |
| device_reset | | &#x2705; | &#x2705; | &check; | &#x2705; | &#x2705; | | |
| get_child_device_component_list_json | | | | | | | &check; | &check; |
| get_child_device_list | | | | | | | &check; | &check; |
| get_child_device_list_json | | | | | | | &check; | &check; |
| get_current_power | | | | | | &#x2705; | | |
| get_device_info | &#x2705; | &#x2705; | &#x2705; | &check; | &#x2705; | &#x2705; | &check; | &check; |
| get_device_info_json | &#x2705; | &#x2705; | &#x2705; | &check; | &#x2705; | &#x2705; | &check; | &check; |
| get_device_usage | | &#x2705; | &#x2705; | &check; | &#x2705; | &#x2705; | | |
| get_energy_data | | | | | | &#x2705; | | |
| get_energy_usage | | | | | | &#x2705; | | |
| off | &#x2705; | &#x2705; | &#x2705; | &check; | &#x2705; | &#x2705; | | |
| on | &#x2705; | &#x2705; | &#x2705; | &check; | &#x2705; | &#x2705; | | |
| refresh_session | &#x2705; | &#x2705; | &#x2705; | &check; | &#x2705; | &#x2705; | &check; | &check; |
| set_brightness | | &#x2705; | &#x2705; | &check; | | | | |
| set_color | | | &#x2705; | &check; | | | | |
| set_color_temperature | | | &#x2705; | &check; | | | | |
| set_hue_saturation | | | &#x2705; | &check; | | | | |
| set_lighting_effect | | | | &check; | | | | |
| set() API \* | | | &#x2705; | &check; | | | | |

\* The `set()` API allows multiple properties to be set in a single request.

Expand Down
2 changes: 1 addition & 1 deletion tapo/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ version = "0.7.10"
edition = "2021"
license = "MIT"
authors = ["Mihai Dinculescu <[email protected]>"]
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"
Expand Down
14 changes: 7 additions & 7 deletions tapo/examples/tapo_h100.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<dyn std::error::Error>> {
Expand Down Expand Up @@ -31,7 +31,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {

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,
Expand All @@ -42,7 +42,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
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?;

Expand All @@ -51,7 +51,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
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?;

Expand All @@ -60,7 +60,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
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?;

Expand All @@ -69,7 +69,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
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?;

Expand All @@ -82,7 +82,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
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?;

Expand Down
55 changes: 55 additions & 0 deletions tapo/examples/tapo_p300.rs
Original file line number Diff line number Diff line change
@@ -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<dyn std::error::Error>> {
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(())
}
2 changes: 2 additions & 0 deletions tapo/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;
Expand All @@ -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::*;
28 changes: 27 additions & 1 deletion tapo/src/api/api_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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<dyn std::error::Error>> {
/// let device = ApiClient::new("[email protected]", "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<String>) -> Result<PowerStripHandler, Error> {
self.login(ip_address).await?;

Ok(PowerStripHandler::new(self))
}

/// Specializes the given [`ApiClient`] into an authenticated [`PlugEnergyMonitoringHandler`].
///
/// # Arguments
Expand Down
2 changes: 2 additions & 0 deletions tapo/src/api/child_devices.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
mod ke100_handler;
mod plug_power_strip_handler;
mod s200b_handler;
mod t100_handler;
mod t110_handler;
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::*;
Expand Down
66 changes: 66 additions & 0 deletions tapo/src/api/child_devices/plug_power_strip_handler.rs
Original file line number Diff line number Diff line change
@@ -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<PlugPowerStripResult, Error> {
let request = TapoRequest::GetDeviceInfo(TapoParams::new(EmptyParams));

self.power_strip_handler
.control_child::<PlugPowerStripResult>(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<serde_json::Value, Error> {
let request = TapoRequest::GetDeviceInfo(TapoParams::new(EmptyParams));

self.power_strip_handler
.control_child::<serde_json::Value>(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::<serde_json::Value>(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::<serde_json::Value>(self.device_id.clone(), request)
.await?;

Ok(())
}
}
8 changes: 4 additions & 4 deletions tapo/src/api/hub_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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<Vec<ChildDeviceResult>, Error> {
pub async fn get_child_device_list(&self) -> Result<Vec<ChildDeviceHubResult>, Error> {
self.client
.get_child_device_list::<ChildDeviceListResult>()
.get_child_device_list::<ChildDeviceListHubResult>()
.await
.map(|r| r.devices)
}
Expand Down
Loading

0 comments on commit 3407b56

Please sign in to comment.