Complete download and convert xml

This commit is contained in:
2022-10-20 18:22:36 -03:00
parent 9545bb1b3d
commit 3a1e3409c3
15 changed files with 582 additions and 163 deletions

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
/target
.idea/
*.xml
*.json

27
Cargo.lock generated
View File

@@ -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"

View File

@@ -1,5 +1,5 @@
[workspace]
members = [
"crates/runner",
"crates/bidgely_adapter",
"crates/runner",
]

78
README.md Normal file
View File

@@ -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<bidgely_adapter::feed::IntervalBlock> = 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<u32> = 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.

View File

@@ -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"

View File

@@ -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<String>,
}
pub async fn auth(base_url: &str, user_id: &str) -> Result<UserAuthResponse, BidgelyError> {
Ok(serde_json::from_str(
&reqwest::get(format!(
"{base_url}/user-auth/cipher?user-id={user_id}&pilot-id=40003"
))
.await?
.text()
.await?,
)?)
}

View File

@@ -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<Entry>,
}
#[derive(serde::Deserialize, serde::Serialize, Debug)]
pub struct Entry {
pub id: String,
pub link: Vec<Link>,
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<IntervalReading>,
}
#[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<Feed, 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
?;
Ok(quick_xml::de::from_str(&xml_data)?)
}

View File

@@ -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<String>,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
#[serde(rename_all(deserialize = "camelCase"))]
pub struct SessionResponse {
pub request_id: String,
pub payload: SessionPayload,
pub error: Option<String>,
}
#[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<String>,
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<MeasurementToUserType>,
}
#[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<Premise>,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
#[serde(rename_all(deserialize = "camelCase"))]
pub struct Premise {
pub uuid: String,
pub premise_id: Option<String>,
pub address: PremiseAddress,
pub supported_measurement_types: Vec<String>, // 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<String>,
pub city: String,
pub state: String,
pub zipcode: String,
pub context: Option<String>,
#[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),
}

View File

@@ -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<String>,
}
#[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<String>,
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<MeasurementToUserType>,
}
#[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<Premise>,
}
#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all(deserialize = "camelCase"))]
pub struct Premise {
pub uuid: String,
pub premise_id: Option<String>,
pub address: PremiseAddress,
pub supported_measurement_types: Vec<String>, // 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<String>,
pub city: String,
pub state: String,
pub zipcode: String,
pub context: Option<String>,
}
pub async fn session(base_url: &str, session: &str) -> Result<SessionResponse, BidgelyError> {
Ok(serde_json::from_str(
&reqwest::get(format!(
"{base_url}/web/web-session/{session}?pilotId=40003&clientId=nsp-dashboard"
))
.await?
.text()
.await?,
)?)
}

View File

@@ -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"] }

22
crates/runner/src/args.rs Normal file
View File

@@ -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<String>,
#[arg(short, long)]
pub input_filename: Option<String>,
#[arg(short, long)]
pub output_filename: Option<String>,
#[arg(short, long)]
pub start: Option<u64>,
#[arg(short, long)]
pub end: Option<u64>,
}

View File

@@ -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(())
}

View File

@@ -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(())
}

View File

@@ -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),
}

View File

@@ -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<bidgely_adapter::feed::IntervalBlock> = 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<u32> = 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(())
}