Complete download and convert xml
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
/target
|
||||
.idea/
|
||||
*.xml
|
||||
*.json
|
||||
27
Cargo.lock
generated
27
Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"crates/runner",
|
||||
"crates/bidgely_adapter",
|
||||
"crates/runner",
|
||||
]
|
||||
|
||||
78
README.md
Normal file
78
README.md
Normal 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.
|
||||
@@ -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"
|
||||
|
||||
20
crates/bidgely_adapter/src/auth.rs
Normal file
20
crates/bidgely_adapter/src/auth.rs
Normal 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?,
|
||||
)?)
|
||||
}
|
||||
188
crates/bidgely_adapter/src/feed.rs
Normal file
188
crates/bidgely_adapter/src/feed.rs
Normal 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)?)
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
117
crates/bidgely_adapter/src/session.rs
Normal file
117
crates/bidgely_adapter/src/session.rs
Normal 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?,
|
||||
)?)
|
||||
}
|
||||
@@ -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
22
crates/runner/src/args.rs
Normal 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>,
|
||||
}
|
||||
24
crates/runner/src/convert.rs
Normal file
24
crates/runner/src/convert.rs
Normal 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(())
|
||||
}
|
||||
26
crates/runner/src/download.rs
Normal file
26
crates/runner/src/download.rs
Normal 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(())
|
||||
}
|
||||
13
crates/runner/src/error.rs
Normal file
13
crates/runner/src/error.rs
Normal 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),
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user