1use crate::client::api::ApiError;
2use crate::client::api::HiveApi;
3use crate::client::authentication::Tokens;
4use crate::helper::url::{get_base_url, Url};
5use crate::Client;
6use chrono::{serde::ts_milliseconds, DateTime, Utc};
7use reqwest::StatusCode;
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10use serde_with::{serde_as, EnumMap};
11use std::collections::HashMap;
12use std::fmt;
13use std::fmt::{Debug, Display, Formatter};
14use std::ops::Deref;
15
16#[derive(Serialize, Deserialize, Debug)]
17#[non_exhaustive]
18#[allow(missing_docs)]
19pub struct Properties {
20 #[serde(rename = "zone")]
21 pub zone_id: Option<String>,
23
24 #[serde(rename = "online")]
25 pub is_online: bool,
27
28 #[serde(rename = "working")]
29 pub is_working: bool,
31
32 pub temperature: Option<f32>,
34
35 #[serde(flatten)]
36 #[allow(missing_docs)]
37 pub extra: HashMap<String, Value>,
38}
39
40#[derive(Serialize, Deserialize, Debug)]
41#[serde(rename_all = "camelCase")]
42#[non_exhaustive]
43#[serde_as]
44#[allow(missing_docs)]
45pub struct Heating {
46 pub id: String,
48
49 #[serde(with = "ts_milliseconds")]
50 pub last_seen: DateTime<Utc>,
52
53 #[serde(with = "ts_milliseconds")]
54 #[serde(rename = "created")]
55 pub created_at: DateTime<Utc>,
57
58 #[serde(rename = "props")]
59 pub properties: Properties,
61
62 pub state: States,
64
65 #[serde(flatten)]
66 #[allow(missing_docs)]
67 pub extra: HashMap<String, Value>,
68}
69
70#[derive(Serialize, Deserialize, Debug)]
71#[serde(rename_all = "camelCase")]
72#[non_exhaustive]
73#[allow(missing_docs)]
74pub struct HotWater {
75 pub id: String,
77
78 #[serde(with = "ts_milliseconds")]
79 pub last_seen: DateTime<Utc>,
81
82 #[serde(with = "ts_milliseconds")]
83 #[serde(rename = "created")]
84 pub created_at: DateTime<Utc>,
86
87 #[serde(rename = "props")]
88 pub properties: Properties,
90
91 pub state: States,
93
94 #[serde(flatten)]
95 #[allow(missing_docs)]
96 pub extra: HashMap<String, Value>,
97}
98
99#[derive(Serialize, Deserialize, Debug)]
100#[serde(rename_all = "lowercase")]
101#[serde(tag = "type")]
102#[non_exhaustive]
103pub enum ProductData {
105 Heating(Heating),
107
108 HotWater(HotWater),
110
111 #[serde(other)]
112 Unknown,
114}
115
116#[derive(Debug, Serialize, Deserialize)]
117#[serde(rename_all = "UPPERCASE")]
118pub enum Mode {
123 Off,
125
126 Schedule,
128
129 Manual,
131}
132
133impl Display for Mode {
134 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
135 match self {
136 Self::Off => write!(f, "Off"),
137 Self::Schedule => write!(f, "Schedule"),
138 Self::Manual => write!(f, "Manual"),
139 }
140 }
141}
142
143#[derive(Debug, Serialize, Deserialize)]
144#[serde(rename_all = "camelCase")]
145#[non_exhaustive]
146pub enum State {
151 #[serde(rename = "target")]
152 TargetTemperature(f32),
154
155 Mode(Mode),
157
158 Name(String),
160
161 Status(String),
163
164 Boost(Option<bool>),
166
167 FrostProtection(u32),
169
170 OptimumStart(bool),
173
174 AutoBoost(String),
176
177 AutoBoostTarget(u32),
179
180 Schedule(HashMap<String, Value>),
182}
183
184impl Display for State {
185 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
186 match self {
187 Self::TargetTemperature(temp) => write!(f, "{temp}"),
188 Self::Mode(value) => write!(f, "{value}"),
189 Self::Name(value) | Self::Status(value) | Self::AutoBoost(value) => {
190 write!(f, "{value}")
191 }
192 Self::Boost(value) => write!(f, "{value:?}"),
193 Self::FrostProtection(value) | Self::AutoBoostTarget(value) => write!(f, "{value}"),
194 Self::OptimumStart(value) => write!(f, "{value}"),
195 Self::Schedule(value) => write!(f, "{value:?}"),
196 }
197 }
198}
199
200#[serde_as]
201#[derive(Debug, Serialize, Deserialize)]
202pub struct States(#[serde_as(as = "EnumMap")] pub Vec<State>);
204
205impl Deref for States {
206 type Target = Vec<State>;
207
208 fn deref(&self) -> &Self::Target {
209 &self.0
210 }
211}
212
213pub struct Product<'a> {
217 client: &'a Client,
218
219 #[allow(missing_docs)]
220 pub data: ProductData,
221}
222
223impl Debug for Product<'_> {
224 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
225 f.debug_struct("Product").field("data", &self.data).finish()
226 }
227}
228
229impl Product<'_> {
230 #[must_use]
231 pub(crate) const fn new(client: &Client, data: ProductData) -> Product<'_> {
232 Product { client, data }
233 }
234
235 pub async fn set_state(&mut self, states: States) -> Result<bool, ApiError> {
244 self.client
245 .set_product_state(
246 match &self.data {
247 ProductData::Heating(data) => &data.id,
248 ProductData::HotWater(data) => &data.id,
249 ProductData::Unknown => "",
250 },
251 match &self.data {
252 ProductData::Heating(_) => "heating",
253 ProductData::HotWater(_) => "hotwater",
254 ProductData::Unknown => "unknown",
255 },
256 states,
257 )
258 .await
259 }
260}
261
262impl HiveApi {
263 pub(crate) async fn get_product_data(
264 &self,
265 tokens: &Tokens,
266 ) -> Result<Vec<ProductData>, ApiError> {
267 let response = self
268 .client
269 .get(get_base_url(&Url::Products))
270 .header("Authorization", &tokens.id_token)
271 .send()
272 .await;
273
274 response?
275 .json::<Vec<ProductData>>()
276 .await
277 .map_err(ApiError::from)
278 }
279
280 pub(crate) async fn set_product_state(
281 &self,
282 tokens: &Tokens,
283 id: &str,
284 r#type: &str,
285 states: States,
286 ) -> Result<bool, ApiError> {
287 let response = self
288 .client
289 .post(get_base_url(&Url::Node {
290 id: Some(id),
291 r#type: Some(r#type),
292 }))
293 .body(serde_json::to_string(&states)?)
294 .header("Authorization", &tokens.id_token)
295 .send()
296 .await?;
297
298 Ok(response.status() == StatusCode::OK)
299 }
300}