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 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 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 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}