Drop thirtyfour/webdriver; use reqwest instead

* Set up a reqwest client using a cookie store
* Log in and update periodically using the reqwest client
This commit is contained in:
Paul van Tilburg 2021-08-16 20:15:43 +02:00
parent 91e86e6034
commit 570cf95497
3 changed files with 135 additions and 264 deletions

202
Cargo.lock generated
View File

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

View File

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

View File

@ -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, ParseError> {
Url::parse(&format!("{}/auth/login", BASE_URL))
}
fn api_url(site_id: &str, endpoint: &str) -> Result<Url, ParseError> {
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<Child> {
// 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<Config> {
@ -74,56 +54,82 @@ async fn load_config() -> Result<Config> {
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<Option<Status>> = 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(&params).send().await?;
Ok(())
}
async fn element_value(driver: &WebDriver, by: By<'_>) -> Result<u32> {
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<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?;
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<Option<Status>> = 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<Json<Status>> {
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 => {