tapo/api/
api_client.rs

1use std::fmt;
2use std::time::Duration;
3
4use async_trait::async_trait;
5use log::debug;
6use reqwest::Client;
7use serde::de::DeserializeOwned;
8
9use crate::api::protocol::{TapoProtocol, TapoProtocolExt};
10use crate::api::{
11    ColorLightHandler, GenericDeviceHandler, HubHandler, LightHandler, PlugEnergyMonitoringHandler,
12    PlugHandler, PowerStripHandler, RgbLightStripHandler, RgbicLightStripHandler,
13};
14use crate::error::{Error, TapoResponseError};
15use crate::requests::{
16    ControlChildParams, EmptyParams, EnergyDataInterval, GetChildDeviceListParams,
17    GetEnergyDataParams, LightingEffect, MultipleRequestParams, PlayAlarmParams, TapoParams,
18    TapoRequest,
19};
20use crate::responses::{
21    ControlChildResult, CurrentPowerResult, DecodableResultExt, EnergyDataResult,
22    EnergyUsageResult, SupportedAlarmTypeListResult, TapoMultipleResponse, TapoResponseExt,
23    TapoResult, validate_response,
24};
25
26const TERMINAL_UUID: &str = "00-00-00-00-00-00";
27
28/// Implemented by all ApiClient implementations.
29#[async_trait]
30pub trait ApiClientExt: std::fmt::Debug + Send + Sync {
31    /// Sets the device info.
32    async fn set_device_info(&self, device_info_params: serde_json::Value) -> Result<(), Error>;
33}
34
35/// Tapo API Client. See [examples](https://github.com/mihai-dinculescu/tapo/tree/main/tapo/examples).
36///
37/// # Example
38///
39/// ```rust,no_run
40/// use tapo::ApiClient;
41///
42/// #[tokio::main]
43/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
44///     let device = ApiClient::new("tapo-username@example.com", "tapo-password")
45///     .l530("192.168.1.100")
46///     .await?;
47///
48///     device.on().await?;
49///
50///     Ok(())
51/// }
52/// ```
53#[derive(Debug, Clone)]
54pub struct ApiClient {
55    tapo_username: String,
56    tapo_password: String,
57    timeout: Option<Duration>,
58    protocol: Option<TapoProtocol>,
59}
60
61/// Tapo API Client constructor.
62impl ApiClient {
63    /// Returns a new instance of [`ApiClient`].
64    /// It is cheaper to [`ApiClient::clone`] an existing instance than to create a new one when multiple devices need to be controller.
65    /// This is because [`ApiClient::clone`] reuses the underlying [`reqwest::Client`].
66    ///
67    /// # Arguments
68    ///
69    /// * `tapo_username` - the Tapo username
70    /// * `tapo_password` - the Tapo password
71    ///
72    /// Note: The default connection timeout is 30 seconds.
73    /// Use [`ApiClient::with_timeout`] to change it.
74    pub fn new(tapo_username: impl Into<String>, tapo_password: impl Into<String>) -> ApiClient {
75        Self {
76            tapo_username: tapo_username.into(),
77            tapo_password: tapo_password.into(),
78            timeout: None,
79            protocol: None,
80        }
81    }
82
83    /// Changes the connection timeout from the default value to the given value.
84    ///
85    /// # Arguments
86    ///
87    /// * `timeout` - The new connection timeout value.
88    pub fn with_timeout(mut self, timeout: Duration) -> ApiClient {
89        self.timeout = Some(timeout);
90        self
91    }
92}
93
94/// Device handler builders.
95impl ApiClient {
96    /// Specializes the given [`ApiClient`] into an authenticated [`GenericDeviceHandler`].
97    ///
98    /// # Arguments
99    ///
100    /// * `ip_address` - the IP address of the device
101    ///
102    /// # Example
103    ///
104    /// ```rust,no_run
105    /// # use tapo::ApiClient;
106    /// # #[tokio::main]
107    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
108    /// let device = ApiClient::new("tapo-username@example.com", "tapo-password")
109    ///     .generic_device("192.168.1.100")
110    ///     .await?;
111    /// device.on().await?;
112    /// # Ok(())
113    /// # }
114    /// ```
115    pub async fn generic_device(
116        mut self,
117        ip_address: impl Into<String>,
118    ) -> Result<GenericDeviceHandler, Error> {
119        self.login(ip_address).await?;
120
121        Ok(GenericDeviceHandler::new(self))
122    }
123
124    /// Specializes the given [`ApiClient`] into an authenticated [`LightHandler`].
125    ///
126    /// # Arguments
127    ///
128    /// * `ip_address` - the IP address of the device
129    ///
130    /// # Example
131    ///
132    /// ```rust,no_run
133    /// # use tapo::ApiClient;
134    /// # #[tokio::main]
135    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
136    /// let device = ApiClient::new("tapo-username@example.com", "tapo-password")
137    ///     .l510("192.168.1.100")
138    ///     .await?;
139    /// device.on().await?;
140    /// # Ok(())
141    /// # }
142    /// ```
143    pub async fn l510(mut self, ip_address: impl Into<String>) -> Result<LightHandler, Error> {
144        self.login(ip_address).await?;
145
146        Ok(LightHandler::new(self))
147    }
148
149    /// Specializes the given [`ApiClient`] into an authenticated [`LightHandler`].
150    ///
151    /// # Arguments
152    ///
153    /// * `ip_address` - the IP address of the device
154    ///
155    /// # Example
156    ///
157    /// ```rust,no_run
158    /// # use tapo::ApiClient;
159    /// # #[tokio::main]
160    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
161    /// let device = ApiClient::new("tapo-username@example.com", "tapo-password")
162    ///     .l520("192.168.1.100")
163    ///     .await?;
164    /// device.on().await?;
165    /// # Ok(())
166    /// # }
167    /// ```
168    pub async fn l520(mut self, ip_address: impl Into<String>) -> Result<LightHandler, Error> {
169        self.login(ip_address).await?;
170
171        Ok(LightHandler::new(self))
172    }
173
174    /// Specializes the given [`ApiClient`] into an authenticated [`ColorLightHandler`].
175    ///
176    /// # Arguments
177    ///
178    /// * `ip_address` - the IP address of the device
179    ///
180    /// # Example
181    ///
182    /// ```rust,no_run
183    /// # use tapo::ApiClient;
184    /// # #[tokio::main]
185    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
186    /// let device = ApiClient::new("tapo-username@example.com", "tapo-password")
187    ///     .l530("192.168.1.100")
188    ///     .await?;
189    /// device.on().await?;
190    /// # Ok(())
191    /// # }
192    /// ```
193    pub async fn l530(mut self, ip_address: impl Into<String>) -> Result<ColorLightHandler, Error> {
194        self.login(ip_address).await?;
195
196        Ok(ColorLightHandler::new(self))
197    }
198
199    /// Specializes the given [`ApiClient`] into an authenticated [`ColorLightHandler`].
200    ///
201    /// # Arguments
202    ///
203    /// * `ip_address` - the IP address of the device
204    ///
205    /// # Example
206    ///
207    /// ```rust,no_run
208    /// # use tapo::ApiClient;
209    /// # #[tokio::main]
210    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
211    /// let device = ApiClient::new("tapo-username@example.com", "tapo-password")
212    ///     .l535("192.168.1.100")
213    ///     .await?;
214    /// device.on().await?;
215    /// # Ok(())
216    /// # }
217    /// ```
218    pub async fn l535(mut self, ip_address: impl Into<String>) -> Result<ColorLightHandler, Error> {
219        self.login(ip_address).await?;
220
221        Ok(ColorLightHandler::new(self))
222    }
223
224    /// Specializes the given [`ApiClient`] into an authenticated [`LightHandler`].
225    ///
226    /// # Arguments
227    ///
228    /// * `ip_address` - the IP address of the device
229    ///
230    /// # Example
231    ///
232    /// ```rust,no_run
233    /// # use tapo::ApiClient;
234    /// # #[tokio::main]
235    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
236    /// let device = ApiClient::new("tapo-username@example.com", "tapo-password")
237    ///     .l610("192.168.1.100")
238    ///     .await?;
239    /// device.on().await?;
240    /// # Ok(())
241    /// # }
242    /// ```
243    pub async fn l610(mut self, ip_address: impl Into<String>) -> Result<LightHandler, Error> {
244        self.login(ip_address).await?;
245
246        Ok(LightHandler::new(self))
247    }
248
249    /// Specializes the given [`ApiClient`] into an authenticated [`ColorLightHandler`].
250    ///
251    /// # Arguments
252    ///
253    /// * `ip_address` - the IP address of the device
254    ///
255    /// # Example
256    ///
257    /// ```rust,no_run
258    /// # use tapo::ApiClient;
259    /// # #[tokio::main]
260    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
261    /// let device = ApiClient::new("tapo-username@example.com", "tapo-password")
262    ///     .l630("192.168.1.100")
263    ///     .await?;
264    /// device.on().await?;
265    /// # Ok(())
266    /// # }
267    /// ```
268    pub async fn l630(mut self, ip_address: impl Into<String>) -> Result<ColorLightHandler, Error> {
269        self.login(ip_address).await?;
270
271        Ok(ColorLightHandler::new(self))
272    }
273
274    /// Specializes the given [`ApiClient`] into an authenticated [`RgbLightStripHandler`].
275    ///
276    /// # Arguments
277    ///
278    /// * `ip_address` - the IP address of the device
279    ///
280    /// # Example
281    ///
282    /// ```rust,no_run
283    /// # use tapo::ApiClient;
284    /// # #[tokio::main]
285    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
286    /// let device = ApiClient::new("tapo-username@example.com", "tapo-password")
287    ///     .l900("192.168.1.100")
288    ///     .await?;
289    /// device.on().await?;
290    /// # Ok(())
291    /// # }
292    /// ```
293    pub async fn l900(
294        mut self,
295        ip_address: impl Into<String>,
296    ) -> Result<RgbLightStripHandler, Error> {
297        self.login(ip_address).await?;
298
299        Ok(RgbLightStripHandler::new(self))
300    }
301
302    /// Specializes the given [`ApiClient`] into an authenticated [`RgbicLightStripHandler`].
303    ///
304    /// # Arguments
305    ///
306    /// * `ip_address` - the IP address of the device
307    ///
308    /// # Example
309    ///
310    /// ```rust,no_run
311    /// # use tapo::ApiClient;
312    /// # #[tokio::main]
313    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
314    /// let device = ApiClient::new("tapo-username@example.com", "tapo-password")
315    ///     .l920("192.168.1.100")
316    ///     .await?;
317    /// device.on().await?;
318    /// # Ok(())
319    /// # }
320    /// ```
321    pub async fn l920(
322        mut self,
323        ip_address: impl Into<String>,
324    ) -> Result<RgbicLightStripHandler, Error> {
325        self.login(ip_address).await?;
326
327        Ok(RgbicLightStripHandler::new(self))
328    }
329
330    /// Specializes the given [`ApiClient`] into an authenticated [`RgbicLightStripHandler`].
331    ///
332    /// # Arguments
333    ///
334    /// * `ip_address` - the IP address of the device
335    ///
336    /// # Example
337    ///
338    /// ```rust,no_run
339    /// # use tapo::ApiClient;
340    /// # #[tokio::main]
341    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
342    /// let device = ApiClient::new("tapo-username@example.com", "tapo-password")
343    ///     .l930("192.168.1.100")
344    ///     .await?;
345    /// device.on().await?;
346    /// # Ok(())
347    /// # }
348    /// ```
349    pub async fn l930(
350        mut self,
351        ip_address: impl Into<String>,
352    ) -> Result<RgbicLightStripHandler, Error> {
353        self.login(ip_address).await?;
354
355        Ok(RgbicLightStripHandler::new(self))
356    }
357
358    /// Specializes the given [`ApiClient`] into an authenticated [`PlugHandler`].
359    ///
360    /// # Arguments
361    ///
362    /// * `ip_address` - the IP address of the device
363    ///
364    /// # Example
365    ///
366    /// ```rust,no_run
367    /// # use tapo::ApiClient;
368    /// # #[tokio::main]
369    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
370    /// let device = ApiClient::new("tapo-username@example.com", "tapo-password")
371    ///     .p100("192.168.1.100")
372    ///     .await?;
373    /// device.on().await?;
374    /// # Ok(())
375    /// # }
376    /// ```
377    pub async fn p100(mut self, ip_address: impl Into<String>) -> Result<PlugHandler, Error> {
378        self.login(ip_address).await?;
379
380        Ok(PlugHandler::new(self))
381    }
382
383    /// Specializes the given [`ApiClient`] into an authenticated [`PlugHandler`].
384    ///
385    /// # Arguments
386    ///
387    /// * `ip_address` - the IP address of the device
388    ///
389    /// # Example
390    ///
391    /// ```rust,no_run
392    /// # use tapo::ApiClient;
393    /// # #[tokio::main]
394    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
395    /// let device = ApiClient::new("tapo-username@example.com", "tapo-password")
396    ///     .p105("192.168.1.100")
397    ///     .await?;
398    /// device.on().await?;
399    /// # Ok(())
400    /// # }
401    /// ```
402    pub async fn p105(mut self, ip_address: impl Into<String>) -> Result<PlugHandler, Error> {
403        self.login(ip_address).await?;
404
405        Ok(PlugHandler::new(self))
406    }
407
408    /// Specializes the given [`ApiClient`] into an authenticated [`PlugEnergyMonitoringHandler`].
409    ///
410    /// # Arguments
411    ///
412    /// * `ip_address` - the IP address of the device
413    ///
414    /// # Example
415    ///
416    /// ```rust,no_run
417    /// # use tapo::ApiClient;
418    /// # #[tokio::main]
419    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
420    /// let device = ApiClient::new("tapo-username@example.com", "tapo-password")
421    ///     .p110("192.168.1.100")
422    ///     .await?;
423    /// device.on().await?;
424    /// # Ok(())
425    /// # }
426    /// ```
427    pub async fn p110(
428        mut self,
429        ip_address: impl Into<String>,
430    ) -> Result<PlugEnergyMonitoringHandler, Error> {
431        self.login(ip_address).await?;
432
433        Ok(PlugEnergyMonitoringHandler::new(self))
434    }
435
436    /// Specializes the given [`ApiClient`] into an authenticated [`PlugEnergyMonitoringHandler`].
437    ///
438    /// # Arguments
439    ///
440    /// * `ip_address` - the IP address of the device
441    ///
442    /// # Example
443    ///
444    /// ```rust,no_run
445    /// # use tapo::ApiClient;
446    /// # #[tokio::main]
447    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
448    /// let device = ApiClient::new("tapo-username@example.com", "tapo-password")
449    ///     .p115("192.168.1.100")
450    ///     .await?;
451    /// device.on().await?;
452    /// # Ok(())
453    /// # }
454    /// ```
455    pub async fn p115(
456        mut self,
457        ip_address: impl Into<String>,
458    ) -> Result<PlugEnergyMonitoringHandler, Error> {
459        self.login(ip_address).await?;
460
461        Ok(PlugEnergyMonitoringHandler::new(self))
462    }
463
464    /// Specializes the given [`ApiClient`] into an authenticated [`PowerStripHandler`].
465    ///
466    /// # Arguments
467    ///
468    /// * `ip_address` - the IP address of the device
469    ///
470    /// # Example
471    ///
472    /// ```rust,no_run
473    /// # use tapo::ApiClient;
474    /// # #[tokio::main]
475    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
476    /// let device = ApiClient::new("tapo-username@example.com", "tapo-password")
477    ///     .p300("192.168.1.100")
478    ///     .await?;
479    /// let child_device_list = device.get_child_device_list().await?;
480    /// println!("Child device list: {child_device_list:?}");
481    /// # Ok(())
482    /// # }
483    /// ```
484    pub async fn p300(mut self, ip_address: impl Into<String>) -> Result<PowerStripHandler, Error> {
485        self.login(ip_address).await?;
486
487        Ok(PowerStripHandler::new(self))
488    }
489
490    /// Specializes the given [`ApiClient`] into an authenticated [`PowerStripHandler`].
491    ///
492    /// # Arguments
493    ///
494    /// * `ip_address` - the IP address of the device
495    ///
496    /// # Example
497    ///
498    /// ```rust,no_run
499    /// # use tapo::ApiClient;
500    /// # #[tokio::main]
501    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
502    /// let device = ApiClient::new("tapo-username@example.com", "tapo-password")
503    ///     .p304("192.168.1.100")
504    ///     .await?;
505    /// let child_device_list = device.get_child_device_list().await?;
506    /// println!("Child device list: {child_device_list:?}");
507    /// # Ok(())
508    /// # }
509    /// ```
510    pub async fn p304(mut self, ip_address: impl Into<String>) -> Result<PowerStripHandler, Error> {
511        self.login(ip_address).await?;
512
513        Ok(PowerStripHandler::new(self))
514    }
515
516    /// Specializes the given [`ApiClient`] into an authenticated [`HubHandler`].
517    ///
518    /// # Arguments
519    ///
520    /// * `ip_address` - the IP address of the device
521    ///
522    /// # Example
523    ///
524    /// ```rust,no_run
525    /// # use tapo::ApiClient;
526    /// # #[tokio::main]
527    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
528    /// let device = ApiClient::new("tapo-username@example.com", "tapo-password")
529    ///     .h100("192.168.1.100")
530    ///     .await?;
531    ///
532    /// let child_device_list = device.get_child_device_list().await?;
533    /// println!("Child device list: {child_device_list:?}");
534    /// # Ok(())
535    /// # }
536    /// ```
537    pub async fn h100(mut self, ip_address: impl Into<String>) -> Result<HubHandler, Error> {
538        self.login(ip_address).await?;
539
540        Ok(HubHandler::new(self))
541    }
542}
543
544/// Tapo API Client private methods.
545impl ApiClient {
546    pub(crate) async fn login(&mut self, ip_address: impl Into<String>) -> Result<(), Error> {
547        let url = format!("http://{}/app", ip_address.into());
548        debug!("Device url: {url}");
549
550        let tapo_username = self.tapo_username.clone();
551        let tapo_password = self.tapo_password.clone();
552
553        self.get_protocol_mut()?
554            .login(url, tapo_username, tapo_password)
555            .await
556    }
557
558    pub(crate) async fn refresh_session(&mut self) -> Result<(), Error> {
559        let tapo_username = self.tapo_username.clone();
560        let tapo_password = self.tapo_password.clone();
561
562        self.get_protocol_mut()?
563            .refresh_session(tapo_username, tapo_password)
564            .await
565    }
566
567    pub(crate) async fn get_supported_alarm_type_list(
568        &self,
569    ) -> Result<SupportedAlarmTypeListResult, Error> {
570        let request = TapoRequest::GetSupportedAlarmTypeList(TapoParams::new(EmptyParams));
571
572        self.get_protocol()?
573            .execute_request::<SupportedAlarmTypeListResult>(request, true)
574            .await?
575            .ok_or_else(|| Error::Tapo(TapoResponseError::EmptyResult))
576    }
577
578    pub(crate) async fn play_alarm(&self, params: PlayAlarmParams) -> Result<(), Error> {
579        let request = TapoRequest::PlayAlarm(TapoParams::new(params));
580
581        self.get_protocol()?
582            .execute_request::<serde_json::Value>(request, true)
583            .await?;
584
585        Ok(())
586    }
587
588    pub(crate) async fn stop_alarm(&self) -> Result<(), Error> {
589        let request = TapoRequest::StopAlarm(TapoParams::new(EmptyParams));
590
591        self.get_protocol()?
592            .execute_request::<serde_json::Value>(request, true)
593            .await?;
594
595        Ok(())
596    }
597
598    pub(crate) async fn device_reset(&self) -> Result<(), Error> {
599        debug!("Device reset...");
600        let request = TapoRequest::DeviceReset(TapoParams::new(EmptyParams));
601
602        self.get_protocol()?
603            .execute_request::<serde_json::Value>(request, true)
604            .await?;
605
606        Ok(())
607    }
608
609    pub(crate) async fn get_device_info<R>(&self) -> Result<R, Error>
610    where
611        R: fmt::Debug + DeserializeOwned + TapoResponseExt + DecodableResultExt,
612    {
613        debug!("Get Device info...");
614        let request = TapoRequest::GetDeviceInfo(TapoParams::new(EmptyParams));
615
616        self.get_protocol()?
617            .execute_request::<R>(request, true)
618            .await?
619            .map(|result| result.decode())
620            .ok_or_else(|| Error::Tapo(TapoResponseError::EmptyResult))?
621    }
622
623    pub(crate) async fn get_device_usage<R>(&self) -> Result<R, Error>
624    where
625        R: fmt::Debug + DeserializeOwned + TapoResponseExt,
626    {
627        debug!("Get Device usage...");
628        let request = TapoRequest::GetDeviceUsage(TapoParams::new(EmptyParams));
629
630        self.get_protocol()?
631            .execute_request::<R>(request, true)
632            .await?
633            .ok_or_else(|| Error::Tapo(TapoResponseError::EmptyResult))
634    }
635
636    pub(crate) async fn set_lighting_effect(
637        &self,
638        lighting_effect: LightingEffect,
639    ) -> Result<(), Error> {
640        debug!("Lighting effect will change to: {lighting_effect:?}");
641
642        let request = TapoRequest::SetLightingEffect(Box::new(
643            TapoParams::new(lighting_effect)
644                .set_request_time_mils()?
645                .set_terminal_uuid(TERMINAL_UUID),
646        ));
647
648        self.get_protocol()?
649            .execute_request::<TapoResult>(request, true)
650            .await?;
651
652        Ok(())
653    }
654
655    pub(crate) async fn get_energy_usage(&self) -> Result<EnergyUsageResult, Error> {
656        debug!("Get Energy usage...");
657        let request = TapoRequest::GetEnergyUsage(TapoParams::new(EmptyParams));
658
659        self.get_protocol()?
660            .execute_request::<EnergyUsageResult>(request, true)
661            .await?
662            .ok_or_else(|| Error::Tapo(TapoResponseError::EmptyResult))
663    }
664
665    pub(crate) async fn get_energy_data(
666        &self,
667        interval: EnergyDataInterval,
668    ) -> Result<EnergyDataResult, Error> {
669        debug!("Get Energy data...");
670        let params = GetEnergyDataParams::new(interval);
671        let request = TapoRequest::GetEnergyData(TapoParams::new(params));
672
673        self.get_protocol()?
674            .execute_request::<EnergyDataResult>(request, true)
675            .await?
676            .ok_or_else(|| Error::Tapo(TapoResponseError::EmptyResult))
677    }
678
679    pub(crate) async fn get_current_power(&self) -> Result<CurrentPowerResult, Error> {
680        debug!("Get Current power...");
681        let request = TapoRequest::GetCurrentPower(TapoParams::new(EmptyParams));
682
683        self.get_protocol()?
684            .execute_request::<CurrentPowerResult>(request, true)
685            .await?
686            .ok_or_else(|| Error::Tapo(TapoResponseError::EmptyResult))
687    }
688
689    pub(crate) async fn get_child_device_list<R>(&self, start_index: u64) -> Result<R, Error>
690    where
691        R: fmt::Debug + DeserializeOwned + TapoResponseExt + DecodableResultExt,
692    {
693        debug!("Get Child device list starting with index {start_index}...");
694        let request = TapoRequest::GetChildDeviceList(TapoParams::new(
695            GetChildDeviceListParams::new(start_index),
696        ));
697
698        self.get_protocol()?
699            .execute_request::<R>(request, true)
700            .await?
701            .map(|result| result.decode())
702            .ok_or_else(|| Error::Tapo(TapoResponseError::EmptyResult))?
703    }
704
705    pub(crate) async fn get_child_device_component_list<R>(&self) -> Result<R, Error>
706    where
707        R: fmt::Debug + DeserializeOwned + TapoResponseExt + DecodableResultExt,
708    {
709        debug!("Get Child device component list...");
710        let request = TapoRequest::GetChildDeviceComponentList(TapoParams::new(EmptyParams));
711
712        self.get_protocol()?
713            .execute_request::<R>(request, true)
714            .await?
715            .map(|result| result.decode())
716            .ok_or_else(|| Error::Tapo(TapoResponseError::EmptyResult))?
717    }
718
719    pub(crate) async fn control_child<R>(
720        &self,
721        device_id: String,
722        child_request: TapoRequest,
723    ) -> Result<Option<R>, Error>
724    where
725        R: fmt::Debug + DeserializeOwned + TapoResponseExt,
726    {
727        debug!("Control child...");
728        let params = MultipleRequestParams::new(vec![child_request]);
729        let request = TapoRequest::MultipleRequest(Box::new(TapoParams::new(params)));
730
731        let params = ControlChildParams::new(device_id, request);
732        let request = TapoRequest::ControlChild(Box::new(TapoParams::new(params)));
733
734        let responses = self
735            .get_protocol()?
736            .execute_request::<ControlChildResult<TapoMultipleResponse<R>>>(request, true)
737            .await?
738            .ok_or_else(|| Error::Tapo(TapoResponseError::EmptyResult))?
739            .response_data
740            .result
741            .responses;
742
743        let response = responses
744            .into_iter()
745            .next()
746            .ok_or_else(|| Error::Tapo(TapoResponseError::EmptyResult))?;
747
748        validate_response(&response)?;
749
750        Ok(response.result)
751    }
752
753    fn get_protocol_mut(&mut self) -> Result<&mut TapoProtocol, Error> {
754        if self.protocol.is_none() {
755            let timeout = self.timeout.unwrap_or_else(|| Duration::from_secs(30));
756
757            let client = Client::builder()
758                .http1_title_case_headers()
759                .timeout(timeout)
760                .build()?;
761            let protocol = TapoProtocol::new(client);
762            self.protocol.replace(protocol);
763        }
764
765        self.protocol.as_mut().ok_or_else(|| {
766            Error::Other(anyhow::anyhow!(
767                "The protocol should have been initialized already"
768            ))
769        })
770    }
771
772    fn get_protocol(&self) -> Result<&TapoProtocol, Error> {
773        self.protocol.as_ref().ok_or_else(|| {
774            Error::Other(anyhow::anyhow!(
775                "The protocol should have been initialized already"
776            ))
777        })
778    }
779}
780
781#[async_trait]
782impl ApiClientExt for ApiClient {
783    async fn set_device_info(&self, device_info_params: serde_json::Value) -> Result<(), Error> {
784        debug!("Device info will change to: {device_info_params:?}");
785
786        let set_device_info_request = TapoRequest::SetDeviceInfo(Box::new(
787            TapoParams::new(device_info_params)
788                .set_request_time_mils()?
789                .set_terminal_uuid(TERMINAL_UUID),
790        ));
791
792        self.get_protocol()?
793            .execute_request::<TapoResult>(set_device_info_request, true)
794            .await?;
795
796        Ok(())
797    }
798}