From 5840acbd954f8845177f3db69b3549c4fd99733b Mon Sep 17 00:00:00 2001 From: Ted <63551230+grtw@users.noreply.github.com> Date: Sat, 16 Apr 2022 16:09:11 -0400 Subject: [PATCH] Initial commit --- .gitattributes | 2 + .gitignore | 20 +++++ Cargo.toml | 17 ++++ LICENSE | 21 +++++ README.md | 2 + src/current_version.rs | 63 +++++++++++++++ src/date_value.rs | 14 ++++ src/meter_type.rs | 45 +++++++++++ src/meter_value.rs | 15 ++++ src/se_ms_api.rs | 100 ++++++++++++++++++++++++ src/site_details.rs | 115 +++++++++++++++++++++++++++ src/site_energy_detailed.rs | 139 +++++++++++++++++++++++++++++++++ src/site_location.rs | 35 +++++++++ src/site_module.rs | 20 +++++ src/site_public_settings.rs | 14 ++++ src/supported_versions.rs | 63 +++++++++++++++ src/time_unit.rs | 48 ++++++++++++ tests/common/mod.rs | 34 ++++++++ tests/integration_reqs_test.rs | 117 +++++++++++++++++++++++++++ 19 files changed, 884 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 src/current_version.rs create mode 100644 src/date_value.rs create mode 100644 src/meter_type.rs create mode 100644 src/meter_value.rs create mode 100644 src/se_ms_api.rs create mode 100644 src/site_details.rs create mode 100644 src/site_energy_detailed.rs create mode 100644 src/site_location.rs create mode 100644 src/site_module.rs create mode 100644 src/site_public_settings.rs create mode 100644 src/supported_versions.rs create mode 100644 src/time_unit.rs create mode 100644 tests/common/mod.rs create mode 100644 tests/integration_reqs_test.rs diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c769c07 --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + +# Visual Studio Code configuration +.vscode/ + +# Has unique data for testing, no need to share. +tests/test_credentials.txt diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..fe84069 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "se_monitoring_server_api" +version = "0.1.0" +edition = "2021" +description = "Library for accessing the SolarEdge Monitoring Server API" +license = "MIT" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "se_ms_api" +path = "src/se_ms_api.rs" + +[dependencies] +chrono = "0.4" +reqwest = { version = "0.11", features = ["json", "blocking", "cookies"] } +serde = { version = "1", features = ["derive"] } diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..72682e3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Thomas Wrather + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..48525fd --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# se_ms_api + SolarEdge Monitoring Server API diff --git a/src/current_version.rs b/src/current_version.rs new file mode 100644 index 0000000..ed17d7e --- /dev/null +++ b/src/current_version.rs @@ -0,0 +1,63 @@ +//! Module for querying the current API version of the SolarEdge monitoring server. + +use crate::SolaredgeCredentials; +use serde::{Deserialize, Serialize}; + +/// Current version request +pub struct CurrentVersionReq {} + +#[derive(Serialize, Deserialize, Debug)] +#[allow(non_snake_case)] +/// Current version response +pub struct CurrentVersionResp { + /// The API version running on the server + pub version: Version, +} + +#[derive(Serialize, Deserialize, Debug)] +#[allow(non_snake_case)] +/// The release version of the server +pub struct Version { + /// The release number running on the server in format. + pub release: String, +} + +impl CurrentVersionReq { + /// Create a current version request message that can be sent to SolarEdge. + pub fn new() -> Self { + CurrentVersionReq {} + } + + /// Send the current version request to Solaredge and return the response. + /// + /// # Arguments + /// + /// * `solaredge` - SolarEdge credentials to use for sending + /// + /// # Returns + /// the SolarEdge response or an error string + pub fn send(&self, solaredge: &SolaredgeCredentials) -> Result { + let url = format!( + "{}version/current?{}", + solaredge.url_start, solaredge.url_end + ); + + let res = match reqwest::blocking::get(&url) { + Ok(r) => r, + Err(e) => return Err(format!("reqwest get error {}", e)), + }; + + let parsed = match res.json::() { + Ok(p) => p, + Err(e) => return Err(format!("JSON parse error {}", e)), + }; + + Ok(parsed) + } +} + +impl Default for CurrentVersionReq { + fn default() -> Self { + Self::new() + } +} diff --git a/src/date_value.rs b/src/date_value.rs new file mode 100644 index 0000000..6ca98f3 --- /dev/null +++ b/src/date_value.rs @@ -0,0 +1,14 @@ +//! Module for handling generic date / value pairs returned by the SolarEdge server monitoring API. + +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug)] +/// A date and value pair returned from the monitoring API. The value units are specified by the unit +/// field elsewhere in the response. +pub struct DateValue { + /// YYYY-mm-dd HH:MM:SS + pub date: String, + + /// Often an integer, but can be float too. Meaning defined by the context of the response. + pub value: Option, +} diff --git a/src/meter_type.rs b/src/meter_type.rs new file mode 100644 index 0000000..24e6b82 --- /dev/null +++ b/src/meter_type.rs @@ -0,0 +1,45 @@ +//! Module for specifying the meter type in SolarEdge server monitoring API requests and responses. + +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] +/// Meters supported by SolarEdge. +pub enum MeterType { + /// Solar energy produced. + Production, + + /// Total energy consumed (solar + grid) + Consumption, + + /// Solar energy consumed. + SelfConsumption, + + /// Solar energy exported to grid. + FeedIn, + + /// Energy purchased from grid. + Purchased, +} + +impl std::fmt::Display for MeterType { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match *self { + MeterType::Production => write!(f, "Production"), + MeterType::Consumption => write!(f, "Consumption"), + MeterType::SelfConsumption => write!(f, "SelfConsumption"), + MeterType::FeedIn => write!(f, "FeedIn"), + MeterType::Purchased => write!(f, "Purchased"), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn meter_type_fmt_unit_test() { + let t = MeterType::Production; + assert_eq!(format!("{}", t), "Production"); + } +} diff --git a/src/meter_value.rs b/src/meter_value.rs new file mode 100644 index 0000000..64681ea --- /dev/null +++ b/src/meter_value.rs @@ -0,0 +1,15 @@ +//! Module for holding values for a specified meter type in SolarEdge server monitoring API responses. + +pub use crate::date_value::DateValue; +use crate::meter_type::MeterType; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug)] +/// Values for the meter type over a range of dates. +pub struct MeterValue { + /// The meter type of the associated values. + pub r#type: MeterType, // had to escape the keyword type to use as a json identifier + + /// Meter readings for each date. + pub values: Vec, +} diff --git a/src/se_ms_api.rs b/src/se_ms_api.rs new file mode 100644 index 0000000..8235fea --- /dev/null +++ b/src/se_ms_api.rs @@ -0,0 +1,100 @@ +//! Library to retrieve data from the SolarEdge Monitoring Server. +//! +//! Based on the API define here: +//! , +//! released January 2022. +//! +//! The basic use case is: +//! 1) Create a SolarEdge struct that contains the site id and api key +//! that will be used for the requests. (The se_monitoring_api.pdf linked +//! above has instructions for getting your site id and api key. +//! 2) Create a request for the information that you want. +//! 3) Send the request using the SolarEdge struct. +//! 4) Read the response to get the information. +//! +//! ```no_run +//! extern crate se_ms_api; +//! use se_ms_api::{SiteDetailsReq, SolaredgeCredentials}; +//! +//! let site_id = "my_site_id"; +//! let api_key = "my_api_key"; +//! +//! let solar_edge = SolaredgeCredentials::new(&site_id, &api_key); // (1) +//! let req = SiteDetailsReq::new(); // (2) +//! let resp = req.send(&solar_edge); // (3) +//! +//! match resp { // (4) +//! Ok(r) => { +//! println!("My site's status is {}.", r.details.status); +//! } +//! Err(e) => { +//! panic!("Unexpected SiteDetails response: {:?}", e); +//! } +//!} +//! ``` +//! Supported API requests/responses include: +//! * [SiteDetailsReq] / [SiteDetailsResp] +//! * [SiteEnergyDetailedReq] / [SiteEnergyDetailedResp] +//! + +#![deny(unused_crate_dependencies)] +#![deny(unused_extern_crates)] +#![warn(missing_docs)] + +pub mod site_details; +pub use site_details::{SiteDetailsReq, SiteDetailsResp}; +pub mod site_energy_detailed; +pub use site_energy_detailed::{SiteEnergyDetailedReq, SiteEnergyDetailedResp}; +pub mod current_version; +pub use current_version::{CurrentVersionReq, CurrentVersionResp}; +pub mod supported_versions; +pub use supported_versions::{SupportedVersionsReq, SupportedVersionsResp}; +pub mod date_value; +pub mod meter_type; +pub use meter_type::MeterType; +pub mod meter_value; +pub mod site_location; +pub mod site_module; +pub mod site_public_settings; +pub mod time_unit; + +const URL_TIME_FORMAT: &str = "%Y-%m-%d %H:%M:%S"; + +/// Struct for accessing SolarEdge's monitoring server for a given site and api key. +/// +/// Used as the parameter for the send() function of all of the possible requests. +pub struct SolaredgeCredentials { + url_start: String, + site_id: String, + url_end: String, +} + +impl SolaredgeCredentials { + const MONITORING_API_URL: &'static str = "https://monitoringapi.solaredge.com/"; + + /// Create a Solaredge destination for the requests from the given site id and api_key. + pub fn new(site_id: &str, api_key: &str) -> Self { + let url_start = SolaredgeCredentials::MONITORING_API_URL.to_string(); + let site_id = site_id.to_string(); + let url_end = format!("api_key={}", api_key); + + SolaredgeCredentials { + url_start, + site_id, + url_end, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn solaredge_new_unit_test() { + let se = SolaredgeCredentials::new("id", "key"); + assert_eq!(se.url_start, SolaredgeCredentials::MONITORING_API_URL); + assert_eq!(se.site_id, "id"); + assert_eq!(se.url_end, "api_key=key"); + } +} diff --git a/src/site_details.rs b/src/site_details.rs new file mode 100644 index 0000000..0d9492a --- /dev/null +++ b/src/site_details.rs @@ -0,0 +1,115 @@ +//! Module for site details requests and responses exchanged with the SolarEdge server monitoring API. + +use crate::site_location::SiteLocation; +use crate::site_module::SiteModule; +use crate::site_public_settings::SitePublicSettings; +use crate::SolaredgeCredentials; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +/// site_details request +pub struct SiteDetailsReq {} + +#[derive(Serialize, Deserialize, Debug)] +#[allow(non_snake_case)] +/// site_details response +pub struct SiteDetailsResp { + /// Detailed information about the monitoring site + pub details: SiteDetails, +} + +#[derive(Serialize, Deserialize, Debug)] +#[allow(non_snake_case)] +/// Detailed information for a single site. +pub struct SiteDetails { + /// ID of the site. Should match the site_id specified in the Solaredge request. + pub id: i32, + + /// Name of the site. + pub name: String, + + /// Account the site belongs to. + pub accountId: i32, + + /// Site status, either Active or Pending Communication. + pub status: String, + + /// Maximum power that can be generated by the site. + pub peakPower: f32, + + /// Last time the site reported in to SolarEdge. + pub lastUpdateTime: String, + + /// Currency type (money) used at the site location + pub currency: String, + + /// Date site was initially brought on line. + pub installationDate: String, + + /// Date site was given permission to operate. + pub ptoDate: Option, + + /// Free form notes about the site. + pub notes: String, + + /// Site type, ("Optimizers & Inverters", "Safety & Monitoring Interface", "Monitoring Combiner Boxes") + pub r#type: String, // had to escape the keyword type to use as an identifier + + /// Site location (mailing address) + pub location: SiteLocation, + + /// Main module type used at the site (e.g solar panel model) + pub primaryModule: SiteModule, + + /// Number of open alerts at the site. + pub alertQuantity: Option, + + /// Highest alert severity at the site. + pub alertSeverity: Option, + + /// Miscellaneous uris associated with the web page for the site. + pub uris: HashMap, + + /// Public settings of the web page for the site. + pub publicSettings: SitePublicSettings, +} + +impl SiteDetailsReq { + /// Create a site details request message that can be sent to SolarEdge. + pub fn new() -> Self { + SiteDetailsReq {} + } + + /// Send the site_details request to Solaredge and return the response. + /// + /// # Arguments + /// + /// * `solaredge` - SolarEdge credentials to use for sending + /// + /// # Returns + /// the SolarEdge response or an error string + pub fn send(&self, solaredge: &SolaredgeCredentials) -> Result { + let url = format!( + "{}site/{}/details?{}", + solaredge.url_start, solaredge.site_id, solaredge.url_end + ); + + let res = match reqwest::blocking::get(&url) { + Ok(r) => r, + Err(e) => return Err(format!("reqwest get error {}", e)), + }; + + let parsed = match res.json::() { + Ok(p) => p, + Err(e) => return Err(format!("JSON parse error: {}", e)), + }; + + Ok(parsed) + } +} + +impl Default for SiteDetailsReq { + fn default() -> Self { + Self::new() + } +} diff --git a/src/site_energy_detailed.rs b/src/site_energy_detailed.rs new file mode 100644 index 0000000..ec87e28 --- /dev/null +++ b/src/site_energy_detailed.rs @@ -0,0 +1,139 @@ +//! Module for detailed site energy measurements from meters such as consumption, export (feed-in), import (purchase), etc. + +use crate::meter_type::MeterType; +use crate::meter_value::MeterValue; +use crate::time_unit::TimeUnit; +use crate::SolaredgeCredentials; +use crate::URL_TIME_FORMAT; +use serde::{Deserialize, Serialize}; + +/// site_energyDetails request +pub struct SiteEnergyDetailedReq { + start_time: String, + end_time: String, + time_unit: String, + meters: String, +} + +#[derive(Serialize, Deserialize, Debug)] +#[allow(non_snake_case)] +/// site_energyDetails response +pub struct SiteEnergyDetailedResp { + /// Energy details + pub energyDetails: EnergyDetails, +} + +#[derive(Serialize, Deserialize, Debug)] +#[allow(non_snake_case)] +/// Energy details +pub struct EnergyDetails { + /// Granularity of the energy detail values (should match the request) + pub timeUnit: String, + + /// Measurement unit (e.g. Wh) + pub unit: String, + + /// For the meter types requested, energy values over the time period + pub meters: Vec, +} + +impl SiteEnergyDetailedReq { + /// Create an energy details request message that can be sent to SolarEdge. + /// + /// # Arguments + /// + /// * `start_time` - beginning of the time period for the energy details + /// * `end_time` - end of the time period for the energy details + /// * `time_unit` - aggregation granularity + /// For the time period requested, energy detail values will be + /// chunked into units of this size. + /// * `meters` - meter types to collect energy details for + pub fn new( + start_time: chrono::NaiveDateTime, + end_time: chrono::NaiveDateTime, + time_unit: Option, + meters: Option>, + ) -> Self { + let start_time = format!("startTime={}&", start_time.format(URL_TIME_FORMAT)); + + let end_time = format!("endTime={}&", end_time.format(URL_TIME_FORMAT)); + + let time_unit = match time_unit { + Some(t) => format!("timeUnit={}&", t), + None => "".to_string(), + }; + + let meters = match meters { + Some(m) => format!( + "meters={}&", + m.iter() + .map(|x| x.to_string()) + .collect::>() + .join(",") + ), + None => "".to_string(), + }; + + SiteEnergyDetailedReq { + start_time, + end_time, + time_unit, + meters, + } + } + + /// Send the site_energyDetails request to Solaredge and return the response. + /// + /// # Arguments + /// + /// * `solaredge` - SolarEdge credentials to use for sending + /// + /// # Returns + /// the SolarEdge response or an error string + pub fn send(&self, solaredge: &SolaredgeCredentials) -> Result { + let url = format!( + "{}site/{}/energyDetails?{}{}{}{}{}", + solaredge.url_start, + solaredge.site_id, + self.meters, + self.time_unit, + self.start_time, + self.end_time, + solaredge.url_end + ); + + //println!("url: {}\n", url); + let res = match reqwest::blocking::get(&url) { + Ok(r) => r, + Err(e) => return Err(format!("reqwest get error {}", e)), + }; + //println!("raw response: {:?}", res); + + let parsed = match res.json::() { + Ok(p) => p, + Err(e) => return Err(format!("JSON parse error {}", e)), + }; + + Ok(parsed) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use chrono::NaiveDateTime; + + #[test] + fn site_energy_detailed_req_new_unit_test() { + let dt = "2022-01-01 00:00:00"; + let ndt = match NaiveDateTime::parse_from_str(dt, "%Y-%m-%d %H:%M:%S") { + Ok(ndt) => ndt, + Err(_) => panic!("test failed"), + }; + let req = SiteEnergyDetailedReq::new(ndt, ndt, None, None); + assert_eq!(req.start_time, format!("startTime={}&", dt)); + assert_eq!(req.end_time, format!("endTime={}&", dt)); + assert_eq!(req.time_unit, ""); + assert_eq!(req.meters, ""); + } +} diff --git a/src/site_location.rs b/src/site_location.rs new file mode 100644 index 0000000..2b77498 --- /dev/null +++ b/src/site_location.rs @@ -0,0 +1,35 @@ +//! Module for holding site location data returned in the SolarEdge server monitoring API responses. + +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug)] +#[allow(non_snake_case)] +/// Location of the SolarEdge inverter. +pub struct SiteLocation { + /// Country of the SolarEdge inverter. + pub country: String, + + /// State of the SolarEdge inverter. + pub state: String, // seems US specific. should this be Option? + + /// City of the SolarEdge inverter. + pub city: String, + + /// Address line 1 of the SolarEdge inverter. + pub address: String, + + /// Address line 2 of the SolarEdge inverter. + pub address2: String, + + /// Zip code 1 of the SolarEdge inverter. + pub zip: String, // seems US specific. should this be Option? + + /// Time zone of the SolarEdge inverter. + pub timeZone: String, + + /// Country code (abbreviation) of the SolarEdge inverter. + pub countryCode: String, + + /// State (abbreviation) of the SolarEdge inverter. + pub stateCode: String, // seems US specific. should this be Option? +} diff --git a/src/site_module.rs b/src/site_module.rs new file mode 100644 index 0000000..d03e6d8 --- /dev/null +++ b/src/site_module.rs @@ -0,0 +1,20 @@ +//! Module for reporting solar panel module information from the SolarEdge server monitoring API. + +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug)] +#[allow(non_snake_case)] +/// Solar panel module information +pub struct SiteModule { + /// solar panel manufacturer + pub manufacturerName: String, + + /// solar panel model name/number + pub modelName: String, + + /// solar panel max output power + pub maximumPower: f32, + + /// solar panel temperature coefficient + pub temperatureCoef: f32, +} diff --git a/src/site_public_settings.rs b/src/site_public_settings.rs new file mode 100644 index 0000000..ddbc1fe --- /dev/null +++ b/src/site_public_settings.rs @@ -0,0 +1,14 @@ +//! Module for reporting information about the web pages for the site served by SolarEdge. + +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug)] +#[allow(non_snake_case)] +/// Information about the public web page for the site provided by SolarEdge. +pub struct SitePublicSettings { + /// Optional name given to the web page fro the site. + pub name: Option, + + /// Is the web page accessible to the public (i.e. no password required)? + pub isPublic: bool, +} diff --git a/src/supported_versions.rs b/src/supported_versions.rs new file mode 100644 index 0000000..194dcc9 --- /dev/null +++ b/src/supported_versions.rs @@ -0,0 +1,63 @@ +//! Module for querying the API versions supported by the SolarEdge monitoring server. + +use crate::SolaredgeCredentials; +use serde::{Deserialize, Serialize}; + +/// Supported versions request +pub struct SupportedVersionsReq {} + +#[derive(Serialize, Deserialize, Debug)] +#[allow(non_snake_case)] +/// Supported versions response +pub struct SupportedVersionsResp { + /// An array of all the API versions supported by the server + pub supported: Vec, +} + +#[derive(Serialize, Deserialize, Debug)] +#[allow(non_snake_case)] +/// A release version supported by the server +pub struct Release { + /// A release number supported by the server in format. + pub release: String, +} + +impl SupportedVersionsReq { + /// Create a supported versions request message that can be sent to SolarEdge. + pub fn new() -> Self { + SupportedVersionsReq {} + } + + /// Send the supported versions request to Solaredge and return the response. + /// + /// # Arguments + /// + /// * `solaredge` - SolarEdge credentials to use for sending + /// + /// # Returns + /// the SolarEdge response or an error string + pub fn send(&self, solaredge: &SolaredgeCredentials) -> Result { + let url = format!( + "{}version/supported?{}", + solaredge.url_start, solaredge.url_end + ); + + let res = match reqwest::blocking::get(&url) { + Ok(r) => r, + Err(e) => return Err(format!("reqwest get error {}", e)), + }; + + let parsed = match res.json::() { + Ok(p) => p, + Err(e) => return Err(format!("JSON parse error {}", e)), + }; + + Ok(parsed) + } +} + +impl Default for SupportedVersionsReq { + fn default() -> Self { + Self::new() + } +} diff --git a/src/time_unit.rs b/src/time_unit.rs new file mode 100644 index 0000000..dabf1b5 --- /dev/null +++ b/src/time_unit.rs @@ -0,0 +1,48 @@ +//! Module for handling units of time used by the SolarEdge server monitoring API. + +#[allow(non_camel_case_types)] +/// Time units specified in SolarEdge server monitoring API requests and responses. +/// Specifies the aggregation granularity of the data. +pub enum TimeUnit { + /// 15 minutes + QUARTER_OF_AN_HOUR, + + /// 60 minutes + HOUR, + + /// 24 hours + DAY, + + /// 7 days + WEEK, + + /// Calendar month + MONTH, + + /// Calendar year + YEAR, +} + +impl std::fmt::Display for TimeUnit { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match *self { + TimeUnit::QUARTER_OF_AN_HOUR => write!(f, "QUARTER_OF_AN_HOUR"), + TimeUnit::HOUR => write!(f, "HOUR"), + TimeUnit::DAY => write!(f, "DAY"), + TimeUnit::WEEK => write!(f, "WEEK"), + TimeUnit::MONTH => write!(f, "MONTH"), + TimeUnit::YEAR => write!(f, "YEAR"), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn time_unit_fmt_unit_test() { + let t = TimeUnit::YEAR; + assert_eq!(format!("{}", t), "YEAR"); + } +} diff --git a/tests/common/mod.rs b/tests/common/mod.rs new file mode 100644 index 0000000..d06ff85 --- /dev/null +++ b/tests/common/mod.rs @@ -0,0 +1,34 @@ +use std::env; +use std::fs; +use std::sync::Once; + +pub const TIME_FORMAT: &'static str = "%Y-%m-%d %H:%M:%S"; + +const TEST_CREDENTIALS_FILE: &'static str = "test_credentials.txt"; + +static mut SITE_ID: String = String::new(); +static mut API_KEY: String = String::new(); +static INIT: Once = Once::new(); + +pub fn get_site_id_and_api_key() -> (&'static str, &'static str) { + unsafe { + INIT.call_once(|| { + let path = env::current_dir().unwrap(); + let path = path.join("tests").join(TEST_CREDENTIALS_FILE); + + let contents = fs::read_to_string(path) + .expect(&format!("Unable to read {}.", TEST_CREDENTIALS_FILE)); + let mut lines = contents.lines(); + if let Some(s) = lines.next() { + SITE_ID = s.to_string(); + } + if let Some(s) = lines.next() { + API_KEY = s.to_string(); + } + if SITE_ID.len() == 0 || API_KEY.len() == 0 { + panic!("Ill formed credentials file."); + } + }); + (&SITE_ID, &API_KEY) + } +} diff --git a/tests/integration_reqs_test.rs b/tests/integration_reqs_test.rs new file mode 100644 index 0000000..9d8028d --- /dev/null +++ b/tests/integration_reqs_test.rs @@ -0,0 +1,117 @@ +use chrono::NaiveDateTime; + +mod common; + +use se_ms_api::{ + CurrentVersionReq, MeterType, SiteDetailsReq, SiteEnergyDetailedReq, SolaredgeCredentials, + SupportedVersionsReq, +}; + +#[test] +fn site_energy_detailed_integration_test() { + let (site_id, api_key) = common::get_site_id_and_api_key(); + + let solar_edge = SolaredgeCredentials::new(&site_id, &api_key); + + let start_ndt = match NaiveDateTime::parse_from_str("2022-01-01 00:00:00", common::TIME_FORMAT) + { + Ok(dt) => dt, + Err(error) => panic!("Error parsing start date: {}", error), + }; + + let end_ndt = match NaiveDateTime::parse_from_str("2022-01-31 00:00:00", common::TIME_FORMAT) { + Ok(dt) => dt, + Err(error) => panic!("Error parsing end date: {}", error), + }; + + let req = SiteEnergyDetailedReq::new( + start_ndt, + end_ndt, + None, + Some(vec![MeterType::SelfConsumption]), + ); + + let resp = req.send(&solar_edge); + + match resp { + Ok(r) => { + assert_eq!(r.energyDetails.unit, "Wh"); + assert_eq!(r.energyDetails.meters.len(), 1); + assert_eq!(r.energyDetails.meters[0].r#type, MeterType::SelfConsumption); + assert_eq!(r.energyDetails.meters[0].values.len(), 31); + + let mut self_consumption: f32 = 0.0; + for v in &r.energyDetails.meters[0].values { + if let Some(value) = v.value { + self_consumption += value; + } + } + assert!(self_consumption as i32 == 292473); + } + Err(e) => { + panic!("Unexpected SiteEnergyDetailedReq response: {:?}", e); + } + }; +} + +#[test] +fn current_version_integration_test() { + let (site_id, api_key) = common::get_site_id_and_api_key(); + + let solar_edge = SolaredgeCredentials::new(&site_id, &api_key); + + let req = CurrentVersionReq::new(); + let resp = req.send(&solar_edge); + + match resp { + Ok(r) => { + assert_eq!(r.version.release, "1.0.0"); + } + bad => { + panic!("Unexpected CurrentVersion response: {:?}", bad); + } + } +} + +#[test] +fn supported_versions_integration_test() { + let (site_id, api_key) = common::get_site_id_and_api_key(); + + let solar_edge = SolaredgeCredentials::new(&site_id, &api_key); + + let req = SupportedVersionsReq::new(); + let resp = req.send(&solar_edge); + + match resp { + Ok(r) => { + assert_eq!(r.supported[0].release, "1.0.0"); + } + bad => { + panic!("Unexpected SupportedVersions response: {:?}", bad); + } + } +} + +#[test] +fn site_details_integration_test() { + let (site_id, api_key) = common::get_site_id_and_api_key(); + + let solar_edge = SolaredgeCredentials::new(&site_id, &api_key); + + let req = SiteDetailsReq::new(); + let resp = req.send(&solar_edge); + + match resp { + Ok(r) => { + assert_eq!(r.details.id.to_string(), site_id); + assert_eq!(r.details.status, "Active"); + assert_eq!(r.details.location.countryCode, "US"); + assert_eq!(r.details.primaryModule.manufacturerName, "LG"); + assert!(r.details.uris.contains_key("SITE_IMAGE")); + assert!(!r.details.publicSettings.isPublic); + } + Err(e) => { + panic!("Unexpected SiteDetails response: {:?}", e); + } + } +}