From 5a2889a0f28c403b649b9d9bfc65f97c5ef6774c Mon Sep 17 00:00:00 2001 From: Paul van Tilburg Date: Sun, 15 Jan 2023 13:23:34 +0100 Subject: [PATCH] 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 --- src/services/hoymiles.rs | 111 +++++++++++++++++++++++++++++++-------- 1 file changed, 89 insertions(+), 22 deletions(-) diff --git a/src/services/hoymiles.rs b/src/services/hoymiles.rs index 46575ad..fb30846 100644 --- a/src/services/hoymiles.rs +++ b/src/services/hoymiles.rs @@ -67,9 +67,68 @@ fn api_url() -> Result { 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, D::Error> +where + D: Deserializer<'de>, + D::Error: serde::de::Error, + T: Deserialize<'de>, +{ + use serde::de::Error; + + match >::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 +where + D: Deserializer<'de>, + D::Error: serde::de::Error, +{ + use serde::de::Error; + + let s = <&str>::deserialize(deserializer)?; + s.parse::().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 +where + D: Deserializer<'de>, + D::Error: serde::de::Error, +{ + use serde::de::Error; + + let s = <&str>::deserialize(deserializer)?; + s.parse::().map_err(D::Error::custom) +} + /// The request passed to the API login endpoint. #[derive(Debug, Serialize)] struct ApiLoginRequest { + /// The body of the API login request. body: ApiLoginRequestBody, } @@ -93,17 +152,23 @@ impl ApiLoginRequest { /// The request body passed to the API login endpoint. #[derive(Debug, Serialize)] struct ApiLoginRequestBody { + /// The username to login with. password: String, + /// The password to login with. user_name: String, } /// The response returned by the API login endpoint. #[derive(Debug, Deserialize)] struct ApiLoginResponse { - // status: String, - // message: String, - /// The embedded response data - data: ApiLoginResponseData, + /// The status (error) code as a string: 0 for OK, another number for error. + #[serde(deserialize_with = "from_integer_str")] + status: u16, + /// The status message. + message: String, + /// The embedded response data. + #[serde(deserialize_with = "from_empty_str_or_object")] + data: Option, // systemNotice: Option, } @@ -117,6 +182,7 @@ struct ApiLoginResponseData { /// The request passed to the API data endpoint. #[derive(Debug, Serialize)] struct ApiDataRequest { + /// The body of the API data request. body: ApiDataRequestBody, } @@ -132,31 +198,24 @@ impl ApiDataRequest { /// The request body passed to the API data endpoint. #[derive(Debug, Serialize)] struct ApiDataRequestBody { + /// The ID of the Hoymiles station. sid: u32, } /// The response returned by the API data endpoint. #[derive(Debug, Deserialize)] struct ApiDataResponse { - // status: String, - // message: String, - // /// The embedded response data - data: ApiDataResponseData, + /// The status (error) code as a string: 0 for OK, another number for error. + #[serde(deserialize_with = "from_integer_str")] + status: u16, + /// The status message. + message: String, + /// The embedded response data. + #[serde(deserialize_with = "from_empty_str_or_object")] + data: Option, // systemNotice: Option, } -/// Deserializes a string ([`&str`]) into a float ([`f32`]). -fn from_float_str<'de, D>(deserializer: D) -> Result -where - D: Deserializer<'de>, - D::Error: serde::de::Error, -{ - use serde::de::Error; - - let s = <&str>::deserialize(deserializer)?; - s.parse::().map_err(D::Error::custom) -} - /// The response data returned by the API data endpoint. #[derive(Debug, Deserialize)] struct ApiDataResponseData { @@ -206,7 +265,11 @@ impl super::Service for Service { .send() .await?; let login_response_data = match login_response.error_for_status() { - Ok(res) => res.json::().await?.data, + Ok(res) => { + let api_response = res.json::().await?; + eprintln!("api_response = {:#?}", &api_response); + api_response.data.expect("No API response data found") + } Err(err) => return Err(err), }; // 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() .await?; let api_data = match api_response.error_for_status() { - Ok(res) => res.json::().await?.data, + Ok(res) => { + let api_response = res.json::().await?; + eprintln!("api_response = {:#?}", &api_response); + api_response.data.expect("No API response data found") + } Err(err) => return Err(err), }; let current_w = api_data.real_power;