Compare commits

...

18 Commits

Author SHA1 Message Date
Paul van Tilburg 35209b6303
Bump the version to 0.2.1 2023-01-16 20:16:30 +01:00
Paul van Tilburg 6707928e37
Update the changelog 2023-01-16 20:15:54 +01:00
Paul van Tilburg e6b0357670
Add serde from rocket; drop depend on serde 2023-01-16 20:07:56 +01:00
Paul van Tilburg 365b847313
Use stderr and a different emoji for error log messages 2023-01-16 20:03:16 +01:00
Paul van Tilburg e1d70e8a59
Catch and raise when API response data cannot be deserialized
* Introduce a `StringOrObject::Value` variant that captures the
  undeserializable JSON value
* Generate an error with the undeserializable JSON value when
  deserialization is attempted
2023-01-16 20:00:53 +01:00
Paul van Tilburg e268a6ebca
Detect when API (login) responses are not correct
* Introduce the `Error::Response` variant so services can raise errors
  if the API response are not valid but a relogin will not help
* Indicate that a login failed for status (error) code 1
* Indicate that an API request failed and relogin is necessary for
  status code 1 or 100
* Raise an error on any non-zero status code otherwise with the message
2023-01-16 19:57:05 +01:00
Paul van Tilburg 93e8295c96
Small formatting and error message fixes 2023-01-15 16:44:45 +01:00
Paul van Tilburg 1d35b88aba
Set a cookie to configure the API language
It will be Simplified Chinese (`zh_cn`) otherwise.
2023-01-15 16:34:40 +01:00
Paul van Tilburg ddcb375345
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.
2023-01-15 16:31:37 +01:00
Paul van Tilburg e0151c3cde
Set last updated field to what is returned by API
In Hoymiles, the date of the last update is part of the API response.
Parse it and use that in `Status` instead of the timestamp provided by
the update loop.

Add a depend on the `chrono` crate for this.
2023-01-15 14:48:24 +01:00
Paul van Tilburg ef13f7e4f2
Update documentation about Hoymiles poll interval 2023-01-15 14:16:11 +01:00
Paul van Tilburg d787c8b3ab
Fix issue in Hoymiles where total energy decreases
Sometimes it can be that `today_eq` is reset when the day switches but
it has not been added to `total_eq` yet. The `total_eq` should always be
non-decreasing, so return the last known value until this is corrected
(this most suredly happens during the night).

Also, allow for `login` and `update` to mutate the state of the service
to be able to update things like the last known total produced energy
value.
2023-01-15 13:41:40 +01:00
Paul van Tilburg 5a2889a0f2
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
2023-01-15 13:23:34 +01:00
Paul van Tilburg 18b52cd422
Small simplification; remove already solved TODO 2023-01-15 12:26:20 +01:00
Paul van Tilburg 70b117d11d
Cargo update 2023-01-14 15:55:48 +01:00
Paul van Tilburg 2b5a64b6b0
FIx some formatting 2023-01-14 15:54:29 +01:00
Paul van Tilburg 01416ee136
Reduce poll interval for Hoymiles to 5 minutes 2023-01-14 13:03:26 +01:00
Paul van Tilburg 536b1564b9
Also set the state class in HA sensors example 2023-01-13 23:22:41 +01:00
9 changed files with 687 additions and 246 deletions

View File

