1mod site;
38
39use chrono::NaiveDateTime;
40use log::{debug, trace};
41use reqwest::StatusCode;
42use std::collections::HashMap;
43use thiserror::Error;
44
45pub use site::{
46 DataPeriod, GeneratedEnergy, GeneratedEnergyValue, GeneratedPower, GeneratedPowerPerTimeUnit,
47 GeneratedPowerValue, Location, Overview, PrimaryModule, PublicSettings, Site, TimeData,
48 TimeUnit,
49};
50
51#[derive(Error, Debug)]
54pub enum SolarApiError {
55 #[error("Could not retrieve data from SolarEdge Monitoring API")]
56 NetworkError(reqwest::Error),
57 #[error("API returned an Error")]
58 ApiError(reqwest::Error),
59 #[error("Not allowed to access API. Is the site id valid? Is your API token valid?")]
60 ForbiddenError(reqwest::Error),
61 #[error("Could not parse result from SolardEdge monitoring api")]
62 ParseError(#[from] serde_json::Error),
63}
64
65impl From<reqwest::Error> for SolarApiError {
66 fn from(error: reqwest::Error) -> Self {
67 if let Some(status) = error.status() {
68 if status.is_client_error() || status.is_server_error() {
69 if status == StatusCode::from_u16(403).unwrap() {
70 return SolarApiError::ForbiddenError(error);
71 }
72 return SolarApiError::ApiError(error);
73 }
74 }
75 SolarApiError::NetworkError(error)
76 }
77}
78
79const BASE_URL: &str = "monitoringapi.solaredge.com";
80
81fn default_map(api_key: &str) -> HashMap<String, String> {
82 let mut map = HashMap::new();
83 map.insert("api_key".into(), api_key.into());
84 map
85}
86
87fn map_to_params(map: &HashMap<String, String>) -> String {
88 let mut params = map
89 .iter()
90 .fold(String::new(), |s, (k, v)| s + &format!("{}={}&", k, v));
91
92 params.pop();
94 params
95}
96
97fn to_url(path: &str, params: &HashMap<String, String>) -> String {
98 let params = map_to_params(params);
99 let url = format!("https://{}{}?{}", BASE_URL, path, params);
100 url
101}
102
103fn call_url(url: &str) -> Result<String, reqwest::Error> {
104 trace!("Calling {}", url);
105 let reply = reqwest::blocking::get(url)?.error_for_status()?;
106
107 trace!("reply: {:?}", reply);
108 let reply_text = reply.text()?;
109 trace!("reply text: {}", reply_text);
110 Ok(reply_text)
111}
112
113pub fn list(api_key: &str) -> Result<Vec<site::Site>, SolarApiError> {
116 debug!("Calling list of sites");
117 let map = default_map(api_key);
118 let url = to_url("/sites/list", &map);
119 let reply_text = call_url(&url)?;
120
121 trace!("Parsing");
122 let reply: site::SitesReply = serde_json::from_str(&reply_text)?;
123
124 Ok((*reply.sites()).clone())
125}
126
127pub fn details(api_key: &str, site_id: u32) -> Result<site::Site, SolarApiError> {
129 debug!("Getting details of {site_id}");
130 let params = default_map(api_key);
131 let path = format!("/site/{site_id}/details");
132 let url = to_url(&path, ¶ms);
133 let reply_text = call_url(&url)?;
134
135 trace!("Parsing json");
136 let site: site::SiteDetails = serde_json::from_str(&reply_text)?;
137
138 Ok(site.details)
139}
140
141pub fn data_period(api_key: &str, site_id: u32) -> Result<site::DataPeriod, SolarApiError> {
143 debug!("Getting data_period of {site_id}");
144 let params = default_map(api_key);
145 let path = format!("/site/{site_id}/dataPeriod");
146 let url = to_url(&path, ¶ms);
147 let reply_text = call_url(&url)?;
148
149 trace!("Parsing json");
150 let period: site::DataPeriodReply = serde_json::from_str(&reply_text)?;
151
152 Ok(period.data_period)
153}
154
155pub fn overview(api_key: &str, site_id: u32) -> Result<site::Overview, SolarApiError> {
157 debug!("Getting overview of {}", site_id);
158 let params = default_map(api_key);
159 let path = format!("/site/{}/overview", site_id);
160 let url = to_url(&path, ¶ms);
161 let reply_text = call_url(&url)?;
162
163 trace!("Parsing json");
164 let overview: site::OverviewReply = serde_json::from_str(&reply_text)?;
165
166 Ok(overview.overview)
167}
168
169pub fn energy(
176 api_key: &str,
177 site_id: u32,
178 period: DataPeriod,
179 time_unit: TimeUnit,
180) -> Result<site::GeneratedEnergy, SolarApiError> {
181 debug!(
182 "Getting energy for {}-{} with unit {}",
183 period.start_date,
184 period.end_date,
185 time_unit.to_param()
186 );
187
188 let mut params = default_map(api_key);
189 params.insert("startDate".into(), period.formatted_start_date());
190 params.insert("endDate".into(), period.formatted_end_date());
191 params.insert("timeUnit".into(), time_unit.to_param().into());
192 let path = format!("/site/{site_id}/energy");
193 let url = to_url(&path, ¶ms);
194 let reply_text = call_url(&url)?;
195
196 trace!("Parsing json");
197 let energy: site::GeneratedEnergyReply = serde_json::from_str(&reply_text)?;
198
199 Ok(energy.energy)
200}
201
202pub fn power(
207 api_key: &str,
208 site_id: u32,
209 start_datetime: NaiveDateTime,
210 end_datetime: NaiveDateTime,
211) -> Result<site::GeneratedPowerPerTimeUnit, SolarApiError> {
212 debug!("Getting power for {}-{}", start_datetime, end_datetime,);
213
214 let mut params = default_map(api_key);
215 params.insert(
216 "startTime".into(),
217 format!("{}", start_datetime.format("%Y-%m-%d %H:%M:%S")),
218 );
219 params.insert(
220 "endTime".into(),
221 format!("{}", end_datetime.format("%Y-%m-%d %H:%M:%S")),
222 );
223 let path = format!("/site/{site_id}/power");
224 let url = to_url(&path, ¶ms);
225 let reply_text = call_url(&url)?;
226
227 trace!("Parsing json");
228 let power: site::GeneratedPowerReply = serde_json::from_str(&reply_text)?;
229
230 Ok(power.power)
231}
232
233#[test]
234fn test_map_to_params() {
235 let mut map = HashMap::new();
236 map.insert("key".to_string(), "value".to_string());
237 map.insert("key2".to_string(), "value2".to_string());
238
239 let params = map_to_params(&map);
240 assert!(params == "key=value&key2=value2" || params == "key2=value2&key=value");
242}