1use crate::Client;
2use crate::client::api::ApiError;
3use crate::client::api::HiveApi;
4use crate::client::authentication::Tokens;
5use crate::helper::url::{Url, get_base_url};
6use chrono::{DateTime, Utc, serde::ts_milliseconds, serde::ts_milliseconds_option};
7use reqwest::StatusCode;
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10use serde_with::{EnumMap, serde_as};
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 = "lowercase")]
42#[serde(tag = "type")]
43#[non_exhaustive]
44pub enum ProductData {
46 Heating {
48 id: String,
50
51 #[serde(default, with = "ts_milliseconds_option")]
52 last_seen: Option<DateTime<Utc>>,
54
55 #[serde(with = "ts_milliseconds")]
56 #[serde(rename = "created")]
57 created_at: DateTime<Utc>,
59
60 #[serde(rename = "props")]
61 properties: Properties,
63
64 state: States,
66
67 #[serde(flatten)]
68 #[allow(missing_docs)]
69 extra: HashMap<String, Value>,
70 },
71
72 HotWater {
74 id: String,
76
77 #[serde(default, with = "ts_milliseconds_option")]
78 last_seen: Option<DateTime<Utc>>,
80
81 #[serde(with = "ts_milliseconds")]
82 #[serde(rename = "created")]
83 created_at: DateTime<Utc>,
85
86 #[serde(rename = "props")]
87 properties: Properties,
89
90 state: States,
92
93 #[serde(flatten)]
94 #[allow(missing_docs)]
95 extra: HashMap<String, Value>,
96 },
97
98 #[serde(other)]
99 Unknown,
101}
102
103#[derive(Debug, Serialize, Deserialize)]
104#[serde(rename_all = "UPPERCASE")]
105pub enum Mode {
110 Off,
112
113 Schedule,
115
116 Manual,
118}
119
120impl Display for Mode {
121 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
122 match self {
123 Self::Off => write!(f, "Off"),
124 Self::Schedule => write!(f, "Schedule"),
125 Self::Manual => write!(f, "Manual"),
126 }
127 }
128}
129
130#[derive(Debug, Serialize, Deserialize)]
131#[serde(rename_all = "camelCase")]
132#[non_exhaustive]
133pub enum State {
138 #[serde(rename = "target")]
139 TargetTemperature(f32),
141
142 Mode(Mode),
144
145 Name(String),
147
148 Status(String),
150
151 Boost(Option<bool>),
153
154 FrostProtection(u32),
156
157 OptimumStart(bool),
160
161 AutoBoost(String),
163
164 AutoBoostTarget(u32),
166
167 Schedule(HashMap<String, Value>),
169}
170
171impl Display for State {
172 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
173 match self {
174 Self::TargetTemperature(temp) => write!(f, "{temp}"),
175 Self::Mode(value) => write!(f, "{value}"),
176 Self::Name(value) | Self::Status(value) | Self::AutoBoost(value) => {
177 write!(f, "{value}")
178 }
179 Self::Boost(value) => write!(f, "{value:?}"),
180 Self::FrostProtection(value) | Self::AutoBoostTarget(value) => write!(f, "{value}"),
181 Self::OptimumStart(value) => write!(f, "{value}"),
182 Self::Schedule(value) => write!(f, "{value:?}"),
183 }
184 }
185}
186
187#[serde_as]
188#[derive(Debug, Serialize, Deserialize)]
189pub struct States(#[serde_as(as = "EnumMap")] pub Vec<State>);
191
192impl Deref for States {
193 type Target = Vec<State>;
194
195 fn deref(&self) -> &Self::Target {
196 &self.0
197 }
198}
199
200pub struct Product<'a> {
204 client: &'a Client,
205
206 #[allow(missing_docs)]
207 pub data: ProductData,
208}
209
210impl Debug for Product<'_> {
211 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
212 f.debug_struct("Product").field("data", &self.data).finish()
213 }
214}
215
216impl Product<'_> {
217 #[must_use]
218 pub(crate) const fn new(client: &Client, data: ProductData) -> Product<'_> {
219 Product { client, data }
220 }
221
222 pub async fn set_state(&mut self, states: States) -> Result<bool, ApiError> {
231 self.client
232 .set_product_state(
233 match &self.data {
234 ProductData::HotWater { id, .. } | ProductData::Heating { id, .. } => id,
235 ProductData::Unknown => "",
236 },
237 match &self.data {
238 ProductData::Heating { .. } => "heating",
239 ProductData::HotWater { .. } => "hotwater",
240 ProductData::Unknown => "unknown",
241 },
242 states,
243 )
244 .await
245 }
246}
247
248impl HiveApi {
249 pub(crate) async fn get_product_data(
250 &self,
251 tokens: &Tokens,
252 ) -> Result<Vec<ProductData>, ApiError> {
253 let response = self
254 .client
255 .get(get_base_url(&Url::Products))
256 .header("Authorization", &tokens.id_token)
257 .send()
258 .await;
259
260 response?
261 .json::<Vec<ProductData>>()
262 .await
263 .map_err(ApiError::from)
264 }
265
266 pub(crate) async fn set_product_state(
267 &self,
268 tokens: &Tokens,
269 id: &str,
270 r#type: &str,
271 states: States,
272 ) -> Result<bool, ApiError> {
273 let response = self
274 .client
275 .post(get_base_url(&Url::Node {
276 id: Some(id),
277 r#type: Some(r#type),
278 }))
279 .body(serde_json::to_string(&states)?)
280 .header("Authorization", &tokens.id_token)
281 .send()
282 .await?;
283
284 Ok(response.status() == StatusCode::OK)
285 }
286}