Skip to content

Commit

Permalink
feat: refactor TessieFairing into generic EVChargeFairing with a Tess…
Browse files Browse the repository at this point in the history
…ie backend
  • Loading branch information
ssaavedra committed Aug 23, 2024
1 parent 3092a87 commit 39bcd29
Show file tree
Hide file tree
Showing 9 changed files with 599 additions and 367 deletions.
167 changes: 167 additions & 0 deletions src/car/fairing.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
//! Fairing to ensure an EV does not exceed a power budget.
//!
//! This is used to implement a poor man's smart-grid EVSE, where the platform
//! will check the average amps drawn by the home from the database over a
//! period and ensure the car does not exceed the power budget.
//!
//! If you are thinking on doing something like this at your own place, this
//! solution can be at least 10x cheaper than running cables to the breaker box
//! and installing an EVSE with grid sensing, and provides a good enough
//! solution. It also allows operation with any "dumb" IEC 61851-implementing
//! EVSE, without requiring you to implement of the more expensive ISO 15118-2
//! protocol in the EVSE communication with the car.
use std::sync::Arc;

use rocket::tokio::sync::Mutex;

use super::EVChargeHandler;

/// This fairing checks if the car is nearby and if it's charging.
///
/// Originally it was implemented as a task that would run every 30 seconds, but
/// it was changed to run on every response to the Rocket app. This is because
/// it actually makes sense to react to changes when we know of them happening.
///
/// This also makes the implementation much simpler, as we don't need to worry
/// about having a different DB pool.
///
/// Since requests can come in parallel, by using a Mutex we can ensure that
/// only one request at a time will check the car status, and we can discard the
/// other.
pub struct EVChargeFairing<H: EVChargeHandler> {
handler: Arc<Mutex<Option<super::task::CarHandler<H>>>>,
}

impl<'a, H: EVChargeHandler> EVChargeFairing<H>
where
H::ConfigParams: From<&'a rocket::figment::Figment>,
{
pub fn new() -> Self {
Self {
handler: Arc::new(Mutex::new(None)),
}
}

/// This function checks if the car is nearby and if it's charging.
///
/// If it is, it will check the average amps drawn by the home from the
/// database over the last 30 seconds and update the car API accordingly to
/// not exceed the amp limit.
async fn check_on_response<'r>(&self, req: &rocket::Request<'r>) -> anyhow::Result<()> {
let _guard = match self.handler.try_lock() {
Ok(guard) => guard,
Err(_) => {
log::info!("Car handler is currently locked, skipping check on this response.");
return Ok(());
} // Ignore if the lock is currently being held elsewhere
};
let handler = _guard.as_ref().unwrap();
// 1. Check that the car is nearby
// 2. Check if the car is charging
// 3. If the car is charging, check the amps drawn by the home from the database over the last 30 seconds and update the car API accordingly to not exceed the amp limit.

// Check if the car is nearby
if handler.is_car_nearby().await? {
log::info!("Car is nearby: TRUE");
// Check if the car is charging
let car_is_charging = handler.is_car_charging().await?;
log::info!("Is car charging? {:?}", car_is_charging);
if car_is_charging {
let (avg_amps, max_amps) = self.get_avg_amps_at_location(req).await?;
handler
.set_current_home_consumption(avg_amps, max_amps)
.await?;
log::info!(
"Retrieved current home consumption as: {} amps (max={})",
avg_amps,
max_amps
);
handler.throttled_calculate_amps().await?;
}
} else {
log::info!("Car is nearby: FALSE");
}

Ok(())
}

