1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
use anyhow::Result;
use iso8601_timestamp::Timestamp;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use thiserror::Error;

/// Struct type that matches the resulting data from the Amber "/sites" REST endpoint.
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
#[serde(rename_all = "camelCase")]
pub struct SiteDetails {
    pub active_from: Timestamp,
    pub channels: Vec<SiteChannels>,
    pub id: String,
    pub network: String,
    pub nmi: String,
    pub status: String,
}
/// Struct type that matches resulting data from "channels" section of the Amber "/sites" endpoint.
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
pub struct SiteChannels {
    pub identifier: String,
    pub tariff: String,
    // type is a reserved word, so rename it.
    #[serde(rename = "type")]
    pub tariff_type: String,
}

/// Struct type that matches the resulting data from the Amber "/prices" REST endpoint.
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct PriceData {
    // type is a reserved word, so rename it.
    #[serde(rename = "type")]
    pub interval_type: String,
    pub date: Timestamp,
    pub duration: u8,
    pub start_time: Timestamp,
    pub end_time: Timestamp,
    pub nem_time: Timestamp,
    pub per_kwh: f32,
    pub renewables: f32,
    pub spot_per_kwh: f32,
    pub channel_type: String,
    pub spike_status: String,
    pub tariff_information: TariffInformation,
    pub descriptor: String,
    pub estimate: Option<bool>,
}

/// Struct type that matches the resulting data from the Amber "/usage" REST endpoint.
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct UsageData {
    #[serde(rename = "type")]
    pub price_type: String,
    pub duration: u8,
    pub date: Timestamp,
    pub end_time: Timestamp,
    pub quality: String,
    pub kwh: f32,
    pub nem_time: Timestamp,
    pub per_kwh: f32,
    pub channel_type: String,
    pub channel_identifier: String,
    pub cost: f32,
    pub renewables: f32,
    pub spot_per_kwh: f32,
    pub start_time: Timestamp,
    pub spike_status: String,
    pub tariff_information: TariffInformation,
    pub descriptor: String,
}

/// Struct type that matches the "taiff_information" from the "/prices" and "/usage" endpoint.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct TariffInformation {
    pub period: String,
}

/// Struct type that matches the resulting data from the Amber "/renewables" REST endpoint.
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct RenewablesData {
    #[serde(rename = "type")]
    pub price_type: String,
    pub duration: u8,
    pub date: Timestamp,
    pub start_time: Timestamp,
    pub end_time: Timestamp,
    pub renewables: f32,
    pub descriptor: String,
}

/// Struct type that provides options for our implementation of a reqwest client.
#[derive(Clone)]
pub struct RestClient {
    pub url: String,
    pub auth_token: String,
    pub client: reqwest::Client,
}

/// Enum type to descibe and handle the error types enounced when using the RestClient.
#[derive(Error, Debug)]
pub enum Error {
    #[error("HTTP Request failed: {0}")]
    ReqwestError(#[from] reqwest::Error),

    #[error("Serde failed to decode json: {0}")]
    SerdeJsonError(#[from] serde_json::Error),

    #[error("Received a non 200 status code of {status_code:?} with message body: {body:?} ")]
    HttpNon200Status { status_code: String, body: String },
}

/// Implementation of our client to interact with the Amber REST API endpoints.
impl RestClient {
    pub fn new_client(url: String, auth_token: String) -> Self {
        Self {
            url,
            auth_token,
            client: Client::new(),
        }
    }

    /// RestClient function to request data from the Amber "/sites" endpoint.
    pub async fn get_site_data(&mut self) -> Result<Vec<SiteDetails>, Error> {
        let auth_token_header = format!("Bearer {}", &self.auth_token);

        let response = self
            .client
            .get(&self.url)
            .header("AUTHORIZATION", auth_token_header)
            .header("CONTENT_TYPE", "application/json")
            .header("ACCEPT", "application/json")
            .send()
            .await?;
        match response.status() {
            reqwest::StatusCode::OK => {
                let response = response.json::<Vec<SiteDetails>>().await?;
                Ok(response)
            }
            _ => Err(Error::HttpNon200Status {
                status_code: (response.status().to_string()),
                body: (response.text().await)?,
            }),
        }
    }

    /// RestClient function to request data from the Amber "/prices" endpoint.
    pub async fn get_price_data(&mut self) -> Result<Vec<PriceData>> {
        let auth_token_header = format!("Bearer {}", &self.auth_token);

        let response = self
            .client
            .get(&self.url)
            .header("AUTHORIZATION", auth_token_header)
            .header("CONTENT_TYPE", "application/json")
            .header("ACCEPT", "application/json")
            .send()
            .await?
            .json::<Vec<PriceData>>()
            .await?;

        Ok(response)
    }

    /// RestClient function to request data from the Amber "/usage" endpoint.
    pub async fn get_usage_data(&mut self) -> Result<Vec<UsageData>> {
        let auth_token_header = format!("Bearer {}", &self.auth_token);

        let response = self
            .client
            .get(&self.url)
            .header("AUTHORIZATION", auth_token_header)
            .header("CONTENT_TYPE", "application/json")
            .header("ACCEPT", "application/json")
            .send()
            .await?
            .json::<Vec<UsageData>>()
            .await?;

        Ok(response)
    }

    /// RustClient function to request data from the Amber "/renewables" endpoint.
    pub async fn get_renewables_data(&self) -> Result<Vec<RenewablesData>> {
        let auth_token_header = format!("Bearer {}", &self.auth_token);

        let response = self
            .client
            .get(&self.url)
            .header("AUTHORIZATION", auth_token_header)
            .header("CONTENT_TYPE", "application/json")
            .header("ACCEPT", "application/json")
            .send()
            .await?
            .json::<Vec<RenewablesData>>()
            .await?;

        Ok(response)
    }
}