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",
|
||||
"rocket",
|
||||
"serde",
|
||||
"thiserror",
|
||||
"toml",
|
||||
"url",
|
||||
]
|
||||
|
@ -1738,6 +1739,26 @@ dependencies = [
|
|||
"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]]
|
||||
name = "thread_local"
|
||||
version = "1.1.4"
|
||||
|
|
|
@ -20,6 +20,7 @@ once_cell = "1.9.0"
|
|||
reqwest = { version = "0.11.6", features = ["cookies", "json"] }
|
||||
rocket = { version = "0.5.0-rc.2", features = ["json"] }
|
||||
serde = "1.0.116"
|
||||
thiserror = "1.0.38"
|
||||
toml = "0.5.6"
|
||||
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.
|
||||
#[enum_dispatch(Service)]
|
||||
pub(crate) enum Services {
|
||||
|
@ -44,8 +60,8 @@ pub(crate) trait Service {
|
|||
fn poll_interval(&self) -> u64;
|
||||
|
||||
/// 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.
|
||||
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 url::ParseError;
|
||||
|
||||
use crate::Status;
|
||||
use crate::{services::Result, Status};
|
||||
|
||||
/// The base URL of Hoymiles 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.
|
||||
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 client = ClientBuilder::new()
|
||||
.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
|
||||
/// provided by the logins response. The login credentials come from the loaded configuration
|
||||
/// (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 login_url = login_url().expect("valid login URL");
|
||||
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);
|
||||
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.
|
||||
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 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.
|
||||
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_data_request = ApiDataRequest::new(self.config.sid);
|
||||
let api_response = self
|
||||
|
@ -321,7 +321,7 @@ impl super::Service for Service {
|
|||
eprintln!("api_response = {:#?}", &api_response);
|
||||
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 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).
|
||||
//! See also: <https://my.autarco.com>.
|
||||
|
||||
use reqwest::{Client, ClientBuilder, Url};
|
||||
use reqwest::{Client, ClientBuilder, StatusCode, Url};
|
||||
use rocket::async_trait;
|
||||
use serde::Deserialize;
|
||||
use url::ParseError;
|
||||
|
||||
use crate::Status;
|
||||
use crate::{
|
||||
services::{Error, Result},
|
||||
Status,
|
||||
};
|
||||
|
||||
/// The base URL of My Autarco site.
|
||||
const BASE_URL: &str = "https://my.autarco.com";
|
||||
|
@ -29,7 +32,7 @@ pub(crate) struct Config {
|
|||
}
|
||||
|
||||
/// 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 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
|
||||
/// from the loaded configuration (see [`Config`]).
|
||||
async fn login(&mut self) -> Result<(), reqwest::Error> {
|
||||
async fn login(&mut self) -> Result<()> {
|
||||
let params = [
|
||||
("username", &self.config.username),
|
||||
("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
|
||||
/// `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.
|
||||
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_energy = match api_response.error_for_status() {
|
||||
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_response = self.client.get(api_power_url).send().await?;
|
||||
let api_power = match api_response.error_for_status() {
|
||||
Ok(res) => res.json::<ApiPower>().await?,
|
||||
Err(err) => return Err(err),
|
||||
Err(err) => return Err(err.into()),
|
||||
};
|
||||
|
||||
Ok(Status {
|
||||
|
|
|
@ -2,11 +2,10 @@
|
|||
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use reqwest::StatusCode;
|
||||
use rocket::tokio::time::sleep;
|
||||
|
||||
use crate::{
|
||||
services::{Service, Services},
|
||||
services::{Error, Service, Services},
|
||||
STATUS,
|
||||
};
|
||||
|
||||
|
@ -38,7 +37,7 @@ pub(super) async fn update_loop(service: Services) -> color_eyre::Result<()> {
|
|||
|
||||
let status = match service.update(timestamp).await {
|
||||
Ok(status) => status,
|
||||
Err(e) if e.status() == Some(StatusCode::UNAUTHORIZED) => {
|
||||
Err(Error::NotAuthorized) => {
|
||||
println!("✨ Update unauthorized, trying to log in again...");
|
||||
service.login().await?;
|
||||
println!("⚡ Logged in successfully!");
|
||||
|
|
Loading…
Reference in New Issue