1use chrono::{NaiveDate, NaiveDateTime};
2use reqwest::blocking::Client;
3use serde::{Deserialize, 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, Clone, Serialize, Deserialize)]
85pub struct DeviceIdentifier {
86 #[serde(alias = "devalias")]
87 pub device_alias: Option<String>,
88 #[serde(alias = "sn")]
89 pub serial_number: String,
90 #[serde(alias = "pn")]
91 pub wifi_pin: String,
92 #[serde(alias = "devaddr")]
93 pub device_address: i64,
94 #[serde(alias = "devcode")]
95 pub device_code: i64,
96}
97
98#[derive(Debug, Clone, PartialEq)]
103pub struct AppProfile {
104 pub app_id: &'static str,
105 pub app_version: &'static str,
106 pub company_key: &'static str,
107 pub base_url: &'static str,
108 pub locale: &'static str,
109 pub source: i32,
110 pub app_client: &'static str,
111}
112
113impl AppProfile {
114 pub fn suffix_context(&self) -> String {
116 format!(
117 "&i18n={}&lang={}&source={}&_app_client_={}&_app_id_={}&_app_version_={}",
118 self.locale, self.locale, self.source, self.app_client, self.app_id, self.app_version
119 )
120 }
121
122 pub const WATCHPOWER: AppProfile = AppProfile {
124 app_id: "wifiapp.volfw.watchpower",
125 app_version: "1.0.6.3",
126 company_key: "bnrl_frRFjEz8Mkn",
127 base_url: "http://android.shinemonitor.com/public/",
128 locale: "pt_BR",
129 source: 1,
130 app_client: "android",
131 };
132
133 pub const RENOCLIENT: AppProfile = AppProfile {
135 app_id: "com.eybond.renoclient",
136 app_version: "1.3.2.0",
137 company_key: "bnrl_frRFjEz8Mkn",
138 base_url: "http://android.shinemonitor.com/public/",
139 locale: "pt_BR",
140 source: 1,
141 app_client: "android",
142 };
143}
144
145pub const KNOWN_APPS: &[AppProfile] = &[AppProfile::WATCHPOWER, AppProfile::RENOCLIENT];
149
150#[derive(Debug, Serialize, Clone)]
151pub struct ShineMonitorLastDataGrid {
152 pub grid_rating_voltage: f32,
153 pub grid_rating_current: f32,
154 pub battery_rating_voltage: f32,
155 pub ac_output_rating_voltage: f32,
156 pub ac_output_rating_current: f32,
157 pub ac_output_rating_frequency: f32,
158 pub ac_output_rating_apparent_power: i32,
159 pub ac_output_rating_active_power: i32,
160}
161
162impl ShineMonitorLastDataGrid {
163 fn from_json(json: &serde_json::Value) -> Self {
164 let mut grid_rating_voltage = None;
165 let mut grid_rating_current = None;
166 let mut battery_rating_voltage = None;
167 let mut ac_output_rating_voltage = None;
168 let mut ac_output_rating_current = None;
169 let mut ac_output_rating_frequency = None;
170 let mut ac_output_rating_apparent_power = None;
171 let mut ac_output_rating_active_power = None;
172
173 for field in json.as_array().unwrap() {
174 match field["id"].as_str().unwrap() {
175 "gd_grid_rating_voltage" => {
176 grid_rating_voltage =
177 Some(field["val"].as_str().unwrap().parse::<f32>().unwrap())
178 }
179 "gd_grid_rating_current" => {
180 grid_rating_current =
181 Some(field["val"].as_str().unwrap().parse::<f32>().unwrap())
182 }
183 "gd_battery_rating_voltage" => {
184 battery_rating_voltage =
185 Some(field["val"].as_str().unwrap().parse::<f32>().unwrap())
186 }
187 "gd_bse_input_voltage_read" => {
188 ac_output_rating_voltage =
189 Some(field["val"].as_str().unwrap().parse::<f32>().unwrap())
190 }
191 "gd_ac_output_rating_current" => {
192 ac_output_rating_current =
193 Some(field["val"].as_str().unwrap().parse::<f32>().unwrap())
194 }
195 "gd_bse_output_frequency_read" => {
196 ac_output_rating_frequency =
197 Some(field["val"].as_str().unwrap().parse::<f32>().unwrap())
198 }
199 "gd_ac_output_rating_apparent_power" => {
200 ac_output_rating_apparent_power =
201 Some(field["val"].as_str().unwrap().parse::<i32>().unwrap())
202 }
203 "gd_ac_output_rating_active_power" => {
204 ac_output_rating_active_power =
205 Some(field["val"].as_str().unwrap().parse::<i32>().unwrap())
206 }
207 _ => continue,
208 }
209 }
210 ShineMonitorLastDataGrid {
211 grid_rating_voltage: grid_rating_voltage.expect("Grid rating voltage not found"),
212 grid_rating_current: grid_rating_current.expect("Grid rating current not found"),
213 battery_rating_voltage: battery_rating_voltage
214 .expect("Battery rating voltage not found"),
215 ac_output_rating_voltage: ac_output_rating_voltage
216 .expect("AC output rating voltage not found"),
217 ac_output_rating_current: ac_output_rating_current
218 .expect("AC output rating current not found"),
219 ac_output_rating_frequency: ac_output_rating_frequency
220 .expect("AC output rating frequency not found"),
221 ac_output_rating_apparent_power: ac_output_rating_apparent_power
222 .expect("AC output rating apparent power not found"),
223 ac_output_rating_active_power: ac_output_rating_active_power
224 .expect("AC output rating active power not found"),
225 }
226 }
227}
228
229#[derive(Debug, Serialize, Clone)]
230pub struct ShineMonitorLastDataSystem {
231 pub model: String,
232 pub main_cpu_firmware_version: String,
233 pub secondary_cpu_firmware_version: String,
234}
235
236impl ShineMonitorLastDataSystem {
237 fn from_json(json: &serde_json::Value) -> Self {
238 let mut model = None;
239 let mut main_cpu_firmware_version = None;
240 let mut secondary_cpu_firmware_version = None;
241
242 for field in json.as_array().unwrap() {
243 match field["id"].as_str().unwrap() {
244 "sy_model" => model = Some(field["val"].as_str().unwrap().to_owned()),
245 "sy_main_cpu1_firmware_version" => {
246 main_cpu_firmware_version = Some(field["val"].as_str().unwrap().to_owned())
247 }
248 "sy_main_cpu2_firmware_version" => {
249 secondary_cpu_firmware_version = Some(field["val"].as_str().unwrap().to_owned())
250 }
251 _ => continue,
252 }
253 }
254 ShineMonitorLastDataSystem {
255 model: model.expect("Model not found"),
256 main_cpu_firmware_version: main_cpu_firmware_version
257 .expect("Main CPU firmware version not found"),
258 secondary_cpu_firmware_version: secondary_cpu_firmware_version
259 .expect("Secondary CPU firmware version not found"),
260 }
261 }
262}
263
264#[derive(Debug, Serialize, Clone)]
265pub struct ShineMonitorLastDataPV {
266 pub pv_input_current: f32,
267}
268
269impl ShineMonitorLastDataPV {
270 fn from_json(json: &serde_json::Value) -> Self {
271 let mut pv_input_current = None;
272 for field in json.as_array().unwrap() {
273 match field["id"].as_str().unwrap() {
274 "pv_input_current" => {
275 pv_input_current = Some(field["val"].as_str().unwrap().parse::<f32>().unwrap())
276 }
277 _ => continue,
278 }
279 }
280 ShineMonitorLastDataPV {
281 pv_input_current: pv_input_current.expect("PV input current not found"),
282 }
283 }
284}
285
286#[derive(Debug, Serialize, Clone)]
287pub struct ShineMonitorLastDataMain {
288 pub grid_voltage: f32,
289 pub grid_frequency: f32,
290 pub pv_input_voltage: f32,
291 pub pv_input_power: i16,
292 pub battery_voltage: f32,
293 pub battery_capacity: i8,
294 pub battery_charging_current: f32,
295 pub battery_discharge_current: f32,
296 pub ac_output_voltage: f32,
297 pub ac_output_frequency: f32,
298 pub ac_output_apparent_power: i32,
299 pub ac_output_active_power: i32,
300 pub output_load_percent: i8,
301}
302
303impl ShineMonitorLastDataMain {
304 fn from_json(json: &serde_json::Value) -> Self {
305 let mut grid_voltage = None;
306 let mut grid_frequency = None;
307 let mut pv_input_voltage = None;
308 let mut pv_input_power = None;
309 let mut battery_voltage = None;
310 let mut battery_capacity = None;
311 let mut battery_charging_current = None;
312 let mut battery_discharge_current = None;
313 let mut ac_output_voltage = None;
314 let mut ac_output_frequency = None;
315 let mut ac_output_apparent_power = None;
316 let mut ac_output_active_power = None;
317 let mut output_load_percent = None;
318 for field in json.as_array().unwrap() {
319 match field["id"].as_str().unwrap() {
320 "bt_grid_voltage" => {
321 grid_voltage = Some(field["val"].as_str().unwrap().parse::<f32>().unwrap())
322 }
323 "bt_grid_frequency" => {
324 grid_frequency = Some(field["val"].as_str().unwrap().parse::<f32>().unwrap())
325 }
326 "bt_voltage_1" => {
327 pv_input_voltage = Some(field["val"].as_str().unwrap().parse::<f32>().unwrap())
328 }
329 "bt_input_power" => {
330 pv_input_power = Some(field["val"].as_str().unwrap().parse::<i16>().unwrap())
331 }
332 "bt_battery_voltage" => {
333 battery_voltage = Some(field["val"].as_str().unwrap().parse::<f32>().unwrap())
334 }
335 "bt_battery_capacity" => {
336 battery_capacity = Some(field["val"].as_str().unwrap().parse::<i8>().unwrap())
337 }
338 "bt_battery_charging_current" => {
339 battery_charging_current =
340 Some(field["val"].as_str().unwrap().parse::<f32>().unwrap())
341 }
342 "bt_battery_discharge_current" => {
343 battery_discharge_current =
344 Some(field["val"].as_str().unwrap().parse::<f32>().unwrap())
345 }
346 "bt_ac_output_voltage" => {
347 ac_output_voltage = Some(field["val"].as_str().unwrap().parse::<f32>().unwrap())
348 }
349 "bt_grid_AC_frequency" => {
350 ac_output_frequency =
351 Some(field["val"].as_str().unwrap().parse::<f32>().unwrap())
352 }
353 "bt_ac_output_apparent_power" => {
354 ac_output_apparent_power =
355 Some(field["val"].as_str().unwrap().parse::<i32>().unwrap())
356 }
357 "bt_load_active_power_sole" => {
358 ac_output_active_power =
359 Some(field["val"].as_str().unwrap().parse::<i32>().unwrap())
360 }
361 "bt_output_load_percent" => {
362 output_load_percent =
363 Some(field["val"].as_str().unwrap().parse::<i8>().unwrap())
364 }
365 _ => continue,
366 }
367 }
368 ShineMonitorLastDataMain {
369 grid_voltage: grid_voltage.expect("Grid voltage not found"),
370 grid_frequency: grid_frequency.expect("Grid frequency not found"),
371 pv_input_voltage: pv_input_voltage.expect("PV input voltage not found"),
372 pv_input_power: pv_input_power.expect("PV input power not found"),
373 battery_voltage: battery_voltage.expect("Battery voltage not found"),
374 battery_capacity: battery_capacity.expect("Battery capacity not found"),
375 battery_charging_current: battery_charging_current
376 .expect("Battery charging current not found"),
377 battery_discharge_current: battery_discharge_current
378 .expect("Battery discharge current not found"),
379 ac_output_voltage: ac_output_voltage.expect("AC output voltage not found"),
380 ac_output_frequency: ac_output_frequency.expect("AC output frequency not found"),
381 ac_output_apparent_power: ac_output_apparent_power
382 .expect("AC output apparent power not found"),
383 ac_output_active_power: ac_output_active_power
384 .expect("AC output active power not found"),
385 output_load_percent: output_load_percent.expect("Output load percent not found"),
386 }
387 }
388}
389
390#[derive(Debug, Serialize, Clone)]
391pub struct ShineMonitorLastData {
392 pub timestamp: NaiveDateTime,
393 pub grid: ShineMonitorLastDataGrid,
394 pub system: ShineMonitorLastDataSystem,
395 pub pv: ShineMonitorLastDataPV,
396 pub main: ShineMonitorLastDataMain,
397}
398
399impl ShineMonitorLastData {
400 fn from_json(json: &serde_json::Value) -> Self {
401 let dat_field = &json["dat"];
402 let pars_field = &dat_field["pars"];
403 ShineMonitorLastData {
404 timestamp: parse_gts(&dat_field["gts"]),
405 grid: ShineMonitorLastDataGrid::from_json(&pars_field["gd_"]),
406 system: ShineMonitorLastDataSystem::from_json(&pars_field["sy_"]),
407 pv: ShineMonitorLastDataPV::from_json(&pars_field["pv_"]),
408 main: ShineMonitorLastDataMain::from_json(&pars_field["bt_"]),
409 }
410 }
411}
412
413fn parse_gts(value: &serde_json::Value) -> NaiveDateTime {
416 if let Some(raw) = value.as_str() {
417 let trimmed = raw.trim();
418 if let Ok(ms) = trimmed.parse::<i64>() {
419 return chrono::DateTime::from_timestamp_millis(ms)
420 .expect("valid epoch ms")
421 .naive_utc();
422 }
423 return NaiveDateTime::parse_from_str(trimmed, "%Y-%m-%d %H:%M:%S")
424 .expect("valid gts string");
425 }
426 if let Some(ms) = value.as_i64() {
427 return chrono::DateTime::from_timestamp_millis(ms)
428 .expect("valid epoch ms")
429 .naive_utc();
430 }
431 panic!("unexpected gts value: {value:?}");
432}
433
434#[derive(Debug, Clone)]
435pub struct ShineMonitorAPI {
436 _base_url: String,
437 _suffix_context: String,
438 _company_key: String,
439 _app_profile: AppProfile,
440 _token: Option<String>,
441 _secret: String,
442 _expire: Option<u64>,
443 _client: Client,
444 _device_params: ShineMonitorDeviceParams,
445}
446
447impl ShineMonitorAPI {
448 pub fn new(serial_number: &str, wifi_pn: &str, dev_code: i32, dev_addr: i32) -> Self {
449 ShineMonitorAPI {
450 _base_url: AppProfile::WATCHPOWER.base_url.to_string(),
451 _suffix_context: AppProfile::WATCHPOWER.suffix_context(),
452 _company_key: AppProfile::WATCHPOWER.company_key.to_string(),
453 _app_profile: AppProfile::WATCHPOWER,
454 _token: None,
455 _secret: "ems_secret".to_string(),
456 _expire: None,
457 _client: Client::new(),
458 _device_params: ShineMonitorDeviceParams {
459 serial_number: serial_number.to_string(),
460 wifi_pn: wifi_pn.to_string(),
461 dev_code,
462 dev_addr,
463 },
464 }
465 }
466
467 pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
468 self._base_url = base_url.into();
469 self
470 }
471
472 pub fn with_suffix_context(mut self, suffix: impl Into<String>) -> Self {
473 self._suffix_context = suffix.into();
474 self
475 }
476
477 pub fn with_company_key(mut self, key: impl Into<String>) -> Self {
478 self._company_key = key.into();
479 self
480 }
481
482 pub fn with_app_profile(mut self, profile: AppProfile) -> Self {
485 self._app_profile = profile.clone();
486 self._base_url = profile.base_url.to_string();
487 self._suffix_context = profile.suffix_context();
488 self._company_key = profile.company_key.to_string();
489 self
490 }
491
492 fn generate_salt() -> String {
493 let start = SystemTime::now();
494 let since_the_epoch = start
495 .duration_since(UNIX_EPOCH)
496 .expect("Time went backwards");
497 (since_the_epoch.as_millis()).to_string()
498 }
499
500 fn sha1_str_lower_case(input: &[u8]) -> String {
501 let mut hasher = Sha1::new();
502 hasher.update(input);
503 format!("{:x}", hasher.finalize())
504 }
505
506 fn hash(&self, args: Vec<&str>) -> String {
507 let arg_concat = args.join("");
508 ShineMonitorAPI::sha1_str_lower_case(arg_concat.as_bytes())
509 }
510
511 pub fn login(&mut self, username: &str, password: &str) -> Result<(), ApiError> {
512 let base_action = format!(
513 "&action=authSource&usr={}&company-key={}{}",
514 username, self._company_key, self._suffix_context
515 );
516
517 let salt = ShineMonitorAPI::generate_salt();
518 let password_hash = self.hash(vec![password]);
519 let sign = self.hash(vec![&salt, &password_hash, &base_action]);
520
521 let url = format!(
522 "{}?sign={}&salt={}{}",
523 self._base_url, sign, salt, base_action
524 );
525
526 let response: serde_json::Value = self._client.get(&url).send()?.json()?;
527
528 if response["err"].as_i64() == Some(0) {
529 self._secret = response["dat"]["secret"].as_str().unwrap().to_string();
530 self._token = Some(response["dat"]["token"].as_str().unwrap().to_string());
531 self._expire = Some(response["dat"]["expire"].as_u64().unwrap());
532 Ok(())
533 } else {
534 Err(ApiError::from_payload(response))
535 }
536 }
537
538 fn _request(&self, action: &str, query: Option<&str>) -> ShineMonitorAPIResult {
539 let base_action = format!(
540 "&action={}&pn={}&devcode={}&sn={}&devaddr={}{}{}",
541 action,
542 self._device_params.wifi_pn,
543 self._device_params.dev_code,
544 self._device_params.serial_number,
545 self._device_params.dev_addr,
546 query.unwrap_or(""),
547 self._suffix_context
548 );
549 self._request_raw(&base_action)
550 }
551
552 pub fn _request_with(&self, action: &str, extra: &str) -> ShineMonitorAPIResult {
556 let base_action = format!("&action={}{}{}", action, extra, self._suffix_context);
557 self._request_raw(&base_action)
558 }
559
560 fn _request_raw(&self, base_action: &str) -> ShineMonitorAPIResult {
561 let token = self
562 ._token
563 .as_ref()
564 .ok_or_else(|| ApiError::local(-1, "not logged in"))?;
565 let salt = ShineMonitorAPI::generate_salt();
566 let sign = self.hash(vec![&salt, &self._secret, token, base_action]);
567 let auth = format!("?sign={}&salt={}&token={}", sign, salt, token);
568 let url = format!("{}{}{}", self._base_url, auth, base_action);
569
570 let response: serde_json::Value = self._client.get(&url).send()?.json()?;
571
572 if response["err"].as_i64() == Some(0) {
573 Ok(response)
574 } else {
575 Err(ApiError::from_payload(response))
576 }
577 }
578
579 pub fn get_daily_data(&self, day: NaiveDate) -> Result<serde_json::Value, ApiError> {
580 let _date = day.format("%Y-%m-%d").to_string();
581 let query = format!("&date={}", _date);
582 self._request("queryDeviceDataOneDay", Some(&query))
583 }
584
585 pub fn get_last_data(&self) -> Result<ShineMonitorLastData, ApiError> {
586 let raw = self._request("querySPDeviceLastData", None)?;
587 Ok(ShineMonitorLastData::from_json(&raw))
588 }
589
590 pub fn get_devices(&self) -> Result<Vec<DeviceIdentifier>, ApiError> {
596 let token = self
597 ._token
598 .as_ref()
599 .ok_or_else(|| ApiError::local(-1, "not logged in"))?;
600 let mut last_err: Option<ApiError> = None;
601
602 let current_id = self._app_profile.app_id;
603 let mut profiles: Vec<&AppProfile> = vec![&self._app_profile];
604 for known in KNOWN_APPS {
605 if known.app_id != current_id {
606 profiles.push(known);
607 }
608 }
609
610 for profile in profiles {
611 let suffix = profile.suffix_context();
612 let base_action = format!("&action=webQueryDeviceEs{}", suffix);
613 let salt = ShineMonitorAPI::generate_salt();
614 let sign = self.hash(vec![&salt, &self._secret, token, &base_action]);
615 let auth = format!("?sign={}&salt={}&token={}", sign, salt, token);
616 let url = format!("{}{}{}", self._base_url, auth, base_action);
617
618 let response: serde_json::Value = match self._client.get(&url).send() {
619 Ok(r) => match r.json() {
620 Ok(v) => v,
621 Err(e) => return Err(ApiError::from(e)),
622 },
623 Err(e) => return Err(ApiError::from(e)),
624 };
625
626 if response["err"].as_i64() == Some(0) {
627 let devices: Vec<DeviceIdentifier> = response["dat"]["device"]
628 .as_array()
629 .map(|arr| {
630 arr.iter()
631 .filter_map(|v| serde_json::from_value(v.clone()).ok())
632 .collect()
633 })
634 .unwrap_or_default();
635 return Ok(devices);
636 }
637
638 let api_err = ApiError::from_payload(response);
639 if api_err.err != 0x0102 {
640 return Err(api_err);
641 }
642 last_err = Some(api_err);
643 }
644
645 Err(last_err.unwrap_or_else(|| {
646 ApiError::local(258, "all app profiles returned ERR_NOT_FOUND_DEVICE")
647 }))
648 }
649}