Improve deserialization for Hoymiles
* Also deserialize the status (error) code and message * Handle `data` fields having the value `""` in API responses if there is an error * Add missing documentation for API struct fields
This commit is contained in:
parent
18b52cd422
commit
5a2889a0f2
|
@ -67,9 +67,68 @@ fn api_url() -> Result<Url, ParseError> {
|
||||||
Url::parse(&format!("{BASE_URL}/pvm-data/data_count_station_real_data"))
|
Url::parse(&format!("{BASE_URL}/pvm-data/data_count_station_real_data"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Captures JSON values that can either be a string or an object.
|
||||||
|
///
|
||||||
|
/// This is used for the API responses where the data field is either an object or an empty string
|
||||||
|
/// instead of `null`.
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
enum StringOrObject<'a, T> {
|
||||||
|
/// The value is an object (deserializable as type `T`).
|
||||||
|
Object(T),
|
||||||
|
/// The value is a string.
|
||||||
|
String(&'a str),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deserialize either a string or an object as an option of type `T`.
|
||||||
|
fn from_empty_str_or_object<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
D::Error: serde::de::Error,
|
||||||
|
T: Deserialize<'de>,
|
||||||
|
{
|
||||||
|
use serde::de::Error;
|
||||||
|
|
||||||
|
match <StringOrObject<'_, T>>::deserialize(deserializer) {
|
||||||
|
Ok(StringOrObject::String(s)) if s.is_empty() => Ok(None),
|
||||||
|
Ok(StringOrObject::String(_)) => Err(Error::custom("Non-empty string not allowed here")),
|
||||||
|
Ok(StringOrObject::Object(t)) => Ok(Some(t)),
|
||||||
|
Err(err) => Err(err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deserializes a string ([`&str`]) into a float ([`f32`]).
|
||||||
|
///
|
||||||
|
/// This is used for the API responses where the value is a float put into a string.
|
||||||
|
fn from_float_str<'de, D>(deserializer: D) -> Result<f32, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
D::Error: serde::de::Error,
|
||||||
|
{
|
||||||
|
use serde::de::Error;
|
||||||
|
|
||||||
|
let s = <&str>::deserialize(deserializer)?;
|
||||||
|
s.parse::<f32>().map_err(D::Error::custom)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deserializes a string ([`&str`]) into an integer ([`u16`]).
|
||||||
|
///
|
||||||
|
/// This is used for the API responses where the value is an integer put into a string.
|
||||||
|
fn from_integer_str<'de, D>(deserializer: D) -> Result<u16, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
D::Error: serde::de::Error,
|
||||||
|
{
|
||||||
|
use serde::de::Error;
|
||||||
|
|
||||||
|
let s = <&str>::deserialize(deserializer)?;
|
||||||
|
s.parse::<u16>().map_err(D::Error::custom)
|
||||||
|
}
|
||||||
|
|
||||||
/// The request passed to the API login endpoint.
|
/// The request passed to the API login endpoint.
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
struct ApiLoginRequest {
|
struct ApiLoginRequest {
|
||||||
|
/// The body of the API login request.
|
||||||
body: ApiLoginRequestBody,
|
body: ApiLoginRequestBody,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,17 +152,23 @@ impl ApiLoginRequest {
|
||||||
/// The request body passed to the API login endpoint.
|
/// The request body passed to the API login endpoint.
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
struct ApiLoginRequestBody {
|
struct ApiLoginRequestBody {
|
||||||
|
/// The username to login with.
|
||||||
password: String,
|
password: String,
|
||||||
|
/// The password to login with.
|
||||||
user_name: String,
|
user_name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The response returned by the API login endpoint.
|
/// The response returned by the API login endpoint.
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct ApiLoginResponse {
|
struct ApiLoginResponse {
|
||||||
// status: String,
|
/// The status (error) code as a string: 0 for OK, another number for error.
|
||||||
// message: String,
|
#[serde(deserialize_with = "from_integer_str")]
|
||||||
/// The embedded response data
|
status: u16,
|
||||||
data: ApiLoginResponseData,
|
/// The status message.
|
||||||
|
message: String,
|
||||||
|
/// The embedded response data.
|
||||||
|
#[serde(deserialize_with = "from_empty_str_or_object")]
|
||||||
|
data: Option<ApiLoginResponseData>,
|
||||||
// systemNotice: Option<String>,
|
// systemNotice: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,6 +182,7 @@ struct ApiLoginResponseData {
|
||||||
/// The request passed to the API data endpoint.
|
/// The request passed to the API data endpoint.
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
struct ApiDataRequest {
|
struct ApiDataRequest {
|
||||||
|
/// The body of the API data request.
|
||||||
body: ApiDataRequestBody,
|
body: ApiDataRequestBody,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,31 +198,24 @@ impl ApiDataRequest {
|
||||||
/// The request body passed to the API data endpoint.
|
/// The request body passed to the API data endpoint.
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
struct ApiDataRequestBody {
|
struct ApiDataRequestBody {
|
||||||
|
/// The ID of the Hoymiles station.
|
||||||
sid: u32,
|
sid: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The response returned by the API data endpoint.
|
/// The response returned by the API data endpoint.
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct ApiDataResponse {
|
struct ApiDataResponse {
|
||||||
// status: String,
|
/// The status (error) code as a string: 0 for OK, another number for error.
|
||||||
// message: String,
|
#[serde(deserialize_with = "from_integer_str")]
|
||||||
// /// The embedded response data
|
status: u16,
|
||||||
data: ApiDataResponseData,
|
/// The status message.
|
||||||
|
message: String,
|
||||||
|
/// The embedded response data.
|
||||||
|
#[serde(deserialize_with = "from_empty_str_or_object")]
|
||||||
|
data: Option<ApiDataResponseData>,
|
||||||
// systemNotice: Option<String>,
|
// systemNotice: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Deserializes a string ([`&str`]) into a float ([`f32`]).
|
|
||||||
fn from_float_str<'de, D>(deserializer: D) -> Result<f32, D::Error>
|
|
||||||
where
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
D::Error: serde::de::Error,
|
|
||||||
{
|
|
||||||
use serde::de::Error;
|
|
||||||
|
|
||||||
let s = <&str>::deserialize(deserializer)?;
|
|
||||||
s.parse::<f32>().map_err(D::Error::custom)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The response data returned by the API data endpoint.
|
/// The response data returned by the API data endpoint.
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct ApiDataResponseData {
|
struct ApiDataResponseData {
|
||||||
|
@ -206,7 +265,11 @@ impl super::Service for Service {
|
||||||
.send()
|
.send()
|
||||||
.await?;
|
.await?;
|
||||||
let login_response_data = match login_response.error_for_status() {
|
let login_response_data = match login_response.error_for_status() {
|
||||||
Ok(res) => res.json::<ApiLoginResponse>().await?.data,
|
Ok(res) => {
|
||||||
|
let api_response = res.json::<ApiLoginResponse>().await?;
|
||||||
|
eprintln!("api_response = {:#?}", &api_response);
|
||||||
|
api_response.data.expect("No API response data found")
|
||||||
|
}
|
||||||
Err(err) => return Err(err),
|
Err(err) => return Err(err),
|
||||||
};
|
};
|
||||||
// Insert the token in the reponse data as the cookie `hm_token` into the cookie jar.
|
// Insert the token in the reponse data as the cookie `hm_token` into the cookie jar.
|
||||||
|
@ -231,7 +294,11 @@ impl super::Service for Service {
|
||||||
.send()
|
.send()
|
||||||
.await?;
|
.await?;
|
||||||
let api_data = match api_response.error_for_status() {
|
let api_data = match api_response.error_for_status() {
|
||||||
Ok(res) => res.json::<ApiDataResponse>().await?.data,
|
Ok(res) => {
|
||||||
|
let api_response = res.json::<ApiDataResponse>().await?;
|
||||||
|
eprintln!("api_response = {:#?}", &api_response);
|
||||||
|
api_response.data.expect("No API response data found")
|
||||||
|
}
|
||||||
Err(err) => return Err(err),
|
Err(err) => return Err(err),
|
||||||
};
|
};
|
||||||
let current_w = api_data.real_power;
|
let current_w = api_data.real_power;
|
||||||
|
|
Loading…
Reference in New Issue