use serde::{de::DeserializeOwned, Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; use std::net::IpAddr; use thiserror::Error; #[derive(Debug, Error)] pub enum Error { #[error("unsupported API version {0}")] UnsupportedApiVersion(u64), #[error("invalid endpoint {0:?}")] InvalidEndpoint(String), #[error("request failed")] Request(#[from] reqwest::Error), #[error("decoding response body failed")] Decode(#[from] serde_json::Error), #[error("received error response {:?}: {}", .0.code, .0.reason)] Response(Status), } #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "PascalCase")] pub struct FroniousResponse { head: CommonResponseHeader, body: T, } #[derive(Debug, Serialize_repr, Deserialize_repr)] #[repr(u8)] pub enum StatusCode { Okay = 0, NotImplemented = 1, Uninitialized = 2, Initialized = 3, Running = 4, Timeout = 5, ArgumentError = 6, LNRequestError = 7, LNRequestTimeout = 8, LNParseError = 9, ConfigIOError = 10, NotSupported = 11, DeviceNotAvailable = 12, UnknownError = 255, } #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "PascalCase")] pub struct Status { code: StatusCode, reason: String, user_message: String, } #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "PascalCase")] pub struct CommonResponseHeader { request_arguments: std::collections::HashMap, status: Status, #[serde(with = "time::serde::rfc3339")] timestamp: time::OffsetDateTime, } #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "PascalCase")] pub struct UnitAndValue { unit: String, value: T, } #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "PascalCase")] pub struct CommonResponseBody { data: T, } #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "PascalCase")] pub struct UnitAndValues { unit: String, values: std::collections::HashMap, } #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "PascalCase")] pub struct ApiVersion { #[serde(rename = "APIVersion")] api_version: i8, #[serde(rename = "BaseURL")] base_url: String, compatibility_range: String, } pub fn get_api_version(ip: IpAddr) -> Result> { let mut url = reqwest::Url::parse("http://placeholder.local/solar_api/GetAPIVersion.cgi")?; let _ = url.set_ip_host(ip); Ok(reqwest::blocking::Client::new().get(url).send()?.json()?) } pub struct DeviceId(u8); #[derive(Debug, Clone, Copy, PartialEq, Eq, Error)] #[error("invalid device ID, must be less than 100: {0}")] pub struct InvalidDeviceId(u8); impl TryFrom for DeviceId { type Error = InvalidDeviceId; fn try_from(device_id: u8) -> Result { if device_id <= 99 { Ok(Self(device_id)) } else { Err(InvalidDeviceId(device_id)) } } } impl From for u8 { fn from(device_id: DeviceId) -> u8 { device_id.0 } } pub trait DataCollection: DeserializeOwned { /// Returns the value of the `DataCollection` GET parameter for this collection. fn param_value() -> &'static str; } #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub struct CumulationInverterData { pac: UnitAndValue, day_energy: UnitAndValue, year_energy: UnitAndValue, total_energy: UnitAndValue, #[serde(rename = "DeviceStatus")] device_status: Option>, } impl DataCollection for CumulationInverterData { fn param_value() -> &'static str { "CumulationInverterData" } } pub fn get_inverter_realtime_data_device( ip: IpAddr, device_id: DeviceId, ) -> Result>, Box> { let device_id = u8::from(device_id).to_string(); let params = [ ("Scope", "Device"), ("DeviceId", &device_id), ("DataCollection", C::param_value()), ]; let mut url = reqwest::Url::parse_with_params( "http://placeholder.local/solar_api/v1/GetInverterRealtimeData.cgi", ¶ms, )?; let _ = url.set_ip_host(ip); Ok(reqwest::blocking::Client::new().get(url).send()?.json()?) }