Skip to main content

shelly_core/api/
gen1.rs

1use crate::Result;
2use crate::error::{self, Error};
3use crate::model::{DeviceInfo, DeviceStatus, PowerReading, SwitchStatus};
4
5use super::{FirmwareInfo, SwitchResult};
6
7pub struct Gen1Device {
8    info: DeviceInfo,
9    base_host: String,
10    client: reqwest::Client,
11    password: Option<String>,
12}
13
14impl Gen1Device {
15    pub fn new(info: DeviceInfo, client: reqwest::Client, password: Option<String>) -> Self {
16        let base_host = info.ip.to_string();
17        Self::new_with_host(info, base_host, client, password)
18    }
19
20    /// Build a `Gen1Device` addressed by an explicit `host[:port]` string
21    /// rather than `info.ip`, so a device that isn't reachable on the
22    /// default port (or that must be reached by a test harness on an
23    /// ephemeral loopback port) can still be targeted.
24    pub fn new_with_host(
25        info: DeviceInfo,
26        base_host: String,
27        client: reqwest::Client,
28        password: Option<String>,
29    ) -> Self {
30        Self {
31            info,
32            base_host,
33            client,
34            password,
35        }
36    }
37
38    fn url(&self, path: &str) -> String {
39        format!("http://{}{path}", self.base_host)
40    }
41
42    async fn get_json(&self, path: &str) -> Result<serde_json::Value> {
43        let url = self.url(path);
44        let mut req = self.client.get(&url);
45        if let Some(ref password) = self.password {
46            req = req.basic_auth("admin", Some(password));
47        }
48        let resp = req.send().await?;
49
50        let status = resp.status();
51        if !status.is_success() {
52            let body = resp.text().await.unwrap_or_default();
53            return Err(error::status_error(status, &url, &body));
54        }
55
56        Ok(resp.json().await?)
57    }
58
59    pub fn info(&self) -> &DeviceInfo {
60        &self.info
61    }
62
63    pub async fn status(&self) -> Result<DeviceStatus> {
64        let status = self.get_json("/status").await?;
65        Ok(DeviceStatus::from_gen1(&status))
66    }
67
68    pub async fn switch_status(&self, id: u8) -> Result<SwitchStatus> {
69        let status = self.get_json("/status").await?;
70
71        let relays = status
72            .get("relays")
73            .and_then(|v| v.as_array())
74            .ok_or_else(|| Error::Parse {
75                message: "no relays in status".to_string(),
76            })?;
77
78        let relay = relays.get(id as usize).ok_or_else(|| Error::Parse {
79            message: format!("relay {id} not found"),
80        })?;
81
82        let meter = status
83            .get("meters")
84            .and_then(|v| v.as_array())
85            .and_then(|m| m.get(id as usize));
86
87        Ok(SwitchStatus::from_gen1_relay_json(id, relay, meter))
88    }
89
90    pub async fn switch_set(&self, id: u8, on: bool) -> Result<SwitchResult> {
91        let turn = if on { "on" } else { "off" };
92        let resp = self.get_json(&format!("/relay/{id}?turn={turn}")).await?;
93
94        let was_on = resp.get("ison").and_then(|v| v.as_bool()).unwrap_or(false);
95
96        Ok(SwitchResult { was_on })
97    }
98
99    pub async fn switch_toggle(&self, id: u8) -> Result<SwitchResult> {
100        let resp = self.get_json(&format!("/relay/{id}?turn=toggle")).await?;
101
102        let was_on = resp.get("ison").and_then(|v| v.as_bool()).unwrap_or(false);
103
104        Ok(SwitchResult { was_on })
105    }
106
107    pub async fn power(&self, id: u8) -> Result<PowerReading> {
108        let status = self.get_json("/status").await?;
109
110        let meter = status
111            .get("meters")
112            .and_then(|v| v.as_array())
113            .and_then(|m| m.get(id as usize))
114            .ok_or_else(|| Error::Parse {
115                message: format!("meter {id} not found"),
116            })?;
117
118        let power = meter.get("power").and_then(|v| v.as_f64()).unwrap_or(0.0);
119        let total = meter.get("total").and_then(|v| v.as_f64()).unwrap_or(0.0);
120
121        let voltage = status.get("voltage").and_then(|v| v.as_f64());
122
123        Ok(PowerReading {
124            id,
125            power_watts: power,
126            voltage,
127            current: None,
128            total_energy_wh: total,
129        })
130    }
131
132    pub async fn firmware_check(&self) -> Result<FirmwareInfo> {
133        let status = self.get_json("/status").await?;
134
135        let update = status.get("update").ok_or_else(|| Error::Parse {
136            message: "no update info in status".to_string(),
137        })?;
138
139        let has_update = update
140            .get("has_update")
141            .and_then(|v| v.as_bool())
142            .unwrap_or(false);
143
144        let current = update
145            .get("old_version")
146            .and_then(|v| v.as_str())
147            .unwrap_or("unknown")
148            .to_string();
149
150        let stable = update
151            .get("new_version")
152            .and_then(|v| v.as_str())
153            .map(String::from);
154
155        let beta = update
156            .get("beta_version")
157            .and_then(|v| v.as_str())
158            .map(String::from);
159
160        Ok(FirmwareInfo {
161            current_version: current,
162            has_update,
163            stable_version: stable,
164            beta_version: beta,
165        })
166    }
167
168    pub async fn config_get(&self) -> Result<serde_json::Value> {
169        self.get_json("/settings").await
170    }
171
172    pub async fn reboot(&self) -> Result<()> {
173        self.get_json("/reboot").await?;
174        Ok(())
175    }
176
177    pub async fn firmware_update(&self) -> Result<()> {
178        self.get_json("/ota?update=true").await?;
179        Ok(())
180    }
181
182    pub async fn config_set(&self, key: &str, value: &str) -> Result<serde_json::Value> {
183        let param = match key {
184            "name" => ("name", value.to_string()),
185            "eco_mode" => ("eco_mode_enabled", value.to_string()),
186            "led_status_disable" => ("led_status_disable", value.to_string()),
187            _ => {
188                return Err(Error::Unsupported {
189                    message: format!(
190                        "unknown config key '{key}'. Supported keys: name, eco_mode, led_status_disable"
191                    ),
192                });
193            }
194        };
195        self.get_json(&format!("/settings?{}={}", param.0, param.1))
196            .await
197    }
198
199    pub async fn schedule_list(&self) -> Result<serde_json::Value> {
200        Err(Error::Unsupported {
201            message: "schedules are not supported on Gen1 devices".to_string(),
202        })
203    }
204
205    pub async fn webhook_list(&self) -> Result<serde_json::Value> {
206        let settings = self.get_json("/settings").await?;
207        Ok(settings
208            .get("actions")
209            .cloned()
210            .unwrap_or(serde_json::json!({})))
211    }
212
213    pub async fn config_restore(&self, config: &serde_json::Value) -> Result<()> {
214        // For Gen1, restore only safe top-level settings
215        // Skip network/WiFi/MQTT/cloud settings
216        const SKIP_KEYS: &[&str] = &[
217            "wifi_ap",
218            "wifi_sta",
219            "wifi_sta1",
220            "mqtt",
221            "coiot",
222            "sntp",
223            "login",
224            "ap_roaming",
225        ];
226
227        let obj = config.as_object().ok_or_else(|| Error::Parse {
228            message: "config must be a JSON object".to_string(),
229        })?;
230
231        let mut params = Vec::new();
232        for (key, value) in obj {
233            if SKIP_KEYS.contains(&key.as_str()) {
234                continue;
235            }
236
237            // Only restore simple scalar values via /settings?key=value
238            match value {
239                serde_json::Value::String(s) => params.push(format!("{key}={s}")),
240                serde_json::Value::Bool(b) => params.push(format!("{key}={b}")),
241                serde_json::Value::Number(n) => params.push(format!("{key}={n}")),
242                _ => continue,
243            }
244        }
245
246        if !params.is_empty() {
247            let query = params.join("&");
248            self.get_json(&format!("/settings?{query}")).await?;
249        }
250
251        Ok(())
252    }
253
254    pub async fn set_name(&self, name: &str) -> Result<()> {
255        let url = self.url("/settings");
256        let mut req = self.client.get(&url).query(&[("name", name)]);
257        if let Some(ref password) = self.password {
258            req = req.basic_auth("admin", Some(password));
259        }
260        let resp = req.send().await?;
261
262        let status = resp.status();
263        if !status.is_success() {
264            let body = resp.text().await.unwrap_or_default();
265            return Err(error::status_error(status, &url, &body));
266        }
267        Ok(())
268    }
269}