matc/clusters/codec/
media_playback.rs

1//! Matter TLV encoders and decoders for Media Playback Cluster
2//! Cluster ID: 0x0506
3//!
4//! This file is automatically generated from MediaPlayback.xml
5
6#![allow(clippy::too_many_arguments)]
7
8use crate::tlv;
9use anyhow;
10use serde_json;
11
12
13// Import serialization helpers for octet strings
14use crate::clusters::helpers::{serialize_opt_bytes_as_hex};
15
16// Enum definitions
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
19#[repr(u8)]
20pub enum Characteristic {
21    /// Textual information meant for display when no other text representation is selected. It is used to clarify dialogue, alternate languages, texted graphics or location/person IDs that are not otherwise covered in the dubbed/localized audio.
22    Forcedsubtitles = 0,
23    /// Textual or audio media component containing a textual description (intended for audio synthesis) or an audio description describing a visual component
24    Describesvideo = 1,
25    /// Simplified or reduced captions as specified in [United States Code Title 47 CFR 79.103(c)(9)].
26    Easytoread = 2,
27    /// A media characteristic that indicates that a track selection option includes frame-based content.
28    Framebased = 3,
29    /// Main media component(s) which is/are intended for presentation if no other information is provided
30    Mainprogram = 4,
31    /// A media characteristic that indicates that a track or media selection option contains original content.
32    Originalcontent = 5,
33    /// A media characteristic that indicates that a track or media selection option contains a language translation and verbal interpretation of spoken dialog.
34    Voiceovertranslation = 6,
35    /// Textual media component containing transcriptions of spoken dialog and auditory cues such as sound effects and music for the hearing impaired.
36    Caption = 7,
37    /// Textual transcriptions of spoken dialog.
38    Subtitle = 8,
39    /// Textual media component containing transcriptions of spoken dialog and auditory cues such as sound effects and music for the hearing impaired.
40    Alternate = 9,
41    /// Media content component that is supplementary to a media content component of a different media component type.
42    Supplementary = 10,
43    /// Experience that contains a commentary (e.g. director’s commentary) (typically audio)
44    Commentary = 11,
45    /// Experience that contains an element that is presented in a different language from the original (e.g. dubbed audio, translated captions)
46    Dubbedtranslation = 12,
47    /// Textual or audio media component containing a textual description (intended for audio synthesis) or an audio description describing a visual component
48    Description = 13,
49    /// Media component containing information intended to be processed by application specific elements.
50    Metadata = 14,
51    /// Experience containing an element for improved intelligibility of the dialogue.
52    Enhancedaudiointelligibility = 15,
53    /// Experience that provides information, about a current emergency, that is intended to enable the protection of life, health, safety, and property, and may also include critical details regarding the emergency and how to respond to the emergency.
54    Emergency = 16,
55    /// Textual representation of a songs’ lyrics, usually in the same language as the associated song as specified in [SMPTE ST 2067-2].
56    Karaoke = 17,
57}
58
59impl Characteristic {
60    /// Convert from u8 value
61    pub fn from_u8(value: u8) -> Option<Self> {
62        match value {
63            0 => Some(Characteristic::Forcedsubtitles),
64            1 => Some(Characteristic::Describesvideo),
65            2 => Some(Characteristic::Easytoread),
66            3 => Some(Characteristic::Framebased),
67            4 => Some(Characteristic::Mainprogram),
68            5 => Some(Characteristic::Originalcontent),
69            6 => Some(Characteristic::Voiceovertranslation),
70            7 => Some(Characteristic::Caption),
71            8 => Some(Characteristic::Subtitle),
72            9 => Some(Characteristic::Alternate),
73            10 => Some(Characteristic::Supplementary),
74            11 => Some(Characteristic::Commentary),
75            12 => Some(Characteristic::Dubbedtranslation),
76            13 => Some(Characteristic::Description),
77            14 => Some(Characteristic::Metadata),
78            15 => Some(Characteristic::Enhancedaudiointelligibility),
79            16 => Some(Characteristic::Emergency),
80            17 => Some(Characteristic::Karaoke),
81            _ => None,
82        }
83    }
84
85    /// Convert to u8 value
86    pub fn to_u8(self) -> u8 {
87        self as u8
88    }
89}
90
91impl From<Characteristic> for u8 {
92    fn from(val: Characteristic) -> Self {
93        val as u8
94    }
95}
96
97#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
98#[repr(u8)]
99pub enum PlaybackState {
100    /// Media is currently playing (includes FF and REW)
101    Playing = 0,
102    /// Media is currently paused
103    Paused = 1,
104    /// Media is not currently playing
105    Notplaying = 2,
106    /// Media is not currently buffering and playback will start when buffer has been filled
107    Buffering = 3,
108}
109
110impl PlaybackState {
111    /// Convert from u8 value
112    pub fn from_u8(value: u8) -> Option<Self> {
113        match value {
114            0 => Some(PlaybackState::Playing),
115            1 => Some(PlaybackState::Paused),
116            2 => Some(PlaybackState::Notplaying),
117            3 => Some(PlaybackState::Buffering),
118            _ => None,
119        }
120    }
121
122    /// Convert to u8 value
123    pub fn to_u8(self) -> u8 {
124        self as u8
125    }
126}
127
128impl From<PlaybackState> for u8 {
129    fn from(val: PlaybackState) -> Self {
130        val as u8
131    }
132}
133
134#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
135#[repr(u8)]
136pub enum Status {
137    /// Succeeded
138    Success = 0,
139    /// Requested playback command is invalid in the current playback state.
140    Invalidstateforcommand = 1,
141    /// Requested playback command is not allowed in the current playback state. For example, attempting to fast-forward during a commercial might return NotAllowed.
142    Notallowed = 2,
143    /// This endpoint is not active for playback.
144    Notactive = 3,
145    /// The FastForward or Rewind Command was issued but the media is already playing back at the fastest speed supported by the server in the respective direction.
146    Speedoutofrange = 4,
147    /// The Seek Command was issued with a value of position outside of the allowed seek range of the media.
148    Seekoutofrange = 5,
149}
150
151impl Status {
152    /// Convert from u8 value
153    pub fn from_u8(value: u8) -> Option<Self> {
154        match value {
155            0 => Some(Status::Success),
156            1 => Some(Status::Invalidstateforcommand),
157            2 => Some(Status::Notallowed),
158            3 => Some(Status::Notactive),
159            4 => Some(Status::Speedoutofrange),
160            5 => Some(Status::Seekoutofrange),
161            _ => None,
162        }
163    }
164
165    /// Convert to u8 value
166    pub fn to_u8(self) -> u8 {
167        self as u8
168    }
169}
170
171impl From<Status> for u8 {
172    fn from(val: Status) -> Self {
173        val as u8
174    }
175}
176
177// Struct definitions
178
179#[derive(Debug, serde::Serialize)]
180pub struct PlaybackPosition {
181    pub updated_at: Option<u64>,
182    pub position: Option<u64>,
183}
184
185#[derive(Debug, serde::Serialize)]
186pub struct TrackAttributes {
187    pub language_code: Option<String>,
188    pub characteristics: Option<Vec<Characteristic>>,
189    pub display_name: Option<String>,
190}
191
192#[derive(Debug, serde::Serialize)]
193pub struct Track {
194    pub id: Option<String>,
195    pub track_attributes: Option<TrackAttributes>,
196}
197
198// Command encoders
199
200/// Encode Rewind command (0x06)
201pub fn encode_rewind(audio_advance_unmuted: bool) -> anyhow::Result<Vec<u8>> {
202    let tlv = tlv::TlvItemEnc {
203        tag: 0,
204        value: tlv::TlvItemValueEnc::StructInvisible(vec![
205        (0, tlv::TlvItemValueEnc::Bool(audio_advance_unmuted)).into(),
206        ]),
207    };
208    Ok(tlv.encode()?)
209}
210
211/// Encode FastForward command (0x07)
212pub fn encode_fast_forward(audio_advance_unmuted: bool) -> anyhow::Result<Vec<u8>> {
213    let tlv = tlv::TlvItemEnc {
214        tag: 0,
215        value: tlv::TlvItemValueEnc::StructInvisible(vec![
216        (0, tlv::TlvItemValueEnc::Bool(audio_advance_unmuted)).into(),
217        ]),
218    };
219    Ok(tlv.encode()?)
220}
221
222/// Encode SkipForward command (0x08)
223pub fn encode_skip_forward(delta_position_milliseconds: u64) -> anyhow::Result<Vec<u8>> {
224    let tlv = tlv::TlvItemEnc {
225        tag: 0,
226        value: tlv::TlvItemValueEnc::StructInvisible(vec![
227        (0, tlv::TlvItemValueEnc::UInt64(delta_position_milliseconds)).into(),
228        ]),
229    };
230    Ok(tlv.encode()?)
231}
232
233/// Encode SkipBackward command (0x09)
234pub fn encode_skip_backward(delta_position_milliseconds: u64) -> anyhow::Result<Vec<u8>> {
235    let tlv = tlv::TlvItemEnc {
236        tag: 0,
237        value: tlv::TlvItemValueEnc::StructInvisible(vec![
238        (0, tlv::TlvItemValueEnc::UInt64(delta_position_milliseconds)).into(),
239        ]),
240    };
241    Ok(tlv.encode()?)
242}
243
244/// Encode Seek command (0x0B)
245pub fn encode_seek(position: u64) -> anyhow::Result<Vec<u8>> {
246    let tlv = tlv::TlvItemEnc {
247        tag: 0,
248        value: tlv::TlvItemValueEnc::StructInvisible(vec![
249        (0, tlv::TlvItemValueEnc::UInt64(position)).into(),
250        ]),
251    };
252    Ok(tlv.encode()?)
253}
254
255/// Encode ActivateAudioTrack command (0x0C)
256pub fn encode_activate_audio_track(track_id: String, audio_output_index: Option<u8>) -> anyhow::Result<Vec<u8>> {
257    let tlv = tlv::TlvItemEnc {
258        tag: 0,
259        value: tlv::TlvItemValueEnc::StructInvisible(vec![
260        (0, tlv::TlvItemValueEnc::String(track_id)).into(),
261        (1, tlv::TlvItemValueEnc::UInt8(audio_output_index.unwrap_or(0))).into(),
262        ]),
263    };
264    Ok(tlv.encode()?)
265}
266
267/// Encode ActivateTextTrack command (0x0D)
268pub fn encode_activate_text_track(track_id: String) -> anyhow::Result<Vec<u8>> {
269    let tlv = tlv::TlvItemEnc {
270        tag: 0,
271        value: tlv::TlvItemValueEnc::StructInvisible(vec![
272        (0, tlv::TlvItemValueEnc::String(track_id)).into(),
273        ]),
274    };
275    Ok(tlv.encode()?)
276}
277
278// Attribute decoders
279
280/// Decode CurrentState attribute (0x0000)
281pub fn decode_current_state(inp: &tlv::TlvItemValue) -> anyhow::Result<PlaybackState> {
282    if let tlv::TlvItemValue::Int(v) = inp {
283        PlaybackState::from_u8(*v as u8).ok_or_else(|| anyhow::anyhow!("Invalid enum value"))
284    } else {
285        Err(anyhow::anyhow!("Expected Integer"))
286    }
287}
288
289/// Decode StartTime attribute (0x0001)
290pub fn decode_start_time(inp: &tlv::TlvItemValue) -> anyhow::Result<Option<u64>> {
291    if let tlv::TlvItemValue::Int(v) = inp {
292        Ok(Some(*v))
293    } else {
294        Ok(None)
295    }
296}
297
298/// Decode Duration attribute (0x0002)
299pub fn decode_duration(inp: &tlv::TlvItemValue) -> anyhow::Result<Option<u64>> {
300    if let tlv::TlvItemValue::Int(v) = inp {
301        Ok(Some(*v))
302    } else {
303        Ok(None)
304    }
305}
306
307/// Decode SampledPosition attribute (0x0003)
308pub fn decode_sampled_position(inp: &tlv::TlvItemValue) -> anyhow::Result<Option<PlaybackPosition>> {
309    if let tlv::TlvItemValue::List(_fields) = inp {
310        // Struct with fields
311        let item = tlv::TlvItem { tag: 0, value: inp.clone() };
312        Ok(Some(PlaybackPosition {
313                updated_at: item.get_int(&[0]),
314                position: item.get_int(&[1]),
315        }))
316    //} else if let tlv::TlvItemValue::Null = inp {
317    //    // Null value for nullable struct
318    //    Ok(None)
319    } else {
320    Ok(None)
321    //    Err(anyhow::anyhow!("Expected struct fields or null"))
322    }
323}
324
325/// Decode PlaybackSpeed attribute (0x0004)
326pub fn decode_playback_speed(inp: &tlv::TlvItemValue) -> anyhow::Result<u8> {
327    if let tlv::TlvItemValue::Int(v) = inp {
328        Ok(*v as u8)
329    } else {
330        Err(anyhow::anyhow!("Expected UInt8"))
331    }
332}
333
334/// Decode SeekRangeEnd attribute (0x0005)
335pub fn decode_seek_range_end(inp: &tlv::TlvItemValue) -> anyhow::Result<Option<u64>> {
336    if let tlv::TlvItemValue::Int(v) = inp {
337        Ok(Some(*v))
338    } else {
339        Ok(None)
340    }
341}
342
343/// Decode SeekRangeStart attribute (0x0006)
344pub fn decode_seek_range_start(inp: &tlv::TlvItemValue) -> anyhow::Result<Option<u64>> {
345    if let tlv::TlvItemValue::Int(v) = inp {
346        Ok(Some(*v))
347    } else {
348        Ok(None)
349    }
350}
351
352/// Decode ActiveAudioTrack attribute (0x0007)
353pub fn decode_active_audio_track(inp: &tlv::TlvItemValue) -> anyhow::Result<Option<Track>> {
354    if let tlv::TlvItemValue::List(_fields) = inp {
355        // Struct with fields
356        let item = tlv::TlvItem { tag: 0, value: inp.clone() };
357        Ok(Some(Track {
358                id: item.get_string_owned(&[0]),
359                track_attributes: {
360                    if let Some(nested_tlv) = item.get(&[1]) {
361                        if let tlv::TlvItemValue::List(_) = nested_tlv {
362                            let nested_item = tlv::TlvItem { tag: 1, value: nested_tlv.clone() };
363                            Some(TrackAttributes {
364                language_code: nested_item.get_string_owned(&[0]),
365                characteristics: {
366                    if let Some(tlv::TlvItemValue::List(l)) = nested_item.get(&[1]) {
367                        let items: Vec<Characteristic> = l.iter().filter_map(|e| { if let tlv::TlvItemValue::Int(v) = &e.value { Characteristic::from_u8(*v as u8) } else { None } }).collect();
368                        Some(items)
369                    } else {
370                        None
371                    }
372                },
373                display_name: nested_item.get_string_owned(&[2]),
374                            })
375                        } else {
376                            None
377                        }
378                    } else {
379                        None
380                    }
381                },
382        }))
383    //} else if let tlv::TlvItemValue::Null = inp {
384    //    // Null value for nullable struct
385    //    Ok(None)
386    } else {
387    Ok(None)
388    //    Err(anyhow::anyhow!("Expected struct fields or null"))
389    }
390}
391
392/// Decode AvailableAudioTracks attribute (0x0008)
393pub fn decode_available_audio_tracks(inp: &tlv::TlvItemValue) -> anyhow::Result<Vec<Track>> {
394    let mut res = Vec::new();
395    if let tlv::TlvItemValue::List(v) = inp {
396        for item in v {
397            res.push(Track {
398                id: item.get_string_owned(&[0]),
399                track_attributes: {
400                    if let Some(nested_tlv) = item.get(&[1]) {
401                        if let tlv::TlvItemValue::List(_) = nested_tlv {
402                            let nested_item = tlv::TlvItem { tag: 1, value: nested_tlv.clone() };
403                            Some(TrackAttributes {
404                language_code: nested_item.get_string_owned(&[0]),
405                characteristics: {
406                    if let Some(tlv::TlvItemValue::List(l)) = nested_item.get(&[1]) {
407                        let items: Vec<Characteristic> = l.iter().filter_map(|e| { if let tlv::TlvItemValue::Int(v) = &e.value { Characteristic::from_u8(*v as u8) } else { None } }).collect();
408                        Some(items)
409                    } else {
410                        None
411                    }
412                },
413                display_name: nested_item.get_string_owned(&[2]),
414                            })
415                        } else {
416                            None
417                        }
418                    } else {
419                        None
420                    }
421                },
422            });
423        }
424    }
425    Ok(res)
426}
427
428/// Decode ActiveTextTrack attribute (0x0009)
429pub fn decode_active_text_track(inp: &tlv::TlvItemValue) -> anyhow::Result<Option<Track>> {
430    if let tlv::TlvItemValue::List(_fields) = inp {
431        // Struct with fields
432        let item = tlv::TlvItem { tag: 0, value: inp.clone() };
433        Ok(Some(Track {
434                id: item.get_string_owned(&[0]),
435                track_attributes: {
436                    if let Some(nested_tlv) = item.get(&[1]) {
437                        if let tlv::TlvItemValue::List(_) = nested_tlv {
438                            let nested_item = tlv::TlvItem { tag: 1, value: nested_tlv.clone() };
439                            Some(TrackAttributes {
440                language_code: nested_item.get_string_owned(&[0]),
441                characteristics: {
442                    if let Some(tlv::TlvItemValue::List(l)) = nested_item.get(&[1]) {
443                        let items: Vec<Characteristic> = l.iter().filter_map(|e| { if let tlv::TlvItemValue::Int(v) = &e.value { Characteristic::from_u8(*v as u8) } else { None } }).collect();
444                        Some(items)
445                    } else {
446                        None
447                    }
448                },
449                display_name: nested_item.get_string_owned(&[2]),
450                            })
451                        } else {
452                            None
453                        }
454                    } else {
455                        None
456                    }
457                },
458        }))
459    //} else if let tlv::TlvItemValue::Null = inp {
460    //    // Null value for nullable struct
461    //    Ok(None)
462    } else {
463    Ok(None)
464    //    Err(anyhow::anyhow!("Expected struct fields or null"))
465    }
466}
467
468/// Decode AvailableTextTracks attribute (0x000A)
469pub fn decode_available_text_tracks(inp: &tlv::TlvItemValue) -> anyhow::Result<Vec<Track>> {
470    let mut res = Vec::new();
471    if let tlv::TlvItemValue::List(v) = inp {
472        for item in v {
473            res.push(Track {
474                id: item.get_string_owned(&[0]),
475                track_attributes: {
476                    if let Some(nested_tlv) = item.get(&[1]) {
477                        if let tlv::TlvItemValue::List(_) = nested_tlv {
478                            let nested_item = tlv::TlvItem { tag: 1, value: nested_tlv.clone() };
479                            Some(TrackAttributes {
480                language_code: nested_item.get_string_owned(&[0]),
481                characteristics: {
482                    if let Some(tlv::TlvItemValue::List(l)) = nested_item.get(&[1]) {
483                        let items: Vec<Characteristic> = l.iter().filter_map(|e| { if let tlv::TlvItemValue::Int(v) = &e.value { Characteristic::from_u8(*v as u8) } else { None } }).collect();
484                        Some(items)
485                    } else {
486                        None
487                    }
488                },
489                display_name: nested_item.get_string_owned(&[2]),
490                            })
491                        } else {
492                            None
493                        }
494                    } else {
495                        None
496                    }
497                },
498            });
499        }
500    }
501    Ok(res)
502}
503
504
505// JSON dispatcher function
506
507/// Decode attribute value and return as JSON string
508///
509/// # Parameters
510/// * `cluster_id` - The cluster identifier
511/// * `attribute_id` - The attribute identifier
512/// * `tlv_value` - The TLV value to decode
513///
514/// # Returns
515/// JSON string representation of the decoded value or error
516pub fn decode_attribute_json(cluster_id: u32, attribute_id: u32, tlv_value: &crate::tlv::TlvItemValue) -> String {
517    // Verify this is the correct cluster
518    if cluster_id != 0x0506 {
519        return format!("{{\"error\": \"Invalid cluster ID. Expected 0x0506, got {}\"}}", cluster_id);
520    }
521
522    match attribute_id {
523        0x0000 => {
524            match decode_current_state(tlv_value) {
525                Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
526                Err(e) => format!("{{\"error\": \"{}\"}}", e),
527            }
528        }
529        0x0001 => {
530            match decode_start_time(tlv_value) {
531                Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
532                Err(e) => format!("{{\"error\": \"{}\"}}", e),
533            }
534        }
535        0x0002 => {
536            match decode_duration(tlv_value) {
537                Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
538                Err(e) => format!("{{\"error\": \"{}\"}}", e),
539            }
540        }
541        0x0003 => {
542            match decode_sampled_position(tlv_value) {
543                Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
544                Err(e) => format!("{{\"error\": \"{}\"}}", e),
545            }
546        }
547        0x0004 => {
548            match decode_playback_speed(tlv_value) {
549                Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
550                Err(e) => format!("{{\"error\": \"{}\"}}", e),
551            }
552        }
553        0x0005 => {
554            match decode_seek_range_end(tlv_value) {
555                Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
556                Err(e) => format!("{{\"error\": \"{}\"}}", e),
557            }
558        }
559        0x0006 => {
560            match decode_seek_range_start(tlv_value) {
561                Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
562                Err(e) => format!("{{\"error\": \"{}\"}}", e),
563            }
564        }
565        0x0007 => {
566            match decode_active_audio_track(tlv_value) {
567                Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
568                Err(e) => format!("{{\"error\": \"{}\"}}", e),
569            }
570        }
571        0x0008 => {
572            match decode_available_audio_tracks(tlv_value) {
573                Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
574                Err(e) => format!("{{\"error\": \"{}\"}}", e),
575            }
576        }
577        0x0009 => {
578            match decode_active_text_track(tlv_value) {
579                Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
580                Err(e) => format!("{{\"error\": \"{}\"}}", e),
581            }
582        }
583        0x000A => {
584            match decode_available_text_tracks(tlv_value) {
585                Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
586                Err(e) => format!("{{\"error\": \"{}\"}}", e),
587            }
588        }
589        _ => format!("{{\"error\": \"Unknown attribute ID: {}\"}}", attribute_id),
590    }
591}
592
593/// Get list of all attributes supported by this cluster
594///
595/// # Returns
596/// Vector of tuples containing (attribute_id, attribute_name)
597pub fn get_attribute_list() -> Vec<(u32, &'static str)> {
598    vec![
599        (0x0000, "CurrentState"),
600        (0x0001, "StartTime"),
601        (0x0002, "Duration"),
602        (0x0003, "SampledPosition"),
603        (0x0004, "PlaybackSpeed"),
604        (0x0005, "SeekRangeEnd"),
605        (0x0006, "SeekRangeStart"),
606        (0x0007, "ActiveAudioTrack"),
607        (0x0008, "AvailableAudioTracks"),
608        (0x0009, "ActiveTextTrack"),
609        (0x000A, "AvailableTextTracks"),
610    ]
611}
612
613// Command listing
614
615pub fn get_command_list() -> Vec<(u32, &'static str)> {
616    vec![
617        (0x00, "Play"),
618        (0x01, "Pause"),
619        (0x02, "Stop"),
620        (0x03, "StartOver"),
621        (0x04, "Previous"),
622        (0x05, "Next"),
623        (0x06, "Rewind"),
624        (0x07, "FastForward"),
625        (0x08, "SkipForward"),
626        (0x09, "SkipBackward"),
627        (0x0B, "Seek"),
628        (0x0C, "ActivateAudioTrack"),
629        (0x0D, "ActivateTextTrack"),
630        (0x0E, "DeactivateTextTrack"),
631    ]
632}
633
634pub fn get_command_name(cmd_id: u32) -> Option<&'static str> {
635    match cmd_id {
636        0x00 => Some("Play"),
637        0x01 => Some("Pause"),
638        0x02 => Some("Stop"),
639        0x03 => Some("StartOver"),
640        0x04 => Some("Previous"),
641        0x05 => Some("Next"),
642        0x06 => Some("Rewind"),
643        0x07 => Some("FastForward"),
644        0x08 => Some("SkipForward"),
645        0x09 => Some("SkipBackward"),
646        0x0B => Some("Seek"),
647        0x0C => Some("ActivateAudioTrack"),
648        0x0D => Some("ActivateTextTrack"),
649        0x0E => Some("DeactivateTextTrack"),
650        _ => None,
651    }
652}
653
654pub fn get_command_schema(cmd_id: u32) -> Option<Vec<crate::clusters::codec::CommandField>> {
655    match cmd_id {
656        0x00 => Some(vec![]),
657        0x01 => Some(vec![]),
658        0x02 => Some(vec![]),
659        0x03 => Some(vec![]),
660        0x04 => Some(vec![]),
661        0x05 => Some(vec![]),
662        0x06 => Some(vec![
663            crate::clusters::codec::CommandField { tag: 0, name: "audio_advance_unmuted", kind: crate::clusters::codec::FieldKind::Bool, optional: false, nullable: false },
664        ]),
665        0x07 => Some(vec![
666            crate::clusters::codec::CommandField { tag: 0, name: "audio_advance_unmuted", kind: crate::clusters::codec::FieldKind::Bool, optional: false, nullable: false },
667        ]),
668        0x08 => Some(vec![
669            crate::clusters::codec::CommandField { tag: 0, name: "delta_position_milliseconds", kind: crate::clusters::codec::FieldKind::U64, optional: false, nullable: false },
670        ]),
671        0x09 => Some(vec![
672            crate::clusters::codec::CommandField { tag: 0, name: "delta_position_milliseconds", kind: crate::clusters::codec::FieldKind::U64, optional: false, nullable: false },
673        ]),
674        0x0B => Some(vec![
675            crate::clusters::codec::CommandField { tag: 0, name: "position", kind: crate::clusters::codec::FieldKind::U64, optional: false, nullable: false },
676        ]),
677        0x0C => Some(vec![
678            crate::clusters::codec::CommandField { tag: 0, name: "track_id", kind: crate::clusters::codec::FieldKind::String, optional: false, nullable: false },
679            crate::clusters::codec::CommandField { tag: 1, name: "audio_output_index", kind: crate::clusters::codec::FieldKind::U8, optional: false, nullable: true },
680        ]),
681        0x0D => Some(vec![
682            crate::clusters::codec::CommandField { tag: 0, name: "track_id", kind: crate::clusters::codec::FieldKind::String, optional: false, nullable: false },
683        ]),
684        0x0E => Some(vec![]),
685        _ => None,
686    }
687}
688
689pub fn encode_command_json(cmd_id: u32, args: &serde_json::Value) -> anyhow::Result<Vec<u8>> {
690    match cmd_id {
691        0x00 => Ok(vec![]),
692        0x01 => Ok(vec![]),
693        0x02 => Ok(vec![]),
694        0x03 => Ok(vec![]),
695        0x04 => Ok(vec![]),
696        0x05 => Ok(vec![]),
697        0x06 => {
698        let audio_advance_unmuted = crate::clusters::codec::json_util::get_bool(args, "audio_advance_unmuted")?;
699        encode_rewind(audio_advance_unmuted)
700        }
701        0x07 => {
702        let audio_advance_unmuted = crate::clusters::codec::json_util::get_bool(args, "audio_advance_unmuted")?;
703        encode_fast_forward(audio_advance_unmuted)
704        }
705        0x08 => {
706        let delta_position_milliseconds = crate::clusters::codec::json_util::get_u64(args, "delta_position_milliseconds")?;
707        encode_skip_forward(delta_position_milliseconds)
708        }
709        0x09 => {
710        let delta_position_milliseconds = crate::clusters::codec::json_util::get_u64(args, "delta_position_milliseconds")?;
711        encode_skip_backward(delta_position_milliseconds)
712        }
713        0x0B => {
714        let position = crate::clusters::codec::json_util::get_u64(args, "position")?;
715        encode_seek(position)
716        }
717        0x0C => {
718        let track_id = crate::clusters::codec::json_util::get_string(args, "track_id")?;
719        let audio_output_index = crate::clusters::codec::json_util::get_opt_u8(args, "audio_output_index")?;
720        encode_activate_audio_track(track_id, audio_output_index)
721        }
722        0x0D => {
723        let track_id = crate::clusters::codec::json_util::get_string(args, "track_id")?;
724        encode_activate_text_track(track_id)
725        }
726        0x0E => Ok(vec![]),
727        _ => Err(anyhow::anyhow!("unknown command ID: 0x{:02X}", cmd_id)),
728    }
729}
730
731#[derive(Debug, serde::Serialize)]
732pub struct PlaybackResponse {
733    pub status: Option<Status>,
734    pub data: Option<String>,
735}
736
737// Command response decoders
738
739/// Decode PlaybackResponse command response (0A)
740pub fn decode_playback_response(inp: &tlv::TlvItemValue) -> anyhow::Result<PlaybackResponse> {
741    if let tlv::TlvItemValue::List(_fields) = inp {
742        let item = tlv::TlvItem { tag: 0, value: inp.clone() };
743        Ok(PlaybackResponse {
744                status: item.get_int(&[0]).and_then(|v| Status::from_u8(v as u8)),
745                data: item.get_string_owned(&[1]),
746        })
747    } else {
748        Err(anyhow::anyhow!("Expected struct fields"))
749    }
750}
751
752// Typed facade (invokes + reads)
753
754/// Invoke `Play` command on cluster `Media Playback`.
755pub async fn play(conn: &crate::controller::Connection, endpoint: u16) -> anyhow::Result<PlaybackResponse> {
756    let tlv = conn.invoke_request2(endpoint, crate::clusters::defs::CLUSTER_ID_MEDIA_PLAYBACK, crate::clusters::defs::CLUSTER_MEDIA_PLAYBACK_CMD_ID_PLAY, &[]).await?;
757    decode_playback_response(&tlv)
758}
759
760/// Invoke `Pause` command on cluster `Media Playback`.
761pub async fn pause(conn: &crate::controller::Connection, endpoint: u16) -> anyhow::Result<PlaybackResponse> {
762    let tlv = conn.invoke_request2(endpoint, crate::clusters::defs::CLUSTER_ID_MEDIA_PLAYBACK, crate::clusters::defs::CLUSTER_MEDIA_PLAYBACK_CMD_ID_PAUSE, &[]).await?;
763    decode_playback_response(&tlv)
764}
765
766/// Invoke `Stop` command on cluster `Media Playback`.
767pub async fn stop(conn: &crate::controller::Connection, endpoint: u16) -> anyhow::Result<PlaybackResponse> {
768    let tlv = conn.invoke_request2(endpoint, crate::clusters::defs::CLUSTER_ID_MEDIA_PLAYBACK, crate::clusters::defs::CLUSTER_MEDIA_PLAYBACK_CMD_ID_STOP, &[]).await?;
769    decode_playback_response(&tlv)
770}
771
772/// Invoke `StartOver` command on cluster `Media Playback`.
773pub async fn start_over(conn: &crate::controller::Connection, endpoint: u16) -> anyhow::Result<PlaybackResponse> {
774    let tlv = conn.invoke_request2(endpoint, crate::clusters::defs::CLUSTER_ID_MEDIA_PLAYBACK, crate::clusters::defs::CLUSTER_MEDIA_PLAYBACK_CMD_ID_STARTOVER, &[]).await?;
775    decode_playback_response(&tlv)
776}
777
778/// Invoke `Previous` command on cluster `Media Playback`.
779pub async fn previous(conn: &crate::controller::Connection, endpoint: u16) -> anyhow::Result<PlaybackResponse> {
780    let tlv = conn.invoke_request2(endpoint, crate::clusters::defs::CLUSTER_ID_MEDIA_PLAYBACK, crate::clusters::defs::CLUSTER_MEDIA_PLAYBACK_CMD_ID_PREVIOUS, &[]).await?;
781    decode_playback_response(&tlv)
782}
783
784/// Invoke `Next` command on cluster `Media Playback`.
785pub async fn next(conn: &crate::controller::Connection, endpoint: u16) -> anyhow::Result<PlaybackResponse> {
786    let tlv = conn.invoke_request2(endpoint, crate::clusters::defs::CLUSTER_ID_MEDIA_PLAYBACK, crate::clusters::defs::CLUSTER_MEDIA_PLAYBACK_CMD_ID_NEXT, &[]).await?;
787    decode_playback_response(&tlv)
788}
789
790/// Invoke `Rewind` command on cluster `Media Playback`.
791pub async fn rewind(conn: &crate::controller::Connection, endpoint: u16, audio_advance_unmuted: bool) -> anyhow::Result<PlaybackResponse> {
792    let tlv = conn.invoke_request2(endpoint, crate::clusters::defs::CLUSTER_ID_MEDIA_PLAYBACK, crate::clusters::defs::CLUSTER_MEDIA_PLAYBACK_CMD_ID_REWIND, &encode_rewind(audio_advance_unmuted)?).await?;
793    decode_playback_response(&tlv)
794}
795
796/// Invoke `FastForward` command on cluster `Media Playback`.
797pub async fn fast_forward(conn: &crate::controller::Connection, endpoint: u16, audio_advance_unmuted: bool) -> anyhow::Result<PlaybackResponse> {
798    let tlv = conn.invoke_request2(endpoint, crate::clusters::defs::CLUSTER_ID_MEDIA_PLAYBACK, crate::clusters::defs::CLUSTER_MEDIA_PLAYBACK_CMD_ID_FASTFORWARD, &encode_fast_forward(audio_advance_unmuted)?).await?;
799    decode_playback_response(&tlv)
800}
801
802/// Invoke `SkipForward` command on cluster `Media Playback`.
803pub async fn skip_forward(conn: &crate::controller::Connection, endpoint: u16, delta_position_milliseconds: u64) -> anyhow::Result<PlaybackResponse> {
804    let tlv = conn.invoke_request2(endpoint, crate::clusters::defs::CLUSTER_ID_MEDIA_PLAYBACK, crate::clusters::defs::CLUSTER_MEDIA_PLAYBACK_CMD_ID_SKIPFORWARD, &encode_skip_forward(delta_position_milliseconds)?).await?;
805    decode_playback_response(&tlv)
806}
807
808/// Invoke `SkipBackward` command on cluster `Media Playback`.
809pub async fn skip_backward(conn: &crate::controller::Connection, endpoint: u16, delta_position_milliseconds: u64) -> anyhow::Result<PlaybackResponse> {
810    let tlv = conn.invoke_request2(endpoint, crate::clusters::defs::CLUSTER_ID_MEDIA_PLAYBACK, crate::clusters::defs::CLUSTER_MEDIA_PLAYBACK_CMD_ID_SKIPBACKWARD, &encode_skip_backward(delta_position_milliseconds)?).await?;
811    decode_playback_response(&tlv)
812}
813
814/// Invoke `Seek` command on cluster `Media Playback`.
815pub async fn seek(conn: &crate::controller::Connection, endpoint: u16, position: u64) -> anyhow::Result<PlaybackResponse> {
816    let tlv = conn.invoke_request2(endpoint, crate::clusters::defs::CLUSTER_ID_MEDIA_PLAYBACK, crate::clusters::defs::CLUSTER_MEDIA_PLAYBACK_CMD_ID_SEEK, &encode_seek(position)?).await?;
817    decode_playback_response(&tlv)
818}
819
820/// Invoke `ActivateAudioTrack` command on cluster `Media Playback`.
821pub async fn activate_audio_track(conn: &crate::controller::Connection, endpoint: u16, track_id: String, audio_output_index: Option<u8>) -> anyhow::Result<()> {
822    conn.invoke_request(endpoint, crate::clusters::defs::CLUSTER_ID_MEDIA_PLAYBACK, crate::clusters::defs::CLUSTER_MEDIA_PLAYBACK_CMD_ID_ACTIVATEAUDIOTRACK, &encode_activate_audio_track(track_id, audio_output_index)?).await?;
823    Ok(())
824}
825
826/// Invoke `ActivateTextTrack` command on cluster `Media Playback`.
827pub async fn activate_text_track(conn: &crate::controller::Connection, endpoint: u16, track_id: String) -> anyhow::Result<()> {
828    conn.invoke_request(endpoint, crate::clusters::defs::CLUSTER_ID_MEDIA_PLAYBACK, crate::clusters::defs::CLUSTER_MEDIA_PLAYBACK_CMD_ID_ACTIVATETEXTTRACK, &encode_activate_text_track(track_id)?).await?;
829    Ok(())
830}
831
832/// Invoke `DeactivateTextTrack` command on cluster `Media Playback`.
833pub async fn deactivate_text_track(conn: &crate::controller::Connection, endpoint: u16) -> anyhow::Result<()> {
834    conn.invoke_request(endpoint, crate::clusters::defs::CLUSTER_ID_MEDIA_PLAYBACK, crate::clusters::defs::CLUSTER_MEDIA_PLAYBACK_CMD_ID_DEACTIVATETEXTTRACK, &[]).await?;
835    Ok(())
836}
837
838/// Read `CurrentState` attribute from cluster `Media Playback`.
839pub async fn read_current_state(conn: &crate::controller::Connection, endpoint: u16) -> anyhow::Result<PlaybackState> {
840    let tlv = conn.read_request2(endpoint, crate::clusters::defs::CLUSTER_ID_MEDIA_PLAYBACK, crate::clusters::defs::CLUSTER_MEDIA_PLAYBACK_ATTR_ID_CURRENTSTATE).await?;
841    decode_current_state(&tlv)
842}
843
844/// Read `StartTime` attribute from cluster `Media Playback`.
845pub async fn read_start_time(conn: &crate::controller::Connection, endpoint: u16) -> anyhow::Result<Option<u64>> {
846    let tlv = conn.read_request2(endpoint, crate::clusters::defs::CLUSTER_ID_MEDIA_PLAYBACK, crate::clusters::defs::CLUSTER_MEDIA_PLAYBACK_ATTR_ID_STARTTIME).await?;
847    decode_start_time(&tlv)
848}
849
850/// Read `Duration` attribute from cluster `Media Playback`.
851pub async fn read_duration(conn: &crate::controller::Connection, endpoint: u16) -> anyhow::Result<Option<u64>> {
852    let tlv = conn.read_request2(endpoint, crate::clusters::defs::CLUSTER_ID_MEDIA_PLAYBACK, crate::clusters::defs::CLUSTER_MEDIA_PLAYBACK_ATTR_ID_DURATION).await?;
853    decode_duration(&tlv)
854}
855
856/// Read `SampledPosition` attribute from cluster `Media Playback`.
857pub async fn read_sampled_position(conn: &crate::controller::Connection, endpoint: u16) -> anyhow::Result<Option<PlaybackPosition>> {
858    let tlv = conn.read_request2(endpoint, crate::clusters::defs::CLUSTER_ID_MEDIA_PLAYBACK, crate::clusters::defs::CLUSTER_MEDIA_PLAYBACK_ATTR_ID_SAMPLEDPOSITION).await?;
859    decode_sampled_position(&tlv)
860}
861
862/// Read `PlaybackSpeed` attribute from cluster `Media Playback`.
863pub async fn read_playback_speed(conn: &crate::controller::Connection, endpoint: u16) -> anyhow::Result<u8> {
864    let tlv = conn.read_request2(endpoint, crate::clusters::defs::CLUSTER_ID_MEDIA_PLAYBACK, crate::clusters::defs::CLUSTER_MEDIA_PLAYBACK_ATTR_ID_PLAYBACKSPEED).await?;
865    decode_playback_speed(&tlv)
866}
867
868/// Read `SeekRangeEnd` attribute from cluster `Media Playback`.
869pub async fn read_seek_range_end(conn: &crate::controller::Connection, endpoint: u16) -> anyhow::Result<Option<u64>> {
870    let tlv = conn.read_request2(endpoint, crate::clusters::defs::CLUSTER_ID_MEDIA_PLAYBACK, crate::clusters::defs::CLUSTER_MEDIA_PLAYBACK_ATTR_ID_SEEKRANGEEND).await?;
871    decode_seek_range_end(&tlv)
872}
873
874/// Read `SeekRangeStart` attribute from cluster `Media Playback`.
875pub async fn read_seek_range_start(conn: &crate::controller::Connection, endpoint: u16) -> anyhow::Result<Option<u64>> {
876    let tlv = conn.read_request2(endpoint, crate::clusters::defs::CLUSTER_ID_MEDIA_PLAYBACK, crate::clusters::defs::CLUSTER_MEDIA_PLAYBACK_ATTR_ID_SEEKRANGESTART).await?;
877    decode_seek_range_start(&tlv)
878}
879
880/// Read `ActiveAudioTrack` attribute from cluster `Media Playback`.
881pub async fn read_active_audio_track(conn: &crate::controller::Connection, endpoint: u16) -> anyhow::Result<Option<Track>> {
882    let tlv = conn.read_request2(endpoint, crate::clusters::defs::CLUSTER_ID_MEDIA_PLAYBACK, crate::clusters::defs::CLUSTER_MEDIA_PLAYBACK_ATTR_ID_ACTIVEAUDIOTRACK).await?;
883    decode_active_audio_track(&tlv)
884}
885
886/// Read `AvailableAudioTracks` attribute from cluster `Media Playback`.
887pub async fn read_available_audio_tracks(conn: &crate::controller::Connection, endpoint: u16) -> anyhow::Result<Vec<Track>> {
888    let tlv = conn.read_request2(endpoint, crate::clusters::defs::CLUSTER_ID_MEDIA_PLAYBACK, crate::clusters::defs::CLUSTER_MEDIA_PLAYBACK_ATTR_ID_AVAILABLEAUDIOTRACKS).await?;
889    decode_available_audio_tracks(&tlv)
890}
891
892/// Read `ActiveTextTrack` attribute from cluster `Media Playback`.
893pub async fn read_active_text_track(conn: &crate::controller::Connection, endpoint: u16) -> anyhow::Result<Option<Track>> {
894    let tlv = conn.read_request2(endpoint, crate::clusters::defs::CLUSTER_ID_MEDIA_PLAYBACK, crate::clusters::defs::CLUSTER_MEDIA_PLAYBACK_ATTR_ID_ACTIVETEXTTRACK).await?;
895    decode_active_text_track(&tlv)
896}
897
898/// Read `AvailableTextTracks` attribute from cluster `Media Playback`.
899pub async fn read_available_text_tracks(conn: &crate::controller::Connection, endpoint: u16) -> anyhow::Result<Vec<Track>> {
900    let tlv = conn.read_request2(endpoint, crate::clusters::defs::CLUSTER_ID_MEDIA_PLAYBACK, crate::clusters::defs::CLUSTER_MEDIA_PLAYBACK_ATTR_ID_AVAILABLETEXTTRACKS).await?;
901    decode_available_text_tracks(&tlv)
902}
903
904#[derive(Debug, serde::Serialize)]
905pub struct StateChangedEvent {
906    pub current_state: Option<PlaybackState>,
907    pub start_time: Option<u64>,
908    pub duration: Option<u64>,
909    pub sampled_position: Option<PlaybackPosition>,
910    pub playback_speed: Option<u8>,
911    pub seek_range_end: Option<u64>,
912    pub seek_range_start: Option<u64>,
913    #[serde(serialize_with = "serialize_opt_bytes_as_hex")]
914    pub data: Option<Vec<u8>>,
915    pub audio_advance_unmuted: Option<bool>,
916}
917
918// Event decoders
919
920/// Decode StateChanged event (0x00, priority: info)
921pub fn decode_state_changed_event(inp: &tlv::TlvItemValue) -> anyhow::Result<StateChangedEvent> {
922    if let tlv::TlvItemValue::List(_fields) = inp {
923        let item = tlv::TlvItem { tag: 0, value: inp.clone() };
924        Ok(StateChangedEvent {
925                                current_state: item.get_int(&[0]).and_then(|v| PlaybackState::from_u8(v as u8)),
926                                start_time: item.get_int(&[1]),
927                                duration: item.get_int(&[2]),
928                                sampled_position: {
929                    if let Some(nested_tlv) = item.get(&[3]) {
930                        if let tlv::TlvItemValue::List(_) = nested_tlv {
931                            let nested_item = tlv::TlvItem { tag: 3, value: nested_tlv.clone() };
932                            Some(PlaybackPosition {
933                updated_at: nested_item.get_int(&[0]),
934                position: nested_item.get_int(&[1]),
935                            })
936                        } else {
937                            None
938                        }
939                    } else {
940                        None
941                    }
942                },
943                                playback_speed: item.get_int(&[4]).map(|v| v as u8),
944                                seek_range_end: item.get_int(&[5]),
945                                seek_range_start: item.get_int(&[6]),
946                                data: item.get_octet_string_owned(&[7]),
947                                audio_advance_unmuted: item.get_bool(&[8]),
948        })
949    } else {
950        Err(anyhow::anyhow!("Expected struct fields"))
951    }
952}
953