Introduce an error type for services
As a result, services don't always have to provide a `reqwest::Error` but also return other errors. The error variant `Error::NotAuthorized` in particular specifies that requests are not or no longer allowed and a login should be (re)attempted. This way, services can indicate that it is in this state and not have to provided a 403 status code `reqwest::Error` to show this. Add a depend on the `thiserror` crate for this.
This commit is contained in:
parent
e0151c3cde
commit
ddcb375345
|
@ -1670,6 +1670,7 @@ dependencies = [
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"rocket",
|
"rocket",
|
||||||
"serde",
|
"serde",
|
||||||
|
"thiserror",
|
||||||
"toml",
|
"toml",
|
||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
@ -1738,6 +1739,26 @@ dependencies = [
|
||||||
"winapi-util",
|
"winapi-util",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror"
|
||||||
|
version = "1.0.38"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror-impl",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror-impl"
|
||||||
|
version = "1.0.38"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thread_local"
|
name = "thread_local"
|
||||||
version = "1.1.4"
|
version = "1.1.4"
|
||||||
|
|
|
@ -20,6 +20,7 @@ once_cell = "1.9.0"
|
||||||
reqwest = { version = "0.11.6", features = ["cookies", "json"] }
|
reqwest = { version = "0.11.6", features = ["cookies", "json"] }
|
||||||
rocket = { version = "0.5.0-rc.2", features = ["json"] }
|
rocket = { version = "0.5.0-rc.2", features = ["json"] }
|
||||||
serde = "1.0.116"
|
serde = "1.0.116"
|
||||||
|
thiserror = "1.0.38"
|
||||||
toml = "0.5.6"
|
toml = "0.5.6"
|
||||||
url = "2.2.2"
|
url = "2.2.2"
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,22 @@ pub(crate) fn get(config: Config) -> color_eyre::Result<Services> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The errors that can occur during service API transactions.
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub(crate) enum Error {
|
||||||
|
/// The service is not or no longer authorized to perform requests.
|
||||||
|
///
|
||||||
|
/// This usually indicates that the service needs to login again.
|
||||||
|
#[error("not/no longer authorized")]
|
||||||
|
NotAuthorized,
|
||||||
|
/// The services encountered some other API request error.
|
||||||
|
#[error("API request error")]
|
||||||
|
Request(#[from] reqwest::Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Type alias for service results.
|
||||||
|
pub(crate) type Result<T, E = Error> = std::result::Result<T, E>;
|
||||||
|
|
||||||
/// The supported cloud services.
|
/// The supported cloud services.
|
||||||
#[enum_dispatch(Service)]
|
#[enum_dispatch(Service)]
|
||||||
pub(crate) enum Services {
|
pub(crate) enum Services {
|
||||||
|
@ -44,8 +60,8 @@ pub(crate) trait Service {
|
||||||
fn poll_interval(&self) -> u64;
|
fn poll_interval(&self) -> u64;
|
||||||
|
|
||||||
/// Perfoms a login on the cloud service (if necessary).
|
/// Perfoms a login on the cloud service (if necessary).
|
||||||
async fn login(&mut self) -> Result<(), reqwest::Error>;
|
async fn login(&mut self) -> Result<()>;
|
||||||
|
|
||||||
/// Retrieves a status update using the API of the cloud service.
|
/// Retrieves a status update using the API of the cloud service.
|
||||||
async fn update(&mut self, timestamp: u64) -> Result<Status, reqwest::Error>;
|
async fn update(&mut self, timestamp: u64) -> Result<Status>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ use rocket::async_trait;
|
||||||
use serde::{Deserialize, Deserializer, Serialize};
|
use serde::{Deserialize, Deserializer, Serialize};
|
||||||
use url::ParseError;
|
use url::ParseError;
|
||||||
|
|
||||||
use crate::Status;
|
use crate::{services::Result, Status};
|
||||||
|
|
||||||
/// The base URL of Hoymiles API gateway.
|
/// The base URL of Hoymiles API gateway.
|
||||||
const BASE_URL: &str = "https://global.hoymiles.com/platform/api/gateway";
|
const BASE_URL: &str = "https://global.hoymiles.com/platform/api/gateway";
|
||||||
|
@ -36,7 +36,7 @@ pub(crate) struct Config {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Instantiates the Hoymiles service.
|
/// Instantiates the Hoymiles service.
|
||||||
pub(crate) fn service(config: Config) -> Result<Service, reqwest::Error> {
|
pub(crate) fn service(config: Config) -> Result<Service> {
|
||||||
let cookie_jar = Arc::new(CookieJar::default());
|
let cookie_jar = Arc::new(CookieJar::default());
|
||||||
let client = ClientBuilder::new()
|
let client = ClientBuilder::new()
|
||||||
.cookie_provider(Arc::clone(&cookie_jar))
|
.cookie_provider(Arc::clone(&cookie_jar))
|
||||||
|
@ -276,7 +276,7 @@ impl super::Service for Service {
|
||||||
/// It mainly stores the acquired cookies in the client's cookie jar and adds the token cookie
|
/// It mainly stores the acquired cookies in the client's cookie jar and adds the token cookie
|
||||||
/// provided by the logins response. The login credentials come from the loaded configuration
|
/// provided by the logins response. The login credentials come from the loaded configuration
|
||||||
/// (see [`Config`]).
|
/// (see [`Config`]).
|
||||||
async fn login(&mut self) -> Result<(), reqwest::Error> {
|
async fn login(&mut self) -> Result<()> {
|
||||||
let base_url = Url::parse(BASE_URL).expect("valid base URL");
|
let base_url = Url::parse(BASE_URL).expect("valid base URL");
|
||||||
let login_url = login_url().expect("valid login URL");
|
let login_url = login_url().expect("valid login URL");
|
||||||
let login_request = ApiLoginRequest::new(&self.config.username, &self.config.password);
|
let login_request = ApiLoginRequest::new(&self.config.username, &self.config.password);
|
||||||
|
@ -292,7 +292,7 @@ impl super::Service for Service {
|
||||||
eprintln!("api_response = {:#?}", &api_response);
|
eprintln!("api_response = {:#?}", &api_response);
|
||||||
api_response.data.expect("No API response data found")
|
api_response.data.expect("No API response data found")
|
||||||
}
|
}
|
||||||
Err(err) => return Err(err),
|
Err(err) => return Err(err.into()),
|
||||||
};
|
};
|
||||||
// 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.
|
||||||
let cookie = format!("hm_token={}", login_response_data.token);
|
let cookie = format!("hm_token={}", login_response_data.token);
|
||||||
|
@ -306,7 +306,7 @@ impl super::Service for Service {
|
||||||
/// It needs the cookies from the login to be able to perform the action.
|
/// It needs the cookies from the login to be able to perform the action.
|
||||||
/// It uses a endpoint to construct the [`Status`] struct, but it needs to summarize the today
|
/// It uses a endpoint to construct the [`Status`] struct, but it needs to summarize the today
|
||||||
/// value with the total value because Hoymiles only includes it after the day has finished.
|
/// value with the total value because Hoymiles only includes it after the day has finished.
|
||||||
async fn update(&mut self, _last_updated: u64) -> Result<Status, reqwest::Error> {
|
async fn update(&mut self, _last_updated: u64) -> Result<Status> {
|
||||||
let api_url = api_url().expect("valid API power URL");
|
let api_url = api_url().expect("valid API power URL");
|
||||||
let api_data_request = ApiDataRequest::new(self.config.sid);
|
let api_data_request = ApiDataRequest::new(self.config.sid);
|
||||||
let api_response = self
|
let api_response = self
|
||||||
|
@ -321,7 +321,7 @@ impl super::Service for Service {
|
||||||
eprintln!("api_response = {:#?}", &api_response);
|
eprintln!("api_response = {:#?}", &api_response);
|
||||||
api_response.data.expect("No API response data found")
|
api_response.data.expect("No API response data found")
|
||||||
}
|
}
|
||||||
Err(err) => return Err(err),
|
Err(err) => return Err(err.into()),
|
||||||
};
|
};
|
||||||
let current_w = api_data.real_power;
|
let current_w = api_data.real_power;
|
||||||
let mut total_kwh = (api_data.total_eq + api_data.today_eq) / 1000.0;
|
let mut total_kwh = (api_data.total_eq + api_data.today_eq) / 1000.0;
|
||||||
|
|
|
@ -4,12 +4,15 @@
|
||||||
//! to retrieve the energy data (using the session cookies).
|
//! to retrieve the energy data (using the session cookies).
|
||||||
//! See also: <https://my.autarco.com>.
|
//! See also: <https://my.autarco.com>.
|
||||||
|
|
||||||
use reqwest::{Client, ClientBuilder, Url};
|
use reqwest::{Client, ClientBuilder, StatusCode, Url};
|
||||||
use rocket::async_trait;
|
use rocket::async_trait;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use url::ParseError;
|
use url::ParseError;
|
||||||
|
|
||||||
use crate::Status;
|
use crate::{
|
||||||
|
services::{Error, Result},
|
||||||
|
Status,
|
||||||
|
};
|
||||||
|
|
||||||
/// The base URL of My Autarco site.
|
/// The base URL of My Autarco site.
|
||||||
const BASE_URL: &str = "https://my.autarco.com";
|
const BASE_URL: &str = "https://my.autarco.com";
|
||||||
|
@ -29,7 +32,7 @@ pub(crate) struct Config {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Instantiates the My Autarco service.
|
/// Instantiates the My Autarco service.
|
||||||
pub(crate) fn service(config: Config) -> Result<Service, reqwest::Error> {
|
pub(crate) fn service(config: Config) -> Result<Service> {
|
||||||
let client = ClientBuilder::new().cookie_store(true).build()?;
|
let client = ClientBuilder::new().cookie_store(true).build()?;
|
||||||
let service = Service { client, config };
|
let service = Service { client, config };
|
||||||
|
|
||||||
|
@ -86,7 +89,7 @@ impl super::Service for Service {
|
||||||
///
|
///
|
||||||
/// It mainly stores the acquired cookie in the client's cookie jar. The login credentials come
|
/// It mainly stores the acquired cookie in the client's cookie jar. The login credentials come
|
||||||
/// from the loaded configuration (see [`Config`]).
|
/// from the loaded configuration (see [`Config`]).
|
||||||
async fn login(&mut self) -> Result<(), reqwest::Error> {
|
async fn login(&mut self) -> Result<()> {
|
||||||
let params = [
|
let params = [
|
||||||
("username", &self.config.username),
|
("username", &self.config.username),
|
||||||
("password", &self.config.password),
|
("password", &self.config.password),
|
||||||
|
@ -101,20 +104,23 @@ impl super::Service for Service {
|
||||||
///
|
///
|
||||||
/// It needs the cookie from the login to be able to perform the action. It uses both the
|
/// It needs the cookie from the login to be able to perform the action. It uses both the
|
||||||
/// `energy` and `power` endpoint to construct the [`Status`] struct.
|
/// `energy` and `power` endpoint to construct the [`Status`] struct.
|
||||||
async fn update(&mut self, last_updated: u64) -> Result<Status, reqwest::Error> {
|
async fn update(&mut self, last_updated: u64) -> Result<Status> {
|
||||||
// Retrieve the data from the API endpoints.
|
// Retrieve the data from the API endpoints.
|
||||||
let api_energy_url = api_url(&self.config.site_id, "energy").expect("valid API energy URL");
|
let api_energy_url = api_url(&self.config.site_id, "energy").expect("valid API energy URL");
|
||||||
let api_response = self.client.get(api_energy_url).send().await?;
|
let api_response = self.client.get(api_energy_url).send().await?;
|
||||||
let api_energy = match api_response.error_for_status() {
|
let api_energy = match api_response.error_for_status() {
|
||||||
Ok(res) => res.json::<ApiEnergy>().await?,
|
Ok(res) => res.json::<ApiEnergy>().await?,
|
||||||
Err(err) => return Err(err),
|
Err(err) if err.status() == Some(StatusCode::UNAUTHORIZED) => {
|
||||||
|
return Err(Error::NotAuthorized)
|
||||||
|
}
|
||||||
|
Err(err) => return Err(err.into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let api_power_url = api_url(&self.config.site_id, "power").expect("valid API power URL");
|
let api_power_url = api_url(&self.config.site_id, "power").expect("valid API power URL");
|
||||||
let api_response = self.client.get(api_power_url).send().await?;
|
let api_response = self.client.get(api_power_url).send().await?;
|
||||||
let api_power = match api_response.error_for_status() {
|
let api_power = match api_response.error_for_status() {
|
||||||
Ok(res) => res.json::<ApiPower>().await?,
|
Ok(res) => res.json::<ApiPower>().await?,
|
||||||
Err(err) => return Err(err),
|
Err(err) => return Err(err.into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Status {
|
Ok(Status {
|
||||||
|
|
|
@ -2,11 +2,10 @@
|
||||||
|
|
||||||
use std::time::{Duration, SystemTime};
|
use std::time::{Duration, SystemTime};
|
||||||
|
|
||||||
use reqwest::StatusCode;
|
|
||||||
use rocket::tokio::time::sleep;
|
use rocket::tokio::time::sleep;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
services::{Service, Services},
|
services::{Error, Service, Services},
|
||||||
STATUS,
|
STATUS,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -38,7 +37,7 @@ pub(super) async fn update_loop(service: Services) -> color_eyre::Result<()> {
|
||||||
|
|
||||||
let status = match service.update(timestamp).await {
|
let status = match service.update(timestamp).await {
|
||||||
Ok(status) => status,
|
Ok(status) => status,
|
||||||
Err(e) if e.status() == Some(StatusCode::UNAUTHORIZED) => {
|
Err(Error::NotAuthorized) => {
|
||||||
println!("✨ Update unauthorized, trying to log in again...");
|
println!("✨ Update unauthorized, trying to log in again...");
|
||||||
service.login().await?;
|
service.login().await?;
|
||||||
println!("⚡ Logged in successfully!");
|
println!("⚡ Logged in successfully!");
|
||||||
|
|
Loading…
Reference in New Issue