use std::collections::HashMap;
use std::ops::Add;
use std::str::FromStr;
use chrono::{DateTime, Local, NaiveTime, TimeDelta, Timelike, Utc};
use md5::{Digest, Md5};
use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
use serde::{Deserialize, Serialize};
use crate::{FoxError, FoxSettings, FoxVariables, TimeSegmentsDataRequest};
use crate::fox_settings::SettableSettingSpec;
use crate::models::{VariablesDataHistory, VariablesData, VariableDataSet, VariableDataPoint, VariableInfo, AvailableVariables};
use crate::models::dto::{ChargingTime, ChargingTimeSchedule, DeviceHistoryData, DeviceHistoryResult, DeviceRealTimeResult, DeviceSettingsResult, DeviceVariablesResult, ErrorCodeInformationResult, GetMainSwitchStatus, MainSwitchStatusResult, RequestDeviceHistoryData, RequestDeviceRealTimeData, RequestSettingsData, SetMainSwitchStatus, SetSetting};
use crate::models::dto_scheduler::{SchedulerTimeSegmentsRequest, SchedulerTimeSegmentsResult};
use crate::models::main_switch::MainSwitchStatus;
use crate::models::scheduler::TimeSegmentsData;
pub(crate) struct FoxHelper {
api_key: String,
sn: String,
base_url: String,
now_millis: fn() -> i64,
}
impl FoxHelper {
pub(crate) fn new(api_key: &str, sn: &str, base_url: &str, now_millis: fn() -> i64) -> Self {
Self {
api_key: api_key.to_string(),
sn: sn.to_string(),
base_url: base_url.to_string(),
now_millis,
}
}
pub(crate) fn pre_get_variables_history(&self, start: DateTime<Utc>, end: DateTime<Utc>, parameters: Vec<FoxVariables>) -> Result<(String, &'static str), FoxError> {
let path = "/op/v0/device/history/query";
let req = RequestDeviceHistoryData {
sn: &self.sn,
variables: parameters.iter().map(|p| p.as_str()).collect(),
begin: start.timestamp_millis(),
end: end.timestamp_millis(),
};
Ok((serde_json::to_string(&req)?, path))
}
pub(crate) fn post_get_variables_history(&self, json: &str) -> Result<VariablesDataHistory, FoxError> {
let fox_data: DeviceHistoryResult = serde_json::from_str(json)?;
let device_history = transform_history_data(fox_data.result)?;
Ok(device_history)
}
pub(crate) fn pre_get_variables(&self, variables: Vec<FoxVariables>) -> Result<(String, &'static str), FoxError> {
let path = "/op/v1/device/real/query";
let req = RequestDeviceRealTimeData {
variables: variables.iter().map(|p| p.as_str()).collect(),
sns: vec![&self.sn],
};
Ok((serde_json::to_string(&req)?, path))
}
pub(crate) fn post_get_variables(&self, json: &str) -> Result<VariablesData, FoxError> {
let fox_data: DeviceRealTimeResult = serde_json::from_str(json)?;
let mut data_points: HashMap<FoxVariables, VariableDataPoint> = HashMap::new();
let Some(first) = fox_data.result.first() else {
return Ok(VariablesData {
data_points,
});
};
for data in first.datas.iter() {
let Ok(p) = FoxVariables::from_str(data.variable.as_str()) else {
continue;
};
let value = data.value;
data_points.insert(p, VariableDataPoint(value));
}
Ok(VariablesData { data_points })
}
pub(crate) fn pre_get_setting(&self, setting: FoxSettings) -> Result<(String, &'static str), FoxError> {
let path = "/op/v0/device/setting/get";
let req = RequestSettingsData { sn: &self.sn, key: setting.as_str() };
Ok((serde_json::to_string(&req)?, path))
}
pub(crate) fn post_get_setting(&self, json: &str) -> Result<String, FoxError> {
let fox_data: DeviceSettingsResult = serde_json::from_str(json)?;
Ok(fox_data.result.value)
}
pub(crate) fn pre_set_setting_typed<S: SettableSettingSpec>(&self, value: S::Value) -> Result<(String, &'static str), FoxError> {
let path = "/op/v0/device/setting/set";
let data = S::format(&value);
let req = SetSetting { sn: &self.sn, key: S::SETTING.as_str(), value: &data };
Ok((serde_json::to_string(&req)?, path))
}
pub (crate) fn pre_set_battery_charging_time_schedule(&self, enable: bool, start: DateTime<Utc>, end: DateTime<Utc>) -> Result<(String,&'static str), FoxError> {
let path = "/op/v0/device/battery/forceChargeTime/set";
let mut start_hour: u8 = 0;
let mut start_minute: u8 = 0;
let mut end_hour: u8 = 0;
let mut end_minute: u8 = 0;
if enable {
let start_local = start.with_timezone(&Local);
let end_local = end.with_timezone(&Local).add(TimeDelta::minutes(-1));
start_hour = start_local.hour() as u8;
start_minute = start_local.minute() as u8;
end_hour = end_local.hour() as u8;
end_minute = end_local.minute() as u8;
}
let schedule = self.build_charge_time_schedule(
enable, start_hour, start_minute, end_hour, end_minute,
false, 0, 0, 0, 0,
)?;
let req_json = serde_json::to_string(&schedule)?;
Ok((req_json, path))
}
pub(crate) fn pre_get_scheduler_time_segments(&self) -> Result<(String, &'static str), FoxError> {
let path = "/op/v3/device/scheduler/get";
#[derive(Serialize)]
struct RequestSchedulerTimeSegments<'a> {
#[serde(rename = "deviceSN")]
device_sn: &'a str,
}
let req = RequestSchedulerTimeSegments { device_sn: &self.sn };
Ok((serde_json::to_string(&req)?, path))
}
pub(crate) fn post_get_scheduler_time_segments(&self, json: &str) -> Result<TimeSegmentsData, FoxError> {
let dto = serde_json::from_str::<SchedulerTimeSegmentsResult>(json)?;
Ok(dto.into())
}
pub fn pre_set_scheduler_time_segments(&self, time_segments: &TimeSegmentsDataRequest) -> Result<(String, &'static str), FoxError> {
let path = "/op/v3/device/scheduler/enable";
let req = SchedulerTimeSegmentsRequest {
device_sn:self.sn.to_string(),
is_default: time_segments.is_default.unwrap_or(false),
groups: time_segments.groups.iter().map(Into::into).collect(),
};
Ok((serde_json::to_string(&req)?, path))
}
pub(crate) fn pre_get_main_switch_status(&self) -> Result<(String, &'static str), FoxError> {
let path = "/op/v1/device/scheduler/get/flag";
let req = GetMainSwitchStatus {
device_sn: self.sn.to_string(),
};
Ok((serde_json::to_string(&req)?, path))
}
pub(crate) fn post_get_main_switch_status(&self, json: &str) -> Result<MainSwitchStatus, FoxError> {
let fox_data: MainSwitchStatusResult = serde_json::from_str(json)?;
Ok(MainSwitchStatus {
support: fox_data.result.support,
enable: fox_data.result.enable,
})
}
pub(crate) fn pre_set_main_switch_status(&self, enable: bool) -> Result<(String, &'static str), FoxError> {
let path = "/op/v1/device/scheduler/set/flag";
let req = SetMainSwitchStatus {
device_sn: self.sn.to_string(),
enable: enable as u8,
};
Ok((serde_json::to_string(&req)?, path))
}
pub(crate) fn pre_get_available_variables(&self) -> Result<&'static str, FoxError> {
let path = "/op/v0/device/variable/get";
Ok(path)
}
pub(crate) fn post_get_available_variables(&self, json: &str) -> Result<AvailableVariables, FoxError> {
let fox_data: DeviceVariablesResult = serde_json::from_str(json)?;
let variables: Vec<VariableInfo> = fox_data
.result
.into_iter()
.filter_map(|mut v| {
let (variable, info) = v.drain().next()?;
let name = info
.name
.get("en")
.cloned()
.unwrap_or_else(|| variable.clone());
Some(VariableInfo {
variable,
name,
unit: info.unit,
enumeration: info.enumeration,
})
})
.collect();
Ok(AvailableVariables { variables })
}
pub(crate) fn pre_get_error_code_information(&self) -> Result<&'static str, FoxError> {
let path = "/op/v0/device/fault/get";
Ok(path)
}
pub(crate) fn post_get_error_code_information(&self, json: &str) -> Result<HashMap<u32, String>, FoxError> {
let fox_data: ErrorCodeInformationResult = serde_json::from_str(json)?;
let result: HashMap<u32, String> = fox_data
.result
.into_iter()
.filter_map(|(code, info)| code.parse::<u32>().ok().map(|num_code| (num_code, info.en)))
.collect();
Ok(result)
}
pub(crate) fn pre_network_post_request(&self, path: &str) -> (String,HeaderMap) {
(
format!("{}{}", self.base_url, path), generate_headers(&self.api_key, path, (self.now_millis)(), Some(vec![("Content-Type", "application/json")])), )
}
pub(crate) fn post_network_post_request(&self, json: String) -> Result<String, FoxError> {
let fox_res: FoxResponse = serde_json::from_str(&json)?;
if fox_res.errno != 0 {
return Err(FoxError::FoxCloud(format!("errno: {}, msg: {}", fox_res.errno, fox_res.msg)));
}
Ok(json)
}
pub(crate) fn pre_network_get_request(&self, path: &str) -> (String,HeaderMap) {
(
format!("{}{}", self.base_url, path), generate_headers(&self.api_key, path, (self.now_millis)(), Some(vec![("Content-Type", "application/json")])), )
}
pub(crate) fn post_network_get_request(&self, json: String) -> Result<String, FoxError> {
let fox_res: FoxResponse = serde_json::from_str(&json)?;
if fox_res.errno != 0 {
return Err(FoxError::FoxCloud(format!("errno: {}, msg: {}", fox_res.errno, fox_res.msg)));
}
Ok(json)
}
fn build_charge_time_schedule(
&self,
mut enable_1: bool, mut start_hour_1: u8, mut start_minute_1: u8, mut end_hour_1: u8, mut end_minute_1: u8,
mut enable_2: bool, mut start_hour_2: u8, mut start_minute_2: u8, mut end_hour_2: u8, mut end_minute_2: u8,
) -> Result<ChargingTimeSchedule, FoxError> {
let start_1 = NaiveTime::from_hms_opt(start_hour_1 as u32, start_minute_1 as u32, 0)
.ok_or(FoxError::ScheduleBuildError("charge schedule 1 start time error".to_string()))?;
let end_1 = NaiveTime::from_hms_opt(end_hour_1 as u32, end_minute_1 as u32, 0)
.ok_or(FoxError::ScheduleBuildError("charge schedule 1 end time error".to_string()))?;
let dur_1 = end_1 - start_1;
if dur_1 < TimeDelta::new(0, 0).unwrap() {
return Err(FoxError::ScheduleBuildError("charge schedule 1 start time is after end time".to_string()));
}
if !enable_1 || dur_1 == TimeDelta::new(0, 0).unwrap() {
enable_1 = false;
start_hour_1 = 0;
start_minute_1 = 0;
end_hour_1 = 0;
end_minute_1 = 0;
}
let start_2 = NaiveTime::from_hms_opt(start_hour_2 as u32, start_minute_2 as u32, 0)
.ok_or(FoxError::ScheduleBuildError("charge schedule 2 start time error".to_string()))?;
let end_2 = NaiveTime::from_hms_opt(end_hour_2 as u32, end_minute_2 as u32, 0)
.ok_or(FoxError::ScheduleBuildError("charge schedule 2 end time error".to_string()))?;
let dur_2 = end_2 - start_2;
if dur_2 < TimeDelta::new(0, 0).unwrap() {
return Err(FoxError::ScheduleBuildError("charge schedule 2 start time is after end time".to_string()));
}
if !enable_2 || dur_2 <= TimeDelta::new(0, 0).unwrap() {
enable_2 = false;
start_hour_2 = 0;
start_minute_2 = 0;
end_hour_2 = 0;
end_minute_2 = 0;
}
if enable_1 && enable_2 {
if start_2 >= start_1 && start_2 <= start_1 + dur_1 {
return Err(FoxError::ScheduleBuildError("overlapping charge schedules".to_string()));
}
if end_2 >= start_1 && end_2 <= start_1 + dur_1 {
return Err(FoxError::ScheduleBuildError("overlapping charge schedules".to_string()));
}
}
Ok(ChargingTimeSchedule {
sn: self.sn.clone(),
enable_1,
start_time_1: ChargingTime { hour: start_hour_1, minute: start_minute_1 },
end_time_1: ChargingTime { hour: end_hour_1, minute: end_minute_1 },
enable_2,
start_time_2: ChargingTime { hour: start_hour_2, minute: start_minute_2 },
end_time_2: ChargingTime { hour: end_hour_2, minute: end_minute_2 },
})
}
}
fn generate_headers(api_key: &str, path: &str, timestamp_millis: i64, extra: Option<Vec<(&str, &str)>>) -> HeaderMap {
let mut headers = HeaderMap::new();
let signature = format!("{}\\r\\n{}\\r\\n{}", path, api_key, timestamp_millis);
let mut hasher = Md5::new();
hasher.update(signature.as_bytes());
let signature_md5 = hasher.finalize().iter().map(|x| format!("{:02x}", x)).collect::<String>();
headers.insert("token", HeaderValue::from_str(api_key).unwrap());
headers.insert("timestamp", HeaderValue::from_str(×tamp_millis.to_string()).unwrap());
headers.insert("signature", HeaderValue::from_str(&signature_md5).unwrap());
headers.insert("lang", HeaderValue::from_str("en").unwrap());
if let Some(h) = extra {
h.iter().for_each(|&(k, v)| {
headers.insert(HeaderName::from_str(k).unwrap(), HeaderValue::from_str(v).unwrap());
});
}
headers
}
fn transform_history_data(input: Vec<DeviceHistoryData>) -> Result<VariablesDataHistory, FoxError> {
let mut series: HashMap<FoxVariables, Vec<VariableDataSet<f64>>> = HashMap::new();
let Some(first) = input.first() else {
return Ok(VariablesDataHistory {
series,
});
};
for set in &first.data_set {
let Ok(p) = FoxVariables::from_str(set.variable.as_str()) else {
continue;
};
for d in set.data.iter() {
let utc = cet_to_utc(&d.time)?;
series
.entry(p)
.or_insert_with(Vec::new)
.push(VariableDataSet {
date_time: utc,
data: d.value,
});
}
}
Ok(VariablesDataHistory {
series,
})
}
fn cet_to_utc(time: &str) -> Result<DateTime<Utc>, FoxError> {
let dt = DateTime::parse_from_str(&time.replace("+", " +"), "%Y-%m-%d %H:%M:%S %Z %z")?;
Ok(dt.with_timezone(&Utc))
}
#[derive(Serialize, Deserialize)]
struct FoxResponse {
errno: u32,
msg: String,
}