fix: energy and power for calendar_history
This commit is contained in:
parent
b07c12d397
commit
6e9888acfa
|
@ -23,7 +23,9 @@ sha256 = "1.0"
|
|||
base64 = "0.13"
|
||||
rand = "0.8"
|
||||
regex = "1.5"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
strum = { version = "0.24.1", features = ["derive"] }
|
||||
urlencoding = "2.1.0"
|
||||
|
||||
[dev-dependencies]
|
||||
test-log = { version = "0.2", default-features = false, features = ["trace"] }
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
mod cli_vehicle;
|
||||
|
||||
use chrono::DateTime;
|
||||
use clap::{Args, Parser, Subcommand};
|
||||
use cli_vehicle::VehicleArgs;
|
||||
use miette::{miette, IntoDiagnostic, WrapErr};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use teslatte::auth::{AccessToken, Authentication, RefreshToken};
|
||||
use teslatte::calendar_history::{CalendarHistoryValues, HistoryKind, HistoryPeriod};
|
||||
use teslatte::energy::EnergySiteId;
|
||||
use teslatte::powerwall::PowerwallId;
|
||||
use teslatte::vehicles::{SetChargeLimit, SetChargingAmps};
|
||||
use teslatte::{Api, VehicleId};
|
||||
|
@ -62,10 +66,71 @@ enum ApiCommand {
|
|||
/// List of energy sites.
|
||||
EnergySites,
|
||||
|
||||
/// Specific energy site.
|
||||
EnergySite(EnergySiteArgs),
|
||||
|
||||
/// Powerwall queries.
|
||||
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)]
|
||||
struct PowerwallArgs {
|
||||
pub id: PowerwallId,
|
||||
|
@ -136,6 +201,9 @@ async fn main() -> miette::Result<()> {
|
|||
ApiCommand::EnergySites => {
|
||||
dbg!(api.energy_sites().await?);
|
||||
}
|
||||
ApiCommand::EnergySite(e) => {
|
||||
e.run(&api).await?;
|
||||
}
|
||||
ApiCommand::Powerwall(p) => {
|
||||
p.run(&api).await?;
|
||||
}
|
||||
|
|
109
src/calendar_history.rs
Normal file
109
src/calendar_history.rs
Normal 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,
|
||||
}
|
|
@ -4,68 +4,30 @@ use crate::vehicles::VehicleData;
|
|||
use crate::{
|
||||
get, get_arg, get_args, post_arg, post_arg_empty, Api, Empty, ExternalVehicleId, VehicleId,
|
||||
};
|
||||
use chrono::{DateTime, FixedOffset};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
use std::str::FromStr;
|
||||
use strum::{Display, EnumString};
|
||||
use url::{Url, UrlQuery};
|
||||
|
||||
#[rustfmt::skip]
|
||||
impl Api {
|
||||
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)]
|
||||
pub struct CalendarHistory {}
|
||||
pub struct EnergySiteId(pub u64);
|
||||
|
||||
trait Values {
|
||||
fn format(&self, url: &str) -> String;
|
||||
}
|
||||
impl FromStr for EnergySiteId {
|
||||
type Err = TeslatteError;
|
||||
|
||||
#[derive(Debug, Clone, strum::Display)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum HistoryKind {
|
||||
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()
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Ok(EnergySiteId(s.parse().map_err(|_| {
|
||||
TeslatteError::DecodeEnergySiteIdError(s.to_string())
|
||||
})?))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct EnergySiteId(u64);
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GatewayId(String);
|
||||
|
||||
|
@ -128,6 +90,8 @@ pub struct Components {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::calendar_history::{CalendarHistoryValues, HistoryKind, HistoryPeriod};
|
||||
use crate::Values;
|
||||
|
||||
#[test]
|
||||
fn energy_match_powerwall() {
|
||||
|
@ -288,4 +252,20 @@ mod tests {
|
|||
"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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,4 +35,7 @@ pub enum TeslatteError {
|
|||
|
||||
#[error("Callback URL did not contain a callback code.")]
|
||||
CouldNotFindCallbackCode,
|
||||
|
||||
#[error("Could not convert \"{0}\" to an EnergySiteId.")]
|
||||
DecodeEnergySiteIdError(String),
|
||||
}
|
||||
|
|
14
src/lib.rs
14
src/lib.rs
|
@ -1,5 +1,6 @@
|
|||
use crate::auth::AccessToken;
|
||||
use crate::error::TeslatteError;
|
||||
use chrono::{DateTime, SecondsFormat, TimeZone};
|
||||
use miette::IntoDiagnostic;
|
||||
use reqwest::Client;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
@ -8,6 +9,7 @@ use std::str::FromStr;
|
|||
use tracing::{debug, instrument, trace};
|
||||
|
||||
pub mod auth;
|
||||
pub mod calendar_history;
|
||||
pub mod energy;
|
||||
pub mod error;
|
||||
pub mod powerwall;
|
||||
|
@ -15,6 +17,10 @@ pub mod vehicles;
|
|||
|
||||
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.
|
||||
///
|
||||
/// 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) fn rfc3339<Tz>(d: &DateTime<Tz>) -> String
|
||||
where
|
||||
Tz: TimeZone,
|
||||
Tz::Offset: Display,
|
||||
{
|
||||
d.to_rfc3339_opts(SecondsFormat::Secs, true)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
Loading…
Reference in a new issue