diff --git a/Cargo.lock b/Cargo.lock index eff9aa8..6749592 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,7 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +version = 3 + [[package]] name = "addr2line" version = "0.15.2" @@ -15,15 +17,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" -[[package]] -name = "aho-corasick" -version = "0.7.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" -dependencies = [ - "memchr", -] - [[package]] name = "async-stream" version = "0.3.2" @@ -82,11 +75,12 @@ version = "0.1.1" dependencies = [ "color-eyre", "lazy_static", + "reqwest", "rocket", "serde", - "thirtyfour", "tokio", "toml", + "url", ] [[package]] @@ -158,20 +152,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" -[[package]] -name = "chrono" -version = "0.4.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" -dependencies = [ - "libc", - "num-integer", - "num-traits", - "serde", - "time 0.1.43", - "winapi", -] - [[package]] name = "color-eyre" version = "0.5.11" @@ -211,6 +191,17 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" +[[package]] +name = "cookie" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03a5d7b21829bc7b4bf4754a978a241ae54ea55a40f92bb20216e54096f4b951" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + [[package]] name = "cookie" version = "0.15.0" @@ -218,10 +209,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffdf8865bac3d9a3bde5bde9088ca431b11f5d37c7a578b8086af77248b76627" dependencies = [ "percent-encoding", - "time 0.2.27", + "time", "version_check", ] +[[package]] +name = "cookie_store" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3818dfca4b0cb5211a659bbcbb94225b7127407b2b135e650d717bfb78ab10d3" +dependencies = [ + "cookie 0.14.4", + "idna", + "log", + "publicsuffix", + "serde", + "serde_json", + "time", + "url", +] + [[package]] name = "core-foundation" version = "0.9.1" @@ -289,17 +296,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" -[[package]] -name = "displaydoc" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc2ab4d5a16117f9029e9a6b5e4e79f4c67f6519bc134210d4d4a04ba31f41b" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "either" version = "1.6.1" @@ -810,25 +806,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "num-integer" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" -dependencies = [ - "autocfg", - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" -dependencies = [ - "autocfg", -] - [[package]] name = "num_cpus" version = "1.13.0" @@ -1005,6 +982,16 @@ dependencies = [ "yansi", ] +[[package]] +name = "publicsuffix" +version = "1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95b4ce31ff0a27d93c8de1849cf58162283752f065a90d508f1105fa6c9a213f" +dependencies = [ + "idna", + "url", +] + [[package]] name = "quote" version = "1.0.9" @@ -1083,23 +1070,6 @@ dependencies = [ "syn", ] -[[package]] -name = "regex" -version = "1.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.6.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" - [[package]] name = "remove_dir_all" version = "0.5.3" @@ -1117,6 +1087,8 @@ checksum = "2296f2fac53979e8ccbc4a1136b25dcefd37be9ed7e4a1f6b05a6029c84ff124" dependencies = [ "base64", "bytes", + "cookie 0.14.4", + "cookie_store", "encoding_rs", "futures-core", "futures-util", @@ -1135,6 +1107,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", + "time", "tokio", "tokio-native-tls", "url", @@ -1174,7 +1147,7 @@ dependencies = [ "serde_json", "state", "tempfile", - "time 0.2.27", + "time", "tokio", "tokio-stream", "tokio-util", @@ -1205,7 +1178,7 @@ version = "0.5.0-rc.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23c8b7d512d2fcac2316ebe590cde67573844b99e6cc9ee0f53375fa16e25ebd" dependencies = [ - "cookie", + "cookie 0.15.0", "either", "http", "hyper", @@ -1222,7 +1195,7 @@ dependencies = [ "smallvec", "stable-pattern", "state", - "time 0.2.27", + "time", "tokio", "uncased", ] @@ -1340,23 +1313,11 @@ version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79" dependencies = [ - "indexmap", "itoa", "ryu", "serde", ] -[[package]] -name = "serde_repr" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98d0516900518c29efa217c298fa1f4e6c6ffc85ae29fd7f4ee48f176e1a9ed5" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "serde_urlencoded" version = "0.7.0" @@ -1497,15 +1458,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" -[[package]] -name = "stringmatch" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229f859beedf0ea6cb2c11a797dea9b7d6e66f8abce9c642a411c6948f67591b" -dependencies = [ - "regex", -] - [[package]] name = "syn" version = "1.0.73" @@ -1531,48 +1483,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "thirtyfour" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffd78805c4ea3cd06dda0c2ba9dddbe03c1968bd55567219fa0f2ce00e7ef54" -dependencies = [ - "async-trait", - "base64", - "chrono", - "displaydoc", - "futures", - "log", - "reqwest", - "serde", - "serde_json", - "serde_repr", - "stringmatch", - "thiserror", - "tokio", - "urlparse", -] - -[[package]] -name = "thiserror" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa6f76457f59514c7eeb4e59d891395fab0b2fd1d40723ae737d64153392e9c6" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a36768c0fbf1bb15eca10defa29526bda730a2376c2ab4393ccfa16fb1a318d" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "thread_local" version = "1.1.3" @@ -1582,16 +1492,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "time" -version = "0.1.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" -dependencies = [ - "libc", - "winapi", -] - [[package]] name = "time" version = "0.2.27" @@ -1855,12 +1755,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "urlparse" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "110352d4e9076c67839003c7788d8604e24dcded13e0b375af3efaa8cf468517" - [[package]] name = "vcpkg" version = "0.2.13" diff --git a/Cargo.toml b/Cargo.toml index 4028be5..1d2717f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,8 +7,9 @@ edition = "2018" [dependencies] color-eyre = "0.5.6" lazy_static = "1.4.0" +reqwest = { version = "0.11", features = ["cookies", "json"] } rocket = { version = "0.5.0-rc.1", features = ["json"] } serde = "1.0.116" toml = "0.5.6" -thirtyfour = { version = "0.25.0", features = ["tokio-runtime"] } tokio = { version = "1.6.1", features = ["process"] } +url = "2" diff --git a/src/main.rs b/src/main.rs index 3cdc9be..c14beaf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,66 +1,46 @@ use std::path::Path; -use std::process::Stdio; use std::sync::Mutex; -use std::thread; use std::time::{Duration, SystemTime}; -use color_eyre::eyre::eyre; 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::sync::oneshot::Receiver; use rocket::tokio::time::sleep; use rocket::{get, routes}; use serde::{Deserialize, Serialize}; -use thirtyfour::prelude::*; -use tokio::process::{Child, Command}; - -/// The port used by the Gecko Driver -const GECKO_DRIVER_PORT: u16 = 4444; +use url::{ParseError, Url}; /// The interval between data polls /// -/// This depends on with which interval Autaurco processes new information from the convertor. +/// This depends on with which interval Autaurco processes new information from the invertor. const POLL_INTERVAL: u64 = 300; -/// The URL to the My Autarco site -const URL: &'static str = "https://my.autarco.com/"; +/// The base URL of My Autarco site +const BASE_URL: &'static str = "https://my.autarco.com"; -/// The login configuration +fn login_url() -> Result { + Url::parse(&format!("{}/auth/login", BASE_URL)) +} + +fn api_url(site_id: &str, endpoint: &str) -> Result { + Url::parse(&format!( + "{}/api/site/{}/kpis/{}", + BASE_URL, site_id, endpoint + )) +} + +/// 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, -} - -/// Spawns the gecko driver -/// -/// Note that the function blocks and delays at least a second to ensure everything is up and -/// running. -fn spawn_driver(port: u16) -> Result { - // This is taken from the webdriver-client crate. - let child = Command::new("geckodriver") - .arg("--port") - .arg(format!("{}", port)) - .stdin(Stdio::null()) - .stderr(Stdio::null()) - .stdout(Stdio::null()) - .kill_on_drop(true) - .spawn()?; - - thread::sleep(Duration::new(1, 500)); - - Ok(child) -} - -#[derive(Clone, Copy, Debug, Serialize)] -struct Status { - current_w: u32, - total_kwh: u32, - last_updated: u64, + /// The Autarco site ID to track + site_id: String, } async fn load_config() -> Result { @@ -74,56 +54,82 @@ async fn load_config() -> Result { Ok(config) } -async fn login(driver: &WebDriver) -> Result<()> { - let config = load_config().await?; +/// 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, +} - driver.get(URL).await?; +lazy_static! { + /// The concurrently accessible current status + static ref STATUS: Mutex> = Mutex::new(None); +} - let input = driver.find_element(By::Id("username")).await?; - input.send_keys(&config.username).await?; - let input = driver.find_element(By::Id("password")).await?; - input.send_keys(&config.password).await?; - let input = driver.find_element(By::Css("button[type=submit]")).await?; - input.click().await?; +/// 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, +} + +/// The power data returned by the power API endpoint +#[derive(Debug, Deserialize)] +struct ApiPower { + /// Current power production (W) + pv_now: u32, +} + +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?; Ok(()) } -async fn element_value(driver: &WebDriver, by: By<'_>) -> Result { - let element = driver.find_element(by).await?; - let text = element.text().await?; - let value = text.parse()?; +async fn update(config: &Config, client: &reqwest::Client, last_updated: u64) -> Result { + // 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?; - Ok(value) + 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, + }) } -lazy_static! { - static ref STATUS: Mutex> = Mutex::new(None); -} - -async fn update_loop(mut rx: Receiver<()>) -> Result<()> { - let mut caps = DesiredCapabilities::firefox(); - caps.set_headless()?; - let driver = WebDriver::new(&format!("http://localhost:{}", GECKO_DRIVER_PORT), &caps).await?; +async fn update_loop() -> Result<()> { + let config = load_config().await?; + let client = reqwest::ClientBuilder::new().cookie_store(true).build()?; // Go to the My Autarco site and login println!("⚡ Logging in..."); - // FIXME: Just dropping the driver hangs the process? - if let Err(e) = login(&driver).await { - driver.quit().await?; - return Err(e); - } + login(&config, &client).await?; let mut last_updated = 0; loop { - // Wait the poll interval to check again! + // Wake up every second and check if there is something to do (quit or update). sleep(Duration::from_secs(1)).await; - // Shut down if there is a signal - if let Ok(()) = rx.try_recv() { - break; - } - let timestamp = SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) .unwrap() @@ -132,35 +138,15 @@ async fn update_loop(mut rx: Receiver<()>) -> Result<()> { continue; } - // Retrieve the data from the elements - let current_w = match element_value(&driver, By::Css("h2#pv-now b")).await { - Ok(value) => value, - Err(error) => { - eprintln!("Failed to retrieve current power: {}", error); - continue; - } - }; - let total_kwh = match element_value(&driver, By::Css("h2#pv-to-date b")).await { - Ok(value) => value, - Err(error) => { - eprintln!("Failed to retrieve total energy production: {}", error); - continue; - } - }; + // TODO: Relogin if the cookie expired? + // TODO: Handle errors instead of panicing! + let status = update(&config, &client, timestamp).await?; last_updated = timestamp; - // Update the status - let mut status_guard = STATUS.lock().expect("Status mutex was poisoned"); - let status = Status { - current_w, - total_kwh, - last_updated, - }; println!("⚡ Updated status to: {:#?}", status); + let mut status_guard = STATUS.lock().expect("Status mutex was poisoned"); status_guard.replace(status); } - - Ok(()) } #[get("/", format = "application/json")] @@ -173,23 +159,13 @@ async fn status() -> Option> { async fn main() -> Result<()> { color_eyre::install()?; - let mut driver_proc = - spawn_driver(GECKO_DRIVER_PORT).expect("Could not find/start the Gecko Driver"); - - let (tx, rx) = rocket::tokio::sync::oneshot::channel(); - let updater = rocket::tokio::spawn(update_loop(rx)); - let rocket = rocket::build().mount("/", routes![status]).ignite().await?; let shutdown = rocket.shutdown(); + let updater = rocket::tokio::spawn(update_loop()); + select! { - result = driver_proc.wait() => { - shutdown.notify(); - tx.send(()).map_err(|_| eyre!("Could not send shutdown signal"))?; - result?; - }, result = rocket.launch() => { - tx.send(()).map_err(|_| eyre!("Could not send shutdown signal"))?; result?; }, result = updater => {