2020-10-09 19:47:43 +02:00
|
|
|
use std::path::Path;
|
2020-10-09 17:07:18 +02:00
|
|
|
use std::sync::Mutex;
|
2020-10-09 13:30:37 +02:00
|
|
|
use std::time::{Duration, SystemTime};
|
2021-06-13 20:51:18 +02:00
|
|
|
|
|
|
|
use color_eyre::Result;
|
|
|
|
use lazy_static::lazy_static;
|
|
|
|
use rocket::serde::json::Json;
|
|
|
|
use rocket::tokio::fs::File;
|
|
|
|
use rocket::tokio::io::AsyncReadExt;
|
|
|
|
use rocket::tokio::select;
|
|
|
|
use rocket::tokio::time::sleep;
|
|
|
|
use rocket::{get, routes};
|
|
|
|
use serde::{Deserialize, Serialize};
|
2021-08-16 20:15:43 +02:00
|
|
|
use url::{ParseError, Url};
|
2020-10-09 19:47:43 +02:00
|
|
|
|
|
|
|
/// The interval between data polls
|
|
|
|
///
|
2021-08-16 20:15:43 +02:00
|
|
|
/// This depends on with which interval Autaurco processes new information from the invertor.
|
2020-10-09 17:07:18 +02:00
|
|
|
const POLL_INTERVAL: u64 = 300;
|
2020-10-09 13:30:37 +02:00
|
|
|
|
2021-08-16 20:15:43 +02:00
|
|
|
/// The base URL of My Autarco site
|
|
|
|
const BASE_URL: &'static str = "https://my.autarco.com";
|
2020-10-09 19:47:43 +02:00
|
|
|
|
2021-08-16 20:15:43 +02:00
|
|
|
fn login_url() -> Result<Url, ParseError> {
|
|
|
|
Url::parse(&format!("{}/auth/login", BASE_URL))
|
2020-10-09 19:47:43 +02:00
|
|
|
}
|
2020-10-09 13:30:37 +02:00
|
|
|
|
2021-08-16 20:15:43 +02:00
|
|
|
fn api_url(site_id: &str, endpoint: &str) -> Result<Url, ParseError> {
|
|
|
|
Url::parse(&format!(
|
|
|
|
"{}/api/site/{}/kpis/{}",
|
|
|
|
BASE_URL, site_id, endpoint
|
|
|
|
))
|
2020-10-09 16:31:21 +02:00
|
|
|
}
|
|
|
|
|
2021-08-16 20:15:43 +02:00
|
|
|
/// The configuration for the My Autarco site
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
|
|
struct Config {
|
|
|
|
/// The username of the account to login with
|
|
|
|
username: String,
|
|
|
|
/// The password of the account to login with
|
|
|
|
password: String,
|
|
|
|
/// The Autarco site ID to track
|
|
|
|
site_id: String,
|
2020-10-09 16:50:11 +02:00
|
|
|
}
|
2020-10-09 16:31:21 +02:00
|
|
|
|
2020-10-09 19:47:43 +02:00
|
|
|
async fn load_config() -> Result<Config> {
|
|
|
|
let config_file_name = Path::new(env!("CARGO_MANIFEST_DIR")).join("autarco.toml");
|
|
|
|
let mut file = File::open(config_file_name).await?;
|
|
|
|
|
|
|
|
let mut contents = String::new();
|
|
|
|
file.read_to_string(&mut contents).await?;
|
|
|
|
let config = toml::from_str(&contents)?;
|
|
|
|
|
|
|
|
Ok(config)
|
|
|
|
}
|
|
|
|
|
2021-08-16 20:15:43 +02:00
|
|
|
/// The current photovoltaic invertor status.
|
|
|
|
#[derive(Clone, Copy, Debug, Serialize)]
|
|
|
|
struct Status {
|
|
|
|
/// Current power production (W)
|
|
|
|
current_w: u32,
|
|
|
|
/// Total energy produced since installation (kWh)
|
|
|
|
total_kwh: u32,
|
|
|
|
/// Timestamp of last update
|
|
|
|
last_updated: u64,
|
|
|
|
}
|
2020-10-09 19:47:43 +02:00
|
|
|
|
2021-08-16 20:15:43 +02:00
|
|
|
lazy_static! {
|
|
|
|
/// The concurrently accessible current status
|
|
|
|
static ref STATUS: Mutex<Option<Status>> = Mutex::new(None);
|
|
|
|
}
|
2020-10-09 13:30:37 +02:00
|
|
|
|
2021-08-16 20:15:43 +02:00
|
|
|
/// The energy data returnes by the energy API endpoint
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
|
|
struct ApiEnergy {
|
|
|
|
/// Total energy produced today (kWh)
|
|
|
|
pv_today: u32,
|
|
|
|
/// Total energy produced this month (kWh)
|
|
|
|
pv_month: u32,
|
|
|
|
/// Total energy produced since installation (kWh)
|
|
|
|
pv_to_date: u32,
|
|
|
|
}
|
2020-10-09 13:30:37 +02:00
|
|
|
|
2021-08-16 20:15:43 +02:00
|
|
|
/// The power data returned by the power API endpoint
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
|
|
struct ApiPower {
|
|
|
|
/// Current power production (W)
|
|
|
|
pv_now: u32,
|
2020-10-09 16:50:11 +02:00
|
|
|
}
|
|
|
|
|
2021-08-16 20:15:43 +02:00
|
|
|
async fn login(config: &Config, client: &reqwest::Client) -> Result<()> {
|
|
|
|
let params = [
|
|
|
|
("username", &config.username),
|
|
|
|
("password", &config.password),
|
|
|
|
];
|
|
|
|
client.post(login_url()?).form(¶ms).send().await?;
|
2020-10-09 16:50:11 +02:00
|
|
|
|
2021-08-16 20:15:43 +02:00
|
|
|
Ok(())
|
2020-10-09 16:50:11 +02:00
|
|
|
}
|
2020-10-09 13:30:37 +02:00
|
|
|
|
2021-08-16 20:15:43 +02:00
|
|
|
async fn update(config: &Config, client: &reqwest::Client, last_updated: u64) -> Result<Status> {
|
|
|
|
// Retrieve the data from the API endpoints
|
|
|
|
let api_energy_url = api_url(&config.site_id, "energy")?;
|
|
|
|
let api_energy: ApiEnergy = client.get(api_energy_url).send().await?.json().await?;
|
|
|
|
|
|
|
|
let api_power_url = api_url(&config.site_id, "power")?;
|
|
|
|
let api_power: ApiPower = client.get(api_power_url).send().await?.json().await?;
|
|
|
|
|
|
|
|
let current_w = api_power.pv_now;
|
|
|
|
let total_kwh = api_energy.pv_to_date;
|
|
|
|
|
|
|
|
// Update the status
|
|
|
|
Ok(Status {
|
|
|
|
current_w,
|
|
|
|
total_kwh,
|
|
|
|
last_updated,
|
|
|
|
})
|
2020-10-09 17:07:18 +02:00
|
|
|
}
|
|
|
|
|
2021-08-16 20:15:43 +02:00
|
|
|
async fn update_loop() -> Result<()> {
|
|
|
|
let config = load_config().await?;
|
|
|
|
let client = reqwest::ClientBuilder::new().cookie_store(true).build()?;
|
2020-10-09 16:50:11 +02:00
|
|
|
|
|
|
|
// Go to the My Autarco site and login
|
2020-10-10 21:14:10 +02:00
|
|
|
println!("⚡ Logging in...");
|
2021-08-16 20:15:43 +02:00
|
|
|
login(&config, &client).await?;
|
2020-10-09 16:50:11 +02:00
|
|
|
|
2020-10-09 23:06:56 +02:00
|
|
|
let mut last_updated = 0;
|
2020-10-09 16:50:11 +02:00
|
|
|
loop {
|
2021-08-16 20:15:43 +02:00
|
|
|
// Wake up every second and check if there is something to do (quit or update).
|
2021-06-13 20:51:18 +02:00
|
|
|
sleep(Duration::from_secs(1)).await;
|
2020-10-09 23:06:56 +02:00
|
|
|
|
|
|
|
let timestamp = SystemTime::now()
|
|
|
|
.duration_since(SystemTime::UNIX_EPOCH)
|
|
|
|
.unwrap()
|
|
|
|
.as_secs();
|
|
|
|
if timestamp - last_updated < POLL_INTERVAL {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2021-08-16 20:15:43 +02:00
|
|
|
// TODO: Relogin if the cookie expired?
|
|
|
|
// TODO: Handle errors instead of panicing!
|
|
|
|
let status = update(&config, &client, timestamp).await?;
|
2020-10-09 23:06:56 +02:00
|
|
|
last_updated = timestamp;
|
2020-10-09 13:30:37 +02:00
|
|
|
|
2020-10-10 21:14:10 +02:00
|
|
|
println!("⚡ Updated status to: {:#?}", status);
|
2021-08-16 20:15:43 +02:00
|
|
|
let mut status_guard = STATUS.lock().expect("Status mutex was poisoned");
|
2020-10-09 17:07:18 +02:00
|
|
|
status_guard.replace(status);
|
2020-10-09 13:30:37 +02:00
|
|
|
}
|
|
|
|
}
|
2020-10-09 17:30:41 +02:00
|
|
|
|
|
|
|
#[get("/", format = "application/json")]
|
2020-10-09 19:47:43 +02:00
|
|
|
async fn status() -> Option<Json<Status>> {
|
2020-10-09 23:06:56 +02:00
|
|
|
let status_guard = STATUS.lock().expect("Status mutex was poisoined");
|
2020-10-09 17:30:41 +02:00
|
|
|
status_guard.map(|status| Json(status))
|
|
|
|
}
|
|
|
|
|
2020-10-09 23:06:56 +02:00
|
|
|
#[rocket::main]
|
2020-10-17 00:41:55 +02:00
|
|
|
async fn main() -> Result<()> {
|
2020-10-17 00:41:19 +02:00
|
|
|
color_eyre::install()?;
|
|
|
|
|
2021-06-13 20:51:18 +02:00
|
|
|
let rocket = rocket::build().mount("/", routes![status]).ignite().await?;
|
|
|
|
let shutdown = rocket.shutdown();
|
2020-10-17 00:41:55 +02:00
|
|
|
|
2021-08-16 20:15:43 +02:00
|
|
|
let updater = rocket::tokio::spawn(update_loop());
|
|
|
|
|
2021-06-13 20:51:18 +02:00
|
|
|
select! {
|
2020-10-17 00:41:55 +02:00
|
|
|
result = rocket.launch() => {
|
|
|
|
result?;
|
|
|
|
},
|
|
|
|
result = updater => {
|
2021-06-13 20:51:18 +02:00
|
|
|
shutdown.notify();
|
2020-10-17 00:41:55 +02:00
|
|
|
result??;
|
|
|
|
}
|
|
|
|
}
|
2020-10-09 23:06:56 +02:00
|
|
|
|
2020-10-17 00:41:55 +02:00
|
|
|
Ok(())
|
2020-10-09 23:06:56 +02:00
|
|
|
}
|