Compare commits
5 commits
2f0822ff4a
...
1a4694bad5
Author | SHA1 | Date | |
---|---|---|---|
1a4694bad5 | |||
5ec9079456 | |||
7d42a56199 | |||
1da05e29e1 | |||
375377f56a |
4 changed files with 163 additions and 72 deletions
34
Cargo.lock
generated
34
Cargo.lock
generated
|
@ -176,7 +176,10 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_repr",
|
||||||
"strum_macros",
|
"strum_macros",
|
||||||
|
"thiserror",
|
||||||
"time",
|
"time",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -732,6 +735,17 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_repr"
|
||||||
|
version = "0.1.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0b2e6b945e9d3df726b65d6ee24060aff8e3533d431f677a9695db04eff9dfdb"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_urlencoded"
|
name = "serde_urlencoded"
|
||||||
version = "0.7.1"
|
version = "0.7.1"
|
||||||
|
@ -827,6 +841,26 @@ dependencies = [
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror"
|
||||||
|
version = "1.0.56"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror-impl",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror-impl"
|
||||||
|
version = "1.0.56"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time"
|
name = "time"
|
||||||
version = "0.3.32"
|
version = "0.3.32"
|
||||||
|
|
|
@ -10,3 +10,6 @@ serde = { version = "1.0", features = ["derive"]}
|
||||||
reqwest = { version = "0.11", features = ["blocking", "json"]}
|
reqwest = { version = "0.11", features = ["blocking", "json"]}
|
||||||
strum_macros = { version = "0.26.1"}
|
strum_macros = { version = "0.26.1"}
|
||||||
time = { version = "0.3.32", features = ["serde", "serde-well-known"]}
|
time = { version = "0.3.32", features = ["serde", "serde-well-known"]}
|
||||||
|
serde_json = "1.0.113"
|
||||||
|
serde_repr = "0.1.18"
|
||||||
|
thiserror = "1.0.56"
|
||||||
|
|
188
src/fronious.rs
188
src/fronious.rs
|
@ -1,16 +1,96 @@
|
||||||
use serde::{Deserialize, Serialize};
|
use reqwest::{blocking::Client, Url};
|
||||||
use std::net::IpAddr;
|
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||||
|
use serde_repr::{Deserialize_repr, Serialize_repr};
|
||||||
|
use std::{borrow::Borrow, collections::HashMap, net::IpAddr};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Error)]
|
||||||
pub struct ValueError(String);
|
pub enum Error {
|
||||||
|
#[error("unsupported API version {0}")]
|
||||||
impl std::fmt::Display for ValueError {
|
UnsupportedApiVersion(u64),
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
#[error("invalid endpoint {0:?}")]
|
||||||
write!(f, "Invalid value given! Reason: {}", self.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),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::error::Error for ValueError {}
|
pub struct Fronius {
|
||||||
|
client: Client,
|
||||||
|
base_url: Url,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Fronius {
|
||||||
|
pub fn connect(ip: IpAddr) -> Result<Self, Error> {
|
||||||
|
let client = Client::new();
|
||||||
|
|
||||||
|
let mut url = reqwest::Url::parse("http://placeholder.local/solar_api/GetAPIVersion.cgi")
|
||||||
|
.expect("Initial base URL should be valid");
|
||||||
|
url.set_ip_host(ip)
|
||||||
|
.expect("Base URL should be a valid base");
|
||||||
|
let api_version: ApiVersion = client.get(url.clone()).send()?.json()?;
|
||||||
|
|
||||||
|
if api_version.api_version != 1 {
|
||||||
|
return Err(Error::UnsupportedApiVersion(api_version.api_version));
|
||||||
|
}
|
||||||
|
|
||||||
|
url.set_path(&api_version.base_url);
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
client,
|
||||||
|
base_url: url,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_request_inner(&self, url: Url) -> Result<serde_json::Value, Error> {
|
||||||
|
let response: FroniousResponse<serde_json::Value> = self.client.get(url).send()?.json()?;
|
||||||
|
|
||||||
|
if response.head.status.code != StatusCode::Okay {
|
||||||
|
return Err(Error::Response(response.head.status));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(response.body)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn make_request<T, I, K, V>(&self, endpoint: &str, params: I) -> Result<T, Error>
|
||||||
|
where
|
||||||
|
T: DeserializeOwned,
|
||||||
|
I: IntoIterator,
|
||||||
|
I::Item: Borrow<(K, V)>,
|
||||||
|
K: AsRef<str>,
|
||||||
|
V: AsRef<str>,
|
||||||
|
{
|
||||||
|
let mut url = self
|
||||||
|
.base_url
|
||||||
|
.join(endpoint)
|
||||||
|
.map_err(|_e| Error::InvalidEndpoint(endpoint.to_string()))?;
|
||||||
|
url.query_pairs_mut().extend_pairs(params);
|
||||||
|
let body = self.make_request_inner(url)?;
|
||||||
|
|
||||||
|
Ok(T::deserialize(body)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_inverter_realtime_data_device<C: DataCollection>(
|
||||||
|
&self,
|
||||||
|
device_id: DeviceId,
|
||||||
|
) -> Result<C, Error> {
|
||||||
|
let device_id = u8::from(device_id).to_string();
|
||||||
|
|
||||||
|
let response: CommonResponseBody<_> = self.make_request(
|
||||||
|
"GetInverterRealtimeData.cgi",
|
||||||
|
[
|
||||||
|
("Scope", "Device"),
|
||||||
|
("DeviceId", &device_id),
|
||||||
|
("DataCollection", C::param_value()),
|
||||||
|
],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(response.data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
#[serde(rename_all = "PascalCase")]
|
||||||
|
@ -19,7 +99,7 @@ pub struct FroniousResponse<T> {
|
||||||
body: T,
|
body: T,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize_repr, Deserialize_repr)]
|
||||||
#[repr(u8)]
|
#[repr(u8)]
|
||||||
pub enum StatusCode {
|
pub enum StatusCode {
|
||||||
Okay = 0,
|
Okay = 0,
|
||||||
|
@ -49,7 +129,7 @@ pub struct Status {
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
#[serde(rename_all = "PascalCase")]
|
||||||
pub struct CommonResponseHeader {
|
pub struct CommonResponseHeader {
|
||||||
request_arguments: std::collections::HashMap<String, String>,
|
request_arguments: HashMap<String, String>,
|
||||||
status: Status,
|
status: Status,
|
||||||
#[serde(with = "time::serde::rfc3339")]
|
#[serde(with = "time::serde::rfc3339")]
|
||||||
timestamp: time::OffsetDateTime,
|
timestamp: time::OffsetDateTime,
|
||||||
|
@ -72,43 +152,33 @@ pub struct CommonResponseBody<T> {
|
||||||
#[serde(rename_all = "PascalCase")]
|
#[serde(rename_all = "PascalCase")]
|
||||||
pub struct UnitAndValues<T> {
|
pub struct UnitAndValues<T> {
|
||||||
unit: String,
|
unit: String,
|
||||||
values: std::collections::HashMap<String, T>,
|
values: HashMap<String, T>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
#[serde(rename_all = "PascalCase")]
|
||||||
pub struct ApiVersion {
|
struct ApiVersion {
|
||||||
#[serde(rename = "APIVersion")]
|
#[serde(rename = "APIVersion")]
|
||||||
api_version: i8,
|
api_version: u64,
|
||||||
#[serde(rename = "BaseURL")]
|
#[serde(rename = "BaseURL")]
|
||||||
base_url: String,
|
base_url: String,
|
||||||
compatibility_range: String,
|
compatibility_range: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_api_version(ip: IpAddr) -> Result<ApiVersion, Box<dyn std::error::Error>> {
|
|
||||||
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 enum Scope {
|
|
||||||
System,
|
|
||||||
Device {
|
|
||||||
device_id: DeviceId,
|
|
||||||
data_collection: DataCollection,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct DeviceId(u8);
|
pub struct DeviceId(u8);
|
||||||
|
|
||||||
impl TryFrom<u8> for DeviceId {
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
|
||||||
type Error = ValueError;
|
#[error("invalid device ID, must be less than 100: {0}")]
|
||||||
|
pub struct InvalidDeviceId(u8);
|
||||||
|
|
||||||
fn try_from(device_id: u8) -> Result<Self, ValueError> {
|
impl TryFrom<u8> for DeviceId {
|
||||||
|
type Error = InvalidDeviceId;
|
||||||
|
|
||||||
|
fn try_from(device_id: u8) -> Result<Self, InvalidDeviceId> {
|
||||||
if device_id <= 99 {
|
if device_id <= 99 {
|
||||||
Ok(Self(device_id))
|
Ok(Self(device_id))
|
||||||
} else {
|
} else {
|
||||||
Err(ValueError("device id not in range!".into()))
|
Err(InvalidDeviceId(device_id))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -119,48 +189,24 @@ impl From<DeviceId> for u8 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(strum_macros::Display)]
|
pub trait DataCollection: DeserializeOwned {
|
||||||
pub enum DataCollection {
|
/// Returns the value of the `DataCollection` GET parameter for this collection.
|
||||||
CumulationInverterData,
|
fn param_value() -> &'static str;
|
||||||
CommonInverterData,
|
|
||||||
ThreePInverterData,
|
|
||||||
MinMaxInverterData,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||||
pub struct CumulationInverterData<T> {
|
pub struct CumulationInverterData {
|
||||||
pac: T,
|
pac: UnitAndValue<u64>,
|
||||||
day_energy: T,
|
day_energy: UnitAndValue<f64>,
|
||||||
year_energy: T,
|
year_energy: UnitAndValue<f64>,
|
||||||
total_energy: T,
|
total_energy: UnitAndValue<f64>,
|
||||||
device_status: Option<std::collections::HashMap<String, String>>,
|
#[serde(rename = "DeviceStatus")]
|
||||||
|
device_status: Option<HashMap<String, serde_json::Value>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_inverter_realtime_data(
|
impl DataCollection for CumulationInverterData {
|
||||||
ip: IpAddr,
|
fn param_value() -> &'static str {
|
||||||
scope: Scope,
|
"CumulationInverterData"
|
||||||
) -> Result<FroniousResponse, Box<dyn std::error::Error>> {
|
|
||||||
let mut params: Vec<(&str, String)> = vec![];
|
|
||||||
|
|
||||||
match scope {
|
|
||||||
Scope::System => {
|
|
||||||
params.push(("Scope", "System".to_owned()));
|
|
||||||
}
|
|
||||||
Scope::Device {
|
|
||||||
device_id,
|
|
||||||
data_collection,
|
|
||||||
} => {
|
|
||||||
params.push(("Scope", "Device".to_owned()));
|
|
||||||
params.push(("DeviceId", (u8::from(device_id)).to_string()));
|
|
||||||
params.push(("DataCollection", data_collection.to_string()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut url = reqwest::Url::parse_with_params(
|
|
||||||
&format!("http://placeholder.local/solar_api/v1/GetInverterRealtimeData.cgi"),
|
|
||||||
¶ms,
|
|
||||||
)?;
|
|
||||||
let _ = url.set_ip_host(ip);
|
|
||||||
Ok(reqwest::blocking::Client::new().get(url).send()?.json()?)
|
|
||||||
}
|
|
||||||
|
|
10
src/main.rs
10
src/main.rs
|
@ -1,9 +1,17 @@
|
||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
|
|
||||||
|
use fronious::{CumulationInverterData, DeviceId, Fronius};
|
||||||
mod fronious;
|
mod fronious;
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let ip = IpAddr::V4(std::net::Ipv4Addr::new(10, 69, 0, 50));
|
let ip = IpAddr::V4(std::net::Ipv4Addr::new(10, 69, 0, 50));
|
||||||
println!("{:#?}", fronious::get_api_version(ip)?);
|
let fronius = Fronius::connect(ip)?;
|
||||||
|
println!(
|
||||||
|
"{:?}",
|
||||||
|
fronius.get_inverter_realtime_data_device::<CumulationInverterData>(
|
||||||
|
DeviceId::try_from(0).unwrap(),
|
||||||
|
)?
|
||||||
|
);
|
||||||
//println!("{:#?}", fronious::get_inverter_realtime_data(ip, fronious::Scope::System)?);
|
//println!("{:#?}", fronious::get_inverter_realtime_data(ip, fronious::Scope::System)?);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue