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