From 95f70989541dd929029e5082b739837b233b8b0a Mon Sep 17 00:00:00 2001 From: pwoerndle <21156233+pwoerndle@users.noreply.github.com> Date: Sat, 25 Nov 2023 19:01:57 +0100 Subject: [PATCH] Initial KE100 support. (#121) Add support for the KE100 TRV devices --- README.md | 19 +- tapo/Cargo.toml | 2 +- tapo/examples/tapo_h100.rs | 11 ++ tapo/examples/tapo_ke100.rs | 50 +++++ tapo/src/api/api_client.rs | 6 +- tapo/src/api/child_devices.rs | 2 + tapo/src/api/child_devices/ke100_handler.rs | 185 ++++++++++++++++++ tapo/src/api/child_devices/s200b_handler.rs | 8 +- tapo/src/api/child_devices/t100_handler.rs | 8 +- tapo/src/api/child_devices/t110_handler.rs | 8 +- tapo/src/api/child_devices/t300_handler.rs | 10 +- tapo/src/api/child_devices/t31x_handler.rs | 8 +- tapo/src/api/hub_handler.rs | 31 ++- tapo/src/lib.rs | 2 +- tapo/src/requests/set_device_info.rs | 2 + tapo/src/requests/set_device_info/trv.rs | 90 +++++++++ .../src/responses/child_device_list_result.rs | 7 + .../child_device_list_result/ke100_result.rs | 68 +++++++ 18 files changed, 487 insertions(+), 30 deletions(-) create mode 100644 tapo/examples/tapo_ke100.rs create mode 100644 tapo/src/api/child_devices/ke100_handler.rs create mode 100644 tapo/src/requests/set_device_info/trv.rs create mode 100644 tapo/src/responses/child_device_list_result/ke100_result.rs diff --git a/README.md b/README.md index d8f5ca1..442b843 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 (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), 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 @@ -49,11 +49,18 @@ Unofficial Tapo API Client. Works with TP-Link Tapo smart devices. Tested with l ## Hub (H100) Support -| Feature | S200B | T100 | T110 | T300 | T310, T315 | -| -------------------------------- | ------: | ------: | ------: | ------: | ---------: | -| get_device_info \* | ✓ | ✓ | ✓ | ✓ | ✓ | -| get_temperature_humidity_records | | | | | ✓ | -| get_trigger_logs | ✓ | ✓ | ✓ | ✓ | | +| Feature | KE100 | S200B | T100 | T110 | T300 | T310, T315 | +| -------------------------------- | ------: | ------: | ------: | ------: | ------: | ---------: | +| get_device_info \* | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| get_device_info_json | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| get_temperature_humidity_records | | | | | | ✓ | +| get_trigger_logs | | ✓ | ✓ | ✓ | ✓ | | +| set_target_temperature | ✓ | | | | | | +| set_min_control_temperature | ✓ | | | | | | +| set_max_control_temperature | ✓ | | | | | | +| set_temperature_offset | ✓ | | | | | | +| set_frost_protection | ✓ | | | | | | +| set_child_protection | ✓ | | | | | | \* Obtained by calling `get_child_device_list` on the hub device or `get_device_info` on a child handler. diff --git a/tapo/Cargo.toml b/tapo/Cargo.toml index a683485..3b1987b 100644 --- a/tapo/Cargo.toml +++ b/tapo/Cargo.toml @@ -4,7 +4,7 @@ version = "0.7.5" 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 (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), 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 40818ea..47fe529 100644 --- a/tapo/examples/tapo_h100.rs +++ b/tapo/examples/tapo_h100.rs @@ -31,6 +31,17 @@ async fn main() -> Result<(), Box> { for child in child_device_list { match child { + ChildDeviceResult::KE100(device) => { + info!( + "Found KE100 child device with nickname: {}, id: {}, current temperature: {} {:?} and target temperature: {} {:?}.", + device.nickname, + device.device_id, + device.current_temperature, + device.temperature_unit, + device.target_temperature, + device.temperature_unit, + ); + } ChildDeviceResult::S200B(device) => { let s200b = hub.s200b(&device.device_id); let trigger_logs = s200b.get_trigger_logs(5, 0).await?; diff --git a/tapo/examples/tapo_ke100.rs b/tapo/examples/tapo_ke100.rs new file mode 100644 index 0000000..edeba1b --- /dev/null +++ b/tapo/examples/tapo_ke100.rs @@ -0,0 +1,50 @@ +/// KE100 TRV Example +use std::env; + +use log::{info, LevelFilter}; +use tapo::responses::TemperatureUnitKE100; +use tapo::ApiClient; + +#[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")?; + // ID of the KE100 device. + // Can be obtained from executing `get_child_device_component_list()` on the hub device. + let device_id = env::var("DEVICE_ID")?; + let target_temperature: u8 = env::var("TARGET_TEMPERATURE")?.parse()?; + + let hub = ApiClient::new(tapo_username, tapo_password)? + .h100(ip_address) + .await?; + + // Get a handler for the child device + let device = hub.ke100(device_id); + + // Get the device info of the child device + let device_info = device.get_device_info().await?; + info!("Device info: {device_info:?}"); + + // Set target temperature. + // KE100 currently only supports Celsius as temperature unit. + info!("Setting target temperature to {target_temperature} degrees Celsius..."); + device + .set_target_temperature(target_temperature, TemperatureUnitKE100::Celsius) + .await?; + + // Get the device info of the child device + let device_info = device.get_device_info().await?; + info!("Device info: {device_info:?}"); + + Ok(()) +} diff --git a/tapo/src/api/api_client.rs b/tapo/src/api/api_client.rs index 1c5c2d7..378d3db 100644 --- a/tapo/src/api/api_client.rs +++ b/tapo/src/api/api_client.rs @@ -578,7 +578,7 @@ impl ApiClient { &self, device_id: String, child_request: TapoRequest, - ) -> Result + ) -> Result, Error> where R: fmt::Debug + DeserializeOwned + TapoResponseExt, { @@ -605,9 +605,7 @@ impl ApiClient { validate_response(&response)?; - response - .result - .ok_or_else(|| Error::Tapo(TapoResponseError::EmptyResult)) + Ok(response.result) } } diff --git a/tapo/src/api/child_devices.rs b/tapo/src/api/child_devices.rs index 424333f..fc0c29d 100644 --- a/tapo/src/api/child_devices.rs +++ b/tapo/src/api/child_devices.rs @@ -1,9 +1,11 @@ +mod ke100_handler; mod s200b_handler; mod t100_handler; mod t110_handler; mod t300_handler; mod t31x_handler; +pub use ke100_handler::*; pub use s200b_handler::*; pub use t100_handler::*; pub use t110_handler::*; diff --git a/tapo/src/api/child_devices/ke100_handler.rs b/tapo/src/api/child_devices/ke100_handler.rs new file mode 100644 index 0000000..e6e3316 --- /dev/null +++ b/tapo/src/api/child_devices/ke100_handler.rs @@ -0,0 +1,185 @@ +use crate::api::HubHandler; +use crate::error::{Error, TapoResponseError}; +use crate::requests::{EmptyParams, TapoParams, TapoRequest, TrvSetDeviceInfoParams}; +use crate::responses::{DecodableResultExt, KE100Result, TemperatureUnitKE100}; + +/// Handler for the [KE100](https://www.tp-link.com/en/search/?q=KE100) devices. +pub struct KE100Handler<'h> { + hub_handler: &'h HubHandler, + device_id: String, +} + +impl<'h> KE100Handler<'h> { + pub(crate) fn new(hub_handler: &'h HubHandler, device_id: String) -> Self { + Self { + hub_handler, + device_id, + } + } + + /// Returns *device info* as [`KE100Result`]. + /// 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.hub_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 contains 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.hub_handler + .control_child(self.device_id.clone(), request) + .await? + .ok_or_else(|| Error::Tapo(TapoResponseError::EmptyResult)) + } + + /// Sets the *target temperature*. + /// + /// # Arguments + /// + /// * `target_temperature` - between `min_control_temperature` and `max_control_temperature` + /// * `temperature_unit` + pub async fn set_target_temperature( + &self, + target_temperature: u8, + temperature_unit: TemperatureUnitKE100, + ) -> Result<(), Error> { + let device_info = self.get_device_info().await?; + + if target_temperature < device_info.min_control_temperature + || target_temperature > device_info.max_control_temperature + { + return Err(Error::Validation { + field: "target_temperature".to_string(), + message: format!("Target temperature must be between {} (min_control_temperature) and {} (max_control_temperature)", device_info.min_control_temperature, device_info.max_control_temperature), + }); + } + + let json = serde_json::to_value( + TrvSetDeviceInfoParams::new() + .target_temperature(target_temperature, temperature_unit)?, + )?; + let request = TapoRequest::SetDeviceInfo(Box::new(TapoParams::new(json))); + + self.hub_handler + .control_child::(self.device_id.clone(), request) + .await?; + + Ok(()) + } + + /// Sets the *minimal control temperature*. + /// + /// # Arguments + /// + /// * `min_control_temperature` + /// * `temperature_unit` + pub async fn set_min_control_temperature( + &self, + min_control_temperature: u8, + temperature_unit: TemperatureUnitKE100, + ) -> Result<(), Error> { + let json = serde_json::to_value( + TrvSetDeviceInfoParams::new() + .min_control_temperature(min_control_temperature, temperature_unit)?, + )?; + let request = TapoRequest::SetDeviceInfo(Box::new(TapoParams::new(json))); + + self.hub_handler + .control_child::(self.device_id.clone(), request) + .await?; + + Ok(()) + } + + /// Sets the *maximum control temperature*. + /// + /// # Arguments + /// + /// * `max_control_temperature` + /// * `temperature_unit` + pub async fn set_max_control_temperature( + &self, + max_control_temperature: u8, + temperature_unit: TemperatureUnitKE100, + ) -> Result<(), Error> { + let json = serde_json::to_value( + TrvSetDeviceInfoParams::new() + .max_control_temperature(max_control_temperature, temperature_unit)?, + )?; + let request = TapoRequest::SetDeviceInfo(Box::new(TapoParams::new(json))); + + self.hub_handler + .control_child::(self.device_id.clone(), request) + .await?; + + Ok(()) + } + + /// Sets *frost protection* on the device to *on* or *off*. + /// + /// # Arguments + /// + /// * `frost_protection_on` - true/false + pub async fn set_frost_protection(&self, frost_protection_on: bool) -> Result<(), Error> { + let json = serde_json::to_value( + TrvSetDeviceInfoParams::new().frost_protection_on(frost_protection_on)?, + )?; + let request = TapoRequest::SetDeviceInfo(Box::new(TapoParams::new(json))); + + self.hub_handler + .control_child::(self.device_id.clone(), request) + .await?; + + Ok(()) + } + + /// Sets *child protection* on the device to *on* or *off*. + /// + /// # Arguments + /// + /// * `child_protection_on` - true/false + pub async fn set_child_protection(&self, child_protection_on: bool) -> Result<(), Error> { + let json = serde_json::to_value( + TrvSetDeviceInfoParams::new().child_protection(child_protection_on)?, + )?; + let request = TapoRequest::SetDeviceInfo(Box::new(TapoParams::new(json))); + + self.hub_handler + .control_child::(self.device_id.clone(), request) + .await?; + + Ok(()) + } + + /// Sets the *temperature offset*. + /// + /// # Arguments + /// + /// * `temperature_offset` - between -10 and 10 + /// * `temperature_unit` + pub async fn set_temperature_offset( + &self, + temperature_offset: i8, + temperature_unit: TemperatureUnitKE100, + ) -> Result<(), Error> { + let json = serde_json::to_value( + TrvSetDeviceInfoParams::new() + .temperature_offset(temperature_offset, temperature_unit)?, + )?; + let request = TapoRequest::SetDeviceInfo(Box::new(TapoParams::new(json))); + + self.hub_handler + .control_child::(self.device_id.clone(), request) + .await?; + + Ok(()) + } +} diff --git a/tapo/src/api/child_devices/s200b_handler.rs b/tapo/src/api/child_devices/s200b_handler.rs index 4e978a4..651f298 100644 --- a/tapo/src/api/child_devices/s200b_handler.rs +++ b/tapo/src/api/child_devices/s200b_handler.rs @@ -1,5 +1,5 @@ use crate::api::HubHandler; -use crate::error::Error; +use crate::error::{Error, TapoResponseError}; use crate::requests::{EmptyParams, GetTriggerLogsParams, TapoParams, TapoRequest}; use crate::responses::{DecodableResultExt, S200BResult}; use crate::responses::{S200BLog, TriggerLogsResult}; @@ -25,7 +25,8 @@ impl<'h> S200BHandler<'h> { self.hub_handler .control_child::(self.device_id.clone(), request) - .await + .await? + .ok_or_else(|| Error::Tapo(TapoResponseError::EmptyResult)) .map(|result| result.decode())? } @@ -47,6 +48,7 @@ impl<'h> S200BHandler<'h> { self.hub_handler .control_child(self.device_id.clone(), child_request) - .await + .await? + .ok_or_else(|| Error::Tapo(TapoResponseError::EmptyResult)) } } diff --git a/tapo/src/api/child_devices/t100_handler.rs b/tapo/src/api/child_devices/t100_handler.rs index 497c495..263fa25 100644 --- a/tapo/src/api/child_devices/t100_handler.rs +++ b/tapo/src/api/child_devices/t100_handler.rs @@ -1,5 +1,5 @@ use crate::api::HubHandler; -use crate::error::Error; +use crate::error::{Error, TapoResponseError}; use crate::requests::{EmptyParams, GetTriggerLogsParams, TapoParams, TapoRequest}; use crate::responses::{DecodableResultExt, T100Result}; use crate::responses::{T100Log, TriggerLogsResult}; @@ -25,7 +25,8 @@ impl<'h> T100Handler<'h> { self.hub_handler .control_child::(self.device_id.clone(), request) - .await + .await? + .ok_or_else(|| Error::Tapo(TapoResponseError::EmptyResult)) .map(|result| result.decode())? } @@ -47,6 +48,7 @@ impl<'h> T100Handler<'h> { self.hub_handler .control_child(self.device_id.clone(), child_request) - .await + .await? + .ok_or_else(|| Error::Tapo(TapoResponseError::EmptyResult)) } } diff --git a/tapo/src/api/child_devices/t110_handler.rs b/tapo/src/api/child_devices/t110_handler.rs index 88ee56d..9b41a74 100644 --- a/tapo/src/api/child_devices/t110_handler.rs +++ b/tapo/src/api/child_devices/t110_handler.rs @@ -1,5 +1,5 @@ use crate::api::HubHandler; -use crate::error::Error; +use crate::error::{Error, TapoResponseError}; use crate::requests::{EmptyParams, GetTriggerLogsParams, TapoParams, TapoRequest}; use crate::responses::{DecodableResultExt, T110Result}; use crate::responses::{T110Log, TriggerLogsResult}; @@ -25,7 +25,8 @@ impl<'h> T110Handler<'h> { self.hub_handler .control_child::(self.device_id.clone(), request) - .await + .await? + .ok_or_else(|| Error::Tapo(TapoResponseError::EmptyResult)) .map(|result| result.decode())? } @@ -47,6 +48,7 @@ impl<'h> T110Handler<'h> { self.hub_handler .control_child(self.device_id.clone(), child_request) - .await + .await? + .ok_or_else(|| Error::Tapo(TapoResponseError::EmptyResult)) } } diff --git a/tapo/src/api/child_devices/t300_handler.rs b/tapo/src/api/child_devices/t300_handler.rs index 0ed21fd..0983e2a 100644 --- a/tapo/src/api/child_devices/t300_handler.rs +++ b/tapo/src/api/child_devices/t300_handler.rs @@ -1,5 +1,5 @@ use crate::api::HubHandler; -use crate::error::Error; +use crate::error::{Error, TapoResponseError}; use crate::requests::{EmptyParams, GetTriggerLogsParams, TapoParams, TapoRequest}; use crate::responses::{DecodableResultExt, T300Result}; use crate::responses::{T300Log, TriggerLogsResult}; @@ -25,7 +25,8 @@ impl<'h> T300Handler<'h> { self.hub_handler .control_child::(self.device_id.clone(), request) - .await + .await? + .ok_or_else(|| Error::Tapo(TapoResponseError::EmptyResult)) .map(|result| result.decode())? } @@ -46,7 +47,8 @@ impl<'h> T300Handler<'h> { let child_request = TapoRequest::GetTriggerLogs(Box::new(TapoParams::new(child_params))); self.hub_handler - .control_child(self.device_id.clone(), child_request) - .await + .control_child::>(self.device_id.clone(), child_request) + .await? + .ok_or_else(|| Error::Tapo(TapoResponseError::EmptyResult)) } } diff --git a/tapo/src/api/child_devices/t31x_handler.rs b/tapo/src/api/child_devices/t31x_handler.rs index 8c0a683..931d251 100644 --- a/tapo/src/api/child_devices/t31x_handler.rs +++ b/tapo/src/api/child_devices/t31x_handler.rs @@ -1,5 +1,5 @@ use crate::api::HubHandler; -use crate::error::Error; +use crate::error::{Error, TapoResponseError}; use crate::requests::{EmptyParams, TapoParams, TapoRequest}; use crate::responses::{ DecodableResultExt, T31XResult, TemperatureHumidityRecords, TemperatureHumidityRecordsRaw, @@ -26,7 +26,8 @@ impl<'h> T31XHandler<'h> { self.hub_handler .control_child::(self.device_id.clone(), request) - .await + .await? + .ok_or_else(|| Error::Tapo(TapoResponseError::EmptyResult)) .map(|result| result.decode())? } @@ -40,7 +41,8 @@ impl<'h> T31XHandler<'h> { let result = self .hub_handler .control_child::(self.device_id.clone(), request) - .await?; + .await? + .ok_or_else(|| Error::Tapo(TapoResponseError::EmptyResult))?; Ok(result.try_into()?) } diff --git a/tapo/src/api/hub_handler.rs b/tapo/src/api/hub_handler.rs index 2aecf6d..1536dc9 100644 --- a/tapo/src/api/hub_handler.rs +++ b/tapo/src/api/hub_handler.rs @@ -3,7 +3,7 @@ use std::fmt; use serde::de::DeserializeOwned; use crate::api::ApiClient; -use crate::api::{S200BHandler, T100Handler, T110Handler, T300Handler, T31XHandler}; +use crate::api::{KE100Handler, S200BHandler, T100Handler, T110Handler, T300Handler, T31XHandler}; use crate::error::Error; use crate::requests::TapoRequest; use crate::responses::{ @@ -67,7 +67,7 @@ impl HubHandler { &self, device_id: String, request_data: TapoRequest, - ) -> Result + ) -> Result, Error> where R: fmt::Debug + DeserializeOwned + TapoResponseExt, { @@ -238,4 +238,31 @@ impl HubHandler { pub fn t315(&self, device_id: impl Into) -> T31XHandler { T31XHandler::new(self, device_id.into()) } + + /// Returns a [`KE100Handler`] for the given `device_id`. + /// + /// # Arguments + /// + /// * `device_id` - the Device ID of the child device + /// + /// # Example + /// + /// ```rust,no_run + /// # use tapo::ApiClient; + /// # #[tokio::main] + /// # async fn main() -> Result<(), Box> { + /// // Connect to the hub + /// let hub = ApiClient::new("tapo-username@example.com", "tapo-password")? + /// .h100("192.168.1.100") + /// .await?; + /// // Get a handler for the child device + /// let device = hub.ke100("0000000000000000000000000000000000000000"); + /// // Get the device info of the child device + /// let device_info = device.get_device_info().await?; + /// # Ok(()) + /// # } + /// ``` + pub fn ke100(&self, device_id: impl Into) -> KE100Handler { + KE100Handler::new(self, device_id.into()) + } } diff --git a/tapo/src/lib.rs b/tapo/src/lib.rs index dccf367..59ef0cd 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 (T100, T110, T300, T310, T315). +//! plugs (P100, P105, P110, P115), hubs (H100), switches (S200B) and sensors (KE100, T100, T110, T300, T310, T315). //! //! # Example with L530 //! ```rust,no_run diff --git a/tapo/src/requests/set_device_info.rs b/tapo/src/requests/set_device_info.rs index 273fb20..70ad713 100644 --- a/tapo/src/requests/set_device_info.rs +++ b/tapo/src/requests/set_device_info.rs @@ -1,8 +1,10 @@ mod color_light; mod generic_device; mod light; +mod trv; pub use color_light::*; pub(crate) use generic_device::*; pub(crate) use light::*; +pub(crate) use trv::*; diff --git a/tapo/src/requests/set_device_info/trv.rs b/tapo/src/requests/set_device_info/trv.rs new file mode 100644 index 0000000..52df4c5 --- /dev/null +++ b/tapo/src/requests/set_device_info/trv.rs @@ -0,0 +1,90 @@ +use serde::Serialize; + +use crate::error::Error; + +use crate::responses::TemperatureUnitKE100; + +#[derive(Debug, Default, Serialize)] +pub(crate) struct TrvSetDeviceInfoParams { + #[serde(skip_serializing_if = "Option::is_none", rename = "target_temp")] + target_temperature: Option, + #[serde(skip_serializing_if = "Option::is_none")] + frost_protection_on: Option, + #[serde(skip_serializing_if = "Option::is_none")] + child_protection: Option, + #[serde(skip_serializing_if = "Option::is_none", rename = "temp_offset")] + temperature_offset: Option, + #[serde(skip_serializing_if = "Option::is_none", rename = "min_temp")] + min_temperature: Option, + #[serde(skip_serializing_if = "Option::is_none", rename = "min_control_temp")] + min_control_temperature: Option, + #[serde(skip_serializing_if = "Option::is_none", rename = "max_control_temp")] + max_control_temperature: Option, + #[serde(skip_serializing_if = "Option::is_none", rename = "temp_unit")] + temperature_unit: Option, +} + +impl TrvSetDeviceInfoParams { + pub fn target_temperature( + mut self, + value: u8, + unit: TemperatureUnitKE100, + ) -> Result { + self.target_temperature = Some(value); + self.temperature_unit = Some(unit); + self.validate() + } + pub fn frost_protection_on(mut self, value: bool) -> Result { + self.frost_protection_on = Some(value); + self.validate() + } + pub fn child_protection(mut self, value: bool) -> Result { + self.child_protection = Some(value); + self.validate() + } + pub fn temperature_offset( + mut self, + value: i8, + unit: TemperatureUnitKE100, + ) -> Result { + self.temperature_offset = Some(value); + self.temperature_unit = Some(unit); + self.validate() + } + pub fn min_control_temperature( + mut self, + value: u8, + unit: TemperatureUnitKE100, + ) -> Result { + self.min_control_temperature = Some(value); + self.temperature_unit = Some(unit); + self.validate() + } + pub fn max_control_temperature( + mut self, + value: u8, + unit: TemperatureUnitKE100, + ) -> Result { + self.max_control_temperature = Some(value); + self.temperature_unit = Some(unit); + self.validate() + } +} + +impl TrvSetDeviceInfoParams { + pub(crate) fn new() -> Self { + Self::default() + } + + pub fn validate(self) -> Result { + if let Some(temperature_offset) = self.temperature_offset { + if !(-10..=10).contains(&temperature_offset) { + return Err(Error::Validation { + field: "temperature_offset".to_string(), + message: "must be between -10 and 10".to_string(), + }); + } + } + Ok(self) + } +} diff --git a/tapo/src/responses/child_device_list_result.rs b/tapo/src/responses/child_device_list_result.rs index e5f82e5..1380b4d 100644 --- a/tapo/src/responses/child_device_list_result.rs +++ b/tapo/src/responses/child_device_list_result.rs @@ -1,9 +1,11 @@ +mod ke100_result; mod s200b_result; mod t100_result; mod t110_result; mod t300_result; mod t31x_result; +pub use ke100_result::*; pub use s200b_result::*; pub use t100_result::*; pub use t110_result::*; @@ -50,6 +52,8 @@ pub enum Status { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "model")] pub enum ChildDeviceResult { + /// KE100 thermostatic radiator valve (TRV). + KE100(Box), /// S200B button switch. S200B(Box), /// T100 motion sensor. @@ -71,6 +75,9 @@ pub enum ChildDeviceResult { impl DecodableResultExt for ChildDeviceResult { fn decode(self) -> Result { match self { + ChildDeviceResult::KE100(device) => { + Ok(ChildDeviceResult::KE100(Box::new(device.decode()?))) + } ChildDeviceResult::S200B(device) => { Ok(ChildDeviceResult::S200B(Box::new(device.decode()?))) } diff --git a/tapo/src/responses/child_device_list_result/ke100_result.rs b/tapo/src/responses/child_device_list_result/ke100_result.rs new file mode 100644 index 0000000..94258e2 --- /dev/null +++ b/tapo/src/responses/child_device_list_result/ke100_result.rs @@ -0,0 +1,68 @@ +use serde::{Deserialize, Serialize}; + +use crate::error::Error; +use crate::responses::{decode_value, DecodableResultExt, Status, TapoResponseExt}; + +/// Temperature unit for KE100 devices. +/// Currently *Celsius* is the only unit supported by KE100. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +#[allow(missing_docs)] +pub enum TemperatureUnitKE100 { + Celsius, +} + +/// KE100 thermostatic radiator valve (TRV). +/// +/// Specific properties: `temperature_unit`, `current_temperature`, `target_temperature`, +/// `min_control_temperature, `max_control_temperature`, `temperature_offset`, +/// `child_protection_on`, `frost_protection_on`. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[allow(missing_docs)] +pub struct KE100Result { + pub at_low_battery: bool, + pub avatar: String, + pub bind_count: u32, + pub category: String, + #[serde(rename = "child_protection")] + pub child_protection_on: bool, + #[serde(rename = "current_temp")] + pub current_temperature: f32, + pub device_id: String, + pub frost_protection_on: bool, + pub fw_ver: String, + pub hw_id: String, + pub hw_ver: String, + pub jamming_rssi: i16, + pub jamming_signal_level: u8, + pub location: String, + pub mac: String, + #[serde(rename = "max_control_temp")] + pub max_control_temperature: u8, + #[serde(rename = "min_control_temp")] + pub min_control_temperature: u8, + pub nickname: String, + pub oem_id: String, + pub parent_device_id: String, + pub r#type: String, + pub region: String, + pub rssi: i16, + pub signal_level: u8, + pub specs: String, + pub status: Status, + #[serde(rename = "target_temp")] + pub target_temperature: f32, + #[serde(rename = "temp_offset")] + pub temperature_offset: i8, + #[serde(rename = "temp_unit")] + pub temperature_unit: TemperatureUnitKE100, +} + +impl TapoResponseExt for KE100Result {} + +impl DecodableResultExt for KE100Result { + fn decode(mut self) -> Result { + self.nickname = decode_value(&self.nickname)?; + Ok(self) + } +}