amber_api/client.rs
1//! # Amber Electric API Client
2//!
3//! This module provides a client for interacting with the [Amber Electric Public API](https://api.amber.com.au/v1).
4
5use crate::{error::Result, models};
6use serde::de::DeserializeOwned;
7use tracing::{debug, instrument};
8
9/// The base URL for the Amber Electric API
10const API_BASE_URL: &str = "https://api.amber.com.au/v1/";
11
12/// Main client for the Amber Electric API
13///
14/// This client provides a high-level interface to all Amber Electric API
15/// endpoints.
16#[derive(Debug, bon::Builder)]
17pub struct Amber {
18 /// HTTP client for making requests
19 agent: ureq::Agent,
20 /// Optional API key for authenticated requests
21 api_key: Option<String>,
22 /// Base URL for the Amber API
23 base_url: String,
24}
25
26impl Default for Amber {
27 /// Create a new default Amber API client.
28 ///
29 /// This create a default client that is authenticated if an API key is set
30 /// in the `AMBER_API_KEY` environment variable.
31 #[inline]
32 fn default() -> Self {
33 debug!("Creating default Amber API client");
34 Self {
35 agent: ureq::agent(),
36 api_key: std::env::var("AMBER_API_KEY")
37 .ok()
38 .filter(|s| !s.is_empty()),
39 base_url: API_BASE_URL.to_owned(),
40 }
41 }
42}
43
44#[bon::bon]
45impl Amber {
46 /// Perform a GET request to the Amber API.
47 #[instrument(skip(self, query), level = "debug")]
48 fn get<T: DeserializeOwned, I, K, V>(&self, path: &str, query: I) -> Result<T>
49 where
50 I: IntoIterator<Item = (K, V)>,
51 K: AsRef<str>,
52 V: AsRef<str>,
53 {
54 let endpoint = format!("{}{}", self.base_url, path);
55 debug!("GET {endpoint}");
56
57 let mut request = self.agent.get(&endpoint);
58 if let Some(api_key) = &self.api_key {
59 request = request.header("Authorization", &format!("Bearer {api_key}"));
60 }
61
62 for (key, value) in query {
63 debug!("Query parameter: {}={}", key.as_ref(), value.as_ref());
64 request = request.query(key.as_ref(), value.as_ref());
65 }
66
67 let mut response = request.call()?;
68 debug!("Status code: {}", response.status());
69 Ok(response.body_mut().read_json()?)
70 }
71
72 /// Returns the current percentage of renewables in the grid for a specific state.
73 ///
74 /// This method retrieves renewable energy data for the specified Australian state.
75 /// The data shows the current percentage of renewable energy in the grid and can
76 /// optionally include historical and forecast data.
77 ///
78 /// # Parameters
79 ///
80 /// - `state`: The Australian state (NSW, VIC, QLD, SA)
81 /// - `next`: Optional number of forecast intervals to return
82 /// - `previous`: Optional number of historical intervals to return
83 /// - `resolution`: Optional interval duration (5 or 30 minutes, default 30)
84 ///
85 /// # Authentication
86 ///
87 /// This endpoint does not require authentication and can be called without an API key.
88 ///
89 /// # Returns
90 ///
91 /// Returns a [`Result`] containing a [`Vec`] of [`Renewable`] objects on success.
92 ///
93 /// # Errors
94 ///
95 /// This method will return an error if:
96 ///
97 /// - There's a network error communicating with the API
98 /// - The API returns an internal server error (HTTP 500)
99 ///
100 /// # Example
101 ///
102 /// ```
103 /// use amber_api::Amber;
104 /// use amber_api::models::{State, Resolution};
105 ///
106 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
107 /// let client = Amber::default();
108 ///
109 /// // Get current renewables data for Victoria
110 /// let renewables = client.current_renewables()
111 /// .state(State::Vic)
112 /// .call()?;
113 ///
114 /// for renewable in renewables {
115 /// println!("{}", renewable);
116 /// }
117 ///
118 /// // Get current data with 8 forecast intervals
119 /// let renewables_with_forecast = client.current_renewables()
120 /// .state(State::Nsw)
121 /// .next(8)
122 /// .resolution(Resolution::FiveMinute)
123 /// .call()?;
124 /// # Ok(())
125 /// # }
126 /// ```
127 ///
128 /// [`Renewable`]: crate::models::Renewable
129 #[inline]
130 #[builder]
131 pub fn current_renewables(
132 &self,
133 state: models::State,
134 next: Option<u32>,
135 previous: Option<u32>,
136 resolution: Option<models::Resolution>,
137 ) -> Result<Vec<models::Renewable>> {
138 self.get(
139 &format!("state/{state}/renewables/current"),
140 [
141 ("next", next.map(|n| n.to_string())),
142 ("previous", previous.map(|p| p.to_string())),
143 ("resolution", resolution.map(|r| r.to_string())),
144 ]
145 .into_iter()
146 .filter_map(|(k, v)| v.map(|val| (k, val))),
147 )
148 }
149
150 /// Return all sites linked to your account.
151 ///
152 /// This method returns information about all electricity sites associated with your
153 /// Amber account. Each site represents a location where you have electricity service.
154 ///
155 /// # Authentication
156 ///
157 /// This method requires authentication via API key. The API key can be provided
158 /// either through the `AMBER_API_KEY` environment variable (when using [`Amber::default()`])
159 /// or by explicitly setting it when building the client.
160 ///
161 /// # Returns
162 ///
163 /// Returns a [`Result`] containing a [`Vec`] of [`Site`] objects on success.
164 ///
165 /// # Errors
166 ///
167 /// This method will return an error if:
168 ///
169 /// - The API key is missing or invalid (HTTP 401)
170 /// - There's a network error communicating with the API
171 /// - The API returns an internal server error (HTTP 500)
172 ///
173 /// # Example
174 ///
175 /// ```
176 /// use amber_api::Amber;
177 ///
178 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
179 /// let client = Amber::default();
180 /// let sites = client.sites()?;
181 ///
182 /// for site in sites {
183 /// println!("Site {}: {} ({})", site.id, site.network, site.status);
184 /// }
185 /// # Ok(())
186 /// # }
187 /// ```
188 ///
189 /// [`Site`]: crate::models::Site
190 #[inline]
191 pub fn sites(&self) -> Result<Vec<crate::models::Site>> {
192 self.get("sites", core::iter::empty::<(&str, &str)>())
193 }
194
195 /// Returns all the prices between the start and end dates for a specific site.
196 ///
197 /// This method retrieves historical pricing data for the specified site between
198 /// the given date range. The date range cannot exceed 7 days.
199 ///
200 /// # Parameters
201 ///
202 /// - `site_id`: ID of the site you are fetching prices for (obtained from [`sites()`])
203 /// - `start_date`: Optional start date for the price range (defaults to today)
204 /// - `end_date`: Optional end date for the price range (defaults to today)
205 /// - `resolution`: Optional interval duration (5 or 30 minutes, defaults to your billing interval)
206 ///
207 /// # Authentication
208 ///
209 /// This method requires authentication via API key. The API key can be provided
210 /// either through the `AMBER_API_KEY` environment variable (when using [`Amber::default()`])
211 /// or by explicitly setting it when building the client.
212 ///
213 /// # Returns
214 ///
215 /// Returns a [`Result`] containing a [`Vec`] of [`Interval`] objects on success.
216 /// Intervals are returned in order: General > Controlled Load > Feed In.
217 ///
218 /// # Errors
219 ///
220 /// This method will return an error if:
221 ///
222 /// - The API key is missing or invalid (HTTP 401)
223 /// - The site ID is invalid (HTTP 400)
224 /// - The site is not found (HTTP 404)
225 /// - The date range exceeds 7 days (HTTP 422)
226 /// - There's a network error communicating with the API
227 /// - The API returns an internal server error (HTTP 500)
228 ///
229 /// # Example
230 ///
231 /// ```
232 /// use std::str::FromStr;
233 ///
234 /// use amber_api::Amber;
235 /// use amber_api::models::Resolution;
236 /// use jiff::civil::Date;
237 ///
238 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
239 /// let client = Amber::default();
240 /// let sites = client.sites()?;
241 /// let site_id = &sites[0].id;
242 ///
243 /// // Get prices for today
244 /// let prices = client.prices()
245 /// .site_id(site_id)
246 /// .call()?;
247 ///
248 /// // Get prices for a specific date range
249 /// let start_date = Date::from_str("2021-05-01").expect("Invalid start date");
250 /// let end_date = Date::from_str("2021-05-03").expect("Invalid end date");
251 /// let prices = client.prices()
252 /// .site_id(site_id)
253 /// .start_date(start_date)
254 /// .end_date(end_date)
255 /// .resolution(Resolution::FiveMinute)
256 /// .call()?;
257 ///
258 /// for interval in prices {
259 /// match interval {
260 /// amber_api::models::Interval::ActualInterval(actual) => {
261 /// println!("Actual price: {:.2}c/kWh", actual.base.per_kwh);
262 /// }
263 /// _ => {} // Handle other interval types as needed
264 /// }
265 /// }
266 /// # Ok(())
267 /// # }
268 /// ```
269 ///
270 /// [`sites()`]: Self::sites
271 /// [`Interval`]: crate::models::Interval
272 #[inline]
273 #[builder]
274 pub fn prices(
275 &self,
276 site_id: &str,
277 start_date: Option<jiff::civil::Date>,
278 end_date: Option<jiff::civil::Date>,
279 resolution: Option<models::Resolution>,
280 ) -> Result<Vec<models::Interval>> {
281 self.get(
282 &format!("sites/{site_id}/prices"),
283 [
284 ("startDate", start_date.map(|d| d.to_string())),
285 ("endDate", end_date.map(|d| d.to_string())),
286 ("resolution", resolution.map(|r| r.to_string())),
287 ]
288 .into_iter()
289 .filter_map(|(k, v)| v.map(|val| (k, val))),
290 )
291 }
292
293 /// Returns the current price for a specific site.
294 ///
295 /// This method retrieves the current pricing data for the specified site,
296 /// optionally including historical and forecast data.
297 ///
298 /// # Parameters
299 ///
300 /// - `site_id`: ID of the site you are fetching prices for (obtained from [`sites()`])
301 /// - `next`: Optional number of forecast intervals to return (max 2048 total)
302 /// - `previous`: Optional number of historical intervals to return (max 2048 total)
303 /// - `resolution`: Optional interval duration (5 or 30 minutes, defaults to your billing interval)
304 ///
305 /// # Authentication
306 ///
307 /// This method requires authentication via API key. The API key can be provided
308 /// either through the `AMBER_API_KEY` environment variable (when using [`Amber::default()`])
309 /// or by explicitly setting it when building the client.
310 ///
311 /// # Returns
312 ///
313 /// Returns a [`Result`] containing a [`Vec`] of [`Interval`] objects on success.
314 /// Intervals are returned in order: General > Controlled Load > Feed In.
315 ///
316 /// # Errors
317 ///
318 /// This method will return an error if:
319 ///
320 /// - The API key is missing or invalid (HTTP 401)
321 /// - The site ID is invalid (HTTP 400)
322 /// - The site is not found (HTTP 404)
323 /// - The total number of intervals exceeds 2048 (HTTP 422)
324 /// - There's a network error communicating with the API
325 /// - The API returns an internal server error (HTTP 500)
326 ///
327 /// # Example
328 ///
329 /// ```
330 /// use amber_api::Amber;
331 /// use amber_api::models::Resolution;
332 ///
333 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
334 /// let client = Amber::default();
335 /// let sites = client.sites()?;
336 /// let site_id = &sites[0].id;
337 ///
338 /// // Get current prices only
339 /// let current_prices = client.current_prices()
340 /// .site_id(site_id)
341 /// .call()?;
342 ///
343 /// // Get current prices with forecast
344 /// let prices_with_forecast = client.current_prices()
345 /// .site_id(site_id)
346 /// .next(8)
347 /// .resolution(Resolution::ThirtyMinute)
348 /// .call()?;
349 ///
350 /// // Get current prices with history and forecast
351 /// let full_prices = client.current_prices()
352 /// .site_id(site_id)
353 /// .previous(8)
354 /// .next(8)
355 /// .call()?;
356 ///
357 /// for interval in current_prices {
358 /// match interval {
359 /// amber_api::models::Interval::CurrentInterval(current) => {
360 /// println!("Current price: {:.2}c/kWh (estimate: {})",
361 /// current.base.per_kwh, current.estimate);
362 /// }
363 /// _ => {} // Handle other interval types as needed
364 /// }
365 /// }
366 /// # Ok(())
367 /// # }
368 /// ```
369 ///
370 /// [`sites()`]: Self::sites
371 /// [`Interval`]: crate::models::Interval
372 #[inline]
373 #[builder]
374 pub fn current_prices(
375 &self,
376 site_id: &str,
377 next: Option<u32>,
378 previous: Option<u32>,
379 resolution: Option<models::Resolution>,
380 ) -> Result<Vec<models::Interval>> {
381 self.get(
382 &format!("sites/{site_id}/prices/current"),
383 [
384 ("next", next.map(|n| n.to_string())),
385 ("previous", previous.map(|p| p.to_string())),
386 ("resolution", resolution.map(|r| r.to_string())),
387 ]
388 .into_iter()
389 .filter_map(|(k, v)| v.map(|val| (k, val))),
390 )
391 }
392
393 /// Returns all usage data between the start and end dates for a specific site.
394 ///
395 /// This method retrieves historical usage data for the specified site between
396 /// the given date range. The date range cannot exceed 7 days, and the API can
397 /// only return 90 days worth of data.
398 ///
399 /// # Parameters
400 ///
401 /// - `site_id`: ID of the site you are fetching usage for (obtained from [`sites()`])
402 /// - `start_date`: Start date for the usage data (required)
403 /// - `end_date`: End date for the usage data (required)
404 /// - `resolution`: Optional interval duration (deprecated, will be ignored)
405 ///
406 /// # Authentication
407 ///
408 /// This method requires authentication via API key. The API key can be provided
409 /// either through the `AMBER_API_KEY` environment variable (when using [`Amber::default()`])
410 /// or by explicitly setting it when building the client.
411 ///
412 /// # Returns
413 ///
414 /// Returns a [`Result`] containing a [`Vec`] of [`Usage`] objects on success.
415 /// Usage data is returned in order: General > Controlled Load > Feed In.
416 ///
417 /// # Errors
418 ///
419 /// This method will return an error if:
420 ///
421 /// - The API key is missing or invalid (HTTP 401)
422 /// - The site ID is invalid (HTTP 400)
423 /// - The site is not found (HTTP 404)
424 /// - The date range exceeds 7 days (HTTP 422)
425 /// - There's a network error communicating with the API
426 /// - The API returns an internal server error (HTTP 500)
427 ///
428 /// # Example
429 ///
430 /// ```
431 /// use std::str::FromStr;
432 ///
433 /// use amber_api::Amber;
434 /// use jiff::civil::Date;
435 ///
436 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
437 /// let client = Amber::default();
438 /// let sites = client.sites()?;
439 /// let site_id = &sites[0].id;
440 ///
441 /// // Get usage data for a specific date range
442 /// let start_date = Date::from_str("2021-05-01").expect("Invalid start date");
443 /// let end_date = Date::from_str("2021-05-03").expect("Invalid end date");
444 /// let usage_data = client.usage()
445 /// .site_id(site_id)
446 /// .start_date(start_date)
447 /// .end_date(end_date)
448 /// .call()?;
449 ///
450 /// for usage in usage_data {
451 /// println!("Channel {}: {:.2} kWh, Cost: ${:.2}",
452 /// usage.channel_identifier, usage.kwh, usage.cost);
453 /// }
454 /// # Ok(())
455 /// # }
456 /// ```
457 ///
458 /// [`sites()`]: Self::sites
459 /// [`Usage`]: crate::models::Usage
460 #[inline]
461 #[builder]
462 pub fn usage(
463 &self,
464 site_id: &str,
465 start_date: jiff::civil::Date,
466 end_date: jiff::civil::Date,
467 ) -> Result<Vec<models::Usage>> {
468 let start_date_str = start_date.to_string();
469 let end_date_str = end_date.to_string();
470 let query_params = [
471 ("startDate", start_date_str.as_str()),
472 ("endDate", end_date_str.as_str()),
473 ];
474
475 self.get(&format!("sites/{site_id}/usage"), query_params)
476 }
477}