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:
Paul van Tilburg 2023-01-15 16:31:37 +01:00
parent e0151c3cde
commit ddcb375345
Signed by: paul
GPG Key ID: C6DE073EDA9EEC4D
6 changed files with 61 additions and 18 deletions

21
Cargo.lock generated
View File

@ -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"

View File

@ -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"

View File

@ -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>;
}

View File

@ -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;

View File

@ -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 {

View File

@ -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!");