/// This function retrieves the average amps drawn at the location from the
/// database over the last 30 seconds.
///
/// It returns a tuple with the average amps and the max amps drawn.
async fn get_avg_amps_at_location<'r>(
&self,
req: &rocket::Request<'r>,
) -> anyhow::Result<(f64, f64)> {
let db = req.guard::<&crate::Logs>().await.unwrap();
let token = req.guard::<&crate::ValidDbToken>().await.unwrap();

log::info!(
"Checking average amps drawn at location for token: {}",
token
);
let result = sqlx::query!("SELECT AVG(amps) as avg_amps, MAX(amps) as max_amps FROM energy_log WHERE token = ? AND created_at > datetime('now', '-30 seconds')", token)
.fetch_one(&**db)
.await?;
let avg_amps: f64 = result.avg_amps.unwrap_or(0.0);
let max_amps: f64 = result.max_amps.unwrap_or(0.0);
log::info!(
"Retrieved average amps: {} and max amps: {}",
avg_amps,
max_amps
);

Ok((avg_amps, max_amps))
}
}

#[rocket::async_trait]
impl<'a, H: EVChargeHandler> rocket::fairing::Fairing for EVChargeFairing<H>
where
H: Send + Sync + 'static,
H::ConfigParams: Send + Sync + 'static,
H::InternalState: Send + Sync + 'static,
H::ConfigParams: From<&'a rocket::figment::Figment>,
{
fn info(&self) -> rocket::fairing::Info {
let type_name = H::get_name();
let name = Box::new(format!("EV Charge Fairing ({})", &type_name)).leak();
rocket::fairing::Info {
name: name,
kind: rocket::fairing::Kind::Response | rocket::fairing::Kind::Ignite,
}
}

/// We initialize the [super::task::CarHandler] and store it in the fairing when the
/// Rocket app is ignited.
async fn on_ignite(
&self,
rocket: rocket::Rocket<rocket::Build>,
) -> rocket::fairing::Result<rocket::Rocket<rocket::Build>> {
let handler = super::task::CarHandler::from(rocket.figment());
let mut guard = self.handler.lock().await;
*guard = Some(handler);

Ok(rocket)
}

async fn on_response<'r>(
&self,
req: &'r rocket::Request<'_>,
_res: &mut rocket::Response<'r>,
) -> () {
// Is this a request to log info?
let route_name = req
.route()
.map(|route| route.name.as_deref())
.flatten()
.unwrap_or("");
if route_name == "post_token" {
match self.check_on_response(req).await {
Ok(_) => log::info!("Car check succeeded."),
Err(e) => log::error!("Car check failure: {}", e),
}
}
}
}
135 changes: 108 additions & 27 deletions src/car/mod.rs
Original file line number Diff line number Diff line change
@@ -1,31 +1,112 @@
//! This module contains the EV-energy related modules.
//!
//! Currently only an implementation for Tesla EVs relying on the 3rd party
//! Tessie API is available. The fairing implementation to interact with the
//! Rocket app is in the [TessieFairing](tessie_fairing::TessieFairing) fairing.
//!
//! The API interaction is implemented in the [tessie_api] module,
//! see more about the API documentation at
//! [Tessie](https://developer.tessie.com/docs/about/). Only a tiny subset of
//! the API is implemented in this module.
//!
//! The benefit of using the Tessie API is that we abstract the complexity of
//! refreshing the Tesla OAuth Tokens, and the awake/asleep state of the EV
//! itself. Tessie will cache the car state and we will avoid waking up the car
//! unless it's already plugged-in, charging (and thus, awake).
//! This module has been designed to be extensible to support multiple EV
//! platforms, and to be able to interact with them in a similar way.
//!
//! This is also a more friendly interaction to end users since Tessie will
//! provide any subscriber with an API token, while the process to set up a
//! Tesla integration requires the user to register for a Tesla API token and
//! manual review from Tesla.
//! Currently only an implementation for Tesla EVs relying on the 3rd party
//! Tessie API is available. It is available in the [tessie] sub-module.
//!
//! For example, the [TessieDriveState](tessie_api::TessieDriveState) struct is
//! only minimally implemented, and the
//! [TessieAPIHandler](tessie_api::TessieAPIHandler) struct only implements the
//! [get_state](tessie_api::TessieAPIHandler::get_state) and the
//! [set_charging_amps](tessie_api::TessieAPIHandler::set_charging_amps)
//! methods, as they are the only ones needed for the current use case.
pub mod tessie_fairing;
mod tessie_api;
mod task;
//! If you want to implement your own EV charge handler, you should implement
//! the [EVChargeHandler] and [EVChargeInternalState] traits in this module. You
//! can look at the [tessie] source code for an example implementation.
use serde::{Deserialize, Serialize};

