diff --git a/Cargo.toml b/Cargo.toml index 82d53fb..f666efd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ regex = "1.5" chrono = { version = "0.4", features = ["serde"] } strum = { version = "0.24.1", features = ["derive"] } urlencoding = "2.1.0" +derive_more = "0.99" [dev-dependencies] test-log = { version = "0.2", default-features = false, features = ["trace"] } diff --git a/examples/cli.rs b/examples/cli.rs index c838e87..6709245 100644 --- a/examples/cli.rs +++ b/examples/cli.rs @@ -8,9 +8,10 @@ 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::powerwall::{PowerwallEnergyHistoryValues, PowerwallId}; use teslatte::vehicles::{SetChargeLimit, SetChargingAmps}; use teslatte::{Api, VehicleId}; +use tracing_subscriber::util::SubscriberInitExt; /// Teslatte /// @@ -145,6 +146,18 @@ impl PowerwallArgs { PowerwallCommand::Status => { dbg!(api.powerwall_status(&self.id).await?); } + PowerwallCommand::History => { + dbg!( + api.powerwall_energy_history(&PowerwallEnergyHistoryValues { + powerwall_id: self.id.clone(), + period: HistoryPeriod::Day, + kind: HistoryKind::Power, + start_date: None, + end_date: None + }) + .await? + ); + } } Ok(()) } @@ -154,6 +167,8 @@ impl PowerwallArgs { enum PowerwallCommand { /// Show the status of the Powerwall. Status, + + History, } #[tokio::main] diff --git a/src/calendar_history.rs b/src/calendar_history.rs index 2a47107..2b3e182 100644 --- a/src/calendar_history.rs +++ b/src/calendar_history.rs @@ -1,9 +1,9 @@ use crate::energy::EnergySiteId; -use crate::{get_args, rfc3339, Api}; +use crate::{get_args, join_query_pairs, rfc3339, Api}; use crate::{TeslatteError, Values}; use chrono::{DateTime, FixedOffset}; use serde::{Deserialize, Serialize}; -use strum::{Display, EnumString}; +use strum::{Display, EnumString, IntoStaticStr}; use urlencoding::encode; #[rustfmt::skip] @@ -11,14 +11,14 @@ impl Api { get_args!(energy_sites_calendar_history, CalendarHistory, "/energy_sites/{}/calendar_history", CalendarHistoryValues); } -#[derive(Debug, Clone, Display, EnumString)] +#[derive(Debug, Clone, Display, EnumString, IntoStaticStr)] #[strum(serialize_all = "snake_case")] pub enum HistoryKind { Power, Energy, } -#[derive(Debug, Clone, Display, EnumString)] +#[derive(Debug, Clone, Display, EnumString, IntoStaticStr)] #[strum(serialize_all = "snake_case")] pub enum HistoryPeriod { Day, @@ -28,7 +28,10 @@ pub enum HistoryPeriod { } pub struct CalendarHistoryValues { + // Modify URL: pub site_id: EnergySiteId, + + // Query params: pub period: HistoryPeriod, pub kind: HistoryKind, pub start_date: Option>, @@ -38,25 +41,19 @@ pub struct CalendarHistoryValues { impl Values for CalendarHistoryValues { fn format(&self, url: &str) -> String { let url = url.replace("{}", &format!("{}", self.site_id.0)); - let mut pairs = vec![ + let mut pairs: Vec<(&str, String)> = 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))); + let start_date = rfc3339(&start_date); + pairs.push(("start_date", start_date)); } if let Some(end_date) = self.end_date { - pairs.push(("end_date", rfc3339(&end_date))); + let end_date = rfc3339(&end_date); + pairs.push(("end_date", end_date)); } - format!( - "{}?{}", - url, - pairs - .iter() - .map(|(k, v)| format!("{}={}", k, v.replace("+", "%2B"))) - .collect::>() - .join("&") - ) + format!("{}?{}", url, join_query_pairs(&pairs)) } } @@ -83,8 +80,8 @@ pub struct PowerSeries { pub solar_power: f64, pub battery_power: f64, pub grid_power: f64, - pub grid_services_power: i64, - pub generator_power: i64, + pub grid_services_power: f64, + pub generator_power: f64, } #[derive(Debug, Clone, Deserialize)] diff --git a/src/energy.rs b/src/energy.rs index 1e11fbe..57cdb23 100644 --- a/src/energy.rs +++ b/src/energy.rs @@ -42,9 +42,8 @@ pub enum EnergySite { /// This is assumed from https://tesla-api.timdorr.com/api-basics/products #[derive(Debug, Clone, Deserialize)] pub struct SolarData { - // `solar_type` must be first in the struct so serde can properly decode. - pub solar_type: String, pub energy_site_id: EnergySiteId, + pub solar_type: String, /// Should always be "solar". pub resource_type: String, pub id: String, @@ -57,9 +56,8 @@ pub struct SolarData { #[derive(Debug, Clone, Deserialize)] pub struct PowerwallData { - // `battery_type` must be first in the struct so serde can properly decode. + pub energy_site_id: EnergySiteId, pub battery_type: String, - pub energy_site_id: i64, /// Should always be "battery". pub resource_type: String, pub site_name: String, diff --git a/src/lib.rs b/src/lib.rs index 4b78a41..a6aa52f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,11 +1,11 @@ use crate::auth::AccessToken; use crate::error::TeslatteError; use chrono::{DateTime, SecondsFormat, TimeZone}; +use derive_more::{Display, FromStr}; use miette::IntoDiagnostic; use reqwest::Client; use serde::{Deserialize, Serialize}; use std::fmt::{Debug, Display, Formatter}; -use std::str::FromStr; use tracing::{debug, instrument, trace}; pub mod auth; @@ -24,22 +24,9 @@ trait Values { /// Vehicle ID used by the owner-api endpoint. /// /// This data comes from [`Api::vehicles()`] `id` field. -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone, Display, FromStr)] pub struct VehicleId(u64); -impl Display for VehicleId { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -impl FromStr for VehicleId { - type Err = miette::Error; - fn from_str(s: &str) -> Result { - Ok(VehicleId(s.parse().into_diagnostic()?)) - } -} - /// Vehicle ID used by other endpoints. /// /// This data comes from [`Api::vehicles()`] `vehicle_id` field. @@ -281,6 +268,14 @@ where d.to_rfc3339_opts(SecondsFormat::Secs, true) } +pub(crate) fn join_query_pairs(pairs: &[(&str, String)]) -> String { + pairs + .iter() + .map(|(k, v)| format!("{}={}", k, v.replace("+", "%2B"))) + .collect::>() + .join("&") +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/powerwall.rs b/src/powerwall.rs index 7571b4c..60bdda1 100644 --- a/src/powerwall.rs +++ b/src/powerwall.rs @@ -1,33 +1,25 @@ +use crate::calendar_history::{HistoryKind, HistoryPeriod}; use crate::energy::GatewayId; use crate::error::TeslatteError; use crate::vehicles::VehicleData; -use crate::{get, get_arg, post_arg, post_arg_empty, Api, Empty, ExternalVehicleId, VehicleId}; +use crate::{ + get, get_arg, get_args, join_query_pairs, post_arg, post_arg_empty, rfc3339, Api, Empty, + ExternalVehicleId, Values, VehicleId, +}; +use chrono::{DateTime, FixedOffset}; +use derive_more::{Display, FromStr}; use serde::{Deserialize, Serialize}; use std::fmt::{Display, Formatter}; -use std::str::FromStr; #[rustfmt::skip] impl Api { get_arg!(powerwall_status, PowerwallStatus, "/powerwalls/{}/status", PowerwallId); + get_args!(powerwall_energy_history, PowerwallEnergyHistory, "/powerwalls/{}/energyhistory", PowerwallEnergyHistoryValues); } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, Display, FromStr)] pub struct PowerwallId(pub String); -impl Display for PowerwallId { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -impl FromStr for PowerwallId { - type Err = TeslatteError; - - fn from_str(s: &str) -> Result { - Ok(PowerwallId(s.to_string())) - } -} - #[derive(Debug, Clone, Deserialize)] pub struct PowerwallStatus { pub site_name: String, @@ -37,3 +29,34 @@ pub struct PowerwallStatus { pub percentage_charged: f64, pub battery_power: i64, } + +#[derive(Debug, Clone)] +pub struct PowerwallEnergyHistoryValues { + pub powerwall_id: PowerwallId, + pub period: HistoryPeriod, + pub kind: HistoryKind, + pub start_date: Option>, + pub end_date: Option>, +} + +impl Values for PowerwallEnergyHistoryValues { + fn format(&self, url: &str) -> String { + let url = url.replace("{}", &format!("{}", self.powerwall_id.0)); + let mut pairs: Vec<(&str, String)> = vec![ + ("period", self.period.to_string()), + ("kind", self.kind.to_string()), + ]; + if let Some(start_date) = self.start_date { + let start_date = rfc3339(&start_date); + pairs.push(("start_date", start_date)); + } + if let Some(end_date) = self.end_date { + let end_date = rfc3339(&end_date); + pairs.push(("end_date", end_date)); + } + format!("{}?{}", url, join_query_pairs(&pairs)) + } +} + +#[derive(Debug, Clone, Deserialize)] +pub struct PowerwallEnergyHistory {} diff --git a/src/vehicles.rs b/src/vehicles.rs index 662b07b..dc1bf3c 100644 --- a/src/vehicles.rs +++ b/src/vehicles.rs @@ -19,9 +19,8 @@ impl Api { #[derive(Debug, Clone, Deserialize)] pub struct VehicleData { - // Leave as first field for serde untagged. - pub vehicle_id: ExternalVehicleId, pub id: VehicleId, + pub vehicle_id: ExternalVehicleId, pub user_id: i64, pub vin: String, pub display_name: String,