matc/clusters/codec/
device_energy_management.rs

1//! Matter TLV encoders and decoders for Device Energy Management Cluster
2//! Cluster ID: 0x0098
3//!
4//! This file is automatically generated from DeviceEnergyManagement.xml
5
6use crate::tlv;
7use anyhow;
8use serde_json;
9
10
11// Enum definitions
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
14#[repr(u8)]
15pub enum AdjustmentCause {
16    /// The adjustment is to optimize the local energy usage
17    Localoptimization = 0,
18    /// The adjustment is to optimize the grid energy usage
19    Gridoptimization = 1,
20}
21
22impl AdjustmentCause {
23    /// Convert from u8 value
24    pub fn from_u8(value: u8) -> Option<Self> {
25        match value {
26            0 => Some(AdjustmentCause::Localoptimization),
27            1 => Some(AdjustmentCause::Gridoptimization),
28            _ => None,
29        }
30    }
31
32    /// Convert to u8 value
33    pub fn to_u8(self) -> u8 {
34        self as u8
35    }
36}
37
38impl From<AdjustmentCause> for u8 {
39    fn from(val: AdjustmentCause) -> Self {
40        val as u8
41    }
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
45#[repr(u8)]
46pub enum Cause {
47    /// The ESA completed the power adjustment as requested
48    Normalcompletion = 0,
49    /// The ESA was set to offline
50    Offline = 1,
51    /// The ESA has developed a fault could not complete the adjustment
52    Fault = 2,
53    /// The user has disabled the ESA's flexibility capability
54    Useroptout = 3,
55    /// The adjustment was cancelled by a client
56    Cancelled = 4,
57}
58
59impl Cause {
60    /// Convert from u8 value
61    pub fn from_u8(value: u8) -> Option<Self> {
62        match value {
63            0 => Some(Cause::Normalcompletion),
64            1 => Some(Cause::Offline),
65            2 => Some(Cause::Fault),
66            3 => Some(Cause::Useroptout),
67            4 => Some(Cause::Cancelled),
68            _ => None,
69        }
70    }
71
72    /// Convert to u8 value
73    pub fn to_u8(self) -> u8 {
74        self as u8
75    }
76}
77
78impl From<Cause> for u8 {
79    fn from(val: Cause) -> Self {
80        val as u8
81    }
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
85#[repr(u8)]
86pub enum CostType {
87    /// Financial cost
88    Financial = 0,
89    /// Grid CO2e grams cost
90    Ghgemissions = 1,
91    /// Consumer comfort impact cost
92    Comfort = 2,
93    /// Temperature impact cost
94    Temperature = 3,
95}
96
97impl CostType {
98    /// Convert from u8 value
99    pub fn from_u8(value: u8) -> Option<Self> {
100        match value {
101            0 => Some(CostType::Financial),
102            1 => Some(CostType::Ghgemissions),
103            2 => Some(CostType::Comfort),
104            3 => Some(CostType::Temperature),
105            _ => None,
106        }
107    }
108
109    /// Convert to u8 value
110    pub fn to_u8(self) -> u8 {
111        self as u8
112    }
113}
114
115impl From<CostType> for u8 {
116    fn from(val: CostType) -> Self {
117        val as u8
118    }
119}
120
121#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
122#[repr(u8)]
123pub enum ESAState {
124    /// The ESA is not available to the EMS (e.g. start-up, maintenance mode)
125    Offline = 0,
126    /// The ESA is working normally and can be controlled by the EMS
127    Online = 1,
128    /// The ESA has developed a fault and cannot provide service
129    Fault = 2,
130    /// The ESA is in the middle of a power adjustment event
131    Poweradjustactive = 3,
132    /// The ESA is currently paused by a client using the PauseRequest command
133    Paused = 4,
134}
135
136impl ESAState {
137    /// Convert from u8 value
138    pub fn from_u8(value: u8) -> Option<Self> {
139        match value {
140            0 => Some(ESAState::Offline),
141            1 => Some(ESAState::Online),
142            2 => Some(ESAState::Fault),
143            3 => Some(ESAState::Poweradjustactive),
144            4 => Some(ESAState::Paused),
145            _ => None,
146        }
147    }
148
149    /// Convert to u8 value
150    pub fn to_u8(self) -> u8 {
151        self as u8
152    }
153}
154
155impl From<ESAState> for u8 {
156    fn from(val: ESAState) -> Self {
157        val as u8
158    }
159}
160
161#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
162#[repr(u8)]
163pub enum ESAType {
164    /// EV Supply Equipment
165    Evse = 0,
166    /// Space heating appliance
167    Spaceheating = 1,
168    /// Water heating appliance
169    Waterheating = 2,
170    /// Space cooling appliance
171    Spacecooling = 3,
172    /// Space heating and cooling appliance
173    Spaceheatingcooling = 4,
174    /// Battery Electric Storage System
175    Batterystorage = 5,
176    /// Solar PV inverter
177    Solarpv = 6,
178    /// Fridge / Freezer
179    Fridgefreezer = 7,
180    /// Washing Machine
181    Washingmachine = 8,
182    /// Dishwasher
183    Dishwasher = 9,
184    /// Cooking appliance
185    Cooking = 10,
186    /// Home water pump (e.g. drinking well)
187    Homewaterpump = 11,
188    /// Irrigation water pump
189    Irrigationwaterpump = 12,
190    /// Pool pump
191    Poolpump = 13,
192    /// Other appliance type
193    Other = 255,
194}
195
196impl ESAType {
197    /// Convert from u8 value
198    pub fn from_u8(value: u8) -> Option<Self> {
199        match value {
200            0 => Some(ESAType::Evse),
201            1 => Some(ESAType::Spaceheating),
202            2 => Some(ESAType::Waterheating),
203            3 => Some(ESAType::Spacecooling),
204            4 => Some(ESAType::Spaceheatingcooling),
205            5 => Some(ESAType::Batterystorage),
206            6 => Some(ESAType::Solarpv),
207            7 => Some(ESAType::Fridgefreezer),
208            8 => Some(ESAType::Washingmachine),
209            9 => Some(ESAType::Dishwasher),
210            10 => Some(ESAType::Cooking),
211            11 => Some(ESAType::Homewaterpump),
212            12 => Some(ESAType::Irrigationwaterpump),
213            13 => Some(ESAType::Poolpump),
214            255 => Some(ESAType::Other),
215            _ => None,
216        }
217    }
218
219    /// Convert to u8 value
220    pub fn to_u8(self) -> u8 {
221        self as u8
222    }
223}
224
225impl From<ESAType> for u8 {
226    fn from(val: ESAType) -> Self {
227        val as u8
228    }
229}
230
231#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
232#[repr(u8)]
233pub enum ForecastUpdateReason {
234    /// The update was due to internal ESA device optimization
235    Internaloptimization = 0,
236    /// The update was due to local EMS optimization
237    Localoptimization = 1,
238    /// The update was due to grid optimization
239    Gridoptimization = 2,
240}
241
242impl ForecastUpdateReason {
243    /// Convert from u8 value
244    pub fn from_u8(value: u8) -> Option<Self> {
245        match value {
246            0 => Some(ForecastUpdateReason::Internaloptimization),
247            1 => Some(ForecastUpdateReason::Localoptimization),
248            2 => Some(ForecastUpdateReason::Gridoptimization),
249            _ => None,
250        }
251    }
252
253    /// Convert to u8 value
254    pub fn to_u8(self) -> u8 {
255        self as u8
256    }
257}
258
259impl From<ForecastUpdateReason> for u8 {
260    fn from(val: ForecastUpdateReason) -> Self {
261        val as u8
262    }
263}
264
265#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
266#[repr(u8)]
267pub enum OptOutState {
268    /// The user has not opted out of either local or grid optimizations
269    Nooptout = 0,
270    /// The user has opted out of local EMS optimizations only
271    Localoptout = 1,
272    /// The user has opted out of grid EMS optimizations only
273    Gridoptout = 2,
274    /// The user has opted out of all external optimizations
275    Optout = 3,
276}
277
278impl OptOutState {
279    /// Convert from u8 value
280    pub fn from_u8(value: u8) -> Option<Self> {
281        match value {
282            0 => Some(OptOutState::Nooptout),
283            1 => Some(OptOutState::Localoptout),
284            2 => Some(OptOutState::Gridoptout),
285            3 => Some(OptOutState::Optout),
286            _ => None,
287        }
288    }
289
290    /// Convert to u8 value
291    pub fn to_u8(self) -> u8 {
292        self as u8
293    }
294}
295
296impl From<OptOutState> for u8 {
297    fn from(val: OptOutState) -> Self {
298        val as u8
299    }
300}
301
302#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
303#[repr(u8)]
304pub enum PowerAdjustReason {
305    /// There is no Power Adjustment active
306    Noadjustment = 0,
307    /// There is PowerAdjustment active due to local EMS optimization
308    Localoptimizationadjustment = 1,
309    /// There is PowerAdjustment active due to grid optimization
310    Gridoptimizationadjustment = 2,
311}
312
313impl PowerAdjustReason {
314    /// Convert from u8 value
315    pub fn from_u8(value: u8) -> Option<Self> {
316        match value {
317            0 => Some(PowerAdjustReason::Noadjustment),
318            1 => Some(PowerAdjustReason::Localoptimizationadjustment),
319            2 => Some(PowerAdjustReason::Gridoptimizationadjustment),
320            _ => None,
321        }
322    }
323
324    /// Convert to u8 value
325    pub fn to_u8(self) -> u8 {
326        self as u8
327    }
328}
329
330impl From<PowerAdjustReason> for u8 {
331    fn from(val: PowerAdjustReason) -> Self {
332        val as u8
333    }
334}
335
336// Struct definitions
337
338#[derive(Debug, serde::Serialize)]
339pub struct Constraints {
340    pub start_time: Option<u64>,
341    pub duration: Option<u32>,
342    pub nominal_power: Option<u32>,
343    pub maximum_energy: Option<u64>,
344    pub load_control: Option<i8>,
345}
346
347#[derive(Debug, serde::Serialize)]
348pub struct Cost {
349    pub cost_type: Option<CostType>,
350    pub value: Option<i32>,
351    pub decimal_points: Option<u8>,
352    pub currency: Option<u16>,
353}
354
355#[derive(Debug, serde::Serialize)]
356pub struct Forecast {
357    pub forecast_id: Option<u32>,
358    pub active_slot_number: Option<u16>,
359    pub start_time: Option<u64>,
360    pub end_time: Option<u64>,
361    pub earliest_start_time: Option<u64>,
362    pub latest_end_time: Option<u64>,
363    pub is_pausable: Option<bool>,
364    pub slots: Option<Vec<Slot>>,
365    pub forecast_update_reason: Option<ForecastUpdateReason>,
366}
367
368#[derive(Debug, serde::Serialize)]
369pub struct PowerAdjustCapability {
370    pub power_adjust_capability: Option<Vec<PowerAdjust>>,
371    pub cause: Option<PowerAdjustReason>,
372}
373
374#[derive(Debug, serde::Serialize)]
375pub struct PowerAdjust {
376    pub min_power: Option<u32>,
377    pub max_power: Option<u32>,
378    pub min_duration: Option<u32>,
379    pub max_duration: Option<u32>,
380}
381
382#[derive(Debug, serde::Serialize)]
383pub struct SlotAdjustment {
384    pub slot_index: Option<u8>,
385    pub nominal_power: Option<u32>,
386    pub duration: Option<u32>,
387}
388
389#[derive(Debug, serde::Serialize)]
390pub struct Slot {
391    pub min_duration: Option<u32>,
392    pub max_duration: Option<u32>,
393    pub default_duration: Option<u32>,
394    pub elapsed_slot_time: Option<u32>,
395    pub remaining_slot_time: Option<u32>,
396    pub slot_is_pausable: Option<bool>,
397    pub min_pause_duration: Option<u32>,
398    pub max_pause_duration: Option<u32>,
399    pub manufacturer_esa_state: Option<u16>,
400    pub nominal_power: Option<u32>,
401    pub min_power: Option<u32>,
402    pub max_power: Option<u32>,
403    pub nominal_energy: Option<u64>,
404    pub costs: Option<Vec<Cost>>,
405    pub min_power_adjustment: Option<u32>,
406    pub max_power_adjustment: Option<u32>,
407    pub min_duration_adjustment: Option<u32>,
408    pub max_duration_adjustment: Option<u32>,
409}
410
411// Command encoders
412
413/// Encode PowerAdjustRequest command (0x00)
414pub fn encode_power_adjust_request(power: u32, duration: u32, cause: AdjustmentCause) -> anyhow::Result<Vec<u8>> {
415    let tlv = tlv::TlvItemEnc {
416        tag: 0,
417        value: tlv::TlvItemValueEnc::StructInvisible(vec![
418        (0, tlv::TlvItemValueEnc::UInt32(power)).into(),
419        (1, tlv::TlvItemValueEnc::UInt32(duration)).into(),
420        (2, tlv::TlvItemValueEnc::UInt8(cause.to_u8())).into(),
421        ]),
422    };
423    Ok(tlv.encode()?)
424}
425
426/// Encode StartTimeAdjustRequest command (0x02)
427pub fn encode_start_time_adjust_request(requested_start_time: u64, cause: AdjustmentCause) -> anyhow::Result<Vec<u8>> {
428    let tlv = tlv::TlvItemEnc {
429        tag: 0,
430        value: tlv::TlvItemValueEnc::StructInvisible(vec![
431        (0, tlv::TlvItemValueEnc::UInt64(requested_start_time)).into(),
432        (1, tlv::TlvItemValueEnc::UInt8(cause.to_u8())).into(),
433        ]),
434    };
435    Ok(tlv.encode()?)
436}
437
438/// Encode PauseRequest command (0x03)
439pub fn encode_pause_request(duration: u32, cause: AdjustmentCause) -> anyhow::Result<Vec<u8>> {
440    let tlv = tlv::TlvItemEnc {
441        tag: 0,
442        value: tlv::TlvItemValueEnc::StructInvisible(vec![
443        (0, tlv::TlvItemValueEnc::UInt32(duration)).into(),
444        (1, tlv::TlvItemValueEnc::UInt8(cause.to_u8())).into(),
445        ]),
446    };
447    Ok(tlv.encode()?)
448}
449
450/// Encode ModifyForecastRequest command (0x05)
451pub fn encode_modify_forecast_request(forecast_id: u32, slot_adjustments: Vec<SlotAdjustment>, cause: AdjustmentCause) -> anyhow::Result<Vec<u8>> {
452    let tlv = tlv::TlvItemEnc {
453        tag: 0,
454        value: tlv::TlvItemValueEnc::StructInvisible(vec![
455        (0, tlv::TlvItemValueEnc::UInt32(forecast_id)).into(),
456        (1, tlv::TlvItemValueEnc::Array(slot_adjustments.into_iter().map(|v| {
457                    let mut fields = Vec::new();
458                    if let Some(x) = v.slot_index { fields.push((0, tlv::TlvItemValueEnc::UInt8(x)).into()); }
459                    if let Some(x) = v.nominal_power { fields.push((1, tlv::TlvItemValueEnc::UInt32(x)).into()); }
460                    if let Some(x) = v.duration { fields.push((2, tlv::TlvItemValueEnc::UInt32(x)).into()); }
461                    (0, tlv::TlvItemValueEnc::StructAnon(fields)).into()
462                }).collect())).into(),
463        (2, tlv::TlvItemValueEnc::UInt8(cause.to_u8())).into(),
464        ]),
465    };
466    Ok(tlv.encode()?)
467}
468
469/// Encode RequestConstraintBasedForecast command (0x06)
470pub fn encode_request_constraint_based_forecast(constraints: Vec<Constraints>, cause: AdjustmentCause) -> anyhow::Result<Vec<u8>> {
471    let tlv = tlv::TlvItemEnc {
472        tag: 0,
473        value: tlv::TlvItemValueEnc::StructInvisible(vec![
474        (0, tlv::TlvItemValueEnc::Array(constraints.into_iter().map(|v| {
475                    let mut fields = Vec::new();
476                    if let Some(x) = v.start_time { fields.push((0, tlv::TlvItemValueEnc::UInt64(x)).into()); }
477                    if let Some(x) = v.duration { fields.push((1, tlv::TlvItemValueEnc::UInt32(x)).into()); }
478                    if let Some(x) = v.nominal_power { fields.push((2, tlv::TlvItemValueEnc::UInt32(x)).into()); }
479                    if let Some(x) = v.maximum_energy { fields.push((3, tlv::TlvItemValueEnc::UInt64(x)).into()); }
480                    if let Some(x) = v.load_control { fields.push((4, tlv::TlvItemValueEnc::Int8(x)).into()); }
481                    (0, tlv::TlvItemValueEnc::StructAnon(fields)).into()
482                }).collect())).into(),
483        (1, tlv::TlvItemValueEnc::UInt8(cause.to_u8())).into(),
484        ]),
485    };
486    Ok(tlv.encode()?)
487}
488
489// Attribute decoders
490
491/// Decode ESAType attribute (0x0000)
492pub fn decode_esa_type(inp: &tlv::TlvItemValue) -> anyhow::Result<ESAType> {
493    if let tlv::TlvItemValue::Int(v) = inp {
494        ESAType::from_u8(*v as u8).ok_or_else(|| anyhow::anyhow!("Invalid enum value"))
495    } else {
496        Err(anyhow::anyhow!("Expected Integer"))
497    }
498}
499
500/// Decode ESACanGenerate attribute (0x0001)
501pub fn decode_esa_can_generate(inp: &tlv::TlvItemValue) -> anyhow::Result<bool> {
502    if let tlv::TlvItemValue::Bool(v) = inp {
503        Ok(*v)
504    } else {
505        Err(anyhow::anyhow!("Expected Bool"))
506    }
507}
508
509/// Decode ESAState attribute (0x0002)
510pub fn decode_esa_state(inp: &tlv::TlvItemValue) -> anyhow::Result<ESAState> {
511    if let tlv::TlvItemValue::Int(v) = inp {
512        ESAState::from_u8(*v as u8).ok_or_else(|| anyhow::anyhow!("Invalid enum value"))
513    } else {
514        Err(anyhow::anyhow!("Expected Integer"))
515    }
516}
517
518/// Decode AbsMinPower attribute (0x0003)
519pub fn decode_abs_min_power(inp: &tlv::TlvItemValue) -> anyhow::Result<u32> {
520    if let tlv::TlvItemValue::Int(v) = inp {
521        Ok(*v as u32)
522    } else {
523        Err(anyhow::anyhow!("Expected UInt32"))
524    }
525}
526
527/// Decode AbsMaxPower attribute (0x0004)
528pub fn decode_abs_max_power(inp: &tlv::TlvItemValue) -> anyhow::Result<u32> {
529    if let tlv::TlvItemValue::Int(v) = inp {
530        Ok(*v as u32)
531    } else {
532        Err(anyhow::anyhow!("Expected UInt32"))
533    }
534}
535
536/// Decode PowerAdjustmentCapability attribute (0x0005)
537pub fn decode_power_adjustment_capability(inp: &tlv::TlvItemValue) -> anyhow::Result<Option<PowerAdjustCapability>> {
538    if let tlv::TlvItemValue::List(_fields) = inp {
539        // Struct with fields
540        let item = tlv::TlvItem { tag: 0, value: inp.clone() };
541        Ok(Some(PowerAdjustCapability {
542                power_adjust_capability: {
543                    if let Some(tlv::TlvItemValue::List(l)) = item.get(&[0]) {
544                        let mut items = Vec::new();
545                        for list_item in l {
546                            items.push(PowerAdjust {
547                min_power: list_item.get_int(&[0]).map(|v| v as u32),
548                max_power: list_item.get_int(&[1]).map(|v| v as u32),
549                min_duration: list_item.get_int(&[2]).map(|v| v as u32),
550                max_duration: list_item.get_int(&[3]).map(|v| v as u32),
551                            });
552                        }
553                        Some(items)
554                    } else {
555                        None
556                    }
557                },
558                cause: item.get_int(&[1]).and_then(|v| PowerAdjustReason::from_u8(v as u8)),
559        }))
560    //} else if let tlv::TlvItemValue::Null = inp {
561    //    // Null value for nullable struct
562    //    Ok(None)
563    } else {
564    Ok(None)
565    //    Err(anyhow::anyhow!("Expected struct fields or null"))
566    }
567}
568
569/// Decode Forecast attribute (0x0006)
570pub fn decode_forecast(inp: &tlv::TlvItemValue) -> anyhow::Result<Option<Forecast>> {
571    if let tlv::TlvItemValue::List(_fields) = inp {
572        // Struct with fields
573        let item = tlv::TlvItem { tag: 0, value: inp.clone() };
574        Ok(Some(Forecast {
575                forecast_id: item.get_int(&[0]).map(|v| v as u32),
576                active_slot_number: item.get_int(&[1]).map(|v| v as u16),
577                start_time: item.get_int(&[2]),
578                end_time: item.get_int(&[3]),
579                earliest_start_time: item.get_int(&[4]),
580                latest_end_time: item.get_int(&[5]),
581                is_pausable: item.get_bool(&[6]),
582                slots: {
583                    if let Some(tlv::TlvItemValue::List(l)) = item.get(&[7]) {
584                        let mut items = Vec::new();
585                        for list_item in l {
586                            items.push(Slot {
587                min_duration: list_item.get_int(&[0]).map(|v| v as u32),
588                max_duration: list_item.get_int(&[1]).map(|v| v as u32),
589                default_duration: list_item.get_int(&[2]).map(|v| v as u32),
590                elapsed_slot_time: list_item.get_int(&[3]).map(|v| v as u32),
591                remaining_slot_time: list_item.get_int(&[4]).map(|v| v as u32),
592                slot_is_pausable: list_item.get_bool(&[5]),
593                min_pause_duration: list_item.get_int(&[6]).map(|v| v as u32),
594                max_pause_duration: list_item.get_int(&[7]).map(|v| v as u32),
595                manufacturer_esa_state: list_item.get_int(&[8]).map(|v| v as u16),
596                nominal_power: list_item.get_int(&[9]).map(|v| v as u32),
597                min_power: list_item.get_int(&[10]).map(|v| v as u32),
598                max_power: list_item.get_int(&[11]).map(|v| v as u32),
599                nominal_energy: list_item.get_int(&[12]),
600                costs: {
601                    if let Some(tlv::TlvItemValue::List(l)) = list_item.get(&[13]) {
602                        let mut items = Vec::new();
603                        for list_item in l {
604                            items.push(Cost {
605                cost_type: list_item.get_int(&[0]).and_then(|v| CostType::from_u8(v as u8)),
606                value: list_item.get_int(&[1]).map(|v| v as i32),
607                decimal_points: list_item.get_int(&[2]).map(|v| v as u8),
608                currency: list_item.get_int(&[3]).map(|v| v as u16),
609                            });
610                        }
611                        Some(items)
612                    } else {
613                        None
614                    }
615                },
616                min_power_adjustment: list_item.get_int(&[14]).map(|v| v as u32),
617                max_power_adjustment: list_item.get_int(&[15]).map(|v| v as u32),
618                min_duration_adjustment: list_item.get_int(&[16]).map(|v| v as u32),
619                max_duration_adjustment: list_item.get_int(&[17]).map(|v| v as u32),
620                            });
621                        }
622                        Some(items)
623                    } else {
624                        None
625                    }
626                },
627                forecast_update_reason: item.get_int(&[8]).and_then(|v| ForecastUpdateReason::from_u8(v as u8)),
628        }))
629    //} else if let tlv::TlvItemValue::Null = inp {
630    //    // Null value for nullable struct
631    //    Ok(None)
632    } else {
633    Ok(None)
634    //    Err(anyhow::anyhow!("Expected struct fields or null"))
635    }
636}
637
638/// Decode OptOutState attribute (0x0007)
639pub fn decode_opt_out_state(inp: &tlv::TlvItemValue) -> anyhow::Result<OptOutState> {
640    if let tlv::TlvItemValue::Int(v) = inp {
641        OptOutState::from_u8(*v as u8).ok_or_else(|| anyhow::anyhow!("Invalid enum value"))
642    } else {
643        Err(anyhow::anyhow!("Expected Integer"))
644    }
645}
646
647
648// JSON dispatcher function
649
650/// Decode attribute value and return as JSON string
651///
652/// # Parameters
653/// * `cluster_id` - The cluster identifier
654/// * `attribute_id` - The attribute identifier
655/// * `tlv_value` - The TLV value to decode
656///
657/// # Returns
658/// JSON string representation of the decoded value or error
659pub fn decode_attribute_json(cluster_id: u32, attribute_id: u32, tlv_value: &crate::tlv::TlvItemValue) -> String {
660    // Verify this is the correct cluster
661    if cluster_id != 0x0098 {
662        return format!("{{\"error\": \"Invalid cluster ID. Expected 0x0098, got {}\"}}", cluster_id);
663    }
664
665    match attribute_id {
666        0x0000 => {
667            match decode_esa_type(tlv_value) {
668                Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
669                Err(e) => format!("{{\"error\": \"{}\"}}", e),
670            }
671        }
672        0x0001 => {
673            match decode_esa_can_generate(tlv_value) {
674                Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
675                Err(e) => format!("{{\"error\": \"{}\"}}", e),
676            }
677        }
678        0x0002 => {
679            match decode_esa_state(tlv_value) {
680                Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
681                Err(e) => format!("{{\"error\": \"{}\"}}", e),
682            }
683        }
684        0x0003 => {
685            match decode_abs_min_power(tlv_value) {
686                Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
687                Err(e) => format!("{{\"error\": \"{}\"}}", e),
688            }
689        }
690        0x0004 => {
691            match decode_abs_max_power(tlv_value) {
692                Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
693                Err(e) => format!("{{\"error\": \"{}\"}}", e),
694            }
695        }
696        0x0005 => {
697            match decode_power_adjustment_capability(tlv_value) {
698                Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
699                Err(e) => format!("{{\"error\": \"{}\"}}", e),
700            }
701        }
702        0x0006 => {
703            match decode_forecast(tlv_value) {
704                Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
705                Err(e) => format!("{{\"error\": \"{}\"}}", e),
706            }
707        }
708        0x0007 => {
709            match decode_opt_out_state(tlv_value) {
710                Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
711                Err(e) => format!("{{\"error\": \"{}\"}}", e),
712            }
713        }
714        _ => format!("{{\"error\": \"Unknown attribute ID: {}\"}}", attribute_id),
715    }
716}
717
718/// Get list of all attributes supported by this cluster
719///
720/// # Returns
721/// Vector of tuples containing (attribute_id, attribute_name)
722pub fn get_attribute_list() -> Vec<(u32, &'static str)> {
723    vec![
724        (0x0000, "ESAType"),
725        (0x0001, "ESACanGenerate"),
726        (0x0002, "ESAState"),
727        (0x0003, "AbsMinPower"),
728        (0x0004, "AbsMaxPower"),
729        (0x0005, "PowerAdjustmentCapability"),
730        (0x0006, "Forecast"),
731        (0x0007, "OptOutState"),
732    ]
733}
734
735#[derive(Debug, serde::Serialize)]
736pub struct PowerAdjustEndEvent {
737    pub cause: Option<Cause>,
738    pub duration: Option<u32>,
739    pub energy_use: Option<u64>,
740}
741
742#[derive(Debug, serde::Serialize)]
743pub struct ResumedEvent {
744    pub cause: Option<Cause>,
745}
746
747// Event decoders
748
749/// Decode PowerAdjustEnd event (0x01, priority: info)
750pub fn decode_power_adjust_end_event(inp: &tlv::TlvItemValue) -> anyhow::Result<PowerAdjustEndEvent> {
751    if let tlv::TlvItemValue::List(_fields) = inp {
752        let item = tlv::TlvItem { tag: 0, value: inp.clone() };
753        Ok(PowerAdjustEndEvent {
754                                cause: item.get_int(&[0]).and_then(|v| Cause::from_u8(v as u8)),
755                                duration: item.get_int(&[1]).map(|v| v as u32),
756                                energy_use: item.get_int(&[2]),
757        })
758    } else {
759        Err(anyhow::anyhow!("Expected struct fields"))
760    }
761}
762
763/// Decode Resumed event (0x03, priority: info)
764pub fn decode_resumed_event(inp: &tlv::TlvItemValue) -> anyhow::Result<ResumedEvent> {
765    if let tlv::TlvItemValue::List(_fields) = inp {
766        let item = tlv::TlvItem { tag: 0, value: inp.clone() };
767        Ok(ResumedEvent {
768                                cause: item.get_int(&[0]).and_then(|v| Cause::from_u8(v as u8)),
769        })
770    } else {
771        Err(anyhow::anyhow!("Expected struct fields"))
772    }
773}
774