@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.2.1] - 2023-01-16
### Changed
* Change poll interval for Hoymiles to 5 minutes
* Catch and raise error when Hoymiles API data responses cannot be deserialized
* Use stderr for error messages (and change prefix emoji)
* Use the `serde` crate via Rocket,; drop depend on the `serde` crate itself
### Fixed
* Also set the state class in HA sensors example
* Improve deserialization of Hoymiles API responses (#7)
* Prevent total energy reported decreasing for Hoymiles (#7)
* Set correct `last_updated` field in status report for Hoymiles (#7)
* Set cookie to configure Hoymiles API language to English (#7)
* Detect when Hoymiles (login/data) API response are not correct (#7)
* Small formatting, error message and documentation fixes
## [0.2.0] - 2023-01-13
### Added
@ -29,6 +48,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
Rename Autarco Scraper project to Solar Grabber.
[Unreleased]: https://git.luon.net/paul/solar-grabber/compare/v0.2.0...HEAD
[Unreleased]: https://git.luon.net/paul/solar-grabber/compare/v0.2.1...HEAD
[0.2.1]: https://git.luon.net/paul/solar-grabber/compare/v0.2.0...v0.2.1
[0.2.0]: https://git.luon.net/paul/solar-grabber/compare/v0.1.1...v0.2.0
[0.1.1]: https://git.luon.net/paul/solar-grabber/src/tag/v0.1.1

593
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
[package]
name = "solar-grabber"
version = "0.2.0"
version = "0.2.1"
authors = ["Paul van Tilburg <paul@luon.net>"]
edition = "2021"
description = """"
@ -12,13 +12,14 @@ repository = "https://git.luon.net/paul/solar-grabber"
license = "MIT"
[dependencies]
chrono = { version = "0.4.23", features = ["serde"] }
color-eyre = "0.6.2"
enum_dispatch = "0.3.9"
md-5 = "0.10.5"
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

@ -134,6 +134,7 @@ sensors:
value_template: '{{ value_json.current_w }}'
unit_of_measurement: W
device_class: power
state_class: measurement
- platform: rest
name: "Photovoltaic Invertor Total Energy Production"
@ -141,6 +142,7 @@ sensors:
value_template: '{{ value_json.total_kwh }}'
unit_of_measurement: kWh
device_class: energy
state_class: total_increasing
```
This assumes your Solar Grabber is running at <http://solar-grabber.domain.tld:2399>.

View File

@ -22,8 +22,11 @@ use std::sync::Mutex;
use once_cell::sync::Lazy;
use rocket::fairing::AdHoc;
use rocket::serde::json::Json;
use rocket::{get, routes, Build, Rocket};
use serde::{Deserialize, Serialize};
use rocket::{
get, routes,
serde::{Deserialize, Serialize},
Build, Rocket,
};
use self::update::update_loop;
@ -32,6 +35,7 @@ static STATUS: Lazy<Mutex<Option<Status>>> = Lazy::new(|| Mutex::new(None));
/// The configuration loaded additionally by Rocket.
#[derive(Debug, Deserialize)]
#[serde(crate = "rocket::serde")]
struct Config {
/// The service-specific configuration
service: services::Config,
@ -39,6 +43,7 @@ struct Config {
/// The current photovoltaic invertor status.
#[derive(Clone, Copy, Debug, Serialize)]
#[serde(crate = "rocket::serde")]
struct Status {
/// Current power production (W)
current_w: f32,

View File

@ -4,8 +4,7 @@ pub(crate) mod hoymiles;
pub(crate) mod my_autarco;
use enum_dispatch::enum_dispatch;
use rocket::async_trait;
use serde::Deserialize;
use rocket::{async_trait, serde::Deserialize};
use crate::Status;
@ -27,6 +26,25 @@ 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 service encountered some other API request error.
#[error("API request error: {0}")]
Request(#[from] reqwest::Error),
/// The service encountered an unsupported API response.
#[error("API service error: {0}")]
Response(String),
}
/// 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 +62,8 @@ pub(crate) trait Service {
fn poll_interval(&self) -> u64;
/// Perfoms a login on the cloud service (if necessary).
async fn login(&self) -> Result<(), reqwest::Error>;
async fn login(&mut self) -> Result<()>;
/// Retrieves a status update using the API of the cloud service.
async fn update(&self, timestamp: u64) -> Result<Status, reqwest::Error>;
async fn update(&mut self, timestamp: u64) -> Result<Status>;
}

View File

@ -6,22 +6,37 @@
use std::sync::Arc;
use chrono::{DateTime, Local, TimeZone};
use md5::{Digest, Md5};
use reqwest::{cookie::Jar as CookieJar, Client, ClientBuilder, Url};
use rocket::async_trait;
use serde::{Deserialize, Deserializer, Serialize};
use rocket::{
async_trait,
serde::{json::Value as JsonValue, Deserialize, Deserializer, Serialize},
};
use url::ParseError;
use crate::Status;
use crate::{
services::{Error, Result},
Status,
};
/// The base URL of Hoymiles API gateway.
const BASE_URL: &str = "https://global.hoymiles.com/platform/api/gateway";
/// The date/time format used by the Hoymiles API.
const DATE_TIME_FORMAT: &str = "%Y-%m-%d %H:%M:%S";
/// The language to switch the API to.
///
/// If not set, it seems it uses `zh_cn`.
const LANGUAGE: &str = "en_us";
/// The interval between data polls (in seconds).
const POLL_INTERVAL: u64 = 900;
const POLL_INTERVAL: u64 = 300;
/// The configuration necessary to access the Hoymiles.
#[derive(Debug, Deserialize)]
#[serde(crate = "rocket::serde")]
pub(crate) struct Config {
/// The username of the account to login with
username: String,
@ -32,15 +47,17 @@ 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))
.build()?;
let total_kwh = 0f32;
let service = Service {
client,
config,
cookie_jar,
total_kwh,
};
Ok(service)
@ -55,6 +72,8 @@ pub(crate) struct Service {
config: Config,
/// The cookie jar used for API requests.
cookie_jar: Arc<CookieJar>,
/// The last known total produced energy value.
total_kwh: f32,
}
/// Returns the login URL for the Hoymiles site.
@ -67,22 +86,104 @@ fn api_url() -> Result<Url, ParseError> {
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`. If the response is not deserializable object, the JSON value is preserved
/// for debugging purposes.
#[derive(Debug, Deserialize)]
#[serde(crate = "rocket::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),
/// The value is not some JSON value not deserializable as type `T`.
Value(JsonValue),
}
/// 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: rocket::serde::de::Error,
T: Deserialize<'de>,
{
use rocket::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)),
Ok(StringOrObject::Value(j)) => Err(Error::custom(&format!(
"Undeserializable JSON object: {}",
j
))),
Err(err) => Err(err),
}
}
/// Deserialize a string ([`&str`]) into a date/time ([`DateTime<Local>`]).
fn from_date_time_str<'de, D>(deserializer: D) -> Result<DateTime<Local>, D::Error>
where
D: Deserializer<'de>,
D::Error: rocket::serde::de::Error,
{
use rocket::serde::de::Error;
let s = <&str>::deserialize(deserializer)?;
Local
.datetime_from_str(s, DATE_TIME_FORMAT)
.map_err(D::Error::custom)
}
/// 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: rocket::serde::de::Error,
{
use rocket::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: rocket::serde::de::Error,
{
use rocket::serde::de::Error;
let s = <&str>::deserialize(deserializer)?;
s.parse::<u16>().map_err(D::Error::custom)
}
/// The request passed to the API login endpoint.
#[derive(Debug, Serialize)]
#[serde(crate = "rocket::serde")]
struct ApiLoginRequest {
/// The body of the API login request.
body: ApiLoginRequestBody,
}
impl ApiLoginRequest {
/// Creates a new API login request.
fn new(username: &str, password: &str) -> Self {
let user_name = username.to_owned();
let mut hasher = Md5::new();
hasher.update(password.as_bytes());
let password = format!("{:x}", hasher.finalize());
// TODO: Hash the password!
let body = ApiLoginRequestBody {
user_name: username.to_owned(),
user_name,
password,
};
@ -92,23 +193,32 @@ impl ApiLoginRequest {
/// The request body passed to the API login endpoint.
#[derive(Debug, Serialize)]
#[serde(crate = "rocket::serde")]
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)]
#[serde(crate = "rocket::serde")]
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<ApiLoginResponseData>,
// systemNotice: Option<String>,
}
/// The response data returned by the API login endpoint.
#[derive(Debug, Deserialize)]
#[serde(crate = "rocket::serde")]
struct ApiLoginResponseData {
/// The token to be used as cookie for API data requests.
token: String,
@ -116,7 +226,9 @@ struct ApiLoginResponseData {
/// The request passed to the API data endpoint.
#[derive(Debug, Serialize)]
#[serde(crate = "rocket::serde")]
struct ApiDataRequest {
/// The body of the API data request.
body: ApiDataRequestBody,
}
@ -131,34 +243,30 @@ impl ApiDataRequest {
/// The request body passed to the API data endpoint.
#[derive(Debug, Serialize)]
#[serde(crate = "rocket::serde")]
struct ApiDataRequestBody {
/// The ID of the Hoymiles station.
sid: u32,
}
/// The response returned by the API data endpoint.
#[derive(Debug, Deserialize)]
#[serde(crate = "rocket::serde")]
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<ApiDataResponseData>,
// 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.
#[derive(Debug, Deserialize)]
#[serde(crate = "rocket::serde")]
struct ApiDataResponseData {
/// Energy produced today (Wh)
#[serde(deserialize_with = "from_float_str")]
@ -174,7 +282,8 @@ struct ApiDataResponseData {
// co2_emission_reducation: f32,
// plant_tree: u32,
// data_time: String,
// last_data_time: String,
#[serde(deserialize_with = "from_date_time_str")]
last_data_time: DateTime<Local>,
// capacitor: f32,
// is_balance: bool,
// is_reflux: bool,
@ -185,7 +294,8 @@ struct ApiDataResponseData {
impl super::Service for Service {
/// The interval between data polls (in seconds).
///
/// Hoymiles processes provides information from the invertor every 15 minutes.
/// Hoymiles processes information from the invertor about every 15 minutes. Since this is not
/// really exact, we need to poll at a higher rate to detect changes faster!
fn poll_interval(&self) -> u64 {
POLL_INTERVAL
}
@ -195,18 +305,40 @@ 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(&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");
// Insert the cookie `hm_token_language` to specific the API language into the cookie jar.
let lang_cookie = format!("hm_token_language={}", LANGUAGE);
self.cookie_jar.add_cookie_str(&lang_cookie, &base_url);
let login_request = ApiLoginRequest::new(&self.config.username, &self.config.password);
let login_response = self.client.post(login_url).json(&login_request).send().await?;
let login_response = self
.client
.post(login_url)
.json(&login_request)
.send()
.await?;
let login_response_data = match login_response.error_for_status() {
Ok(res) => res.json::<ApiLoginResponse>().await?.data,
Err(err) => return Err(err),
Ok(res) => {
let login_response = res.json::<ApiLoginResponse>().await?;
match login_response.status {
0 => login_response.data.expect("No API response data found"),
1 => return Err(Error::NotAuthorized),
_ => {
return Err(Error::Response(format!(
"{} ({})",
login_response.message, login_response.status
)))
}
}
}
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);
self.cookie_jar.add_cookie_str(&cookie, &base_url);
let token_cookie = format!("hm_token={}", login_response_data.token);
self.cookie_jar.add_cookie_str(&token_cookie, &base_url);
Ok(())
}
@ -216,16 +348,43 @@ 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(&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.client.post(api_url).json(&api_data_request).send().await?;
let api_response = self
.client
.post(api_url)
.json(&api_data_request)
.send()
.await?;
let api_data = match api_response.error_for_status() {
Ok(res) => res.json::<ApiDataResponse>().await?.data,
Err(err) => return Err(err),
Ok(res) => {
let api_response = res.json::<ApiDataResponse>().await?;
match api_response.status {
0 => api_response.data.expect("No API response data found"),
1 | 100 => return Err(Error::NotAuthorized),
_ => {
return Err(Error::Response(format!(
"{} ({})",
api_response.message, api_response.status
)))
}
}
}
Err(err) => return Err(err.into()),
};
let current_w = api_data.real_power;
let 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;
let last_updated = api_data.last_data_time.timestamp() as u64;
// Sometimes it can be that `today_eq` is reset when the day switches but it has not been
// added to `total_eq` yet. The `total_eq` should always be non-decreasing, so return the
// last known value until this is corrected (this most suredly happens during the night).
if total_kwh <= self.total_kwh {
total_kwh = self.total_kwh
} else {
self.total_kwh = total_kwh;
}
Ok(Status {
current_w,

View File

@ -4,12 +4,14 @@
//! to retrieve the energy data (using the session cookies).
//! See also: <https://my.autarco.com>.
use reqwest::{Client, ClientBuilder, Url};
use rocket::async_trait;
use serde::Deserialize;
use reqwest::{Client, ClientBuilder, StatusCode, Url};
use rocket::{async_trait, 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";
@ -19,6 +21,7 @@ const POLL_INTERVAL: u64 = 300;
/// The configuration necessary to access the My Autarco API.
#[derive(Debug, Deserialize)]
#[serde(crate = "rocket::serde")]
pub(crate) struct Config {
/// The username of the account to login with
username: String,
@ -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 };
@ -57,6 +60,7 @@ fn api_url(site_id: &str, endpoint: &str) -> Result<Url, ParseError> {
/// The energy data returned by the energy API endpoint.
#[derive(Debug, Deserialize)]
#[serde(crate = "rocket::serde")]
struct ApiEnergy {
/// Total energy produced today (kWh)
// pv_today: u32,
@ -68,6 +72,7 @@ struct ApiEnergy {
/// The power data returned by the power API endpoint.
#[derive(Debug, Deserialize)]
#[serde(crate = "rocket::serde")]
struct ApiPower {
/// Current power production (W)
pv_now: u32,
@ -86,7 +91,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(&self) -> Result<(), reqwest::Error> {
async fn login(&mut self) -> Result<()> {
let params = [
("username", &self.config.username),
("password", &self.config.password),
@ -101,20 +106,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(&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,
};
@ -15,6 +14,8 @@ use crate::{
/// It updates the mutex-guarded current update [`Status`](crate::Status) struct which can be
/// retrieved via Rocket.
pub(super) async fn update_loop(service: Services) -> color_eyre::Result<()> {
let mut service = service;
// Log in on the cloud service.
println!("⚡ Logging in...");
service.login().await?;
@ -36,14 +37,14 @@ 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) => {
println!(" Update unauthorized, trying to log in again...");
Err(Error::NotAuthorized) => {
eprintln!("💥 Update unauthorized, trying to log in again...");
service.login().await?;
println!("⚡ Logged in successfully!");
continue;
}
Err(e) => {
println!(" Failed to update status: {}", e);
eprintln!("💥 Failed to update status: {}", e);
continue;
}
};