macro_rules! tapo_handler {
(
$(#[$meta:meta])*
$name:ident($device_info:ty),
on_off,
device_usage = $device_usage:ty,
device_management,
) => {
tapo_handler!(@base $(#[$meta])* $name($device_info));
tapo_handler!(@on_off $name);
tapo_handler!(@device_usage $name, $device_usage);
tapo_handler!(@device_management $name);
};
(
$(#[$meta:meta])*
$name:ident($device_info:ty),
on_off,
device_usage = $device_usage:ty,
) => {
tapo_handler!(@base $(#[$meta])* $name($device_info));
tapo_handler!(@on_off $name);
tapo_handler!(@device_usage $name, $device_usage);
};
(
$(#[$meta:meta])*
$name:ident($device_info:ty),
on_off,
device_management,
) => {
tapo_handler!(@base $(#[$meta])* $name($device_info));
tapo_handler!(@on_off $name);
tapo_handler!(@device_management $name);
};
(
$(#[$meta:meta])*
$name:ident($device_info:ty),
on_off,
) => {
tapo_handler!(@base $(#[$meta])* $name($device_info));
tapo_handler!(@on_off $name);
};
(
$(#[$meta:meta])*
$name:ident($device_info:ty),
device_usage = $device_usage:ty,
device_management,
) => {
tapo_handler!(@base $(#[$meta])* $name($device_info));
tapo_handler!(@device_usage $name, $device_usage);
tapo_handler!(@device_management $name);
};
(
$(#[$meta:meta])*
$name:ident($device_info:ty),
device_usage = $device_usage:ty,
) => {
tapo_handler!(@base $(#[$meta])* $name($device_info));
tapo_handler!(@device_usage $name, $device_usage);
};
(
$(#[$meta:meta])*
$name:ident($device_info:ty),
device_management,
) => {
tapo_handler!(@base $(#[$meta])* $name($device_info));
tapo_handler!(@device_management $name);
};
(
$(#[$meta:meta])*
$name:ident($device_info:ty),
) => {
tapo_handler!(@base $(#[$meta])* $name($device_info));
};
(@base $(#[$meta:meta])* $name:ident($device_info:ty)) => {
$(#[$meta])*
#[derive(Debug)]
pub struct $name {
client: std::sync::Arc<tokio::sync::RwLock<crate::tapo_vendor::api::ApiClient>>,
ip: String,
port: u16,
}
impl $name {
pub(crate) fn new(client: crate::tapo_vendor::api::ApiClient, (ip, port): (String, u16)) -> Self {
Self {
client: std::sync::Arc::new(tokio::sync::RwLock::new(client)),
ip,
port,
}
}
}
tapo_handler!(@methods $name($device_info));
tapo_handler!(@handler_ext $name);
};
(@methods $name:ident($device_info:ty)) => {
impl $name {
pub fn address(&self) -> (String, u16) {
(self.ip.clone(), self.port)
}
pub async fn refresh_session(&mut self) -> Result<&mut Self, crate::tapo_vendor::error::Error> {
self.client.write().await.refresh_session().await?;
Ok(self)
}
#[doc = concat!(
"Returns *device info* as [`", stringify!($device_info), "`].\n",
"It is not guaranteed to contain all the properties returned from the Tapo API.\n",
"If the deserialization fails, or if a property that you care about it's not present,",
)]
#[cfg_attr(any(), doc = concat!(
"try [`", stringify!($name), "::get_device_info_json`] (requires the `debug` feature).",
))]
#[cfg_attr(not(any()), doc = concat!(
"try `", stringify!($name), "::get_device_info_json` (requires the `debug` feature).",
))]
pub async fn get_device_info(&self) -> Result<$device_info, crate::tapo_vendor::error::Error> {
self.client.read().await.get_device_info().await
}
#[cfg(any())]
pub async fn get_device_info_json(
&self,
) -> Result<serde_json::Value, crate::tapo_vendor::error::Error> {
self.client.read().await.get_device_info().await
}
#[cfg(any())]
pub async fn get_component_list(
&self,
) -> Result<Vec<crate::tapo_vendor::responses::Component>, crate::tapo_vendor::error::Error> {
self.client.read().await.get_component_list().await
}
}
};
(@handler_ext $name:ident) => {
#[async_trait::async_trait]
impl crate::tapo_vendor::api::HandlerExt for $name {
async fn get_client(
&self,
) -> tokio::sync::RwLockReadGuard<'_, dyn crate::tapo_vendor::api::ApiClientExt> {
tokio::sync::RwLockReadGuard::map(
self.client.read().await,
|client: &crate::tapo_vendor::api::ApiClient| -> &dyn crate::tapo_vendor::api::ApiClientExt { client },
)
}
}
};
(@on_off $name:ident) => {
impl $name {
pub async fn on(&self) -> Result<(), crate::tapo_vendor::error::Error> {
let json = serde_json::to_value(
crate::tapo_vendor::requests::GenericSetDeviceInfoParams::device_on(true)?,
)?;
crate::tapo_vendor::api::ApiClientExt::set_device_info(&*self.client.read().await, json).await
}
pub async fn off(&self) -> Result<(), crate::tapo_vendor::error::Error> {
let json = serde_json::to_value(
crate::tapo_vendor::requests::GenericSetDeviceInfoParams::device_on(false)?,
)?;
crate::tapo_vendor::api::ApiClientExt::set_device_info(&*self.client.read().await, json).await
}
}
};
(@device_usage $name:ident, $device_usage:ty) => {
impl $name {
#[doc = concat!("Returns *device usage* as [`", stringify!($device_usage), "`].")]
pub async fn get_device_usage(&self) -> Result<$device_usage, crate::tapo_vendor::error::Error> {
self.client.read().await.get_device_usage().await
}
}
};
(@device_management $name:ident) => {
impl $name {
pub async fn device_reboot(&self, delay_s: u16) -> Result<(), crate::tapo_vendor::error::Error> {
crate::tapo_vendor::api::ApiClientExt::device_reboot(&*self.client.read().await, delay_s).await
}
pub async fn device_reset(&self) -> Result<(), crate::tapo_vendor::error::Error> {
crate::tapo_vendor::api::ApiClientExt::device_reset(&*self.client.read().await).await
}
}
};
}
macro_rules! tapo_child_handler {
(
$(#[$meta:meta])*
$name:ident($device_info:ty),
on_off,
) => {
tapo_child_handler!(@base $(#[$meta])* $name($device_info), GetDeviceInfo);
tapo_child_handler!(@on_off $name);
};
(
$(#[$meta:meta])*
$name:ident($device_info:ty),
trigger_logs = $log:ty,
) => {
tapo_child_handler!(@base $(#[$meta])* $name($device_info), GetDeviceInfo);
tapo_child_handler!(@trigger_logs $name, $log);
};
(
$(#[$meta:meta])*
$name:ident($device_info:ty),
camel_case_device_info,
) => {
tapo_child_handler!(@base $(#[$meta])* $name($device_info), GetDeviceInfoCamelCase);
};
(
$(#[$meta:meta])*
$name:ident($device_info:ty),
) => {
tapo_child_handler!(@base $(#[$meta])* $name($device_info), GetDeviceInfo);
};
(@base $(#[$meta:meta])* $name:ident($device_info:ty), $get_device_info:ident) => {
$(#[$meta])*
pub struct $name {
client: std::sync::Arc<tokio::sync::RwLock<crate::tapo_vendor::api::ApiClient>>,
device_id: String,
}
impl $name {
pub(crate) fn new(
client: std::sync::Arc<tokio::sync::RwLock<crate::tapo_vendor::api::ApiClient>>,
device_id: String,
) -> Self {
Self { client, device_id }
}
#[doc = concat!(
"Returns *device info* as [`", stringify!($device_info), "`].\n",
"It is not guaranteed to contain all the properties returned from the Tapo API.\n",
"If the deserialization fails, or if a property that you care about it's not present,",
)]
#[cfg_attr(any(), doc = concat!(
"try [`", stringify!($name), "::get_device_info_json`] (requires the `debug` feature).",
))]
#[cfg_attr(not(any()), doc = concat!(
"try `", stringify!($name), "::get_device_info_json` (requires the `debug` feature).",
))]
pub async fn get_device_info(&self) -> Result<$device_info, crate::tapo_vendor::error::Error> {
let request = crate::tapo_vendor::requests::TapoRequest::$get_device_info(
crate::tapo_vendor::requests::TapoParams::new(crate::tapo_vendor::requests::EmptyParams),
);
self.client
.read()
.await
.control_child::<$device_info>(self.device_id.clone(), request)
.await?
.ok_or_else(|| {
crate::tapo_vendor::error::Error::Tapo(crate::tapo_vendor::error::TapoResponseError::EmptyResult)
})
.map(|result| crate::tapo_vendor::responses::DecodableResultExt::decode(result))?
}
#[cfg(any())]
pub async fn get_device_info_json(
&self,
) -> Result<serde_json::Value, crate::tapo_vendor::error::Error> {
let request = crate::tapo_vendor::requests::TapoRequest::$get_device_info(
crate::tapo_vendor::requests::TapoParams::new(crate::tapo_vendor::requests::EmptyParams),
);
self.client
.read()
.await
.control_child::<serde_json::Value>(self.device_id.clone(), request)
.await?
.ok_or_else(|| {
crate::tapo_vendor::error::Error::Tapo(crate::tapo_vendor::error::TapoResponseError::EmptyResult)
})
}
#[cfg(any())]
pub async fn get_component_list(
&self,
) -> Result<Vec<crate::tapo_vendor::responses::Component>, crate::tapo_vendor::error::Error> {
let request = crate::tapo_vendor::requests::TapoRequest::ComponentNegotiation(
crate::tapo_vendor::requests::TapoParams::new(crate::tapo_vendor::requests::EmptyParams),
);
let result: crate::tapo_vendor::responses::ComponentListResult = self
.client
.read()
.await
.control_child(self.device_id.clone(), request)
.await?
.ok_or_else(|| {
crate::tapo_vendor::error::Error::Tapo(crate::tapo_vendor::error::TapoResponseError::EmptyResult)
})?;
Ok(result.component_list)
}
}
};
(@on_off $name:ident) => {
impl $name {
pub async fn on(&self) -> Result<(), crate::tapo_vendor::error::Error> {
let json = serde_json::to_value(
crate::tapo_vendor::requests::GenericSetDeviceInfoParams::device_on(true)?,
)?;
let request = crate::tapo_vendor::requests::TapoRequest::SetDeviceInfo(
Box::new(crate::tapo_vendor::requests::TapoParams::new(json)),
);
self.client
.read()
.await
.control_child::<serde_json::Value>(self.device_id.clone(), request)
.await?;
Ok(())
}
pub async fn off(&self) -> Result<(), crate::tapo_vendor::error::Error> {
let json = serde_json::to_value(
crate::tapo_vendor::requests::GenericSetDeviceInfoParams::device_on(false)?,
)?;
let request = crate::tapo_vendor::requests::TapoRequest::SetDeviceInfo(
Box::new(crate::tapo_vendor::requests::TapoParams::new(json)),
);
self.client
.read()
.await
.control_child::<serde_json::Value>(self.device_id.clone(), request)
.await?;
Ok(())
}
}
};
(@trigger_logs $name:ident, $log:ty) => {
impl $name {
pub async fn get_trigger_logs(
&self,
page_size: u64,
start_id: u64,
) -> Result<crate::tapo_vendor::responses::TriggerLogsResult<$log>, crate::tapo_vendor::error::Error> {
let params = crate::tapo_vendor::requests::GetTriggerLogsParams::new(page_size, start_id);
let request = crate::tapo_vendor::requests::TapoRequest::GetTriggerLogs(Box::new(
crate::tapo_vendor::requests::TapoParams::new(params),
));
self.client
.read()
.await
.control_child(self.device_id.clone(), request)
.await?
.ok_or_else(|| {
crate::tapo_vendor::error::Error::Tapo(crate::tapo_vendor::error::TapoResponseError::EmptyResult)
})
}
}
};
}
macro_rules! hub_alarm_handlers {
($name:ident) => {
impl $name {
#[cfg(any())]
pub async fn get_supported_ringtone_list(
&self,
) -> Result<Vec<String>, crate::tapo_vendor::error::Error> {
self.client
.read()
.await
.get_supported_alarm_type_list()
.await
.map(|response| response.alarm_type_list)
}
pub async fn play_alarm(
&self,
ringtone: crate::tapo_vendor::requests::AlarmRingtone,
volume: crate::tapo_vendor::requests::AlarmVolume,
duration: crate::tapo_vendor::requests::AlarmDuration,
) -> Result<(), crate::tapo_vendor::error::Error> {
self.client
.read()
.await
.play_alarm(crate::tapo_vendor::requests::PlayAlarmParams::new(
ringtone, volume, duration,
)?)
.await
}
pub async fn stop_alarm(&self) -> Result<(), crate::tapo_vendor::error::Error> {
self.client.read().await.stop_alarm().await
}
}
};
}
macro_rules! camera_device_management_handlers {
($name:ident) => {
impl $name {
pub async fn device_reboot(&self) -> Result<(), crate::tapo_vendor::error::Error> {
let request = crate::tapo_vendor::requests::TapoRequest::SmartCamRebootDevice(
crate::tapo_vendor::requests::TapoParams::new(
crate::tapo_vendor::requests::SmartCamRebootDeviceParams::new(),
),
);
self.client
.read()
.await
.execute_smart_cam_command(request)
.await
}
pub async fn device_reset(&self) -> Result<(), crate::tapo_vendor::error::Error> {
let request = crate::tapo_vendor::requests::TapoRequest::SmartCamSoftReset(
crate::tapo_vendor::requests::TapoParams::new(
crate::tapo_vendor::requests::SmartCamSoftResetParams::factory(),
),
);
self.client
.read()
.await
.execute_smart_cam_command(request)
.await
}
}
};
}
macro_rules! camera_detection_handlers {
($name:ident, builder = $builder:ident) => {
impl $name {
#[doc = concat!(
"let device = ApiClient::new(\"tapo-username@example.com\", \"tapo-password\")
",
" .", stringify!($builder), "(\"192.168.1.100\")
",
" .await?;
",
)]
pub async fn get_detections(
&self,
start_time: chrono::DateTime<chrono::Utc>,
end_time: chrono::DateTime<chrono::Utc>,
) -> Result<Vec<crate::tapo_vendor::responses::DetectionResult>, crate::tapo_vendor::error::Error> {
self.client
.read()
.await
.get_detections(start_time, end_time)
.await
}
}
};
}
macro_rules! camera_stream_handlers {
($name:ident) => {
impl $name {
pub fn get_rtsp_stream_url(
&self,
username: &str,
password: &str,
) -> crate::tapo_vendor::responses::RtspStreamUrl {
crate::tapo_vendor::api::camera_stream::rtsp_stream_url(
&self.ip, username, password,
)
}
pub async fn get_snapshot(
&self,
username: &str,
password: &str,
) -> Result<crate::tapo_vendor::responses::Snapshot, crate::tapo_vendor::error::Error>
{
let timeout = self.client.read().await.timeout();
crate::tapo_vendor::api::camera_stream::snapshot(
&self.ip, username, password, timeout,
)
.await
}
}
};
}
macro_rules! hub_child_handlers {
(
$name:ident,
$child:ident
$(, child_device_list_note = $child_device_list_note:literal)?
$(,)?
) => {
impl $name {
#[doc = concat!(
"Returns *child device list* as [`", stringify!($child), "`](crate::tapo_vendor::responses::", stringify!($child), ").\n",
"It is not guaranteed to contain all the properties returned from the Tapo API\n",
"or to support all the possible devices connected to the hub.\n",
"If the deserialization fails, or if a property that you care about it's not present,",
)]
#[cfg_attr(any(), doc = concat!(
"try [`", stringify!($name), "::get_child_device_list_json`] (requires the `debug` feature).",
))]
#[cfg_attr(not(any()), doc = concat!(
"try `", stringify!($name), "::get_child_device_list_json` (requires the `debug` feature).",
))]
$(#[doc = $child_device_list_note])?
pub async fn get_child_device_list(
&self,
) -> Result<Vec<crate::tapo_vendor::responses::$child>, crate::tapo_vendor::error::Error> {
let mut results = Vec::new();
let mut start_index = 0;
let mut fetch = true;
while fetch {
let devices = self
.client
.read()
.await
.get_child_device_list::<crate::tapo_vendor::responses::ChildDeviceListHubResult<
crate::tapo_vendor::responses::$child,
>>(
start_index,
)
.await
.map(|r| r.devices)?;
fetch = devices.len() == 10;
start_index += 10;
results.extend(devices);
}
Ok(results)
}
#[cfg(any())]
pub async fn get_child_device_list_json(
&self,
start_index: u64,
) -> Result<serde_json::Value, crate::tapo_vendor::error::Error> {
self.client
.read()
.await
.get_child_device_list(start_index)
.await
}
#[cfg(any())]
pub async fn get_child_device_component_list(
&self,
) -> Result<Vec<crate::tapo_vendor::responses::ChildDeviceComponentList>, crate::tapo_vendor::error::Error> {
self.client
.read()
.await
.get_child_device_component_list()
.await
}
}
impl $name {
hub_child_handlers!(@checked $child, ke100, KE100Handler, KE100);
hub_child_handlers!(@checked $child, s200, S200Handler, S200);
hub_child_handlers!(@checked $child, s210, S210Handler, S210);
hub_child_handlers!(@checked $child, t100, T100Handler, T100);
hub_child_handlers!(@checked $child, t110, T110Handler, T110);
hub_child_handlers!(@checked $child, t300, T300Handler, T300);
hub_child_handlers!(@checked $child, t31x, T31XHandler, T31X);
}
impl $name {
hub_child_handlers!(@unchecked $name, ke100_unchecked, ke100, KE100Handler);
hub_child_handlers!(@unchecked $name, s200_unchecked, s200, S200Handler);
hub_child_handlers!(@unchecked $name, s210_unchecked, s210, S210Handler);
hub_child_handlers!(@unchecked $name, t100_unchecked, t100, T100Handler);
hub_child_handlers!(@unchecked $name, t110_unchecked, t110, T110Handler);
hub_child_handlers!(@unchecked $name, t300_unchecked, t300, T300Handler);
hub_child_handlers!(@unchecked $name, t31x_unchecked, t31x, T31XHandler);
}
};
(@checked $child:ident, $method:ident, $handler:ident, $variant:ident) => {
#[doc = concat!(
"Returns a [`", stringify!($handler), "`](crate::tapo_vendor::", stringify!($handler),
") for the given [`HubDevice`](crate::tapo_vendor::HubDevice).\n\n",
"# Arguments\n\n",
"* `identifier` - a hub device identifier",
)]
pub async fn $method(
&self,
identifier: crate::tapo_vendor::api::HubDevice,
) -> Result<crate::tapo_vendor::api::$handler, crate::tapo_vendor::error::Error> {
let device_id = self
.get_child_device_list()
.await?
.into_iter()
.find_map(|child| match child {
crate::tapo_vendor::responses::$child::$variant(c) => match &identifier {
crate::tapo_vendor::api::HubDevice::ByDeviceId(id) if c.device_id == *id => {
Some(c.device_id)
}
crate::tapo_vendor::api::HubDevice::ByNickname(nickname) if c.nickname == *nickname => {
Some(c.device_id)
}
_ => None,
},
_ => None,
})
.ok_or(crate::tapo_vendor::error::Error::DeviceNotFound)?;
Ok(crate::tapo_vendor::api::$handler::new(self.client.clone(), device_id))
}
};
(@unchecked $name:ident, $method:ident, $checked:ident, $handler:ident) => {
#[doc = concat!(
"Returns a [`", stringify!($handler), "`](crate::tapo_vendor::", stringify!($handler),
") for the given `device_id` without first\n",
"listing the hub's children to verify the device exists or matches the\n",
"requested model. The device id is trusted; if it is wrong or refers to\n",
"a different model, subsequent operations on the returned handler will\n",
"fail at request time. Use this when you already have a valid device id\n",
"(e.g. from a prior [`", stringify!($name), "::get_child_device_list`] call) to avoid\n",
"the extra validation round-trip performed by [`",
stringify!($name), "::", stringify!($checked), "`].",
)]
pub fn $method(&self, device_id: String) -> crate::tapo_vendor::api::$handler {
crate::tapo_vendor::api::$handler::new(self.client.clone(), device_id)
}
};
}