1use serde::Deserialize;
2use serde_aux::prelude::*;
3use std::time::Duration;
4use thiserror::Error;
5use tokio_tungstenite::{connect_async, tungstenite::protocol::Message};
6use tracing::{debug, error};
7
8#[derive(Error, Debug)]
9#[non_exhaustive]
10pub enum Error {
11 #[error(transparent)]
12 WebsocketErr(#[from] tungstenite::error::Error),
13
14 #[error(transparent)]
15 HttpErr(#[from] reqwest::Error),
16
17 #[error(transparent)]
18 HttpHdrErr(#[from] reqwest::header::InvalidHeaderValue),
19
20 #[error("{code}{}", match .message {
22 Some(msg) => format!(" - {}", &msg),
23 None => "".to_owned(),
24 })]
25 SungrowError { code: u16, message: Option<String> },
26
27 #[error(transparent)]
28 JSONError(#[from] serde_json::Error),
29
30 #[error("Expected attached data")]
31 ExpectedData,
32
33 #[error("No token")]
34 NoToken,
35}
36
37impl From<Error> for std::io::Error {
38 fn from(e: Error) -> Self {
39 use std::io::ErrorKind;
40 std::io::Error::new(ErrorKind::Other, e)
43 }
44}
45
46#[derive(Debug)]
47pub struct Client {
48 http: reqwest::Client,
49 host: String,
50 username: String,
51 password: String,
52 token: Option<String>,
53 devices: Vec<Device>,
54}
55
56pub struct ClientBuilder {
57 host: String,
58 username: String,
59 password: String,
60 token: Option<String>,
61 user_agent: String,
62
63 read_timeout: Option<Duration>,
64 connect_timeout: Option<Duration>,
65 timeout: Option<Duration>,
66}
67
68static DEFAULT_USERNAME: &str = "admin";
70static DEFAULT_PASSWORD: &str = "pw8888";
71static DEFAULT_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
72
73impl ClientBuilder {
74 pub fn new(host: String) -> Self {
75 Self {
76 host: host.into(),
77 username: DEFAULT_USERNAME.into(),
78 password: DEFAULT_PASSWORD.into(),
79 user_agent: DEFAULT_USER_AGENT.into(),
80 token: None,
81
82 connect_timeout: Some(Duration::from_secs(1)),
83 read_timeout: Some(Duration::from_secs(1)),
84 timeout: Some(Duration::from_secs(1)),
85 }
86 }
87
88 pub fn build(self) -> Result<Client> {
89 use reqwest::header;
90
91 let mut headers = reqwest::header::HeaderMap::new();
92 headers.insert(header::ACCEPT, "application/json".parse()?);
93 headers.insert(header::CONNECTION, "keep-alive".parse()?);
94
95 let mut http_builder = reqwest::ClientBuilder::new()
96 .user_agent(header::HeaderValue::from_str(&self.user_agent)?)
97 .default_headers(headers)
98 .pool_max_idle_per_host(1)
99 .redirect(reqwest::redirect::Policy::none())
100 .referer(false);
101
102 if let Some(timeout) = self.timeout {
103 http_builder = http_builder.timeout(timeout);
104 }
105 if let Some(timeout) = self.connect_timeout {
106 http_builder = http_builder.connect_timeout(timeout);
107 }
108 if let Some(timeout) = self.read_timeout {
109 http_builder = http_builder.read_timeout(timeout);
110 }
111
112 let http = http_builder.build()?;
113
114 Ok(Client {
115 host: self.host,
116 username: self.username.into(),
117 password: self.password.into(),
118 token: self.token.into(),
119 devices: vec![],
120 http,
121 })
122 }
123
124 pub fn username(mut self, username: String) -> Self {
125 self.username = username;
126 self
127 }
128
129 pub fn password(mut self, password: String) -> Self {
130 self.password = password;
131 self
132 }
133
134 pub fn token(mut self, token: String) -> Self {
135 self.token = Some(token);
136 self
137 }
138
139 pub fn user_agent(mut self, user_agent: String) -> Self {
140 self.user_agent = user_agent.into();
141 self
142 }
143
144 pub fn read_timeout(mut self, timeout: Option<Duration>) -> Self {
145 self.read_timeout = timeout;
146 self
147 }
148
149 pub fn connect_timeout(mut self, timeout: Option<Duration>) -> Self {
150 self.connect_timeout = timeout;
151 self
152 }
153
154 pub fn timeout(mut self, timeout: Option<Duration>) -> Self {
155 self.timeout = timeout;
156 self
157 }
158}
159
160type Result<T> = std::result::Result<T, Error>;
161
162impl Client {
163 const WS_PORT: u16 = 8082;
164
165 async fn token(&mut self) -> Result<String> {
166 if self.token.is_none() {
167 self.token = Some(self.get_token().await?);
168 }
169
170 Ok(self.token.as_ref().expect("no token").clone())
171 }
172
173 async fn get_token(&self) -> Result<String> {
174 use futures_util::SinkExt;
175 use futures_util::StreamExt;
176 use serde_json::json;
177
178 let ws_url = format!("ws://{}:{}/ws/home/overview", &self.host, Self::WS_PORT);
179 debug!(%ws_url, "Connecting to WiNet-S websocket");
180
181 let (mut ws, _) = connect_async(ws_url).await?;
182
183 let connect = Message::Text(
184 json!({
185 "lang": "en_us",
186 "token": self.token.as_deref().unwrap_or_default(),
187 "service": "connect"
188 })
189 .to_string(),
190 );
191 debug!(%connect, "Sending connect message");
192 ws.send(connect).await?;
193
194 while let Some(Ok(Message::Text(msg))) = ws.next().await {
195 debug!(%msg, "Got WS message");
196
197 if let SungrowResult {
198 code: 1,
199 data: Some(ResultData::WebSocketMessage(msg)),
200 ..
201 } = serde_json::from_str(&msg)?
202 {
203 match msg {
204 WebSocketMessage::Connect { token: Some(token) } => {
205 let login = Message::Text(
206 json!({
207 "lang": "en_us",
208 "token": token,
209 "service": "login",
210 "username": &self.username,
211 "passwd": &self.password,
212 })
213 .to_string(),
214 );
215 debug!(%login, "Sending login message");
216 ws.send(login).await?;
217 }
218 WebSocketMessage::Login { token } => {
219 return Ok(token.expect(
220 "Login message should have a token, if not an error response",
221 ));
222 }
223 message => {
224 debug!(?message, "Got other message");
225 }
226 }
227 }
228 }
229
230 Err(Error::NoToken)
231 }
232
233 pub async fn connect(&mut self) -> Result<()> {
234 let data: ResultData = parse_response(
235 self.http
236 .post(format!("http://{}/inverter/list", &self.host))
237 .send()
238 .await?,
239 )
240 .await?;
241
242 if let ResultData::DeviceList(ResultList { items, .. }) = data {
243 self.devices = items;
244 } else {
245 return Err(Error::ExpectedData);
246 }
247
248 self.token = Some(self.get_token().await?);
249 Ok(())
250 }
251
252 pub async fn read_register(
254 &mut self,
255 register_type: RegisterType,
256 address: u16,
257 count: u16,
258 ) -> Result<Vec<u16>> {
259 let token = self.token().await?;
260
261 let device = &self.devices[0];
263
264 let request = self
265 .http
266 .get(format!("http://{}{}", &self.host, "/device/getParam"))
267 .header(reqwest::header::ACCEPT, "application/json")
268 .query(&[
269 ("dev_id", device.dev_id),
270 ("dev_type", device.dev_type),
271 ("param_type", register_type.param()),
272 ("type", 3),
273 ])
274 .query(&[
275 ("dev_code", device.dev_code),
276 ("param_addr", address),
277 ("param_num", count),
278 ])
279 .query(&[("lang", "en_us"), ("token", &token)]);
280 debug!(?request, "sending request");
281 let response = request.send().await?;
282
283 let result = parse_response(response).await?;
284
285 if let ResultData::GetParam { param_value } = result {
286 Ok(param_value)
287 } else {
288 Err(Error::ExpectedData)
289 }
290 }
291
292 #[tracing::instrument(level = "debug")]
293 pub async fn write_register(&mut self, address: u16, data: &[u16]) -> Result<()> {
294 if data.is_empty() {
295 return Err(Error::ExpectedData);
296 }
297 let device = &self.devices[0];
299
300 use serde_json::json;
301 let body = json!({
302 "dev_id": device.dev_id,
303 "dev_type": device.dev_type,
304 "dev_code": device.dev_code,
305 "param_addr": address.to_string(),
306 "param_size": data.len().to_string(),
307 "param_value": data[0].to_string(),
308 "lang": "en_us",
309 "token": &self.token().await?,
310 });
311 let request = self
312 .http
313 .post(format!("http://{}{}", &self.host, "/device/setParam"))
314 .header(reqwest::header::ACCEPT, "application/json")
315 .json(&body);
316 let response = request.send().await?;
317 parse_response(response).await?;
318 Ok(())
319 }
320
321 pub async fn running_state(&mut self) -> Result<RunningState> {
322 let raw = *self
323 .read_register(RegisterType::Input, 13001, 1)
324 .await?
325 .first()
326 .ok_or(Error::ExpectedData)?;
327 let bits: RunningStateBits = raw.into();
328
329 let battery_state = if bits.intersects(RunningStateBits::BatteryCharging) {
330 BatteryState::Charging
331 } else if bits.intersects(RunningStateBits::BatteryDischarging) {
332 BatteryState::Discharging
333 } else {
334 BatteryState::Inactive
335 };
336
337 let trading_state = if bits.intersects(RunningStateBits::ImportingPower) {
338 TradingState::Importing
339 } else if bits.intersects(RunningStateBits::ExportingPower) {
340 TradingState::Exporting
341 } else {
342 TradingState::Inactive
343 };
344
345 Ok(RunningState {
346 battery_state,
347 trading_state,
348 generating_pv_power: bits.intersects(RunningStateBits::GeneratingPVPower),
349 positive_load_power: bits.intersects(RunningStateBits::LoadActive),
350 power_generated_from_load: bits.intersects(RunningStateBits::GeneratingPVPower),
351 state: bits,
352 })
353 }
354}
355
356#[derive(Debug)]
357pub enum BatteryState {
358 Charging,
359 Discharging,
360 Inactive,
361}
362
363#[derive(Debug)]
364pub enum TradingState {
365 Importing,
366 Exporting,
367 Inactive,
368}
369
370#[derive(Debug)]
371pub struct RunningState {
372 state: RunningStateBits,
373 pub battery_state: BatteryState,
374 pub trading_state: TradingState,
375 pub generating_pv_power: bool,
376 pub positive_load_power: bool,
377 pub power_generated_from_load: bool,
378}
379
380impl RunningState {
381 pub fn raw(&self) -> RunningStateBits {
382 self.state
383 }
384}
385
386#[bitmask_enum::bitmask(u16)]
388pub enum RunningStateBits {
389 GeneratingPVPower = 0b00000001,
390 BatteryCharging = 0b00000010,
391 BatteryDischarging = 0b00000100,
392 LoadActive = 0b00001000,
393 LoadReactive = 0b00000000,
394 ExportingPower = 0b00010000,
395 ImportingPower = 0b00100000,
396 PowerGeneratedFromLoad = 0b0100000,
397}
398
399#[tracing::instrument(level = "debug")]
400async fn parse_response<T>(response: reqwest::Response) -> Result<T>
401where
402 Result<T>: From<SungrowResult>,
403{
404 let body = response.text().await?;
405 debug!(%body, "parsing");
406 let sg_result = serde_json::from_slice::<SungrowResult>(body.as_bytes());
407 sg_result?.into()
408}
409
410#[derive(Debug)]
411pub enum RegisterType {
412 Input,
413 Holding,
414}
415
416impl RegisterType {
417 fn param(&self) -> u8 {
418 match self {
419 Self::Input => 0,
420 Self::Holding => 1,
421 }
422 }
423}
424
425#[derive(Clone, Debug, Deserialize)]
444struct Device {
445 dev_id: u8,
446 dev_code: u16,
447
448 dev_type: u8,
476
477 #[allow(dead_code)]
479 #[serde(deserialize_with = "serde_aux::prelude::deserialize_number_from_string")]
480 phys_addr: u8,
481 }
494
495#[test]
496fn test_deserialize_device() {
497 let json = r#"{
498 "id": 1,
499 "dev_id": 1,
500 "dev_code": 3343,
501 "dev_type": 35,
502 "dev_procotol": 2,
503 "inv_type": 0,
504 "dev_sn": "REDACTED",
505 "dev_name": "SH5.0RS(COM1-001)",
506 "dev_model": "SH5.0RS",
507 "port_name": "COM1",
508 "phys_addr": "1",
509 "logc_addr": "1",
510 "link_status": 1,
511 "init_status": 1,
512 "dev_special": "0"
513 }"#;
514
515 let dev: Device = serde_json::from_str(json).unwrap();
516
517 assert!(matches!(
518 dev,
519 Device {
520 dev_id: 1,
521 dev_code: 3343,
522 dev_type: 35,
523 phys_addr: 1
524 }
525 ));
526}
527#[derive(Clone, Debug, Deserialize)]
528#[serde(tag = "service", rename_all = "lowercase")]
529enum WebSocketMessage {
530 Connect {
531 token: Option<String>,
532 },
538 Login {
539 token: Option<String>,
540 },
541
542 Other,
555}
556
557#[derive(Clone, Debug, Deserialize)]
558struct ResultList<T> {
559 #[serde(rename = "list")]
561 items: Vec<T>,
562}
563
564#[derive(Clone, Debug, Deserialize)]
565#[serde(untagged)]
566enum ResultData {
567 GetParam {
569 #[serde(deserialize_with = "words_from_string")]
570 param_value: Vec<u16>,
571 },
572 DeviceList(ResultList<Device>),
573 WebSocketMessage(WebSocketMessage),
574
575 Other,
585}
586
587#[test]
588fn test_deserialize_get_param() {
589 let json = r#"{"param_value": "82 00 "}"#;
590 let data: ResultData = serde_json::from_str(json).unwrap();
591 assert!(matches!(data, ResultData::GetParam { .. }));
592
593 let json = r#"{
594 "result_code": 1,
595 "result_msg": "success",
596 "result_data": {
597 "param_value": "82 00 "
598 }
599 }"#;
600
601 let data: SungrowResult = serde_json::from_str(json).unwrap();
602 assert!(matches!(
603 data,
604 SungrowResult {
605 code: 1,
606 message: Some(m),
607 data: Some(ResultData::GetParam { .. })
608 } if m == "success"
609 ));
610}
611
612#[derive(Clone, Debug, Deserialize)]
615struct SungrowResult {
616 #[serde(rename = "result_code")]
626 code: u16,
627
628 #[serde(rename = "result_msg")]
629 message: Option<String>, #[serde(rename = "result_data")]
633 data: Option<ResultData>,
634}
635
636impl From<SungrowResult> for Result<Option<ResultData>> {
637 fn from(sg_result: SungrowResult) -> Self {
638 match sg_result {
639 SungrowResult { code: 1, data, .. } => Ok(data),
640 SungrowResult { code, message, .. } => Err(Error::SungrowError { code, message }),
641 }
642 }
643}
644impl From<SungrowResult> for Result<ResultData> {
645 fn from(sg_result: SungrowResult) -> Self {
646 let data: Result<Option<ResultData>> = sg_result.into();
647
648 if let Some(data) = data? {
649 Ok(data)
650 } else {
651 Err(Error::ExpectedData)
652 }
653 }
654}
655impl From<SungrowResult> for Result<()> {
656 fn from(sg_result: SungrowResult) -> Self {
657 let data: Result<Option<ResultData>> = sg_result.into();
658 data.map(|_| ())
659 }
660}
661
662fn words_from_string<'de, D>(deserializer: D) -> std::result::Result<Vec<u16>, D::Error>
669where
670 D: serde::Deserializer<'de>,
671{
672 StringOrVecToVec::new(' ', |s| u8::from_str_radix(s, 16), true).into_deserializer()(
673 deserializer,
674 )
675 .map(|vec| {
676 vec.chunks_exact(2)
677 .map(|bytes| u16::from_be_bytes(bytes.try_into().unwrap()))
678 .collect()
679 })
680}
681
682#[test]
683fn test_words_from_string() {
684 #[derive(serde::Deserialize, Debug)]
685 struct MyStruct {
686 #[serde(deserialize_with = "words_from_string")]
687 list: Vec<u16>,
688 }
689
690 let s = r#" { "list": "00 AA 00 01 00 0D 00 1E 00 0F 00 00 00 55 " } "#;
691 let a: MyStruct = serde_json::from_str(s).unwrap();
692 assert_eq!(
693 &a.list,
694 &[0x00AA, 0x0001, 0x000D, 0x001E, 0x000F, 0x0000, 0x0055]
695 );
696}