fix: energy and power for calendar_history

This commit is contained in:
gak 2022-07-24 13:00:05 +10:00
parent b07c12d397
commit 6e9888acfa
6 changed files with 225 additions and 49 deletions

View file

@ -23,7 +23,9 @@ sha256 = "1.0"
base64 = "0.13" base64 = "0.13"
rand = "0.8" rand = "0.8"
regex = "1.5" regex = "1.5"
chrono = { version = "0.4", features = ["serde"] }
strum = { version = "0.24.1", features = ["derive"] } strum = { version = "0.24.1", features = ["derive"] }
urlencoding = "2.1.0"
[dev-dependencies] [dev-dependencies]
test-log = { version = "0.2", default-features = false, features = ["trace"] } test-log = { version = "0.2", default-features = false, features = ["trace"] }

View file

@ -1,9 +1,13 @@
mod cli_vehicle; mod cli_vehicle;
use chrono::DateTime;
use clap::{Args, Parser, Subcommand}; use clap::{Args, Parser, Subcommand};
use cli_vehicle::VehicleArgs; use cli_vehicle::VehicleArgs;
use miette::{miette, IntoDiagnostic, WrapErr};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use teslatte::auth::{AccessToken, Authentication, RefreshToken}; use teslatte::auth::{AccessToken, Authentication, RefreshToken};
use teslatte::calendar_history::{CalendarHistoryValues, HistoryKind, HistoryPeriod};
use teslatte::energy::EnergySiteId;
use teslatte::powerwall::PowerwallId; use teslatte::powerwall::PowerwallId;
use teslatte::vehicles::{SetChargeLimit, SetChargingAmps}; use teslatte::vehicles::{SetChargeLimit, SetChargingAmps};
use teslatte::{Api, VehicleId}; use teslatte::{Api, VehicleId};
@ -62,10 +66,71 @@ enum ApiCommand {
/// List of energy sites. /// List of energy sites.
EnergySites, EnergySites,
/// Specific energy site.
EnergySite(EnergySiteArgs),
/// Powerwall queries. /// Powerwall queries.
Powerwall(PowerwallArgs), Powerwall(PowerwallArgs),
} }
#[derive(Debug, Args)]
struct EnergySiteArgs {
pub id: EnergySiteId,
#[clap(subcommand)]
pub command: EnergySiteCommand,
}
impl EnergySiteArgs {
pub async fn run(&self, api: &Api) -> miette::Result<()> {
match &self.command {
EnergySiteCommand::CalendarHistory(args) => {
let start_date = args
.start
.as_ref()
.map(|s| DateTime::parse_from_rfc3339(&s).into_diagnostic())
.transpose()
.wrap_err("start_date")?;
let end_date = args
.end
.as_ref()
.map(|s| DateTime::parse_from_rfc3339(&s).into_diagnostic())
.transpose()
.wrap_err("end_date")?;
let values = CalendarHistoryValues {
site_id: self.id.clone(),
kind: args.kind.clone(),
period: args.period.clone(),
start_date,
end_date,
};
let history = api.energy_sites_calendar_history(&values).await?;
println!("{:#?}", history);
}
}
Ok(())
}
}
#[derive(Debug, Subcommand)]
enum EnergySiteCommand {
CalendarHistory(CalendarHistoryArgs),
}
#[derive(Debug, Args)]
struct CalendarHistoryArgs {
pub kind: HistoryKind,
#[clap(short, long, default_value = "day")]
pub period: HistoryPeriod,
#[clap(short, long)]
start: Option<String>,
#[clap(short, long)]
end: Option<String>,
}
#[derive(Debug, Args)] #[derive(Debug, Args)]
struct PowerwallArgs { struct PowerwallArgs {
pub id: PowerwallId, pub id: PowerwallId,
@ -136,6 +201,9 @@ async fn main() -> miette::Result<()> {
ApiCommand::EnergySites => { ApiCommand::EnergySites => {
dbg!(api.energy_sites().await?); dbg!(api.energy_sites().await?);
} }
ApiCommand::EnergySite(e) => {
e.run(&api).await?;
}
ApiCommand::Powerwall(p) => { ApiCommand::Powerwall(p) => {
p.run(&api).await?; p.run(&api).await?;
} }

109
src/calendar_history.rs Normal file
View file

@ -0,0 +1,109 @@
use crate::energy::EnergySiteId;
use crate::{get_args, rfc3339, Api};
use crate::{TeslatteError, Values};
use chrono::{DateTime, FixedOffset};
use serde::{Deserialize, Serialize};
use strum::{Display, EnumString};
use urlencoding::encode;
#[rustfmt::skip]
impl Api {
get_args!(energy_sites_calendar_history, CalendarHistory, "/energy_sites/{}/calendar_history", CalendarHistoryValues);
}
#[derive(Debug, Clone, Display, EnumString)]
#[strum(serialize_all = "snake_case")]
pub enum HistoryKind {
Power,
Energy,
}
#[derive(Debug, Clone, Display, EnumString)]
#[strum(serialize_all = "snake_case")]
pub enum HistoryPeriod {
Day,
Month,
Year,
Lifetime,
}
pub struct CalendarHistoryValues {
pub site_id: EnergySiteId,
pub period: HistoryPeriod,
pub kind: HistoryKind,
pub start_date: Option<DateTime<FixedOffset>>,
pub end_date: Option<DateTime<FixedOffset>>,
}
impl Values for CalendarHistoryValues {
fn format(&self, url: &str) -> String {
let url = url.replace("{}", &format!("{}", self.site_id.0));
let mut pairs = vec![
("period", self.period.to_string()),
("kind", self.kind.to_string()),
];
if let Some(start_date) = self.start_date {
pairs.push(("start_date", rfc3339(&start_date)));
}
if let Some(end_date) = self.end_date {
pairs.push(("end_date", rfc3339(&end_date)));
}
format!(
"{}?{}",
url,
pairs
.iter()
.map(|(k, v)| format!("{}={}", k, v.replace("+", "%2B")))
.collect::<Vec<_>>()
.join("&")
)
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct CalendarHistory {
pub serial_number: String,
/// Only appears in energy kind.
pub period: Option<String>,
pub installation_time_zone: String,
/// Optional because if there are no `Series` fields, this field is omitted.
pub time_series: Option<Vec<Series>>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
pub enum Series {
Power(PowerSeries),
Energy(EnergySeries),
}
#[derive(Debug, Clone, Deserialize)]
pub struct PowerSeries {
pub timestamp: DateTime<FixedOffset>,
pub solar_power: f64,
pub battery_power: f64,
pub grid_power: f64,
pub grid_services_power: i64,
pub generator_power: i64,
}
#[derive(Debug, Clone, Deserialize)]
pub struct EnergySeries {
pub timestamp: DateTime<FixedOffset>,
pub solar_energy_exported: f64,
pub generator_energy_exported: f64,
pub grid_energy_imported: f64,
pub grid_services_energy_imported: f64,
pub grid_services_energy_exported: f64,
pub grid_energy_exported_from_solar: f64,
pub grid_energy_exported_from_generator: f64,
pub grid_energy_exported_from_battery: f64,
pub battery_energy_exported: f64,
pub battery_energy_imported_from_grid: f64,
pub battery_energy_imported_from_solar: f64,
pub battery_energy_imported_from_generator: f64,
pub consumer_energy_imported_from_grid: f64,
pub consumer_energy_imported_from_solar: f64,
pub consumer_energy_imported_from_battery: f64,
pub consumer_energy_imported_from_generator: f64,
}

View file

@ -4,68 +4,30 @@ use crate::vehicles::VehicleData;
use crate::{ use crate::{
get, get_arg, get_args, post_arg, post_arg_empty, Api, Empty, ExternalVehicleId, VehicleId, get, get_arg, get_args, post_arg, post_arg_empty, Api, Empty, ExternalVehicleId, VehicleId,
}; };
use chrono::{DateTime, FixedOffset};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use url::Url; use std::str::FromStr;
use strum::{Display, EnumString};
use url::{Url, UrlQuery};
#[rustfmt::skip] #[rustfmt::skip]
impl Api { impl Api {
get!(energy_sites, Vec<EnergySite>, "/products"); get!(energy_sites, Vec<EnergySite>, "/products");
// https://owner-api.teslamotors.com/api/1/energy_sites/1370797147/calendar_history?period=day&kind=power
get_args!(energy_sites_calendar_history, CalendarHistory, "/energy_sites/{}/calendar_history", CalendarHistoryValues);
} }
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
pub struct CalendarHistory {} pub struct EnergySiteId(pub u64);
trait Values { impl FromStr for EnergySiteId {
fn format(&self, url: &str) -> String; type Err = TeslatteError;
}
#[derive(Debug, Clone, strum::Display)] fn from_str(s: &str) -> Result<Self, Self::Err> {
#[strum(serialize_all = "snake_case")] Ok(EnergySiteId(s.parse().map_err(|_| {
pub enum HistoryKind { TeslatteError::DecodeEnergySiteIdError(s.to_string())
Power, })?))
Energy,
}
#[derive(Debug, Clone, strum::Display)]
#[strum(serialize_all = "snake_case")]
pub enum HistoryPeriod {
Day,
Week,
Month,
Year,
}
pub struct CalendarHistoryValues {
site_id: EnergySiteId,
period: HistoryPeriod,
kind: HistoryKind,
start_date: Option<chrono::DateTime<chrono::Utc>>,
end_date: Option<chrono::DateTime<chrono::Utc>>,
}
impl Values for CalendarHistoryValues {
fn format(&self, url: &str) -> String {
let url = url.replace("{}", &format!("{}", self.site_id.0));
let mut url = Url::parse(&url).unwrap();
let mut pairs = url.query_pairs_mut();
pairs.append_pair("period", &self.period.to_string());
pairs.append_pair("kind", &self.kind.to_string());
if let Some(start_date) = self.start_date {
pairs.append_pair("start_date", &start_date.to_rfc3339());
}
if let Some(end_date) = self.end_date {
pairs.append_pair("end_date", &end_date.to_rfc3339());
}
drop(pairs);
url.to_string()
} }
} }
#[derive(Debug, Clone, Deserialize)]
pub struct EnergySiteId(u64);
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GatewayId(String); pub struct GatewayId(String);
@ -128,6 +90,8 @@ pub struct Components {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::calendar_history::{CalendarHistoryValues, HistoryKind, HistoryPeriod};
use crate::Values;
#[test] #[test]
fn energy_match_powerwall() { fn energy_match_powerwall() {
@ -288,4 +252,20 @@ mod tests {
"https://base.com/e/123/history?period=month&kind=energy" "https://base.com/e/123/history?period=month&kind=energy"
); );
} }
#[test]
fn calendar_history_values_dates() {
let v = CalendarHistoryValues {
site_id: EnergySiteId(123),
period: HistoryPeriod::Month,
kind: HistoryKind::Energy,
start_date: Some(DateTime::parse_from_rfc3339("2020-01-01T00:00:00Z").unwrap()),
end_date: Some(DateTime::parse_from_rfc3339("2020-01-31T23:59:59Z").unwrap()),
};
let url = v.format("https://base.com/e/{}/history");
assert_eq!(
url,
"https://base.com/e/123/history?period=month&kind=energy&start_date=2020-01-01T00:00:00Z&end_date=2020-01-31T23:59:59Z"
);
}
} }

