local tesla status cache
This commit is contained in:
parent
375a27bd96
commit
f9dba75462
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -1994,6 +1994,7 @@ name = "tesla-charge-controller"
|
||||||
version = "0.1.4"
|
version = "0.1.4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
"include_dir",
|
"include_dir",
|
||||||
"rocket",
|
"rocket",
|
||||||
|
|
|
@ -22,3 +22,4 @@ thiserror = "1.0"
|
||||||
rocket = { version = "0.5", features = ["json"] }
|
rocket = { version = "0.5", features = ["json"] }
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
include_dir = "0.7"
|
include_dir = "0.7"
|
||||||
|
chrono = "0.4"
|
||||||
|
|
136
src/api_interface.rs
Normal file
136
src/api_interface.rs
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::{
|
||||||
|
path::PathBuf,
|
||||||
|
sync::{Arc, RwLock},
|
||||||
|
time::{Duration, Instant},
|
||||||
|
};
|
||||||
|
use teslatte::{
|
||||||
|
auth::{AccessToken, RefreshToken},
|
||||||
|
vehicles::{Endpoint, GetVehicleData},
|
||||||
|
FleetApi, FleetVehicleApi, VehicleId,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{errors::*, types::CarState};
|
||||||
|
|
||||||
|
pub struct TeslaInterface {
|
||||||
|
pub state: Arc<RwLock<CarState>>,
|
||||||
|
api: FleetApi,
|
||||||
|
vehicle: Box<teslatte::vehicles::VehicleData>,
|
||||||
|
last_refresh: Instant,
|
||||||
|
auth_path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
|
struct AuthInfo {
|
||||||
|
access_token: AccessToken,
|
||||||
|
refresh_token: Option<RefreshToken>,
|
||||||
|
}
|
||||||
|
|
||||||
|
const REFRESH_INTERVAL: Duration = Duration::from_secs(12 * 60 * 60);
|
||||||
|
|
||||||
|
impl TeslaInterface {
|
||||||
|
pub async fn load(auth_path: PathBuf) -> Result<Self, AuthLoadError> {
|
||||||
|
let key: AuthInfo = ron::from_str(&std::fs::read_to_string(&auth_path)?)?;
|
||||||
|
let mut api = FleetApi::new(key.access_token, key.refresh_token);
|
||||||
|
api.refresh().await?;
|
||||||
|
let last_refresh = Instant::now();
|
||||||
|
println!("Refreshed auth key");
|
||||||
|
let vehicle = api
|
||||||
|
.products()
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|v| match v {
|
||||||
|
teslatte::products::Product::Vehicle(vehicle) => Some(vehicle),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.next()
|
||||||
|
.context("No vehicles attached to account!")?;
|
||||||
|
|
||||||
|
let interface = Self {
|
||||||
|
state: Arc::new(RwLock::new(Default::default())),
|
||||||
|
api,
|
||||||
|
last_refresh,
|
||||||
|
auth_path,
|
||||||
|
vehicle,
|
||||||
|
};
|
||||||
|
interface.save_key()?;
|
||||||
|
|
||||||
|
Ok(interface)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_key(&self) -> Result<(), SaveError> {
|
||||||
|
std::fs::write(
|
||||||
|
self.auth_path.clone(),
|
||||||
|
ron::ser::to_string(&AuthInfo {
|
||||||
|
access_token: self.api.access_token.clone(),
|
||||||
|
refresh_token: self.api.refresh_token.clone(),
|
||||||
|
})?,
|
||||||
|
)?;
|
||||||
|
println!("Auth successfully saved");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn refresh(&mut self) {
|
||||||
|
println!("refreshing...");
|
||||||
|
self.refresh_keys().await;
|
||||||
|
self.refresh_state().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn refresh_state(&mut self) {
|
||||||
|
match get_state(&self.api, self.vehicle.id.clone()).await {
|
||||||
|
Ok(new_state) => {
|
||||||
|
self.last_refresh = Instant::now();
|
||||||
|
let mut state = self.state.write().expect("State handler panicked!!");
|
||||||
|
|
||||||
|
if let Some(new_charge_state) = new_state.charge_state {
|
||||||
|
state.charge_state = Some(new_charge_state);
|
||||||
|
}
|
||||||
|
if let Some(new_location_data) = new_state.location_data {
|
||||||
|
state.location_data = Some(new_location_data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => eprintln!("Error getting charge state: {e:#?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
async fn refresh_keys(&mut self) {
|
||||||
|
if Instant::now().duration_since(self.last_refresh) >= REFRESH_INTERVAL {
|
||||||
|
match self.api.refresh().await {
|
||||||
|
Ok(_) => {
|
||||||
|
let now = Instant::now();
|
||||||
|
match self.save_key() {
|
||||||
|
Ok(_) => self.last_refresh = now,
|
||||||
|
Err(e) => eprintln!("error saving auth token: {e:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => eprintln!("error refreshing auth token: {e:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_state(api: &FleetApi, vehicle_id: VehicleId) -> Result<CarState> {
|
||||||
|
let vehicle_data = api
|
||||||
|
.vehicle_data(&GetVehicleData {
|
||||||
|
vehicle_id: vehicle_id.clone(),
|
||||||
|
endpoints: vec![Endpoint::ChargeState].into(),
|
||||||
|
// endpoints: vec![Endpoint::VehicleDataCombo].into(),
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
let charge_state = vehicle_data.charge_state.map(|v| v.into());
|
||||||
|
|
||||||
|
let vehicle_data = api
|
||||||
|
.vehicle_data(&GetVehicleData {
|
||||||
|
vehicle_id,
|
||||||
|
endpoints: vec![Endpoint::LocationData].into(),
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
let location_data = vehicle_data.drive_state.and_then(|v| v.try_into().ok());
|
||||||
|
|
||||||
|
Ok(CarState {
|
||||||
|
charge_state,
|
||||||
|
location_data,
|
||||||
|
})
|
||||||
|
}
|
|
@ -2,7 +2,7 @@ use std::time::Duration;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::Coords;
|
use crate::types::Coords;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
|
|
|
@ -27,6 +27,8 @@ pub enum AuthLoadError {
|
||||||
Teslatte(#[from] teslatte::error::TeslatteError),
|
Teslatte(#[from] teslatte::error::TeslatteError),
|
||||||
#[error("save error")]
|
#[error("save error")]
|
||||||
Save(#[from] SaveError),
|
Save(#[from] SaveError),
|
||||||
|
#[error("other error")]
|
||||||
|
Anyhow(#[from] anyhow::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AuthLoadError {
|
impl AuthLoadError {
|
||||||
|
@ -36,6 +38,7 @@ impl AuthLoadError {
|
||||||
AuthLoadError::StdIo(e) => format!("Error reading access token from disk: {e:?}"),
|
AuthLoadError::StdIo(e) => format!("Error reading access token from disk: {e:?}"),
|
||||||
AuthLoadError::RonSpanned(e) => format!("Error deserialising access token: {e:?}"),
|
AuthLoadError::RonSpanned(e) => format!("Error deserialising access token: {e:?}"),
|
||||||
AuthLoadError::Save(e) => e.error_string(),
|
AuthLoadError::Save(e) => e.error_string(),
|
||||||
|
AuthLoadError::Anyhow(e) => e.to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
133
src/main.rs
133
src/main.rs
|
@ -1,23 +1,15 @@
|
||||||
#![feature(async_closure)]
|
#![feature(never_type)]
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate rocket;
|
extern crate rocket;
|
||||||
|
|
||||||
use anyhow::Result;
|
use api_interface::TeslaInterface;
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use crate::config::Config;
|
||||||
use std::{
|
|
||||||
path::PathBuf,
|
|
||||||
time::{Duration, Instant},
|
|
||||||
};
|
|
||||||
use teslatte::{
|
|
||||||
auth::{AccessToken, RefreshToken},
|
|
||||||
FleetApi,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{config::Config, errors::*};
|
|
||||||
|
|
||||||
|
mod api_interface;
|
||||||
mod config;
|
mod config;
|
||||||
mod errors;
|
mod errors;
|
||||||
mod server;
|
mod server;
|
||||||
|
@ -54,108 +46,27 @@ async fn main() {
|
||||||
ron::ser::to_string_pretty(&Config::default(), Default::default()).unwrap()
|
ron::ser::to_string_pretty(&Config::default(), Default::default()).unwrap()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Commands::Watch => match get_auth(auth_path).await {
|
Commands::Watch => match TeslaInterface::load(auth_path).await {
|
||||||
Ok(auth) => {
|
Ok(mut interface) => {
|
||||||
let config: Config =
|
let config: Config =
|
||||||
ron::from_str(&std::fs::read_to_string(&config_path).unwrap()).unwrap();
|
ron::from_str(&std::fs::read_to_string(&config_path).unwrap()).unwrap();
|
||||||
let products = auth.api().products().await;
|
|
||||||
match products {
|
let server_handle = server::launch_server(server::ServerState {
|
||||||
Ok(res) => match res.first() {
|
config,
|
||||||
Some(teslatte::products::Product::Vehicle(vehicle)) => {
|
state: interface.state.clone(),
|
||||||
server::launch_server(server::ServerState {
|
});
|
||||||
config,
|
|
||||||
auth,
|
tokio::task::spawn(async move {
|
||||||
vehicle: vehicle.clone(),
|
let mut interval = tokio::time::interval(std::time::Duration::from_secs(120));
|
||||||
})
|
loop {
|
||||||
.await;
|
interval.tick().await;
|
||||||
}
|
interface.refresh().await;
|
||||||
_ => println!("No first item"),
|
}
|
||||||
},
|
});
|
||||||
Err(e) => println!("Error getting products: {e:#?}"),
|
|
||||||
}
|
server_handle.await;
|
||||||
}
|
}
|
||||||
Err(e) => println!("{}", e.error_string()),
|
Err(e) => println!("{}", e.error_string()),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
|
|
||||||
pub struct Coords {
|
|
||||||
pub latitude: f64,
|
|
||||||
pub longitude: f64,
|
|
||||||
}
|
|
||||||
|
|
||||||
const COORD_PRECISION: f64 = 0.001;
|
|
||||||
|
|
||||||
impl Coords {
|
|
||||||
fn overlaps(&self, other: &Coords) -> bool {
|
|
||||||
(self.latitude - other.latitude).abs() < COORD_PRECISION
|
|
||||||
&& (self.longitude - other.longitude).abs() < COORD_PRECISION
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_auth(auth_path: PathBuf) -> Result<FleetApiAuth, AuthLoadError> {
|
|
||||||
let key: AuthInfo = ron::from_str(&std::fs::read_to_string(&auth_path)?)?;
|
|
||||||
let mut api = FleetApi::new(key.access_token, key.refresh_token);
|
|
||||||
api.refresh().await?;
|
|
||||||
println!("Refreshed auth key");
|
|
||||||
save_key(&auth_path, &api)?;
|
|
||||||
// api.print_responses = teslatte::PrintResponses::Pretty;
|
|
||||||
Ok(FleetApiAuth::new(api, Instant::now(), auth_path))
|
|
||||||
}
|
|
||||||
|
|
||||||
struct FleetApiAuth {
|
|
||||||
api: FleetApi,
|
|
||||||
last_refresh: Instant,
|
|
||||||
auth_path: PathBuf,
|
|
||||||
}
|
|
||||||
|
|
||||||
const REFRESH_INTERVAL: Duration = Duration::from_secs(12 * 60 * 60);
|
|
||||||
|
|
||||||
impl FleetApiAuth {
|
|
||||||
fn new(api: FleetApi, last_refresh: Instant, auth_path: PathBuf) -> Self {
|
|
||||||
Self {
|
|
||||||
api,
|
|
||||||
last_refresh,
|
|
||||||
auth_path,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn api(&self) -> &FleetApi {
|
|
||||||
&self.api
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(unused)]
|
|
||||||
async fn refresh(&mut self) {
|
|
||||||
if Instant::now().duration_since(self.last_refresh) >= REFRESH_INTERVAL {
|
|
||||||
match self.api.refresh().await {
|
|
||||||
Ok(_) => {
|
|
||||||
let now = Instant::now();
|
|
||||||
match save_key(&self.auth_path, &self.api) {
|
|
||||||
Ok(_) => self.last_refresh = now,
|
|
||||||
Err(e) => eprintln!("error saving auth token: {e:?}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => eprintln!("error refreshing auth token: {e:?}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
|
||||||
struct AuthInfo {
|
|
||||||
access_token: AccessToken,
|
|
||||||
refresh_token: Option<RefreshToken>,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn save_key(auth_path: &PathBuf, api: &FleetApi) -> Result<(), SaveError> {
|
|
||||||
std::fs::write(
|
|
||||||
auth_path,
|
|
||||||
ron::ser::to_string(&AuthInfo {
|
|
||||||
access_token: api.access_token.clone(),
|
|
||||||
refresh_token: api.refresh_token.clone(),
|
|
||||||
})?,
|
|
||||||
)?;
|
|
||||||
println!("Auth successfully saved");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use anyhow::{Context, Result};
|
use std::sync::{Arc, RwLock};
|
||||||
|
|
||||||
use rocket::{
|
use rocket::{
|
||||||
fairing::{Fairing, Info, Kind},
|
fairing::{Fairing, Info, Kind},
|
||||||
http::Header,
|
http::Header,
|
||||||
|
@ -6,21 +7,18 @@ use rocket::{
|
||||||
Request, Response, State,
|
Request, Response, State,
|
||||||
};
|
};
|
||||||
|
|
||||||
use teslatte::{
|
use crate::{
|
||||||
vehicles::{Endpoint, GetVehicleData, VehicleData},
|
config::Config,
|
||||||
FleetVehicleApi,
|
types::{CarState, ChargeState},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{config::Config, types::ChargeState, Coords, FleetApiAuth};
|
|
||||||
|
|
||||||
use self::static_handler::UiStatic;
|
use self::static_handler::UiStatic;
|
||||||
|
|
||||||
mod static_handler;
|
mod static_handler;
|
||||||
|
|
||||||
pub struct ServerState {
|
pub struct ServerState {
|
||||||
pub config: Config,
|
pub config: Config,
|
||||||
pub auth: FleetApiAuth,
|
pub state: Arc<RwLock<CarState>>,
|
||||||
pub vehicle: Box<VehicleData>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn launch_server(state: ServerState) {
|
pub async fn launch_server(state: ServerState) {
|
||||||
|
@ -45,19 +43,14 @@ fn rocket(state: ServerState) -> rocket::Rocket<rocket::Build> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/home")]
|
#[get("/home")]
|
||||||
async fn home(state: &State<ServerState>) -> Option<String> {
|
async fn home(state: &State<ServerState>) -> Option<Json<bool>> {
|
||||||
let coords = state.get_coords().await.ok()?;
|
let location_data = &state.state.read().ok()?.location_data?;
|
||||||
Some(if coords.overlaps(&state.config.coords) {
|
Some(Json(location_data.coords.overlaps(&state.config.coords)))
|
||||||
String::from("At home")
|
|
||||||
} else {
|
|
||||||
String::from("Not home")
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/charge-state")]
|
#[get("/charge-state")]
|
||||||
async fn charge_state(state: &State<ServerState>) -> Option<Json<ChargeState>> {
|
async fn charge_state(state: &State<ServerState>) -> Option<Json<ChargeState>> {
|
||||||
let charge_state = state.get_charge_state().await.ok()?;
|
Some(Json(state.state.read().ok()?.charge_state?))
|
||||||
Some(Json(charge_state))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/flash")]
|
#[post("/flash")]
|
||||||
|
@ -66,39 +59,9 @@ async fn flash(state: &State<ServerState>) {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ServerState {
|
impl ServerState {
|
||||||
async fn get_coords(&self) -> Result<Coords> {
|
|
||||||
let vehicle_data = self
|
|
||||||
.auth
|
|
||||||
.api()
|
|
||||||
.vehicle_data(&GetVehicleData {
|
|
||||||
vehicle_id: self.vehicle.id.clone(),
|
|
||||||
endpoints: vec![Endpoint::LocationData].into(),
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
let drive_state = vehicle_data.drive_state.context("no drive state")?;
|
|
||||||
let latitude = drive_state.latitude.context("no latitude")?;
|
|
||||||
let longitude = drive_state.longitude.context("no longitude")?;
|
|
||||||
Ok(Coords {
|
|
||||||
latitude,
|
|
||||||
longitude,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_charge_state(&self) -> Result<ChargeState> {
|
|
||||||
let vehicle_data = self
|
|
||||||
.auth
|
|
||||||
.api()
|
|
||||||
.vehicle_data(&GetVehicleData {
|
|
||||||
vehicle_id: self.vehicle.id.clone(),
|
|
||||||
endpoints: vec![Endpoint::ChargeState].into(),
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
let charge_state = vehicle_data.charge_state.context("no drive state")?;
|
|
||||||
Ok(charge_state.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn flash(&self) {
|
async fn flash(&self) {
|
||||||
let _ = self.auth.api().flash_lights(&self.vehicle.vin).await;
|
println!("stubbed flash function");
|
||||||
|
// let _ = self.auth.api().flash_lights(&self.vehicle.vin).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
44
src/types.rs
44
src/types.rs
|
@ -1,5 +1,13 @@
|
||||||
|
use anyhow::Context;
|
||||||
|
use chrono::DateTime;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct CarState {
|
||||||
|
pub charge_state: Option<ChargeState>,
|
||||||
|
pub location_data: Option<LocationData>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
|
#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
|
||||||
pub struct ChargeState {
|
pub struct ChargeState {
|
||||||
pub battery_level: i64,
|
pub battery_level: i64,
|
||||||
|
@ -33,3 +41,39 @@ impl From<teslatte::vehicles::ChargeState> for ChargeState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
|
||||||
|
pub struct LocationData {
|
||||||
|
pub coords: Coords,
|
||||||
|
pub gps_as_of: DateTime<chrono::Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<teslatte::vehicles::DriveState> for LocationData {
|
||||||
|
type Error = anyhow::Error;
|
||||||
|
|
||||||
|
fn try_from(value: teslatte::vehicles::DriveState) -> Result<Self, Self::Error> {
|
||||||
|
let gps_as_of =
|
||||||
|
chrono::DateTime::from_timestamp(value.gps_as_of.context("no gps timestamp!")?, 0)
|
||||||
|
.context("could not process timestamp!")?;
|
||||||
|
let coords = Coords {
|
||||||
|
latitude: value.latitude.context("no longitude provided!")?,
|
||||||
|
longitude: value.longitude.context("no latitude provided!")?,
|
||||||
|
};
|
||||||
|
Ok(Self { coords, gps_as_of })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
|
||||||
|
pub struct Coords {
|
||||||
|
pub latitude: f64,
|
||||||
|
pub longitude: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
const COORD_PRECISION: f64 = 0.001;
|
||||||
|
|
||||||
|
impl Coords {
|
||||||
|
pub fn overlaps(&self, other: &Coords) -> bool {
|
||||||
|
(self.latitude - other.latitude).abs() < COORD_PRECISION
|
||||||
|
&& (self.longitude - other.longitude).abs() < COORD_PRECISION
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
2
watch.sh
2
watch.sh
|
@ -1,6 +1,6 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
(
|
(
|
||||||
trap 'kill 0' SIGINT
|
trap 'kill 0' SIGINT
|
||||||
cargo watch -x check -s 'touch .trigger' &
|
cargo watch -w "src" -x check -s 'touch .trigger' &
|
||||||
cargo watch --no-vcs-ignores -w .trigger -x "run -- --config-dir test-config watch"
|
cargo watch --no-vcs-ignores -w .trigger -x "run -- --config-dir test-config watch"
|
||||||
)
|
)
|
||||||
|
|
|
@ -38,6 +38,8 @@
|
||||||
fetch(api_url + "/charge-state")
|
fetch(api_url + "/charge-state")
|
||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.then((json) => update_charge_state(json));
|
.then((json) => update_charge_state(json));
|
||||||
|
|
||||||
|
fetch(api_url + "/home").then((response) => response.json()).then((response) => update_home(response));
|
||||||
}
|
}
|
||||||
|
|
||||||
function update_charge_state(charge_state) {
|
function update_charge_state(charge_state) {
|
||||||
|
@ -67,6 +69,19 @@
|
||||||
info_div.appendChild(el);
|
info_div.appendChild(el);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function update_home(response) {
|
||||||
|
var info_div = document.getElementById("info");
|
||||||
|
|
||||||
|
el = document.createElement('p');
|
||||||
|
if (response) {
|
||||||
|
el.appendChild(document.createTextNode("Home"));
|
||||||
|
} else {
|
||||||
|
el.appendChild(document.createTextNode("Not home"));
|
||||||
|
}
|
||||||
|
|
||||||
|
info_div.appendChild(el);
|
||||||
|
}
|
||||||
|
|
||||||
function get_emoji(charge_state) {
|
function get_emoji(charge_state) {
|
||||||
if (charge_state.charge_rate > 0) {
|
if (charge_state.charge_rate > 0) {
|
||||||
return "🔌";
|
return "🔌";
|
||||||
|
|
Loading…
Reference in a new issue