watchpower_api/
lib.rs

1use chrono::{DateTime, NaiveDate, NaiveDateTime};
2use reqwest::blocking::Client;
3use serde::Serialize;
4use sha1::{Digest, Sha1};
5use std::time::{SystemTime, UNIX_EPOCH};
6
7type WatchPowerAPIResult = Result<serde_json::Value, Box<dyn std::error::Error>>;
8
9#[derive(Debug, Serialize, Clone)]
10struct WatchPowerDailyData {
11    // TODO
12}
13
14impl WatchPowerDailyData {
15    fn from_json(json: &serde_json::Value) -> Self {
16        todo!()
17    }
18}
19
20#[derive(Debug, Serialize, Clone)]
21struct WatchPowerDeviceParams {
22    serial_number: String,
23    wifi_pn: String,
24    dev_code: i32,
25    dev_addr: i32,
26}
27
28#[derive(Debug, Serialize, Clone)]
29struct WatchPowerFlowData {
30    grid_voltage: f32,
31    grid_frequency: f32,
32    ac_output_voltage: f32,
33    ac_output_active_power: i32,
34    output_load_percent: i8,
35    battery_capacity: i8,
36    battery_voltage: f32,
37    pv_input_voltage: f32,
38    pv_input_power: f32,
39}
40
41impl WatchPowerFlowData {
42    fn from_json(json: &serde_json::Value) -> Self {
43        todo!()
44    }
45}
46
47#[derive(Debug, Serialize, Clone)]
48pub struct WatchPowerLastDataGrid {
49    pub grid_rating_voltage: f32,
50    pub grid_rating_current: f32,
51    pub battery_rating_voltage: f32,
52    pub ac_output_rating_voltage: f32,
53    pub ac_output_rating_current: f32,
54    pub ac_output_rating_frequency: f32,
55    pub ac_output_rating_apparent_power: i32,
56    pub ac_output_rating_active_power: i32,
57}
58
59impl WatchPowerLastDataGrid {
60    fn from_json(json: &serde_json::Value) -> Self {
61        let mut grid_rating_voltage = None;
62        let mut grid_rating_current = None;
63        let mut battery_rating_voltage = None;
64        let mut ac_output_rating_voltage = None;
65        let mut ac_output_rating_current = None;
66        let mut ac_output_rating_frequency = None;
67        let mut ac_output_rating_apparent_power = None;
68        let mut ac_output_rating_active_power = None;
69
70        for field in json.as_array().unwrap() {
71            match field["id"].as_str().unwrap() {
72                "gd_grid_rating_voltage" => {
73                    grid_rating_voltage =
74                        Some(field["val"].as_str().unwrap().parse::<f32>().unwrap())
75                }
76                "gd_grid_rating_current" => {
77                    grid_rating_current =
78                        Some(field["val"].as_str().unwrap().parse::<f32>().unwrap())
79                }
80                "gd_battery_rating_voltage" => {
81                    battery_rating_voltage =
82                        Some(field["val"].as_str().unwrap().parse::<f32>().unwrap())
83                }
84                "gd_bse_input_voltage_read" => {
85                    ac_output_rating_voltage =
86                        Some(field["val"].as_str().unwrap().parse::<f32>().unwrap())
87                }
88                "gd_ac_output_rating_current" => {
89                    ac_output_rating_current =
90                        Some(field["val"].as_str().unwrap().parse::<f32>().unwrap())
91                }
92                "gd_bse_output_frequency_read" => {
93                    ac_output_rating_frequency =
94                        Some(field["val"].as_str().unwrap().parse::<f32>().unwrap())
95                }
96                "gd_ac_output_rating_apparent_power" => {
97                    ac_output_rating_apparent_power =
98                        Some(field["val"].as_str().unwrap().parse::<i32>().unwrap())
99                }
100                "gd_ac_output_rating_active_power" => {
101                    ac_output_rating_active_power =
102                        Some(field["val"].as_str().unwrap().parse::<i32>().unwrap())
103                }
104                _ => continue,
105            }
106        }
107        WatchPowerLastDataGrid {
108            grid_rating_voltage: grid_rating_voltage.expect("Grid rating voltage not found"),
109            grid_rating_current: grid_rating_current.expect("Grid rating current not found"),
110            battery_rating_voltage: battery_rating_voltage
111                .expect("Battery rating voltage not found"),
112            ac_output_rating_voltage: ac_output_rating_voltage
113                .expect("AC output rating voltage not found"),
114            ac_output_rating_current: ac_output_rating_current
115                .expect("AC output rating current not found"),
116            ac_output_rating_frequency: ac_output_rating_frequency
117                .expect("AC output rating frequency not found"),
118            ac_output_rating_apparent_power: ac_output_rating_apparent_power
119                .expect("AC output rating apparent power not found"),
120            ac_output_rating_active_power: ac_output_rating_active_power
121                .expect("AC output rating active power not found"),
122        }
123    }
124}
125
126#[derive(Debug, Serialize, Clone)]
127pub struct WatchPowerLastDataSystem {
128    pub model: String,
129    pub main_cpu_firmware_version: String,
130    pub secondary_cpu_firmware_version: String,
131}
132
133impl WatchPowerLastDataSystem {
134    fn from_json(json: &serde_json::Value) -> Self {
135        let mut model = None;
136        let mut main_cpu_firmware_version = None;
137        let mut secondary_cpu_firmware_version = None;
138
139        for field in json.as_array().unwrap() {
140            match field["id"].as_str().unwrap() {
141                "sy_model" => model = Some(field["val"].as_str().unwrap().to_owned()),
142                "sy_main_cpu1_firmware_version" => {
143                    main_cpu_firmware_version = Some(field["val"].as_str().unwrap().to_owned())
144                }
145                "sy_main_cpu2_firmware_version" => {
146                    secondary_cpu_firmware_version = Some(field["val"].as_str().unwrap().to_owned())
147                }
148                _ => continue,
149            }
150        }
151        WatchPowerLastDataSystem {
152            model: model.expect("Model not found"),
153            main_cpu_firmware_version: main_cpu_firmware_version
154                .expect("Main CPU firmware version not found"),
155            secondary_cpu_firmware_version: secondary_cpu_firmware_version
156                .expect("Secondary CPU firmware version not found"),
157        }
158    }
159}
160
161#[derive(Debug, Serialize, Clone)]
162pub struct WatchPowerLastDataPV {
163    pub pv_input_current: f32,
164}
165
166impl WatchPowerLastDataPV {
167    fn from_json(json: &serde_json::Value) -> Self {
168        let mut pv_input_current = None;
169        for field in json.as_array().unwrap() {
170            match field["id"].as_str().unwrap() {
171                "pv_input_current" => {
172                    pv_input_current = Some(field["val"].as_str().unwrap().parse::<f32>().unwrap())
173                }
174                _ => continue,
175            }
176        }
177        WatchPowerLastDataPV {
178            pv_input_current: pv_input_current.expect("PV input current not found"),
179        }
180    }
181}
182
183#[derive(Debug, Serialize, Clone)]
184pub struct WatchPowerLastDataMain {
185    pub grid_voltage: f32,
186    pub grid_frequency: f32,
187    pub pv_input_voltage: f32,
188    pub pv_input_power: i16,
189    pub battery_voltage: f32,
190    pub battery_capacity: i8,
191    pub battery_charging_current: f32,
192    pub battery_discharge_current: f32,
193    pub ac_output_voltage: f32,
194    pub ac_output_frequency: f32,
195    pub ac_output_apparent_power: i32,
196    pub ac_output_active_power: i32,
197    pub output_load_percent: i8,
198}
199
200impl WatchPowerLastDataMain {
201    fn from_json(json: &serde_json::Value) -> Self {
202        let mut grid_voltage = None;
203        let mut grid_frequency = None;
204        let mut pv_input_voltage = None;
205        let mut pv_input_power = None;
206        let mut battery_voltage = None;
207        let mut battery_capacity = None;
208        let mut battery_charging_current = None;
209        let mut battery_discharge_current = None;
210        let mut ac_output_voltage = None;
211        let mut ac_output_frequency = None;
212        let mut ac_output_apparent_power = None;
213        let mut ac_output_active_power = None;
214        let mut output_load_percent = None;
215        for field in json.as_array().unwrap() {
216            match field["id"].as_str().unwrap() {
217                "bt_grid_voltage" => {
218                    grid_voltage = Some(field["val"].as_str().unwrap().parse::<f32>().unwrap())
219                }
220                "bt_grid_frequency" => {
221                    grid_frequency = Some(field["val"].as_str().unwrap().parse::<f32>().unwrap())
222                }
223                "bt_voltage_1" => {
224                    pv_input_voltage = Some(field["val"].as_str().unwrap().parse::<f32>().unwrap())
225                }
226                "bt_input_power" => {
227                    pv_input_power = Some(field["val"].as_str().unwrap().parse::<i16>().unwrap())
228                }
229                "bt_battery_voltage" => {
230                    battery_voltage = Some(field["val"].as_str().unwrap().parse::<f32>().unwrap())
231                }
232                "bt_battery_capacity" => {
233                    battery_capacity = Some(field["val"].as_str().unwrap().parse::<i8>().unwrap())
234                }
235                "bt_battery_charging_current" => {
236                    battery_charging_current =
237                        Some(field["val"].as_str().unwrap().parse::<f32>().unwrap())
238                }
239                "bt_battery_discharge_current" => {
240                    battery_discharge_current =
241                        Some(field["val"].as_str().unwrap().parse::<f32>().unwrap())
242                }
243                "bt_ac_output_voltage" => {
244                    ac_output_voltage = Some(field["val"].as_str().unwrap().parse::<f32>().unwrap())
245                }
246                "bt_grid_AC_frequency" => {
247                    ac_output_frequency =
248                        Some(field["val"].as_str().unwrap().parse::<f32>().unwrap())
249                }
250                "bt_ac_output_apparent_power" => {
251                    ac_output_apparent_power =
252                        Some(field["val"].as_str().unwrap().parse::<i32>().unwrap())
253                }
254                "bt_load_active_power_sole" => {
255                    ac_output_active_power =
256                        Some(field["val"].as_str().unwrap().parse::<i32>().unwrap())
257                }
258                "bt_output_load_percent" => {
259                    output_load_percent =
260                        Some(field["val"].as_str().unwrap().parse::<i8>().unwrap())
261                }
262                _ => continue,
263            }
264        }
265        WatchPowerLastDataMain {
266            grid_voltage: grid_voltage.expect("Grid voltage not found"),
267            grid_frequency: grid_frequency.expect("Grid frequency not found"),
268            pv_input_voltage: pv_input_voltage.expect("PV input voltage not found"),
269            pv_input_power: pv_input_power.expect("PV input power not found"),
270            battery_voltage: battery_voltage.expect("Battery voltage not found"),
271            battery_capacity: battery_capacity.expect("Battery capacity not found"),
272            battery_charging_current: battery_charging_current
273                .expect("Battery charging current not found"),
274            battery_discharge_current: battery_discharge_current
275                .expect("Battery discharge current not found"),
276            ac_output_voltage: ac_output_voltage.expect("AC output voltage not found"),
277            ac_output_frequency: ac_output_frequency.expect("AC output frequency not found"),
278            ac_output_apparent_power: ac_output_apparent_power
279                .expect("AC output apparent power not found"),
280            ac_output_active_power: ac_output_active_power
281                .expect("AC output active power not found"),
282            output_load_percent: output_load_percent.expect("Output load percent not found"),
283        }
284    }
285}
286
287#[derive(Debug, Serialize, Clone)]
288pub struct WatchPowerLastData {
289    pub timestamp: NaiveDateTime,
290    pub grid: WatchPowerLastDataGrid,
291    pub system: WatchPowerLastDataSystem,
292    pub pv: WatchPowerLastDataPV,
293    pub main: WatchPowerLastDataMain,
294}
295
296impl WatchPowerLastData {
297    fn from_json(json: &serde_json::Value) -> Self {
298        let dat_field = &json["dat"];
299        let pars_field = &dat_field["pars"];
300        WatchPowerLastData {
301            timestamp: DateTime::from_timestamp_millis(
302                dat_field["gts"].as_str().unwrap().parse::<i64>().unwrap(),
303            )
304            .unwrap()
305            .naive_local(),
306            grid: WatchPowerLastDataGrid::from_json(&pars_field["gd_"]),
307            system: WatchPowerLastDataSystem::from_json(&pars_field["sy_"]),
308            pv: WatchPowerLastDataPV::from_json(&pars_field["pv_"]),
309            main: WatchPowerLastDataMain::from_json(&pars_field["bt_"]),
310        }
311    }
312}
313
314#[derive(Debug, Clone)]
315pub struct WatchPowerAPI {
316    _base_url: String,
317    _suffix_context: String,
318    _company_key: String,
319    _token: Option<String>,
320    _secret: String,
321    _expire: Option<u64>,
322    _client: Client,
323    _device_params: WatchPowerDeviceParams,
324}
325
326impl WatchPowerAPI {
327    pub fn new(serial_number: &str, wifi_pn: &str, dev_code: i32, dev_addr: i32) -> Self {
328        WatchPowerAPI {
329            _base_url: "http://android.shinemonitor.com/public/".to_string(),
330            _suffix_context: "&i18n=pt_BR&lang=pt_BR&source=1&_app_client_=android&_app_id_=wifiapp.volfw.watchpower&_app_version_=1.0.6.3".to_string(),
331            _company_key: "bnrl_frRFjEz8Mkn".to_string(),
332            _token: None,
333            _secret: "ems_secret".to_string(),
334            _expire: None,
335            _client: Client::new(),
336            _device_params: WatchPowerDeviceParams{serial_number: serial_number.to_string(), wifi_pn: wifi_pn.to_string(), dev_code: dev_code, dev_addr: dev_addr},
337        }
338    }
339
340    fn generate_salt() -> String {
341        let start = SystemTime::now();
342        let since_the_epoch = start
343            .duration_since(UNIX_EPOCH)
344            .expect("Time went backwards");
345        (since_the_epoch.as_millis()).to_string()
346    }
347
348    fn sha1_str_lower_case(input: &[u8]) -> String {
349        let mut hasher = Sha1::new();
350        hasher.update(input);
351        format!("{:x}", hasher.finalize())
352    }
353
354    fn hash(&self, args: Vec<&str>) -> String {
355        let arg_concat = args.join("");
356        WatchPowerAPI::sha1_str_lower_case(arg_concat.as_bytes())
357    }
358
359    pub fn login(
360        &mut self,
361        username: &str,
362        password: &str,
363    ) -> Result<(), Box<dyn std::error::Error>> {
364        let base_action = format!(
365            "&action=authSource&usr={}&company-key={}{}",
366            username, self._company_key, self._suffix_context
367        );
368
369        let salt = WatchPowerAPI::generate_salt();
370        let password_hash = self.hash(vec![password]);
371        let sign = self.hash(vec![&salt, &password_hash, &base_action]);
372
373        let url = format!(
374            "{}?sign={}&salt={}{}",
375            self._base_url, sign, salt, base_action
376        );
377
378        let response: serde_json::Value = self._client.get(&url).send()?.json()?;
379
380        if response["err"].as_u64() == Some(0) {
381            self._secret = response["dat"]["secret"].as_str().unwrap().to_string();
382            self._token = Some(response["dat"]["token"].as_str().unwrap().to_string());
383            self._expire = Some(response["dat"]["expire"].as_u64().unwrap());
384            Ok(())
385        } else {
386            Err(Box::new(std::io::Error::new(
387                std::io::ErrorKind::Other,
388                format!("Login error: {:?}", response),
389            )))
390        }
391    }
392
393    fn _request(&self, action: &str, query: Option<&str>) -> WatchPowerAPIResult {
394        let base_action = format!(
395            "&action={}&pn={}&devcode={}&sn={}&devaddr={}{}{}",
396            action,
397            self._device_params.wifi_pn,
398            self._device_params.dev_code,
399            self._device_params.serial_number,
400            self._device_params.dev_addr,
401            query.unwrap_or(""),
402            self._suffix_context
403        );
404        let salt = WatchPowerAPI::generate_salt();
405        let sign = self.hash(vec![
406            &salt,
407            &self._secret,
408            self._token.as_ref().unwrap(),
409            &base_action,
410        ]);
411        let auth = format!(
412            "?sign={}&salt={}&token={}",
413            sign,
414            salt,
415            self._token.as_ref().unwrap()
416        );
417        let url = format!("{}{}{}", self._base_url, auth, base_action);
418
419        let response: serde_json::Value = self._client.get(&url).send()?.json()?;
420
421        if response["err"] == 0 {
422            Ok(response)
423        } else {
424            Err(Box::new(std::io::Error::new(
425                std::io::ErrorKind::Other,
426                format!("API error: {:?}", response),
427            )))
428        }
429    }
430
431    fn get_daily_data(
432        &self,
433        day: NaiveDate,
434    ) -> Result<WatchPowerDailyData, Box<dyn std::error::Error>> {
435        let _date = day.format("%Y-%m-%d").to_string();
436        let query = format!("&date={}", _date);
437        match self._request("queryDeviceDataOneDay", Some(&query)) {
438            Ok(raw) => Ok(WatchPowerDailyData::from_json(&raw)),
439            Err(e) => Err(e),
440        }
441    }
442
443    fn get_power_flow(&self) -> Result<WatchPowerFlowData, Box<dyn std::error::Error>> {
444        match self._request("queryDeviceFlowPower", None) {
445            Ok(raw) => Ok(WatchPowerFlowData::from_json(&raw)),
446            Err(e) => Err(e),
447        }
448    }
449
450    pub fn get_last_data(&self) -> Result<WatchPowerLastData, Box<dyn std::error::Error>> {
451        match self._request("querySPDeviceLastData", None) {
452            Ok(raw) => Ok(WatchPowerLastData::from_json(&raw)),
453            Err(e) => Err(e),
454        }
455    }
456}