View file

@ -35,4 +35,7 @@ pub enum TeslatteError {
#[error("Callback URL did not contain a callback code.")] #[error("Callback URL did not contain a callback code.")]
CouldNotFindCallbackCode, CouldNotFindCallbackCode,
#[error("Could not convert \"{0}\" to an EnergySiteId.")]
DecodeEnergySiteIdError(String),
} }

View file

@ -1,5 +1,6 @@
use crate::auth::AccessToken; use crate::auth::AccessToken;
use crate::error::TeslatteError; use crate::error::TeslatteError;
use chrono::{DateTime, SecondsFormat, TimeZone};
use miette::IntoDiagnostic; use miette::IntoDiagnostic;
use reqwest::Client; use reqwest::Client;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -8,6 +9,7 @@ use std::str::FromStr;
use tracing::{debug, instrument, trace}; use tracing::{debug, instrument, trace};
pub mod auth; pub mod auth;
pub mod calendar_history;
pub mod energy; pub mod energy;
pub mod error; pub mod error;
pub mod powerwall; pub mod powerwall;
@ -15,6 +17,10 @@ pub mod vehicles;
const API_URL: &str = "https://owner-api.teslamotors.com"; const API_URL: &str = "https://owner-api.teslamotors.com";
trait Values {
fn format(&self, url: &str) -> String;
}
/// Vehicle ID used by the owner-api endpoint. /// Vehicle ID used by the owner-api endpoint.
/// ///
/// This data comes from [`Api::vehicles()`] `id` field. /// This data comes from [`Api::vehicles()`] `id` field.
@ -267,6 +273,14 @@ macro_rules! post_arg_empty {
} }
pub(crate) use post_arg_empty; pub(crate) use post_arg_empty;
pub(crate) fn rfc3339<Tz>(d: &DateTime<Tz>) -> String
where
Tz: TimeZone,
Tz::Offset: Display,
{
d.to_rfc3339_opts(SecondsFormat::Secs, true)
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;