Skip to main content

matc/clusters/codec/
time_sync.rs

1//! Matter TLV encoders and decoders for Time Synchronization Cluster
2//! Cluster ID: 0x0038
3//!
4//! This file is automatically generated from TimeSync.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 Granularity {
18    /// This indicates that the node is not currently synchronized with a UTC Time source and its clock is based on the Last Known Good UTC Time only.
19    Notimegranularity = 0,
20    /// This indicates the node was synchronized to an upstream source in the past, but sufficient clock drift has occurred such that the clock error is now > 5 seconds.
21    Minutesgranularity = 1,
22    /// This indicates the node is synchronized to an upstream source using a low resolution protocol. UTC Time is accurate to ± 5 seconds.
23    Secondsgranularity = 2,
24    /// This indicates the node is synchronized to an upstream source using high resolution time-synchronization protocol such as NTP, or has built-in GNSS with some amount of jitter applying its GNSS timestamp. UTC Time is accurate to ± 50 ms.
25    Millisecondsgranularity = 3,
26    /// This indicates the node is synchronized to an upstream source using a highly precise time-synchronization protocol such as PTP, or has built-in GNSS. UTC time is accurate to ± 10 μs.
27    Microsecondsgranularity = 4,
28}
29
30impl Granularity {
31    /// Convert from u8 value
32    pub fn from_u8(value: u8) -> Option<Self> {
33        match value {
34            0 => Some(Granularity::Notimegranularity),
35            1 => Some(Granularity::Minutesgranularity),
36            2 => Some(Granularity::Secondsgranularity),
37            3 => Some(Granularity::Millisecondsgranularity),
38            4 => Some(Granularity::Microsecondsgranularity),
39            _ => None,
40        }
41    }
42
43    /// Convert to u8 value
44    pub fn to_u8(self) -> u8 {
45        self as u8
46    }
47}
48
49impl From<Granularity> for u8 {
50    fn from(val: Granularity) -> Self {
51        val as u8
52    }
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
56#[repr(u8)]
57pub enum StatusCode {
58    /// Node rejected the attempt to set the UTC time
59    Timenotaccepted = 2,
60}
61
62impl StatusCode {
63    /// Convert from u8 value
64    pub fn from_u8(value: u8) -> Option<Self> {
65        match value {
66            2 => Some(StatusCode::Timenotaccepted),
67            _ => None,
68        }
69    }
70
71    /// Convert to u8 value
72    pub fn to_u8(self) -> u8 {
73        self as u8
74    }
75}
76
77impl From<StatusCode> for u8 {
78    fn from(val: StatusCode) -> Self {
79        val as u8
80    }
81}
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
84#[repr(u8)]
85pub enum TimeSource {
86    /// Node is not currently synchronized with a UTC Time source.
87    None = 0,
88    /// Node uses an unlisted time source.
89    Unknown = 1,
90    /// Node received time from a client using the SetUTCTime Command.
91    Admin = 2,
92    /// Synchronized time by querying the Time Synchronization cluster of another Node.
93    Nodetimecluster = 3,
94    /// SNTP from a server not in the Matter network. NTS is not used.
95    Nonmattersntp = 4,
96    /// NTP from servers not in the Matter network. None of the servers used NTS.
97    Nonmatterntp = 5,
98    /// SNTP from a server within the Matter network. NTS is not used.
99    Mattersntp = 6,
100    /// NTP from servers within the Matter network. None of the servers used NTS.
101    Matterntp = 7,
102    /// NTP from multiple servers in the Matter network and external. None of the servers used NTS.
103    Mixedntp = 8,
104    /// SNTP from a server not in the Matter network. NTS is used.
105    Nonmattersntpnts = 9,
106    /// NTP from servers not in the Matter network. NTS is used on at least one server.
107    Nonmatterntpnts = 10,
108    /// SNTP from a server within the Matter network. NTS is used.
109    Mattersntpnts = 11,
110    /// NTP from a server within the Matter network. NTS is used on at least one server.
111    Matterntpnts = 12,
112    /// NTP from multiple servers in the Matter network and external. NTS is used on at least one server.
113    Mixedntpnts = 13,
114    /// Time synchronization comes from a vendor cloud-based source (e.g. "Date" header in authenticated HTTPS connection).
115    Cloudsource = 14,
116    /// Time synchronization comes from PTP.
117    Ptp = 15,
118    /// Time synchronization comes from a GNSS source.
119    Gnss = 16,
120}
121
122impl TimeSource {
123    /// Convert from u8 value
124    pub fn from_u8(value: u8) -> Option<Self> {
125        match value {
126            0 => Some(TimeSource::None),
127            1 => Some(TimeSource::Unknown),
128            2 => Some(TimeSource::Admin),
129            3 => Some(TimeSource::Nodetimecluster),
130            4 => Some(TimeSource::Nonmattersntp),
131            5 => Some(TimeSource::Nonmatterntp),
132            6 => Some(TimeSource::Mattersntp),
133            7 => Some(TimeSource::Matterntp),
134            8 => Some(TimeSource::Mixedntp),
135            9 => Some(TimeSource::Nonmattersntpnts),
136            10 => Some(TimeSource::Nonmatterntpnts),
137            11 => Some(TimeSource::Mattersntpnts),
138            12 => Some(TimeSource::Matterntpnts),
139            13 => Some(TimeSource::Mixedntpnts),
140            14 => Some(TimeSource::Cloudsource),
141            15 => Some(TimeSource::Ptp),
142            16 => Some(TimeSource::Gnss),
143            _ => None,
144        }
145    }
146
147    /// Convert to u8 value
148    pub fn to_u8(self) -> u8 {
149        self as u8
150    }
151}
152
153impl From<TimeSource> for u8 {
154    fn from(val: TimeSource) -> Self {
155        val as u8
156    }
157}
158
159#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
160#[repr(u8)]
161pub enum TimeZoneDatabase {
162    /// Node has a full list of the available time zones
163    Full = 0,
164    /// Node has a partial list of the available time zones
165    Partial = 1,
166    /// Node does not have a time zone database
167    None = 2,
168}
169
170impl TimeZoneDatabase {
171    /// Convert from u8 value
172    pub fn from_u8(value: u8) -> Option<Self> {
173        match value {
174            0 => Some(TimeZoneDatabase::Full),
175            1 => Some(TimeZoneDatabase::Partial),
176            2 => Some(TimeZoneDatabase::None),
177            _ => None,
178        }
179    }
180
181    /// Convert to u8 value
182    pub fn to_u8(self) -> u8 {
183        self as u8
184    }
185}
186
187impl From<TimeZoneDatabase> for u8 {
188    fn from(val: TimeZoneDatabase) -> Self {
189        val as u8
190    }
191}
192
193// Struct definitions
194
195#[derive(Debug, serde::Serialize)]
196pub struct DSTOffset {
197    pub offset: Option<i32>,
198    pub valid_starting: Option<u64>,
199    pub valid_until: Option<u64>,
200}
201
202#[derive(Debug, serde::Serialize)]
203pub struct FabricScopedTrustedTimeSource {
204    pub node_id: Option<u64>,
205    pub endpoint: Option<u16>,
206}
207
208#[derive(Debug, serde::Serialize)]
209pub struct TimeZone {
210    pub offset: Option<i32>,
211    pub valid_at: Option<u64>,
212    pub name: Option<String>,
213}
214
215#[derive(Debug, serde::Serialize)]
216pub struct TrustedTimeSource {
217    pub fabric_index: Option<u8>,
218    pub node_id: Option<u64>,
219    pub endpoint: Option<u16>,
220}
221
222// Command encoders
223
224/// Encode SetUTCTime command (0x00)
225pub fn encode_set_utc_time(utc_time: u64, granularity: Granularity, time_source: Option<TimeSource>) -> anyhow::Result<Vec<u8>> {
226    let mut tlv_fields: Vec<tlv::TlvItemEnc> = Vec::new();
227    tlv_fields.push((0, tlv::TlvItemValueEnc::UInt64(utc_time)).into());
228    tlv_fields.push((1, tlv::TlvItemValueEnc::UInt8(granularity.to_u8())).into());
229    if let Some(x) = time_source { tlv_fields.push((2, tlv::TlvItemValueEnc::UInt8(x.to_u8())).into()); }
230    let tlv = tlv::TlvItemEnc {
231        tag: 0,
232        value: tlv::TlvItemValueEnc::StructInvisible(tlv_fields),
233    };
234    Ok(tlv.encode()?)
235}
236
237/// Encode SetTrustedTimeSource command (0x01)
238pub fn encode_set_trusted_time_source(trusted_time_source: Option<FabricScopedTrustedTimeSource>) -> anyhow::Result<Vec<u8>> {
239            // Encode optional struct FabricScopedTrustedTimeSourceStruct
240            let trusted_time_source_enc = if let Some(s) = trusted_time_source {
241                let mut fields = Vec::new();
242                if let Some(x) = s.node_id { fields.push((0, tlv::TlvItemValueEnc::UInt64(x)).into()); }
243                if let Some(x) = s.endpoint { fields.push((1, tlv::TlvItemValueEnc::UInt16(x)).into()); }
244                tlv::TlvItemValueEnc::StructInvisible(fields)
245            } else {
246                tlv::TlvItemValueEnc::StructInvisible(Vec::new())
247            };
248    let tlv = tlv::TlvItemEnc {
249        tag: 0,
250        value: tlv::TlvItemValueEnc::StructInvisible(vec![
251        (0, trusted_time_source_enc).into(),
252        ]),
253    };
254    Ok(tlv.encode()?)
255}
256
257/// Encode SetTimeZone command (0x02)
258pub fn encode_set_time_zone(time_zone: Vec<TimeZone>) -> anyhow::Result<Vec<u8>> {
259    let tlv = tlv::TlvItemEnc {
260        tag: 0,
261        value: tlv::TlvItemValueEnc::StructInvisible(vec![
262        (0, tlv::TlvItemValueEnc::Array(time_zone.into_iter().map(|v| {
263                    let mut fields = Vec::new();
264                    if let Some(x) = v.offset { fields.push((0, tlv::TlvItemValueEnc::Int32(x)).into()); }
265                    if let Some(x) = v.valid_at { fields.push((1, tlv::TlvItemValueEnc::UInt64(x)).into()); }
266                    if let Some(x) = v.name { fields.push((2, tlv::TlvItemValueEnc::String(x.clone())).into()); }
267                    (0, tlv::TlvItemValueEnc::StructAnon(fields)).into()
268                }).collect())).into(),
269        ]),
270    };
271    Ok(tlv.encode()?)
272}
273
274/// Encode SetDSTOffset command (0x04)
275pub fn encode_set_dst_offset(dst_offset: Vec<DSTOffset>) -> anyhow::Result<Vec<u8>> {
276    let tlv = tlv::TlvItemEnc {
277        tag: 0,
278        value: tlv::TlvItemValueEnc::StructInvisible(vec![
279        (0, tlv::TlvItemValueEnc::Array(dst_offset.into_iter().map(|v| {
280                    let mut fields = Vec::new();
281                    if let Some(x) = v.offset { fields.push((0, tlv::TlvItemValueEnc::Int32(x)).into()); }
282                    if let Some(x) = v.valid_starting { fields.push((1, tlv::TlvItemValueEnc::UInt64(x)).into()); }
283                    if let Some(x) = v.valid_until { fields.push((2, tlv::TlvItemValueEnc::UInt64(x)).into()); }
284                    (0, tlv::TlvItemValueEnc::StructAnon(fields)).into()
285                }).collect())).into(),
286        ]),
287    };
288    Ok(tlv.encode()?)
289}
290
291/// Encode SetDefaultNTP command (0x05)
292pub fn encode_set_default_ntp(default_ntp: Option<String>) -> anyhow::Result<Vec<u8>> {
293    let tlv = tlv::TlvItemEnc {
294        tag: 0,
295        value: tlv::TlvItemValueEnc::StructInvisible(vec![
296        (0, tlv::TlvItemValueEnc::String(default_ntp.unwrap_or("".to_string()))).into(),
297        ]),
298    };
299    Ok(tlv.encode()?)
300}
301
302// Attribute decoders
303
304/// Decode UTCTime attribute (0x0000)
305pub fn decode_utc_time(inp: &tlv::TlvItemValue) -> anyhow::Result<Option<u64>> {
306    if let tlv::TlvItemValue::Int(v) = inp {
307        Ok(Some(*v))
308    } else {
309        Ok(None)
310    }
311}
312
313/// Decode Granularity attribute (0x0001)
314pub fn decode_granularity(inp: &tlv::TlvItemValue) -> anyhow::Result<Granularity> {
315    if let tlv::TlvItemValue::Int(v) = inp {
316        Granularity::from_u8(*v as u8).ok_or_else(|| anyhow::anyhow!("Invalid enum value"))
317    } else {
318        Err(anyhow::anyhow!("Expected Integer"))
319    }
320}
321
322/// Decode TimeSource attribute (0x0002)
323pub fn decode_time_source(inp: &tlv::TlvItemValue) -> anyhow::Result<TimeSource> {
324    if let tlv::TlvItemValue::Int(v) = inp {
325        TimeSource::from_u8(*v as u8).ok_or_else(|| anyhow::anyhow!("Invalid enum value"))
326    } else {
327        Err(anyhow::anyhow!("Expected Integer"))
328    }
329}
330
331/// Decode TrustedTimeSource attribute (0x0003)
332pub fn decode_trusted_time_source(inp: &tlv::TlvItemValue) -> anyhow::Result<Option<TrustedTimeSource>> {
333    if let tlv::TlvItemValue::List(_fields) = inp {
334        // Struct with fields
335        let item = tlv::TlvItem { tag: 0, value: inp.clone() };
336        Ok(Some(TrustedTimeSource {
337                fabric_index: item.get_int(&[0]).map(|v| v as u8),
338                node_id: item.get_int(&[1]),
339                endpoint: item.get_int(&[2]).map(|v| v as u16),
340        }))
341    //} else if let tlv::TlvItemValue::Null = inp {
342    //    // Null value for nullable struct
343    //    Ok(None)
344    } else {
345    Ok(None)
346    //    Err(anyhow::anyhow!("Expected struct fields or null"))
347    }
348}
349
350/// Decode DefaultNTP attribute (0x0004)
351pub fn decode_default_ntp(inp: &tlv::TlvItemValue) -> anyhow::Result<Option<String>> {
352    if let tlv::TlvItemValue::String(v) = inp {
353        Ok(Some(v.clone()))
354    } else {
355        Ok(None)
356    }
357}
358
359/// Decode TimeZone attribute (0x0005)
360pub fn decode_time_zone(inp: &tlv::TlvItemValue) -> anyhow::Result<Vec<TimeZone>> {
361    let mut res = Vec::new();
362    if let tlv::TlvItemValue::List(v) = inp {
363        for item in v {
364            res.push(TimeZone {
365                offset: item.get_int(&[0]).map(|v| v as i32),
366                valid_at: item.get_int(&[1]),
367                name: item.get_string_owned(&[2]),
368            });
369        }
370    }
371    Ok(res)
372}
373
374/// Decode DSTOffset attribute (0x0006)
375pub fn decode_dst_offset(inp: &tlv::TlvItemValue) -> anyhow::Result<Vec<DSTOffset>> {
376    let mut res = Vec::new();
377    if let tlv::TlvItemValue::List(v) = inp {
378        for item in v {
379            res.push(DSTOffset {
380                offset: item.get_int(&[0]).map(|v| v as i32),
381                valid_starting: item.get_int(&[1]),
382                valid_until: item.get_int(&[2]),
383            });
384        }
385    }
386    Ok(res)
387}
388
389/// Decode LocalTime attribute (0x0007)
390pub fn decode_local_time(inp: &tlv::TlvItemValue) -> anyhow::Result<Option<u64>> {
391    if let tlv::TlvItemValue::Int(v) = inp {
392        Ok(Some(*v))
393    } else {
394        Ok(None)
395    }
396}
397
398/// Decode TimeZoneDatabase attribute (0x0008)
399pub fn decode_time_zone_database(inp: &tlv::TlvItemValue) -> anyhow::Result<TimeZoneDatabase> {
400    if let tlv::TlvItemValue::Int(v) = inp {
401        TimeZoneDatabase::from_u8(*v as u8).ok_or_else(|| anyhow::anyhow!("Invalid enum value"))
402    } else {
403        Err(anyhow::anyhow!("Expected Integer"))
404    }
405}
406
407/// Decode NTPServerAvailable attribute (0x0009)
408pub fn decode_ntp_server_available(inp: &tlv::TlvItemValue) -> anyhow::Result<bool> {
409    if let tlv::TlvItemValue::Bool(v) = inp {
410        Ok(*v)
411    } else {
412        Err(anyhow::anyhow!("Expected Bool"))
413    }
414}
415
416/// Decode TimeZoneListMaxSize attribute (0x000A)
417pub fn decode_time_zone_list_max_size(inp: &tlv::TlvItemValue) -> anyhow::Result<u8> {
418    if let tlv::TlvItemValue::Int(v) = inp {
419        Ok(*v as u8)
420    } else {
421        Err(anyhow::anyhow!("Expected UInt8"))
422    }
423}
424
425/// Decode DSTOffsetListMaxSize attribute (0x000B)
426pub fn decode_dst_offset_list_max_size(inp: &tlv::TlvItemValue) -> anyhow::Result<u8> {
427    if let tlv::TlvItemValue::Int(v) = inp {
428        Ok(*v as u8)
429    } else {
430        Err(anyhow::anyhow!("Expected UInt8"))
431    }
432}
433
434/// Decode SupportsDNSResolve attribute (0x000C)
435pub fn decode_supports_dns_resolve(inp: &tlv::TlvItemValue) -> anyhow::Result<bool> {
436    if let tlv::TlvItemValue::Bool(v) = inp {
437        Ok(*v)
438    } else {
439        Err(anyhow::anyhow!("Expected Bool"))
440    }
441}
442
443
444// JSON dispatcher function
445
446/// Decode attribute value and return as JSON string
447///
448/// # Parameters
449/// * `cluster_id` - The cluster identifier
450/// * `attribute_id` - The attribute identifier
451/// * `tlv_value` - The TLV value to decode
452///
453/// # Returns
454/// JSON string representation of the decoded value or error
455pub fn decode_attribute_json(cluster_id: u32, attribute_id: u32, tlv_value: &crate::tlv::TlvItemValue) -> String {
456    // Verify this is the correct cluster
457    if cluster_id != 0x0038 {
458        return format!("{{\"error\": \"Invalid cluster ID. Expected 0x0038, got {}\"}}", cluster_id);
459    }
460
461    match attribute_id {
462        0x0000 => {
463            match decode_utc_time(tlv_value) {
464                Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
465                Err(e) => format!("{{\"error\": \"{}\"}}", e),
466            }
467        }
468        0x0001 => {
469            match decode_granularity(tlv_value) {
470                Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
471                Err(e) => format!("{{\"error\": \"{}\"}}", e),
472            }
473        }
474        0x0002 => {
475            match decode_time_source(tlv_value) {
476                Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
477                Err(e) => format!("{{\"error\": \"{}\"}}", e),
478            }
479        }
480        0x0003 => {
481            match decode_trusted_time_source(tlv_value) {
482                Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
483                Err(e) => format!("{{\"error\": \"{}\"}}", e),
484            }
485        }
486        0x0004 => {
487            match decode_default_ntp(tlv_value) {
488                Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
489                Err(e) => format!("{{\"error\": \"{}\"}}", e),
490            }
491        }
492        0x0005 => {
493            match decode_time_zone(tlv_value) {
494                Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
495                Err(e) => format!("{{\"error\": \"{}\"}}", e),
496            }
497        }
498        0x0006 => {
499            match decode_dst_offset(tlv_value) {
500                Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
501                Err(e) => format!("{{\"error\": \"{}\"}}", e),
502            }
503        }
504        0x0007 => {
505            match decode_local_time(tlv_value) {
506                Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
507                Err(e) => format!("{{\"error\": \"{}\"}}", e),
508            }
509        }
510        0x0008 => {
511            match decode_time_zone_database(tlv_value) {
512                Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
513                Err(e) => format!("{{\"error\": \"{}\"}}", e),
514            }
515        }
516        0x0009 => {
517            match decode_ntp_server_available(tlv_value) {
518                Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
519                Err(e) => format!("{{\"error\": \"{}\"}}", e),
520            }
521        }
522        0x000A => {
523            match decode_time_zone_list_max_size(tlv_value) {
524                Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
525                Err(e) => format!("{{\"error\": \"{}\"}}", e),
526            }
527        }
528        0x000B => {
529            match decode_dst_offset_list_max_size(tlv_value) {
530                Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
531                Err(e) => format!("{{\"error\": \"{}\"}}", e),
532            }
533        }
534        0x000C => {
535            match decode_supports_dns_resolve(tlv_value) {
536                Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
537                Err(e) => format!("{{\"error\": \"{}\"}}", e),
538            }
539        }
540        _ => format!("{{\"error\": \"Unknown attribute ID: {}\"}}", attribute_id),
541    }
542}
543
544/// Get list of all attributes supported by this cluster
545///
546/// # Returns
547/// Vector of tuples containing (attribute_id, attribute_name)
548pub fn get_attribute_list() -> Vec<(u32, &'static str)> {
549    vec![
550        (0x0000, "UTCTime"),
551        (0x0001, "Granularity"),
552        (0x0002, "TimeSource"),
553        (0x0003, "TrustedTimeSource"),
554        (0x0004, "DefaultNTP"),
555        (0x0005, "TimeZone"),
556        (0x0006, "DSTOffset"),
557        (0x0007, "LocalTime"),
558        (0x0008, "TimeZoneDatabase"),
559        (0x0009, "NTPServerAvailable"),
560        (0x000A, "TimeZoneListMaxSize"),
561        (0x000B, "DSTOffsetListMaxSize"),
562        (0x000C, "SupportsDNSResolve"),
563    ]
564}
565
566// Command listing
567
568pub fn get_command_list() -> Vec<(u32, &'static str)> {
569    vec![
570        (0x00, "SetUTCTime"),
571        (0x01, "SetTrustedTimeSource"),
572        (0x02, "SetTimeZone"),
573        (0x04, "SetDSTOffset"),
574        (0x05, "SetDefaultNTP"),
575    ]
576}
577
578pub fn get_command_name(cmd_id: u32) -> Option<&'static str> {
579    match cmd_id {
580        0x00 => Some("SetUTCTime"),
581        0x01 => Some("SetTrustedTimeSource"),
582        0x02 => Some("SetTimeZone"),
583        0x04 => Some("SetDSTOffset"),
584        0x05 => Some("SetDefaultNTP"),
585        _ => None,
586    }
587}
588
589pub fn get_command_schema(cmd_id: u32) -> Option<Vec<crate::clusters::codec::CommandField>> {
590    match cmd_id {
591        0x00 => Some(vec![
592            crate::clusters::codec::CommandField { tag: 0, name: "utc_time", kind: crate::clusters::codec::FieldKind::U64, optional: false, nullable: false },
593            crate::clusters::codec::CommandField { tag: 1, name: "granularity", kind: crate::clusters::codec::FieldKind::Enum { name: "Granularity", variants: &[(0, "Notimegranularity"), (1, "Minutesgranularity"), (2, "Secondsgranularity"), (3, "Millisecondsgranularity"), (4, "Microsecondsgranularity")] }, optional: false, nullable: false },
594            crate::clusters::codec::CommandField { tag: 2, name: "time_source", kind: crate::clusters::codec::FieldKind::Enum { name: "TimeSource", variants: &[(0, "None"), (1, "Unknown"), (2, "Admin"), (3, "Nodetimecluster"), (4, "Nonmattersntp"), (5, "Nonmatterntp"), (6, "Mattersntp"), (7, "Matterntp"), (8, "Mixedntp"), (9, "Nonmattersntpnts"), (10, "Nonmatterntpnts"), (11, "Mattersntpnts"), (12, "Matterntpnts"), (13, "Mixedntpnts"), (14, "Cloudsource"), (15, "Ptp"), (16, "Gnss")] }, optional: true, nullable: false },
595        ]),
596        0x01 => Some(vec![
597            crate::clusters::codec::CommandField { tag: 0, name: "trusted_time_source", kind: crate::clusters::codec::FieldKind::Struct { name: "FabricScopedTrustedTimeSourceStruct" }, optional: false, nullable: true },
598        ]),
599        0x02 => Some(vec![
600            crate::clusters::codec::CommandField { tag: 0, name: "time_zone", kind: crate::clusters::codec::FieldKind::List { entry_type: "TimeZoneStruct" }, optional: false, nullable: false },
601        ]),
602        0x04 => Some(vec![
603            crate::clusters::codec::CommandField { tag: 0, name: "dst_offset", kind: crate::clusters::codec::FieldKind::List { entry_type: "DSTOffsetStruct" }, optional: false, nullable: false },
604        ]),
605        0x05 => Some(vec![
606            crate::clusters::codec::CommandField { tag: 0, name: "default_ntp", kind: crate::clusters::codec::FieldKind::String, optional: false, nullable: true },
607        ]),
608        _ => None,
609    }
610}
611
612pub fn encode_command_json(cmd_id: u32, args: &serde_json::Value) -> anyhow::Result<Vec<u8>> {
613    match cmd_id {
614        0x00 => {
615        let utc_time = crate::clusters::codec::json_util::get_u64(args, "utc_time")?;
616        let granularity = {
617            let n = crate::clusters::codec::json_util::get_u64(args, "granularity")?;
618            Granularity::from_u8(n as u8).ok_or_else(|| anyhow::anyhow!("invalid Granularity: {}", n))?
619        };
620        let time_source = crate::clusters::codec::json_util::get_opt_u64(args, "time_source")?
621            .and_then(|n| TimeSource::from_u8(n as u8));
622        encode_set_utc_time(utc_time, granularity, time_source)
623        }
624        0x01 => Err(anyhow::anyhow!("command \"SetTrustedTimeSource\" has complex args: use raw mode")),
625        0x02 => Err(anyhow::anyhow!("command \"SetTimeZone\" has complex args: use raw mode")),
626        0x04 => Err(anyhow::anyhow!("command \"SetDSTOffset\" has complex args: use raw mode")),
627        0x05 => {
628        let default_ntp = crate::clusters::codec::json_util::get_opt_string(args, "default_ntp")?;
629        encode_set_default_ntp(default_ntp)
630        }
631        _ => Err(anyhow::anyhow!("unknown command ID: 0x{:02X}", cmd_id)),
632    }
633}
634
635#[derive(Debug, serde::Serialize)]
636pub struct SetTimeZoneResponse {
637    pub dst_offset_required: Option<bool>,
638}
639
640// Command response decoders
641
642/// Decode SetTimeZoneResponse command response (03)
643pub fn decode_set_time_zone_response(inp: &tlv::TlvItemValue) -> anyhow::Result<SetTimeZoneResponse> {
644    if let tlv::TlvItemValue::List(_fields) = inp {
645        let item = tlv::TlvItem { tag: 0, value: inp.clone() };
646        Ok(SetTimeZoneResponse {
647                dst_offset_required: item.get_bool(&[0]),
648        })
649    } else {
650        Err(anyhow::anyhow!("Expected struct fields"))
651    }
652}
653
654// Typed facade (invokes + reads)
655
656/// Invoke `SetUTCTime` command on cluster `Time Synchronization`.
657pub async fn set_utc_time(conn: &crate::controller::Connection, endpoint: u16, utc_time: u64, granularity: Granularity, time_source: Option<TimeSource>) -> anyhow::Result<()> {
658    conn.invoke_request(endpoint, crate::clusters::defs::CLUSTER_ID_TIME_SYNCHRONIZATION, crate::clusters::defs::CLUSTER_TIME_SYNCHRONIZATION_CMD_ID_SETUTCTIME, &encode_set_utc_time(utc_time, granularity, time_source)?).await?;
659    Ok(())
660}
661
662/// Invoke `SetTrustedTimeSource` command on cluster `Time Synchronization`.
663pub async fn set_trusted_time_source(conn: &crate::controller::Connection, endpoint: u16, trusted_time_source: Option<FabricScopedTrustedTimeSource>) -> anyhow::Result<()> {
664    conn.invoke_request(endpoint, crate::clusters::defs::CLUSTER_ID_TIME_SYNCHRONIZATION, crate::clusters::defs::CLUSTER_TIME_SYNCHRONIZATION_CMD_ID_SETTRUSTEDTIMESOURCE, &encode_set_trusted_time_source(trusted_time_source)?).await?;
665    Ok(())
666}
667
668/// Invoke `SetTimeZone` command on cluster `Time Synchronization`.
669pub async fn set_time_zone(conn: &crate::controller::Connection, endpoint: u16, time_zone: Vec<TimeZone>) -> anyhow::Result<SetTimeZoneResponse> {
670    let tlv = conn.invoke_request2(endpoint, crate::clusters::defs::CLUSTER_ID_TIME_SYNCHRONIZATION, crate::clusters::defs::CLUSTER_TIME_SYNCHRONIZATION_CMD_ID_SETTIMEZONE, &encode_set_time_zone(time_zone)?).await?;
671    decode_set_time_zone_response(&tlv)
672}
673
674/// Invoke `SetDSTOffset` command on cluster `Time Synchronization`.
675pub async fn set_dst_offset(conn: &crate::controller::Connection, endpoint: u16, dst_offset: Vec<DSTOffset>) -> anyhow::Result<()> {
676    conn.invoke_request(endpoint, crate::clusters::defs::CLUSTER_ID_TIME_SYNCHRONIZATION, crate::clusters::defs::CLUSTER_TIME_SYNCHRONIZATION_CMD_ID_SETDSTOFFSET, &encode_set_dst_offset(dst_offset)?).await?;
677    Ok(())
678}
679
680/// Invoke `SetDefaultNTP` command on cluster `Time Synchronization`.
681pub async fn set_default_ntp(conn: &crate::controller::Connection, endpoint: u16, default_ntp: Option<String>) -> anyhow::Result<()> {
682    conn.invoke_request(endpoint, crate::clusters::defs::CLUSTER_ID_TIME_SYNCHRONIZATION, crate::clusters::defs::CLUSTER_TIME_SYNCHRONIZATION_CMD_ID_SETDEFAULTNTP, &encode_set_default_ntp(default_ntp)?).await?;
683    Ok(())
684}
685
686/// Read `UTCTime` attribute from cluster `Time Synchronization`.
687pub async fn read_utc_time(conn: &crate::controller::Connection, endpoint: u16) -> anyhow::Result<Option<u64>> {
688    let tlv = conn.read_request2(endpoint, crate::clusters::defs::CLUSTER_ID_TIME_SYNCHRONIZATION, crate::clusters::defs::CLUSTER_TIME_SYNCHRONIZATION_ATTR_ID_UTCTIME).await?;
689    decode_utc_time(&tlv)
690}
691
692/// Read `Granularity` attribute from cluster `Time Synchronization`.
693pub async fn read_granularity(conn: &crate::controller::Connection, endpoint: u16) -> anyhow::Result<Granularity> {
694    let tlv = conn.read_request2(endpoint, crate::clusters::defs::CLUSTER_ID_TIME_SYNCHRONIZATION, crate::clusters::defs::CLUSTER_TIME_SYNCHRONIZATION_ATTR_ID_GRANULARITY).await?;
695    decode_granularity(&tlv)
696}
697
698/// Read `TimeSource` attribute from cluster `Time Synchronization`.
699pub async fn read_time_source(conn: &crate::controller::Connection, endpoint: u16) -> anyhow::Result<TimeSource> {
700    let tlv = conn.read_request2(endpoint, crate::clusters::defs::CLUSTER_ID_TIME_SYNCHRONIZATION, crate::clusters::defs::CLUSTER_TIME_SYNCHRONIZATION_ATTR_ID_TIMESOURCE).await?;
701    decode_time_source(&tlv)
702}
703
704/// Read `TrustedTimeSource` attribute from cluster `Time Synchronization`.
705pub async fn read_trusted_time_source(conn: &crate::controller::Connection, endpoint: u16) -> anyhow::Result<Option<TrustedTimeSource>> {
706    let tlv = conn.read_request2(endpoint, crate::clusters::defs::CLUSTER_ID_TIME_SYNCHRONIZATION, crate::clusters::defs::CLUSTER_TIME_SYNCHRONIZATION_ATTR_ID_TRUSTEDTIMESOURCE).await?;
707    decode_trusted_time_source(&tlv)
708}
709
710/// Read `DefaultNTP` attribute from cluster `Time Synchronization`.
711pub async fn read_default_ntp(conn: &crate::controller::Connection, endpoint: u16) -> anyhow::Result<Option<String>> {
712    let tlv = conn.read_request2(endpoint, crate::clusters::defs::CLUSTER_ID_TIME_SYNCHRONIZATION, crate::clusters::defs::CLUSTER_TIME_SYNCHRONIZATION_ATTR_ID_DEFAULTNTP).await?;
713    decode_default_ntp(&tlv)
714}
715
716/// Read `TimeZone` attribute from cluster `Time Synchronization`.
717pub async fn read_time_zone(conn: &crate::controller::Connection, endpoint: u16) -> anyhow::Result<Vec<TimeZone>> {
718    let tlv = conn.read_request2(endpoint, crate::clusters::defs::CLUSTER_ID_TIME_SYNCHRONIZATION, crate::clusters::defs::CLUSTER_TIME_SYNCHRONIZATION_ATTR_ID_TIMEZONE).await?;
719    decode_time_zone(&tlv)
720}
721
722/// Read `DSTOffset` attribute from cluster `Time Synchronization`.
723pub async fn read_dst_offset(conn: &crate::controller::Connection, endpoint: u16) -> anyhow::Result<Vec<DSTOffset>> {
724    let tlv = conn.read_request2(endpoint, crate::clusters::defs::CLUSTER_ID_TIME_SYNCHRONIZATION, crate::clusters::defs::CLUSTER_TIME_SYNCHRONIZATION_ATTR_ID_DSTOFFSET).await?;
725    decode_dst_offset(&tlv)
726}
727
728/// Read `LocalTime` attribute from cluster `Time Synchronization`.
729pub async fn read_local_time(conn: &crate::controller::Connection, endpoint: u16) -> anyhow::Result<Option<u64>> {
730    let tlv = conn.read_request2(endpoint, crate::clusters::defs::CLUSTER_ID_TIME_SYNCHRONIZATION, crate::clusters::defs::CLUSTER_TIME_SYNCHRONIZATION_ATTR_ID_LOCALTIME).await?;
731    decode_local_time(&tlv)
732}
733
734/// Read `TimeZoneDatabase` attribute from cluster `Time Synchronization`.
735pub async fn read_time_zone_database(conn: &crate::controller::Connection, endpoint: u16) -> anyhow::Result<TimeZoneDatabase> {
736    let tlv = conn.read_request2(endpoint, crate::clusters::defs::CLUSTER_ID_TIME_SYNCHRONIZATION, crate::clusters::defs::CLUSTER_TIME_SYNCHRONIZATION_ATTR_ID_TIMEZONEDATABASE).await?;
737    decode_time_zone_database(&tlv)
738}
739
740/// Read `NTPServerAvailable` attribute from cluster `Time Synchronization`.
741pub async fn read_ntp_server_available(conn: &crate::controller::Connection, endpoint: u16) -> anyhow::Result<bool> {
742    let tlv = conn.read_request2(endpoint, crate::clusters::defs::CLUSTER_ID_TIME_SYNCHRONIZATION, crate::clusters::defs::CLUSTER_TIME_SYNCHRONIZATION_ATTR_ID_NTPSERVERAVAILABLE).await?;
743    decode_ntp_server_available(&tlv)
744}
745
746/// Read `TimeZoneListMaxSize` attribute from cluster `Time Synchronization`.
747pub async fn read_time_zone_list_max_size(conn: &crate::controller::Connection, endpoint: u16) -> anyhow::Result<u8> {
748    let tlv = conn.read_request2(endpoint, crate::clusters::defs::CLUSTER_ID_TIME_SYNCHRONIZATION, crate::clusters::defs::CLUSTER_TIME_SYNCHRONIZATION_ATTR_ID_TIMEZONELISTMAXSIZE).await?;
749    decode_time_zone_list_max_size(&tlv)
750}
751
752/// Read `DSTOffsetListMaxSize` attribute from cluster `Time Synchronization`.
753pub async fn read_dst_offset_list_max_size(conn: &crate::controller::Connection, endpoint: u16) -> anyhow::Result<u8> {
754    let tlv = conn.read_request2(endpoint, crate::clusters::defs::CLUSTER_ID_TIME_SYNCHRONIZATION, crate::clusters::defs::CLUSTER_TIME_SYNCHRONIZATION_ATTR_ID_DSTOFFSETLISTMAXSIZE).await?;
755    decode_dst_offset_list_max_size(&tlv)
756}
757
758/// Read `SupportsDNSResolve` attribute from cluster `Time Synchronization`.
759pub async fn read_supports_dns_resolve(conn: &crate::controller::Connection, endpoint: u16) -> anyhow::Result<bool> {
760    let tlv = conn.read_request2(endpoint, crate::clusters::defs::CLUSTER_ID_TIME_SYNCHRONIZATION, crate::clusters::defs::CLUSTER_TIME_SYNCHRONIZATION_ATTR_ID_SUPPORTSDNSRESOLVE).await?;
761    decode_supports_dns_resolve(&tlv)
762}
763
764#[derive(Debug, serde::Serialize)]
765pub struct DSTStatusEvent {
766    pub dst_offset_active: Option<bool>,
767}
768
769#[derive(Debug, serde::Serialize)]
770pub struct TimeZoneStatusEvent {
771    pub offset: Option<i32>,
772    pub name: Option<String>,
773}
774
775// Event decoders
776
777/// Decode DSTStatus event (0x01, priority: info)
778pub fn decode_dst_status_event(inp: &tlv::TlvItemValue) -> anyhow::Result<DSTStatusEvent> {
779    if let tlv::TlvItemValue::List(_fields) = inp {
780        let item = tlv::TlvItem { tag: 0, value: inp.clone() };
781        Ok(DSTStatusEvent {
782                                dst_offset_active: item.get_bool(&[0]),
783        })
784    } else {
785        Err(anyhow::anyhow!("Expected struct fields"))
786    }
787}
788
789/// Decode TimeZoneStatus event (0x02, priority: info)
790pub fn decode_time_zone_status_event(inp: &tlv::TlvItemValue) -> anyhow::Result<TimeZoneStatusEvent> {
791    if let tlv::TlvItemValue::List(_fields) = inp {
792        let item = tlv::TlvItem { tag: 0, value: inp.clone() };
793        Ok(TimeZoneStatusEvent {
794                                offset: item.get_int(&[0]).map(|v| v as i32),
795                                name: item.get_string_owned(&[1]),
796        })
797    } else {
798        Err(anyhow::anyhow!("Expected struct fields"))
799    }
800}
801
802
803// Event JSON dispatcher
804
805/// Decode event value and return as JSON string
806pub fn decode_event_json(cluster_id: u32, event_id: u32, tlv_value: &crate::tlv::TlvItemValue) -> String {
807    if cluster_id != 0x0038 {
808        return format!("{{\"error\": \"Invalid cluster ID. Expected 0x0038, got {}\"}}", cluster_id);
809    }
810
811    match event_id {
812        0x00 => "{}".to_string(),
813        0x01 => {
814            match decode_dst_status_event(tlv_value) {
815                Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
816                Err(e) => format!("{{\"error\": \"{}\"}}", e),
817            }
818        }
819        0x02 => {
820            match decode_time_zone_status_event(tlv_value) {
821                Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
822                Err(e) => format!("{{\"error\": \"{}\"}}", e),
823            }
824        }
825        0x03 => "{}".to_string(),
826        0x04 => "{}".to_string(),
827        _ => format!("{{\"error\": \"Unknown event ID: {}\"}}", event_id),
828    }
829}
830
831/// Get list of all events supported by this cluster
832///
833/// # Returns
834/// Vector of tuples containing (event_id, event_name)
835pub fn get_event_list() -> Vec<(u32, &'static str)> {
836    vec![
837        (0x00, "DSTTableEmpty"),
838        (0x01, "DSTStatus"),
839        (0x02, "TimeZoneStatus"),
840        (0x03, "TimeFailure"),
841        (0x04, "MissingTrustedTimeSource"),
842    ]
843}
844