diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3ffb372 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/target +.idea/ +*.xml +*.json diff --git a/Cargo.lock b/Cargo.lock index eac6d1c..cca6785 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,7 +29,11 @@ checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" name = "bidgely_adapter" version = "0.1.0" dependencies = [ + "quick-xml", + "reqwest", "serde", + "serde_json", + "thiserror", ] [[package]] @@ -443,9 +447,9 @@ dependencies = [ "bidgely_adapter", "clap", "quick-xml", - "reqwest", "serde", "serde_json", + "thiserror", "tokio", ] @@ -603,6 +607,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "58e21a144a0ffb5fad7b464babcdab934a325ad69b7c0373bcfef5cbd9799ca9" dependencies = [ "memchr", + "serde", ] [[package]] @@ -831,6 +836,26 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "thiserror" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tinyvec" version = "1.6.0" diff --git a/Cargo.toml b/Cargo.toml index f7b34f9..be846eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] members = [ - "crates/runner", "crates/bidgely_adapter", + "crates/runner", ] diff --git a/README.md b/README.md new file mode 100644 index 0000000..ccf57c9 --- /dev/null +++ b/README.md @@ -0,0 +1,78 @@ +# NSP Data +___ + +Nova scotia power now provides data from smart meters as a downloadable XML file. This project has three goals: + +1. To make it easier to download this data +2. To convert this data to more readable formats (i.e: JSON) +3. To provide insights into this data + +## Usage + +### Downloading Data + +In order to download data you must have your user ID. I am currently working on a way to retrieve this programmatically +but I have not yet figured this out. You can however easily grab it from your browser's dev tools by inspecting +your local storage on: [https://nsp.bidgely.com/dashboard](https://nsp.bidgely.com/dashboard). You should see it in the +values for both the `analyticsSessionInfo` and `lastVisitedPremisesUuid` keys. + +Once you've got your UUID, you can download data using the following: + +`cargo run --bin nspdata -- --uid ${YOUR_UUID} --action download --start ${EPOCH_SECONDS_START} --end ${EPOCH_SECONDS_END} --output-filename ${FILENAME}` + +### Converting Data + +I currently only support converting to JSON so there are no arguments for destination format. Provided you have an input +file; obtained either by downloading as per the above or manually on the NSP website, you can convert to JSON as follows: + +`cargo run --bin nspdata -- --action convert --input-filename ${INPUT_FILENAME} --output-filename ${OUTPUT_FILENAME}` + +### Insights + +I am currently working on some insights locally but haven't pushed anything yet. Values in the `IntervalReading` are in +KWh and I have verified that summing up the values for a day match the values shown on the NSP dashboard but. + + +For example, if you wanted to see how much power you used each day in a period you could do something like: + +***DISCLAIMER: CRUDE, UNOPTIMIZED CODE.*** + +```rust + +let user_id = "your-user-id"; +let start = 1664593200; +let end = 1665413999; + +let user_auth_response = bidgely_adapter::auth::auth(BIDGELY_BASE_URL, user_id).await?; +let session_response = bidgely_adapter::session::session(BIDGELY_BASE_URL, user_auth_response.payload.as_str()).await?; +let feed = bidgely_adapter::feed::get_feed(BIDGELY_BASE_URL, user_id, session_response.payload.token_details.access_token.as_str(), start, end).await?; + +let interval_blocks: Vec = feed + .entry + .into_iter() + .filter_map(|entry| + match entry.content.to_inner() { + bidgely_adapter::feed::ContentType::IntervalBlock(e) => Some(e), + _ => None, + }) + .collect(); + +let mut days: Vec = vec![]; + +interval_blocks.iter().for_each(|interval_block| { + let total = interval_block + .interval_reading + .iter() + .fold(0, |acc, x| acc + x.value); + days.push(total) +}); + +days.iter().for_each(|day| println!("{:?}", day)); + +``` + +## Things I've Noticed + +Usage data is not immediately available. I have had mixed results retrieving data less than two days old. Data more than +two days old can reliably be retrieved. I'm not sure why this is but NSP, Bidgely, or UtilityAPI must be doing some +batch processing before making data publicly available. \ No newline at end of file diff --git a/crates/bidgely_adapter/Cargo.toml b/crates/bidgely_adapter/Cargo.toml index 05248c7..16dec57 100644 --- a/crates/bidgely_adapter/Cargo.toml +++ b/crates/bidgely_adapter/Cargo.toml @@ -6,4 +6,8 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +quick-xml = { version = "0.25.0", features = ["serde", "serialize"] } +reqwest = "0.11.12" serde = { version = "1.0", features = ["derive"] } +serde_json = { version = "1.0" } +thiserror = "1.0" diff --git a/crates/bidgely_adapter/src/auth.rs b/crates/bidgely_adapter/src/auth.rs new file mode 100644 index 0000000..5bcd94b --- /dev/null +++ b/crates/bidgely_adapter/src/auth.rs @@ -0,0 +1,20 @@ +use crate::BidgelyError; + +#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)] +#[serde(rename_all(deserialize = "camelCase"))] +pub struct UserAuthResponse { + pub request_id: String, + pub payload: String, + pub error: Option, +} + +pub async fn auth(base_url: &str, user_id: &str) -> Result { + Ok(serde_json::from_str( + &reqwest::get(format!( + "{base_url}/user-auth/cipher?user-id={user_id}&pilot-id=40003" + )) + .await? + .text() + .await?, + )?) +} diff --git a/crates/bidgely_adapter/src/feed.rs b/crates/bidgely_adapter/src/feed.rs new file mode 100644 index 0000000..c036b2c --- /dev/null +++ b/crates/bidgely_adapter/src/feed.rs @@ -0,0 +1,188 @@ +use crate::BidgelyError; +use std::fs; + +#[derive(serde::Deserialize, serde::Serialize, Debug)] +#[serde(rename_all = "lowercase")] +pub struct Feed { + pub id: String, + pub title: String, + pub updated: String, + #[serde(rename(deserialize = "entry"))] + pub entries: Vec, +} + +#[derive(serde::Deserialize, serde::Serialize, Debug)] +pub struct Entry { + pub id: String, + pub link: Vec, + pub title: String, + pub content: Content, + pub published: String, + pub updated: String, +} + +#[derive(serde::Deserialize, serde::Serialize, Debug)] +pub struct Link { + pub href: String, + pub rel: String, + #[serde(rename = "type")] + pub kind: String, +} + +#[derive(serde::Deserialize, serde::Serialize, Debug)] +pub struct Content { + #[serde(rename(deserialize = "$value"))] + inner: ContentType, +} + +impl Content { + pub fn to_inner(self) -> ContentType { + self.inner + } +} + +impl std::ops::Deref for Content { + type Target = ContentType; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +#[derive(serde::Deserialize, serde::Serialize, Debug)] +pub enum ContentType { + LocalTimeParameters(LocalTimeParameters), + UsagePoint(UsagePoint), + ReadingType(ReadingType), + MeterReading, + IntervalBlock(IntervalBlock), + #[serde(other)] + Other, +} + +#[derive(serde::Deserialize, serde::Serialize, Debug)] +#[serde(rename_all(deserialize = "camelCase"))] +pub struct LocalTimeParameters { + pub dst_end_rule: String, + pub dst_offset: String, + pub dst_start_rule: String, + pub tz_offset: String, +} + +#[derive(serde::Deserialize, serde::Serialize, Debug)] +#[serde(rename_all(deserialize = "PascalCase"))] +pub struct UsagePoint { + pub service_category: ServiceCategory, +} + +#[derive(serde::Deserialize, serde::Serialize, Debug)] +pub struct ServiceCategory { + pub kind: u32, +} + +#[derive(serde::Deserialize, serde::Serialize, Debug)] +#[serde(rename_all(deserialize = "camelCase"))] +pub struct ReadingType { + pub accumulation_behaviour: u32, + pub commodity: u32, + pub data_qualifier: u32, + pub default_quality: u32, + pub flow_direction: u32, + pub interval_length: u32, + pub kind: u32, + pub phase: u32, + pub power_of_ten_multiplier: u32, + pub time_attribute: u32, +} + +#[derive(serde::Deserialize, serde::Serialize, Debug)] +pub struct IntervalBlock { + pub interval: Interval, + #[serde(rename = "IntervalReading")] + pub interval_reading: Vec, +} + +#[derive(serde::Deserialize, serde::Serialize, Debug)] +pub struct Interval { + pub duration: u64, + pub start: u64, +} + +#[derive(serde::Deserialize, serde::Serialize, Debug)] +pub struct IntervalReading { + #[serde(rename = "ReadingQuality")] + pub reading_quality: ReadingQuality, + #[serde(rename = "timePeriod")] + pub time_period: TimePeriod, + pub value: u32, +} + +#[derive(serde::Deserialize, serde::Serialize, Debug)] +pub struct ReadingQuality { + pub quality: u32, // todo: pub enum ReadingQuality +} + +#[derive(serde::Deserialize, serde::Serialize, Debug)] +#[serde(rename_all(deserialize = "camelCase"))] +pub struct TimePeriod { + pub duration: u32, + pub start: u32, +} + +pub async fn download_and_save_feed_xml( + base_url: &str, + user_id: &str, + token: &str, + start: u64, + end: u64, + output_filename: &str, +) -> Result<(), BidgelyError> { + let client = reqwest::Client::new(); + let xml_data = client.get(format!( + "{base_url}/dashboard/users/{user_id}/gb-download?start={start}&end={end}&measurement-type=ELECTRIC" + )) + .header(reqwest::header::CONTENT_TYPE, "application/json;charset=UTF-8") + .header(reqwest::header::AUTHORIZATION, format!("Bearer {token}")) + .send() + .await + ? + .text() + .await + ?; + + fs::write( + format!( + "{}.xml", + std::path::Path::new(output_filename) + .file_stem() + .and_then(std::ffi::OsStr::to_str) + .unwrap_or_else(|| "output") + ), + xml_data, + )?; + + Ok(()) +} + +pub async fn get_feed( + base_url: &str, + user_id: &str, + token: &str, + start: u64, + end: u64, +) -> Result { + let client = reqwest::Client::new(); + let xml_data = client.get(format!( + "{base_url}/dashboard/users/{user_id}/gb-download?start={start}&end={end}&measurement-type=ELECTRIC" + )) + .header(reqwest::header::CONTENT_TYPE, "application/json;charset=UTF-8") + .header(reqwest::header::AUTHORIZATION, format!("Bearer {token}")) + .send() + .await + ? + .text() + .await + ?; + + Ok(quick_xml::de::from_str(&xml_data)?) +} diff --git a/crates/bidgely_adapter/src/lib.rs b/crates/bidgely_adapter/src/lib.rs index cc3f88f..feab71d 100644 --- a/crates/bidgely_adapter/src/lib.rs +++ b/crates/bidgely_adapter/src/lib.rs @@ -1,114 +1,15 @@ -use serde::{Deserialize, Serialize}; +pub mod auth; +pub mod feed; +pub mod session; -#[derive(Debug, Default, Clone, Serialize, Deserialize)] -#[serde(rename_all(deserialize = "camelCase"))] -pub struct UserAuthResponse { - pub request_id: String, - pub payload: String, - pub error: Option, -} - -#[derive(Debug, Default, Clone, Serialize, Deserialize)] -#[serde(rename_all(deserialize = "camelCase"))] -pub struct SessionResponse { - pub request_id: String, - pub payload: SessionPayload, - pub error: Option, -} - -#[derive(Debug, Default, Clone, Serialize, Deserialize)] -#[serde(rename_all(deserialize = "camelCase"))] -pub struct SessionPayload { - pub pilot_id: u64, - pub client_id: String, - pub token_details: TokenDetails, - pub user_profile_details: UserProfileDetails, - pub user_type_details: UserTypeDetails, - pub premises_details: PremisesDetails, -} - -#[derive(Debug, Default, Clone, Serialize, Deserialize)] -#[serde(rename_all(deserialize = "camelCase"))] -pub struct TokenDetails { - pub access_token: String, - pub expiry_time_in_millis: u64, -} - -#[derive(Debug, Default, Clone, Serialize, Deserialize)] -#[serde(rename_all(deserialize = "camelCase"))] -pub struct UserProfileDetails { - pub user_id: String, - pub partner_user_id: String, - pub meter_id: Option, - pub first_name: String, - pub last_name: String, - pub fuel_type: String, // todo: type this as enum FuelType - pub email: String, - pub utility_tags: UtilityTags, - pub home_accounts: HomeAccounts, -} - -#[derive(Debug, Default, Clone, Serialize, Deserialize)] -#[serde(rename_all(deserialize = "snake_case"))] -pub struct UtilityTags { - pub account_number: String, -} - -#[derive(Debug, Default, Clone, Serialize, Deserialize)] -#[serde(rename_all(deserialize = "camelCase"))] -pub struct HomeAccounts { - pub address: String, - pub has_solar: bool, - pub postal_code: String, - pub rate: RatePlanInfo, -} - -#[derive(Debug, Default, Clone, Serialize, Deserialize)] -#[serde(rename_all(deserialize = "camelCase"))] -pub struct RatePlanInfo { - pub rate_plan_id: String, -} - -#[derive(Debug, Default, Clone, Serialize, Deserialize)] -#[serde(rename_all(deserialize = "camelCase"))] -pub struct UserTypeDetails { - pub user_segment: String, // todo: type this as enum UserSegment - pub measurement_to_user_type_mappings: Vec, -} - -#[derive(Debug, Default, Clone, Serialize, Deserialize)] -#[serde(rename_all(deserialize = "camelCase"))] -pub struct MeasurementToUserType { - pub measurement_type: String, - pub user_type: String, - pub max_contract_end: u32, -} - -#[derive(Debug, Default, Clone, Serialize, Deserialize)] -#[serde(rename_all(deserialize = "camelCase"))] -pub struct PremisesDetails { - pub partner_user_id: String, - pub premises: Vec, -} - -#[derive(Debug, Default, Clone, Serialize, Deserialize)] -#[serde(rename_all(deserialize = "camelCase"))] -pub struct Premise { - pub uuid: String, - pub premise_id: Option, - pub address: PremiseAddress, - pub supported_measurement_types: Vec, // toto: type this as enum MeasurementType - pub dashboard_last_visited: u32, - pub status: String, // todo: type this as enum PremiseStatus -} - -#[derive(Debug, Default, Clone, Serialize, Deserialize)] -#[serde(rename_all(deserialize = "camelCase"))] -pub struct PremiseAddress { - pub address_line_1: String, - pub address_line_2: Option, - pub city: String, - pub state: String, - pub zipcode: String, - pub context: Option, +#[derive(thiserror::Error, Debug)] +pub enum BidgelyError { + #[error("reqwest error")] + Reqwest(#[from] reqwest::Error), + #[error("Serde JSON Error")] + SerdeJson(#[from] serde_json::Error), + #[error("Quick XML De Error")] + DeError(#[from] quick_xml::DeError), + #[error("Unable to write file")] + IoError(#[from] std::io::Error), } diff --git a/crates/bidgely_adapter/src/session.rs b/crates/bidgely_adapter/src/session.rs new file mode 100644 index 0000000..012a36e --- /dev/null +++ b/crates/bidgely_adapter/src/session.rs @@ -0,0 +1,117 @@ +use crate::BidgelyError; + +#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)] +#[serde(rename_all(deserialize = "camelCase"))] +pub struct SessionResponse { + pub request_id: String, + pub payload: SessionPayload, + pub error: Option, +} + +#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)] +#[serde(rename_all(deserialize = "camelCase"))] +pub struct SessionPayload { + pub pilot_id: u64, + pub client_id: String, + pub token_details: TokenDetails, + pub user_profile_details: UserProfileDetails, + pub user_type_details: UserTypeDetails, + pub premises_details: PremisesDetails, +} + +#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)] +#[serde(rename_all(deserialize = "camelCase"))] +pub struct TokenDetails { + pub access_token: String, + pub expiry_time_in_millis: u64, +} + +#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)] +#[serde(rename_all(deserialize = "camelCase"))] +pub struct UserProfileDetails { + pub user_id: String, + pub partner_user_id: String, + pub meter_id: Option, + pub first_name: String, + pub last_name: String, + pub fuel_type: String, // todo: type this as enum FuelType + pub email: String, + pub utility_tags: UtilityTags, + pub home_accounts: HomeAccounts, +} + +#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)] +#[serde(rename_all(deserialize = "snake_case"))] +pub struct UtilityTags { + pub account_number: String, +} + +#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)] +#[serde(rename_all(deserialize = "camelCase"))] +pub struct HomeAccounts { + pub address: String, + pub has_solar: bool, + pub postal_code: String, + pub rate: RatePlanInfo, +} + +#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)] +#[serde(rename_all(deserialize = "camelCase"))] +pub struct RatePlanInfo { + pub rate_plan_id: String, +} + +#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)] +#[serde(rename_all(deserialize = "camelCase"))] +pub struct UserTypeDetails { + pub user_segment: String, // todo: type this as enum UserSegment + pub measurement_to_user_type_mappings: Vec, +} + +#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)] +#[serde(rename_all(deserialize = "camelCase"))] +pub struct MeasurementToUserType { + pub measurement_type: String, + pub user_type: String, + pub max_contract_end: u32, +} + +#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)] +#[serde(rename_all(deserialize = "camelCase"))] +pub struct PremisesDetails { + pub partner_user_id: String, + pub premises: Vec, +} + +#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)] +#[serde(rename_all(deserialize = "camelCase"))] +pub struct Premise { + pub uuid: String, + pub premise_id: Option, + pub address: PremiseAddress, + pub supported_measurement_types: Vec, // toto: type this as enum MeasurementType + pub dashboard_last_visited: u32, + pub status: String, // todo: type this as enum PremiseStatus +} + +#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)] +#[serde(rename_all(deserialize = "camelCase"))] +pub struct PremiseAddress { + pub address_line_1: String, + pub address_line_2: Option, + pub city: String, + pub state: String, + pub zipcode: String, + pub context: Option, +} + +pub async fn session(base_url: &str, session: &str) -> Result { + Ok(serde_json::from_str( + &reqwest::get(format!( + "{base_url}/web/web-session/{session}?pilotId=40003&clientId=nsp-dashboard" + )) + .await? + .text() + .await?, + )?) +} diff --git a/crates/runner/Cargo.toml b/crates/runner/Cargo.toml index 4c4ed6f..64cc5fc 100644 --- a/crates/runner/Cargo.toml +++ b/crates/runner/Cargo.toml @@ -2,14 +2,13 @@ name = "nspdata" version = "0.1.0" edition = "2021" - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] bidgely_adapter = { path = "../bidgely_adapter" } clap = { version = "4.0.17", features = ["derive"] } -quick-xml = "0.25.0" -reqwest = "0.11.12" +quick-xml = { version = "0.25.0", features = ["serde", "serialize"] } serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0" } +thiserror = "1.0" tokio = { version = "1.21.2", features = ["full"] } diff --git a/crates/runner/src/args.rs b/crates/runner/src/args.rs new file mode 100644 index 0000000..53b9e48 --- /dev/null +++ b/crates/runner/src/args.rs @@ -0,0 +1,22 @@ +#[derive(clap::ValueEnum, Debug, Clone)] +pub enum Action { + Convert, + Download, +} + +#[derive(clap::Parser, Debug)] +#[command(author, version, about, long_about = None)] +pub struct Args { + #[arg(short, long, value_enum)] + pub action: Action, + #[arg(short, long)] + pub uid: Option, + #[arg(short, long)] + pub input_filename: Option, + #[arg(short, long)] + pub output_filename: Option, + #[arg(short, long)] + pub start: Option, + #[arg(short, long)] + pub end: Option, +} diff --git a/crates/runner/src/convert.rs b/crates/runner/src/convert.rs new file mode 100644 index 0000000..e9c47f4 --- /dev/null +++ b/crates/runner/src/convert.rs @@ -0,0 +1,24 @@ +pub fn convert(args: &crate::args::Args) -> Result<(), crate::error::Error> { + println!("converting nsp xml to json"); + match &args.input_filename { + Some(file_name) => { + let xml_string = std::fs::read_to_string(file_name)?; + let feed: bidgely_adapter::feed::Feed = quick_xml::de::from_str(&xml_string)?; + let output = serde_json::to_string(&feed)?; + + let output_filename = match &args.output_filename { + None => std::path::Path::new(file_name) + .file_stem() + .and_then(std::ffi::OsStr::to_str) + .unwrap_or_else(|| "output"), + Some(output_filename) => output_filename.as_str(), + }; + + std::fs::write(output_filename, output).expect("Unable to write file"); + } + None => Err(crate::error::Error::BadArgument( + "Must provide filename for convert".to_string(), + ))?, + }; + Ok(()) +} diff --git a/crates/runner/src/download.rs b/crates/runner/src/download.rs new file mode 100644 index 0000000..07301c3 --- /dev/null +++ b/crates/runner/src/download.rs @@ -0,0 +1,26 @@ +pub async fn download(base_url: &str, args: &crate::args::Args) -> Result<(), crate::error::Error> { + println!("downloading nsp xml"); + match (&args.uid, &args.output_filename, args.start, args.end) { + (Some(user_id), Some(output_filename), Some(start), Some(end)) => { + let user_auth_response = bidgely_adapter::auth::auth(base_url, user_id).await?; + + let session_response = + bidgely_adapter::session::session(base_url, user_auth_response.payload.as_str()) + .await?; + + bidgely_adapter::feed::download_and_save_feed_xml( + base_url, + user_id, + session_response.payload.token_details.access_token.as_str(), + start, + end, + output_filename, + ) + .await?; + } + _ => Err(crate::error::Error::BadArgument( + "Must Provide UID, Start, End, and Output Filename For Download".to_string(), + ))?, + }; + Ok(()) +} diff --git a/crates/runner/src/error.rs b/crates/runner/src/error.rs new file mode 100644 index 0000000..d7f30a9 --- /dev/null +++ b/crates/runner/src/error.rs @@ -0,0 +1,13 @@ +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("Bidgely Error")] + BidgelyError(#[from] bidgely_adapter::BidgelyError), + #[error("Serde JSON Error")] + SerdeJson(#[from] serde_json::Error), + #[error("Quick XML De Error")] + DeError(#[from] quick_xml::DeError), + #[error("IO Error")] + IoError(#[from] std::io::Error), + #[error("Bad Argument: {0}")] + BadArgument(String), +} diff --git a/crates/runner/src/main.rs b/crates/runner/src/main.rs index 5810cb9..537d0a6 100644 --- a/crates/runner/src/main.rs +++ b/crates/runner/src/main.rs @@ -1,60 +1,58 @@ -use bidgely_adapter::{SessionResponse, UserAuthResponse}; +use crate::args::{Action, Args}; use clap::Parser; +pub mod args; +pub mod convert; +pub mod download; +pub mod error; + const BIDGELY_BASE_URL: &'static str = "https://caapi.bidgely.com/v2.0"; -#[derive(Parser, Debug)] -#[command(author, version, about, long_about = None)] -struct Args { - #[arg(short, long)] - uid: String, -} - #[tokio::main] -async fn main() { +async fn main() -> Result<(), error::Error> { let args: Args = Args::parse(); - let user_id = args.uid.as_str(); - let user_auth_response: UserAuthResponse = serde_json::from_str( - &reqwest::get(format!( - "{BIDGELY_BASE_URL}/user-auth/cipher?user-id={user_id}&pilot-id=40003" - )) - .await - .unwrap() - .text() - .await - .unwrap(), + match args.action { + Action::Convert => convert::convert(&args)?, + Action::Download => download::download(BIDGELY_BASE_URL, &args).await?, + } + + let user_id = "your-user-id"; + let start = 1664593200; + let end = 1665413999; + let user_auth_response = bidgely_adapter::auth::auth(BIDGELY_BASE_URL, user_id).await?; + let session_response = + bidgely_adapter::session::session(BIDGELY_BASE_URL, user_auth_response.payload.as_str()) + .await?; + let feed = bidgely_adapter::feed::get_feed( + BIDGELY_BASE_URL, + user_id, + session_response.payload.token_details.access_token.as_str(), + start, + end, ) - .unwrap(); + .await?; - let session = user_auth_response.payload; + let interval_blocks: Vec = feed + .entry + .into_iter() + .filter_map(|entry| match entry.content.to_inner() { + bidgely_adapter::feed::ContentType::IntervalBlock(e) => Some(e), + _ => None, + }) + .collect(); - let session_response: SessionResponse = serde_json::from_str( - &reqwest::get(format!( - "{BIDGELY_BASE_URL}/web/web-session/{session}?pilotId=40003&clientId=nsp-dashboard" - )) - .await - .unwrap() - .text() - .await - .unwrap(), - ) - .unwrap(); + let mut days: Vec = vec![]; - let token = session_response.payload.token_details.access_token; + interval_blocks.iter().for_each(|interval_block| { + let total = interval_block + .interval_reading + .iter() + .fold(0, |acc, x| acc + x.value); + days.push(total) + }); - let client = reqwest::Client::new(); - let xml_data = client.get(format!( - "{BIDGELY_BASE_URL}/dashboard/users/{user_id}/gb-download?start=1660694400&end=1665964800&measurement-type=ELECTRIC" - )) - .header(reqwest::header::CONTENT_TYPE, "application/json;charset=UTF-8") - .header(reqwest::header::AUTHORIZATION, format!("Bearer {token}")) - .send() - .await - .unwrap() - .text() - .await - .unwrap(); + days.iter().for_each(|day| println!("{:?}", day)); - println!("{:?}", xml_data); + Ok(()) }