matc/clusters/codec/
water_heater_management.rs1use crate::tlv;
7use anyhow;
8use serde_json;
9
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
14#[repr(u8)]
15pub enum BoostState {
16 Inactive = 0,
18 Active = 1,
20}
21
22impl BoostState {
23 pub fn from_u8(value: u8) -> Option<Self> {
25 match value {
26 0 => Some(BoostState::Inactive),
27 1 => Some(BoostState::Active),
28 _ => None,
29 }
30 }
31
32 pub fn to_u8(self) -> u8 {
34 self as u8
35 }
36}
37
38impl From<BoostState> for u8 {
39 fn from(val: BoostState) -> Self {
40 val as u8
41 }
42}
43
44pub type WaterHeaterHeatSource = u8;
48
49pub mod waterheaterheatsource {
51 pub const IMMERSION_ELEMENT1: u8 = 0x01;
53 pub const IMMERSION_ELEMENT2: u8 = 0x02;
55 pub const HEAT_PUMP: u8 = 0x04;
57 pub const BOILER: u8 = 0x08;
59 pub const OTHER: u8 = 0x10;
61}
62
63#[derive(Debug, serde::Serialize)]
66pub struct WaterHeaterBoostInfo {
67 pub duration: Option<u32>,
68 pub one_shot: Option<bool>,
69 pub emergency_boost: Option<bool>,
70 pub temporary_setpoint: Option<i16>,
71 pub target_percentage: Option<u8>,
72 pub target_reheat: Option<u8>,
73}
74
75pub fn encode_boost(boost_info: WaterHeaterBoostInfo) -> anyhow::Result<Vec<u8>> {
79 let mut boost_info_fields = Vec::new();
81 if let Some(x) = boost_info.duration { boost_info_fields.push((0, tlv::TlvItemValueEnc::UInt32(x)).into()); }
82 if let Some(x) = boost_info.one_shot { boost_info_fields.push((1, tlv::TlvItemValueEnc::Bool(x)).into()); }
83 if let Some(x) = boost_info.emergency_boost { boost_info_fields.push((2, tlv::TlvItemValueEnc::Bool(x)).into()); }
84 if let Some(x) = boost_info.temporary_setpoint { boost_info_fields.push((3, tlv::TlvItemValueEnc::Int16(x)).into()); }
85 let tlv = tlv::TlvItemEnc {
88 tag: 0,
89 value: tlv::TlvItemValueEnc::StructInvisible(vec![
90 (0, tlv::TlvItemValueEnc::StructInvisible(boost_info_fields)).into(),
91 ]),
92 };
93 Ok(tlv.encode()?)
94}
95
96pub fn decode_heater_types(inp: &tlv::TlvItemValue) -> anyhow::Result<WaterHeaterHeatSource> {
100 if let tlv::TlvItemValue::Int(v) = inp {
101 Ok(*v as u8)
102 } else {
103 Err(anyhow::anyhow!("Expected Integer"))
104 }
105}
106
107pub fn decode_heat_demand(inp: &tlv::TlvItemValue) -> anyhow::Result<WaterHeaterHeatSource> {
109 if let tlv::TlvItemValue::Int(v) = inp {
110 Ok(*v as u8)
111 } else {
112 Err(anyhow::anyhow!("Expected Integer"))
113 }
114}
115
116pub fn decode_tank_volume(inp: &tlv::TlvItemValue) -> anyhow::Result<u16> {
118 if let tlv::TlvItemValue::Int(v) = inp {
119 Ok(*v as u16)
120 } else {
121 Err(anyhow::anyhow!("Expected UInt16"))
122 }
123}
124
125pub fn decode_estimated_heat_required(inp: &tlv::TlvItemValue) -> anyhow::Result<u64> {
127 if let tlv::TlvItemValue::Int(v) = inp {
128 Ok(*v)
129 } else {
130 Err(anyhow::anyhow!("Expected UInt64"))
131 }
132}
133
134pub fn decode_tank_percentage(inp: &tlv::TlvItemValue) -> anyhow::Result<u8> {
136 if let tlv::TlvItemValue::Int(v) = inp {
137 Ok(*v as u8)
138 } else {
139 Err(anyhow::anyhow!("Expected UInt8"))
140 }
141}
142
143pub fn decode_boost_state(inp: &tlv::TlvItemValue) -> anyhow::Result<BoostState> {
145 if let tlv::TlvItemValue::Int(v) = inp {
146 BoostState::from_u8(*v as u8).ok_or_else(|| anyhow::anyhow!("Invalid enum value"))
147 } else {
148 Err(anyhow::anyhow!("Expected Integer"))
149 }
150}
151
152
153pub fn decode_attribute_json(cluster_id: u32, attribute_id: u32, tlv_value: &crate::tlv::TlvItemValue) -> String {
165 if cluster_id != 0x0094 {
167 return format!("{{\"error\": \"Invalid cluster ID. Expected 0x0094, got {}\"}}", cluster_id);
168 }
169
170 match attribute_id {
171 0x0000 => {
172 match decode_heater_types(tlv_value) {
173 Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
174 Err(e) => format!("{{\"error\": \"{}\"}}", e),
175 }
176 }
177 0x0001 => {
178 match decode_heat_demand(tlv_value) {
179 Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
180 Err(e) => format!("{{\"error\": \"{}\"}}", e),
181 }
182 }
183 0x0002 => {
184 match decode_tank_volume(tlv_value) {
185 Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
186 Err(e) => format!("{{\"error\": \"{}\"}}", e),
187 }
188 }
189 0x0003 => {
190 match decode_estimated_heat_required(tlv_value) {
191 Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
192 Err(e) => format!("{{\"error\": \"{}\"}}", e),
193 }
194 }
195 0x0004 => {
196 match decode_tank_percentage(tlv_value) {
197 Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
198 Err(e) => format!("{{\"error\": \"{}\"}}", e),
199 }
200 }
201 0x0005 => {
202 match decode_boost_state(tlv_value) {
203 Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
204 Err(e) => format!("{{\"error\": \"{}\"}}", e),
205 }
206 }
207 _ => format!("{{\"error\": \"Unknown attribute ID: {}\"}}", attribute_id),
208 }
209}
210
211pub fn get_attribute_list() -> Vec<(u32, &'static str)> {
216 vec![
217 (0x0000, "HeaterTypes"),
218 (0x0001, "HeatDemand"),
219 (0x0002, "TankVolume"),
220 (0x0003, "EstimatedHeatRequired"),
221 (0x0004, "TankPercentage"),
222 (0x0005, "BoostState"),
223 ]
224}
225
226#[derive(Debug, serde::Serialize)]
227pub struct BoostStartedEvent {
228 pub boost_info: Option<WaterHeaterBoostInfo>,
229}
230
231pub fn decode_boost_started_event(inp: &tlv::TlvItemValue) -> anyhow::Result<BoostStartedEvent> {
235 if let tlv::TlvItemValue::List(_fields) = inp {
236 let item = tlv::TlvItem { tag: 0, value: inp.clone() };
237 Ok(BoostStartedEvent {
238 boost_info: {
239 if let Some(nested_tlv) = item.get(&[0]) {
240 if let tlv::TlvItemValue::List(_) = nested_tlv {
241 let nested_item = tlv::TlvItem { tag: 0, value: nested_tlv.clone() };
242 Some(WaterHeaterBoostInfo {
243 duration: nested_item.get_int(&[0]).map(|v| v as u32),
244 one_shot: nested_item.get_bool(&[1]),
245 emergency_boost: nested_item.get_bool(&[2]),
246 temporary_setpoint: nested_item.get_int(&[3]).map(|v| v as i16),
247 target_percentage: nested_item.get_int(&[4]).map(|v| v as u8),
248 target_reheat: nested_item.get_int(&[5]).map(|v| v as u8),
249 })
250 } else {
251 None
252 }
253 } else {
254 None
255 }
256 },
257 })
258 } else {
259 Err(anyhow::anyhow!("Expected struct fields"))
260 }
261}
262