fritzapi/
client.rs

1use crate::api;
2use crate::error::{FritzError, Result};
3use crate::fritz_xml;
4use crate::AVMDevice;
5
6/// The main interface to get data from the fritz box API.
7#[derive(Clone)]
8pub struct FritzClient {
9    user: String,
10    password: String,
11    sid: Option<String>,
12}
13
14impl FritzClient {
15    pub fn new(user: impl ToString, password: impl ToString) -> Self {
16        FritzClient {
17            user: user.to_string(),
18            password: password.to_string(),
19            sid: None,
20        }
21    }
22
23    /// Returns list of all smart home devices. See [devices::AVMDevice].
24    pub fn list_devices(&mut self) -> Result<Vec<AVMDevice>> {
25        let xml = self.request(api::Commands::GetDeviceListInfos)?;
26        let devices = fritz_xml::parse_device_infos(xml)?;
27        Ok(devices
28            .into_iter()
29            .map(AVMDevice::from_xml_device)
30            .collect())
31    }
32
33    pub fn device_stats(&mut self, ain: impl ToString) -> Result<Vec<crate::stats::DeviceStats>> {
34        let ain = ain.to_string();
35        let xml = self.request(api::Commands::GetBasicDeviceStats { ain })?;
36        fritz_xml::parse_device_stats(xml)
37    }
38
39    pub fn turn_on(&mut self, ain: impl ToString) -> Result<()> {
40        let ain = ain.to_string();
41        self.request(api::Commands::SetSwitchOn { ain })?;
42        Ok(())
43    }
44
45    pub fn turn_off(&mut self, ain: impl ToString) -> Result<()> {
46        let ain = ain.to_string();
47        self.request(api::Commands::SetSwitchOff { ain })?;
48        Ok(())
49    }
50
51    pub fn toggle(&mut self, ain: impl ToString) -> Result<()> {
52        let ain = ain.to_string();
53        self.request(api::Commands::SetSwitchToggle { ain })?;
54        Ok(())
55    }
56
57    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
58
59    /// Triggers a higher refresh rate for smart plugs (Fritz!Dect 2xx).
60    ///
61    /// *Note: This function uses an unofficial and undocumented API which may stop working at any time.
62    /// It has been verified to work with a Fritz!Box 7560 running FRITZ!OS 07.29. Other models
63    /// and software versions are likely to work as well.*
64    ///
65    /// By default, the consumption data (current watts, voltage, temperature etc.)
66    /// is updated every 2 minutes. Using this function, the update interval can be
67    /// reduced to ~10 seconds. The higher refresh rate will last for 1-2 minutes and
68    /// will fall back to the default (2 minutes) afterwards. Call this function
69    /// repeatedly (e.g. every 30 seconds) to maintain the higher refresh rate.
70    ///
71    /// The `fritz_dect_2xx_reader` example shows how to read data from smart plugs
72    /// using the higher refresh rate.
73    ///
74    /// ### Background
75    ///
76    /// During testing of the smart plug API, we discovered that the update interval
77    /// decreases from 2 minutes to 10 seconds when looking at the consumption data
78    /// in the browser (e.g. using <http://fritz.box/myfritz/>) or in the app.
79    ///
80    /// Analysis of the network traffic between the website and the Fritz!Box revealed
81    /// that the client regularly sends a request that activates the higher refresh rate.
82    /// The request can be replicated on the terminal using `curl` and a valid session id:
83    ///
84    /// ```bash
85    /// curl -d 'sid=123456790&c=smarthome&a=getData' http://fritz.box/myfritz/api/data.lua
86    /// ```
87    ///
88    /// This function performs basically the same request as the `curl` command above.
89    pub fn trigger_high_refresh_rate(&mut self) -> Result<()> {
90        let sid = match self.sid.clone().or_else(|| self.update_sid().ok()) {
91            None => return Err(FritzError::Forbidden),
92            Some(sid) => sid,
93        };
94        let mut params = std::collections::HashMap::new();
95        params.insert("sid", sid.as_ref());
96        params.insert("c", "smarthome");
97        params.insert("a", "getData");
98        let client = reqwest::blocking::Client::builder()
99            .redirect(reqwest::redirect::Policy::none())
100            .build()?
101            .post("http://fritz.box/myfritz/api/data.lua")
102            .form(&params);
103        let response = client.send()?;
104        let status = response.status();
105
106        if status != 200 {
107            return Err(FritzError::TriggerHighRefreshRateError(status));
108        }
109        Ok(())
110    }
111
112    // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
113
114    fn update_sid(&mut self) -> Result<String> {
115        let sid = api::get_sid(&self.user, &self.password)?;
116        self.sid = Some(sid.clone());
117        Ok(sid)
118    }
119
120    #[instrument(level = "trace", skip(self))]
121    fn request(&mut self, cmd: api::Commands) -> Result<String> {
122        self.request_attempt(cmd, 0)
123    }
124
125    #[instrument(level = "trace", skip(self))]
126    fn request_attempt(&mut self, cmd: api::Commands, request_count: usize) -> Result<String> {
127        let sid = match self.sid.clone().or_else(|| self.update_sid().ok()) {
128            None => return Err(FritzError::Forbidden),
129            Some(sid) => sid,
130        };
131        match api::request(cmd.clone(), sid) {
132            Err(FritzError::Forbidden) if request_count == 0 => {
133                let _ = self.update_sid();
134                self.request_attempt(cmd, request_count + 1)
135            }
136            result => result,
137        }
138    }
139}