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