pub mod fairing;
pub mod tessie;
pub mod task;

/// The internal state of the EV charge handler.
///
/// Implementing this trait for your own EV charge handler will allow the
/// [EVChargeFairing](fairing::EVChargeFairing) to interact with it.
pub trait EVChargeInternalState: std::fmt::Debug + Clone {
/// Returns true if the car is currently charging
fn is_charging(&self) -> bool;

/// Returns true if the car is charging or about to start charging.
///
/// This is used to check if we need to quickly check again the state, for
/// example when the amp reading is not yet useful because you can reliably
/// know it is still ramping up.
fn is_charge_starting(&self) -> bool;

/// Returns the current amps being drawn by the car
fn get_current_charge(&self) -> f64;

/// Returns the max amps that we requested the charge to use
fn get_last_requested_amps(&self) -> usize;

/// Returns the distance in kilometers between the car and a point
fn get_car_distance_to_point_km(&self, point: &LatLon) -> f64;
}

pub trait EVChargeHandler {
type ConfigParams: for<'a> From<&'a rocket::figment::Figment>;
type InternalState: EVChargeInternalState;

/// Get the name of the EV charge handler
///
/// This is just a fancy name to return the name of the handler. It is used
/// when displaying the full fairing name.
///
/// If you don't override this method, the default implementation will return
/// the full type name of the handler.
fn get_name() -> &'static str {
std::any::type_name::<Self>()
}

/// Create a new instance of the EV charge handler
///
/// This method should initialize the handler with the given configuration
/// parameters.
///
/// The configuration parameters should be extractable from the Rocket.toml
/// file, so the implementation for the [EVChargeHandler::ConfigParams] must
/// implement the `From<&'a rocket::figment::Figment>` trait.
fn new(config: Self::ConfigParams) -> Self;

/// Get the current state of the EV
///
/// This method should return the current state of the EV as reported by the
/// API.
/// Keep in mind that you have to ask the API enough information to be able
/// to implement the [EVChargeInternalState] trait.
///
/// We will do our best to cache the state for you, and to avoid calling this
/// method too often, but if you need a specific rate limit, you should
/// implement it in your own handler; or make a PR :-)
fn get_state(&self) -> impl std::future::Future<Output = anyhow::Result<Self::InternalState>> + std::marker::Send;

/// Request the car to charge with a specific amount of amps
fn request_charge_amps(&self, amps: usize) -> impl std::future::Future<Output = anyhow::Result<()>> + std::marker::Send;
}


/// A simple struct to store latitude and longitude
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct LatLon {
pub lat: f64,
pub lon: f64,
}

impl LatLon {
/// Returns the distance in kilometers between two LatLon points in Earth
///
/// This method uses the Haversine formula to calculate the distance between
/// two points on Earth.
pub fn distance(&self, other: &LatLon) -> f64 {
const EARTH_RADIUS: f64 = 6371.0;
let d_lat = (other.lat - self.lat).to_radians();
let d_lon = (other.lon - self.lon).to_radians();
let lat1 = self.lat.to_radians();
let lat2 = other.lat.to_radians();

let a = (d_lat / 2.0).sin() * (d_lat / 2.0).sin()
+ lat1.cos() * lat2.cos() * (d_lon / 2.0).sin() * (d_lon / 2.0).sin();

let c = 2.0 * a.sqrt().atan2((1.0 - a).sqrt());
// 6371 is the Earth radius in kilometers
EARTH_RADIUS * c
}
}
Loading

0 comments on commit 39bcd29

Please sign in to comment.