huawei_client 0.0.2

Huawei Client for FusionSolar API
Documentation
use std::collections::HashMap;
use std::ops::Add;

use std::sync::Mutex;
use std::time::{Duration, Instant};

use async_trait::async_trait;
use log::trace;
use reqwest::header::HeaderMap;
use crate::api_requests::{GetDevListRequest, GetDevRealKpiRequest, LoginRequest};
use crate::api_responses::{GetDevListData, GetDevListResponse, GetDevRealKpiDataInverterItem, GetDevRealKpiDataItem, GetDevRealKpiDataPowerSensorItem, GetDevRealKpiResponse, GetStationData, GetStationListResponse, GetStationRealKpiResponse, LoginResponse, RealtimeData};
use crate::client::HuaweiClient;
use crate::errors::Error;
use crate::errors::Error::{GetDataError, LoginError};

pub struct HuaweiClientImpl {
    base_url: String,
    username: String,
    password: String,
    xsrf_token: Mutex<Option<String>>,
    login_expiration: Mutex<Instant>,
    client: reqwest::Client,
}

impl HuaweiClientImpl {
    pub fn new(base_url: &str, username: &str, password: &str) -> Self {
        Self {
            base_url: base_url.to_string(),
            username: username.to_string(),
            password: password.to_string(),
            xsrf_token: Mutex::new(None),
            login_expiration: Mutex::new(Instant::now()),
            client: reqwest::Client::new(),
        }
    }
    pub fn default(username: &str, password: &str) -> Self {
        Self::new("https://eu5.fusionsolar.huawei.com", username, password)
    }

    pub async fn login(&self) -> Result<String, Error> {
        let params = LoginRequest::new(self.username.as_str(), self.password.as_str());
        let result = self
            .client
            .post(format!("{}/thirdData/login", self.base_url))
            .json(&params)
            .send()
            .await
            .map_err(|e| LoginError(e.to_string()))?;

        trace!("result: {:?}", result);
        let result_headers = result.headers().clone();

        let login_response: LoginResponse =
            result.json().await.map_err(|e| LoginError(e.to_string()))?;
        trace!("body: {:?}", login_response);
        if !login_response.success {
            return Err(LoginError(
                login_response
                    .message
                    .unwrap_or(format!("failCode:{}", login_response.fail_code)),
            ));
        }

        return if let Some(xsrf_token) = result_headers.get("xsrf-token") {
            let token = xsrf_token.to_str().unwrap().to_string();
            let mut mut_xsrf_token = self.xsrf_token.lock().unwrap();
            *mut_xsrf_token = Some(token.clone());

            let mut login_expiration = self.login_expiration.lock().unwrap();
            *login_expiration = Instant::now().add(Duration::from_secs(20 * 60));

            Ok(token)
        } else {
            Err(LoginError(
                "XSRF token header not found in the response".to_string(),
            ))
        };
    }

    pub fn xsrf_token(&self) -> Option<String> {
        self.xsrf_token.lock().unwrap().clone()
    }

    async fn ensure_token(&self) -> Result<HeaderMap, Error> {
        let mut headers = HeaderMap::new();
        let login_expiration = self.login_expiration.lock().unwrap().clone();
        if login_expiration > Instant::now() {
            if let Some(xsrf_token) = self.xsrf_token() {
                headers.insert("XSRF-TOKEN", xsrf_token.parse().unwrap());
                return Ok(headers);
            }
        }
        headers.insert("XSRF-TOKEN", self.login().await?.parse().unwrap());

        Ok(headers)
    }

    async fn get_realtime_device_data<T: GetDevRealKpiDataItem>(
        &self,
        device_id: i64,
    ) -> Result<T, Error> {
        let headers = self.ensure_token().await?;
        let params = GetDevRealKpiRequest {
            dev_ids: device_id.to_string(),
            dev_type_id: T::get_device_type(),
        };
        let http_response = self
            .client
            .post(format!("{}/thirdData/getDevRealKpi", self.base_url))
            .headers(headers)
            .json(&params)
            .send()
            .await
            .map_err(|e| GetDataError(e.to_string()))?;

        let http_status_code = http_response.status();
        if http_status_code != 200 {
            let body = http_response.text().await.map_err(|e| GetDataError(e.to_string()))?;
            return Err(GetDataError(format!("Http error: status:{}, error:{}", http_status_code, body)));
        }

        let resp: GetDevRealKpiResponse<T> = http_response
            .json()
            .await
            .map_err(|e| GetDataError(e.to_string()))?;

        if !resp.success {
            return Err(GetDataError(
                resp.message
                    .unwrap_or(format!("failCode:{}", resp.fail_code)),
            ));
        }
        Ok(resp.data.unwrap().first().unwrap().data_item_map.clone())
    }
}

