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"
|
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"] }
|
||||||
|
|
|
@ -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
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::{
|
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"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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),
|
||||||
}
|
}
|
||||||
|
|
14
src/lib.rs
14
src/lib.rs
|
@ -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::*;
|
||||||
|
|
Loading…
Reference in a new issue