#[async_trait]
impl HuaweiClient for HuaweiClientImpl {
    async fn get_station_list(&self) -> Result<Vec<GetStationData>, Error> {
        let headers = self.ensure_token().await?;

        let http_response = self
            .client
            .post(format!("{}/thirdData/getStationList", self.base_url))
            .headers(headers)
            .send()
            .await
            .map_err(|e| GetDataError(e.to_string()))?;

        let http_status_code = http_response.status();
        if http_status_code != 200 {
            let body = http_response.text().await.map_err(|e| GetDataError(e.to_string()))?;
            return Err(GetDataError(format!("Http error: status:{}, error:{}", http_status_code, body)));
        }

        let resp: GetStationListResponse = http_response
            .json()
            .await
            .map_err(|e| GetDataError(e.to_string()))?;

        if !resp.success {
            return Err(GetDataError(
                resp.message
                    .unwrap_or(format!("failCode:{}", resp.fail_code)),
            ));
        }
        Ok(resp.data.unwrap())
    }

    async fn get_realtime_station_data(&self, station_code: &str) -> Result<RealtimeData, Error> {
        let params = HashMap::from([("stationCodes", station_code)]);
        let headers = self.ensure_token().await?;

        let request = self
            .client
            .post(format!("{}/thirdData/getStationRealKpi", self.base_url))
            .json(&params)
            .headers(headers)
            .build()
            .map_err(|e| GetDataError(e.to_string()))?;
        trace!("request:{:?}", request);

        let http_response = self
            .client
            .execute(request)
            .await
            .map_err(|e| GetDataError(e.to_string()))?;

        let http_status_code = http_response.status();
        if http_status_code != 200 {
            let body = http_response.text().await.map_err(|e| GetDataError(e.to_string()))?;
            return Err(GetDataError(format!("Http error: status:{}, error:{}", http_status_code, body)));
        }

        let resp: GetStationRealKpiResponse = http_response
            .json()
            .await
            .map_err(|e| GetDataError(e.to_string()))?;

        if !resp.success {
            return Err(GetDataError(
                resp.message
                    .unwrap_or(format!("failCode:{}", resp.fail_code)),
            ));
        }
        Ok(resp.data.unwrap().first().unwrap().data_item_map.clone())
    }

    async fn get_device_list(&self, station_code: &str) -> Result<Vec<GetDevListData>, Error> {
        let headers = self.ensure_token().await?;
        let params = GetDevListRequest {
            station_codes: station_code.to_string(),
        };
        let http_response = self
            .client
            .post(format!("{}/thirdData/getDevList", self.base_url))
            .headers(headers)
            .json(&params)
            .send()
            .await
            .map_err(|e| GetDataError(e.to_string()))?;

        let http_status_code = http_response.status();
        if http_status_code != 200 {
            let body = http_response.text().await.map_err(|e| GetDataError(e.to_string()))?;
            return Err(GetDataError(format!("Http error: status:{}, error:{}", http_status_code, body)));
        }

        let resp: GetDevListResponse = http_response
            .json()
            .await
            .map_err(|e| GetDataError(e.to_string()))?;

        if !resp.success {
            return Err(GetDataError(
                resp.message
                    .unwrap_or(format!("failCode:{}", resp.fail_code)),
            ));
        }
        Ok(resp.data.unwrap())
    }

    async fn get_realtime_residential_inverter_data(
        &self,
        device_id: i64,
    ) -> Result<GetDevRealKpiDataInverterItem, Error> {
        self.get_realtime_device_data(device_id).await
    }

    async fn get_realtime_power_sensor_data(
        &self,
        device_id: i64,
    ) -> Result<GetDevRealKpiDataPowerSensorItem, Error> {
        self.get_realtime_device_data(device_id).await
    }
}