Skip to main content

matc/clusters/codec/
content_launcher.rs

1//! Matter TLV encoders and decoders for Content Launcher Cluster
2//! Cluster ID: 0x050A
3//!
4//! This file is automatically generated from ContentLauncher.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 MetricType {
18    /// Dimensions defined in a number of Pixels
19    Pixels = 0,
20    /// Dimensions defined as a percentage
21    Percentage = 1,
22}
23
24impl MetricType {
25    /// Convert from u8 value
26    pub fn from_u8(value: u8) -> Option<Self> {
27        match value {
28            0 => Some(MetricType::Pixels),
29            1 => Some(MetricType::Percentage),
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<MetricType> for u8 {
41    fn from(val: MetricType) -> Self {
42        val as u8
43    }
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
47#[repr(u8)]
48pub enum ParameterEnum {
49    /// Actor represents an actor credited in video media content; for example, “Gaby Hoffman”
50    Actor = 0,
51    /// Channel represents the identifying data for a television channel; for example, "PBS"
52    Channel = 1,
53    /// A character represented in video media content; for example, “Snow White”
54    Character = 2,
55    /// A director of the video media content; for example, “Spike Lee”
56    Director = 3,
57    /// An event is a reference to a type of event; examples would include sports, music, or other types of events. For example, searching for "Football games" would search for a 'game' event entity and a 'football' sport entity.
58    Event = 4,
59    /// A franchise is a video entity which can represent a number of video entities, like movies or TV shows. For example, take the fictional franchise "Intergalactic Wars" which represents a collection of movie trilogies, as well as animated and live action TV shows. This entity type was introduced to account for requests by customers such as "Find Intergalactic Wars movies", which would search for all 'Intergalactic Wars' programs of the MOVIE MediaType, rather than attempting to match to a single title.
60    Franchise = 5,
61    /// Genre represents the genre of video media content such as action, drama or comedy.
62    Genre = 6,
63    /// League represents the categorical information for a sporting league; for example, "NCAA"
64    League = 7,
65    /// Popularity indicates whether the user asks for popular content.
66    Popularity = 8,
67    /// The provider (MSP) the user wants this media to be played on; for example, "Netflix".
68    Provider = 9,
69    /// Sport represents the categorical information of a sport; for example, football
70    Sport = 10,
71    /// SportsTeam represents the categorical information of a professional sports team; for example, "University of Washington Huskies"
72    Sportsteam = 11,
73    /// The type of content requested. Supported types are "Movie", "MovieSeries", "TVSeries", "TVSeason", "TVEpisode", "Trailer", "SportsEvent", "LiveEvent", and "Video"
74    Type = 12,
75    /// Video represents the identifying data for a specific piece of video content; for example, "Manchester by the Sea".
76    Video = 13,
77    /// Season represents the specific season number within a TV series.
78    Season = 14,
79    /// Episode represents a specific episode number within a Season in a TV series.
80    Episode = 15,
81    /// Represents a search text input across many parameter types or even outside of the defined param types.
82    Any = 16,
83}
84
85impl ParameterEnum {
86    /// Convert from u8 value
87    pub fn from_u8(value: u8) -> Option<Self> {
88        match value {
89            0 => Some(ParameterEnum::Actor),
90            1 => Some(ParameterEnum::Channel),
91            2 => Some(ParameterEnum::Character),
92            3 => Some(ParameterEnum::Director),
93            4 => Some(ParameterEnum::Event),
94            5 => Some(ParameterEnum::Franchise),
95            6 => Some(ParameterEnum::Genre),
96            7 => Some(ParameterEnum::League),
97            8 => Some(ParameterEnum::Popularity),
98            9 => Some(ParameterEnum::Provider),
99            10 => Some(ParameterEnum::Sport),
100            11 => Some(ParameterEnum::Sportsteam),
101            12 => Some(ParameterEnum::Type),
102            13 => Some(ParameterEnum::Video),
103            14 => Some(ParameterEnum::Season),
104            15 => Some(ParameterEnum::Episode),
105            16 => Some(ParameterEnum::Any),
106            _ => None,
107        }
108    }
109
110    /// Convert to u8 value
111    pub fn to_u8(self) -> u8 {
112        self as u8
113    }
114}
115
116impl From<ParameterEnum> for u8 {
117    fn from(val: ParameterEnum) -> Self {
118        val as u8
119    }
120}
121
122#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
123#[repr(u8)]
124pub enum Status {
125    /// Command succeeded
126    Success = 0,
127    /// Requested URL could not be reached by device.
128    Urlnotavailable = 1,
129    /// Requested URL returned 401 error code.
130    Authfailed = 2,
131    /// Requested Text Track (in PlaybackPreferences) not available
132    Texttracknotavailable = 3,
133    /// Requested Audio Track (in PlaybackPreferences) not available
134    Audiotracknotavailable = 4,
135}
136
137impl Status {
138    /// Convert from u8 value
139    pub fn from_u8(value: u8) -> Option<Self> {
140        match value {
141            0 => Some(Status::Success),
142            1 => Some(Status::Urlnotavailable),
143            2 => Some(Status::Authfailed),
144            3 => Some(Status::Texttracknotavailable),
145            4 => Some(Status::Audiotracknotavailable),
146            _ => None,
147        }
148    }
149
150    /// Convert to u8 value
151    pub fn to_u8(self) -> u8 {
152        self as u8
153    }
154}
155
156impl From<Status> for u8 {
157    fn from(val: Status) -> Self {
158        val as u8
159    }
160}
161
162// Bitmap definitions
163
164/// SupportedProtocols bitmap type
165pub type SupportedProtocols = u8;
166
167/// Constants for SupportedProtocols
168pub mod supportedprotocols {
169    /// Device supports Dynamic Adaptive Streaming over HTTP (DASH)
170    pub const DASH: u8 = 0x01;
171    /// Device supports HTTP Live Streaming (HLS)
172    pub const HLS: u8 = 0x02;
173}
174
175// Struct definitions
176
177#[derive(Debug, serde::Serialize)]
178pub struct AdditionalInfo {
179    pub name: Option<String>,
180    pub value: Option<String>,
181}
182
183#[derive(Debug, serde::Serialize)]
184pub struct BrandingInformation {
185    pub provider_name: Option<String>,
186    pub background: Option<StyleInformation>,
187    pub logo: Option<StyleInformation>,
188    pub progress_bar: Option<StyleInformation>,
189    pub splash: Option<StyleInformation>,
190    pub water_mark: Option<StyleInformation>,
191}
192
193#[derive(Debug, serde::Serialize)]
194pub struct ContentSearch {
195    pub parameter_list: Option<Vec<Parameter>>,
196}
197
198#[derive(Debug, serde::Serialize)]
199pub struct Dimension {
200    pub width: Option<u8>,
201    pub height: Option<u8>,
202    pub metric: Option<MetricType>,
203}
204
205#[derive(Debug, serde::Serialize)]
206pub struct Parameter {
207    pub type_: Option<ParameterEnum>,
208    pub value: Option<String>,
209    pub external_id_list: Option<Vec<AdditionalInfo>>,
210}
211
212#[derive(Debug, serde::Serialize)]
213pub struct PlaybackPreferences {
214    pub playback_position: Option<u64>,
215    pub text_track: Option<TrackPreference>,
216    pub audio_tracks: Option<Vec<TrackPreference>>,
217}
218
219#[derive(Debug, serde::Serialize)]
220pub struct StyleInformation {
221    pub image_url: Option<String>,
222    pub color: Option<String>,
223    pub size: Option<Dimension>,
224}
225
226#[derive(Debug, serde::Serialize)]
227pub struct TrackPreference {
228    pub language_code: Option<String>,
229    pub characteristics: Option<Vec<u8>>,
230    pub audio_output_index: Option<u8>,
231}
232
233// Command encoders
234
235/// Encode LaunchContent command (0x00)
236pub fn encode_launch_content(search: ContentSearch, auto_play: bool, data: Option<String>, playback_preferences: Option<PlaybackPreferences>, use_current_context: Option<bool>) -> anyhow::Result<Vec<u8>> {
237            // Encode struct ContentSearchStruct
238            let mut search_fields = Vec::new();
239            if let Some(listv) = search.parameter_list {
240                let inner_vec: Vec<_> = listv.into_iter().map(|inner| {
241                    let mut nested_fields = Vec::new();
242                        if let Some(x) = inner.type_ { nested_fields.push((0, tlv::TlvItemValueEnc::UInt8(x.to_u8())).into()); }
243                        if let Some(x) = inner.value { nested_fields.push((1, tlv::TlvItemValueEnc::String(x.clone())).into()); }
244                        if let Some(listv) = inner.external_id_list {
245                            let inner_vec: Vec<_> = listv.into_iter().map(|inner| {
246                                let mut nested_fields = Vec::new();
247                                    if let Some(x) = inner.name { nested_fields.push((0, tlv::TlvItemValueEnc::String(x.clone())).into()); }
248                                    if let Some(x) = inner.value { nested_fields.push((1, tlv::TlvItemValueEnc::String(x.clone())).into()); }
249                                (0, tlv::TlvItemValueEnc::StructAnon(nested_fields)).into()
250                            }).collect();
251                            nested_fields.push((2, tlv::TlvItemValueEnc::Array(inner_vec)).into());
252                        }
253                    (0, tlv::TlvItemValueEnc::StructAnon(nested_fields)).into()
254                }).collect();
255                search_fields.push((0, tlv::TlvItemValueEnc::Array(inner_vec)).into());
256            }
257    let mut tlv_fields: Vec<tlv::TlvItemEnc> = Vec::new();
258    tlv_fields.push((0, tlv::TlvItemValueEnc::StructInvisible(search_fields)).into());
259    tlv_fields.push((1, tlv::TlvItemValueEnc::Bool(auto_play)).into());
260    if let Some(x) = data { tlv_fields.push((2, tlv::TlvItemValueEnc::String(x)).into()); }
261    if let Some(playback_preferences) = playback_preferences {
262        // Encode struct PlaybackPreferencesStruct
263        let mut playback_preferences_fields = Vec::new();
264        if let Some(x) = playback_preferences.playback_position { playback_preferences_fields.push((0, tlv::TlvItemValueEnc::UInt64(x)).into()); }
265        if let Some(inner) = playback_preferences.text_track {
266        let mut text_track_nested_fields = Vec::new();
267        if let Some(x) = inner.language_code { text_track_nested_fields.push((0, tlv::TlvItemValueEnc::String(x.clone())).into()); }
268        if let Some(listv) = inner.characteristics { text_track_nested_fields.push((1, tlv::TlvItemValueEnc::StructAnon(listv.into_iter().map(|x| (0, tlv::TlvItemValueEnc::UInt8(x)).into()).collect())).into()); }
269        if let Some(x) = inner.audio_output_index { text_track_nested_fields.push((2, tlv::TlvItemValueEnc::UInt8(x)).into()); }
270        playback_preferences_fields.push((1, tlv::TlvItemValueEnc::StructInvisible(text_track_nested_fields)).into());
271        }
272        if let Some(listv) = playback_preferences.audio_tracks {
273        let inner_vec: Vec<_> = listv.into_iter().map(|inner| {
274        let mut nested_fields = Vec::new();
275        if let Some(x) = inner.language_code { nested_fields.push((0, tlv::TlvItemValueEnc::String(x.clone())).into()); }
276        if let Some(listv) = inner.characteristics { nested_fields.push((1, tlv::TlvItemValueEnc::StructAnon(listv.into_iter().map(|x| (0, tlv::TlvItemValueEnc::UInt8(x)).into()).collect())).into()); }
277        if let Some(x) = inner.audio_output_index { nested_fields.push((2, tlv::TlvItemValueEnc::UInt8(x)).into()); }
278        (0, tlv::TlvItemValueEnc::StructAnon(nested_fields)).into()
279        }).collect();
280        playback_preferences_fields.push((2, tlv::TlvItemValueEnc::Array(inner_vec)).into());
281        }
282        tlv_fields.push((3, tlv::TlvItemValueEnc::StructInvisible(playback_preferences_fields)).into());
283    }
284    if let Some(x) = use_current_context { tlv_fields.push((4, tlv::TlvItemValueEnc::Bool(x)).into()); }
285    let tlv = tlv::TlvItemEnc {
286        tag: 0,
287        value: tlv::TlvItemValueEnc::StructInvisible(tlv_fields),
288    };
289    Ok(tlv.encode()?)
290}
291
292/// Encode LaunchURL command (0x01)
293pub fn encode_launch_url(content_url: String, display_string: Option<String>, branding_information: Option<BrandingInformation>, playback_preferences: Option<PlaybackPreferences>) -> anyhow::Result<Vec<u8>> {
294    let mut tlv_fields: Vec<tlv::TlvItemEnc> = Vec::new();
295    tlv_fields.push((0, tlv::TlvItemValueEnc::String(content_url)).into());
296    if let Some(x) = display_string { tlv_fields.push((1, tlv::TlvItemValueEnc::String(x)).into()); }
297    if let Some(branding_information) = branding_information {
298        // Encode struct BrandingInformationStruct
299        let mut branding_information_fields = Vec::new();
300        if let Some(x) = branding_information.provider_name { branding_information_fields.push((0, tlv::TlvItemValueEnc::String(x.clone())).into()); }
301        if let Some(inner) = branding_information.background {
302        let mut background_nested_fields = Vec::new();
303        if let Some(x) = inner.image_url { background_nested_fields.push((0, tlv::TlvItemValueEnc::String(x.clone())).into()); }
304        if let Some(x) = inner.color { background_nested_fields.push((1, tlv::TlvItemValueEnc::String(x.clone())).into()); }
305        if let Some(inner) = inner.size {
306        let mut size_nested_fields = Vec::new();
307        // TODO: encoding for field width (double) not implemented
308        // TODO: encoding for field height (double) not implemented
309        if let Some(x) = inner.metric { size_nested_fields.push((2, tlv::TlvItemValueEnc::UInt8(x.to_u8())).into()); }
310        background_nested_fields.push((2, tlv::TlvItemValueEnc::StructInvisible(size_nested_fields)).into());
311        }
312        branding_information_fields.push((1, tlv::TlvItemValueEnc::StructInvisible(background_nested_fields)).into());
313        }
314        if let Some(inner) = branding_information.logo {
315        let mut logo_nested_fields = Vec::new();
316        if let Some(x) = inner.image_url { logo_nested_fields.push((0, tlv::TlvItemValueEnc::String(x.clone())).into()); }
317        if let Some(x) = inner.color { logo_nested_fields.push((1, tlv::TlvItemValueEnc::String(x.clone())).into()); }
318        if let Some(inner) = inner.size {
319        let mut size_nested_fields = Vec::new();
320        // TODO: encoding for field width (double) not implemented
321        // TODO: encoding for field height (double) not implemented
322        if let Some(x) = inner.metric { size_nested_fields.push((2, tlv::TlvItemValueEnc::UInt8(x.to_u8())).into()); }
323        logo_nested_fields.push((2, tlv::TlvItemValueEnc::StructInvisible(size_nested_fields)).into());
324        }
325        branding_information_fields.push((2, tlv::TlvItemValueEnc::StructInvisible(logo_nested_fields)).into());
326        }
327        if let Some(inner) = branding_information.progress_bar {
328        let mut progress_bar_nested_fields = Vec::new();
329        if let Some(x) = inner.image_url { progress_bar_nested_fields.push((0, tlv::TlvItemValueEnc::String(x.clone())).into()); }
330        if let Some(x) = inner.color { progress_bar_nested_fields.push((1, tlv::TlvItemValueEnc::String(x.clone())).into()); }
331        if let Some(inner) = inner.size {
332        let mut size_nested_fields = Vec::new();
333        // TODO: encoding for field width (double) not implemented
334        // TODO: encoding for field height (double) not implemented
335        if let Some(x) = inner.metric { size_nested_fields.push((2, tlv::TlvItemValueEnc::UInt8(x.to_u8())).into()); }
336        progress_bar_nested_fields.push((2, tlv::TlvItemValueEnc::StructInvisible(size_nested_fields)).into());
337        }
338        branding_information_fields.push((3, tlv::TlvItemValueEnc::StructInvisible(progress_bar_nested_fields)).into());
339        }
340        if let Some(inner) = branding_information.splash {
341        let mut splash_nested_fields = Vec::new();
342        if let Some(x) = inner.image_url { splash_nested_fields.push((0, tlv::TlvItemValueEnc::String(x.clone())).into()); }
343        if let Some(x) = inner.color { splash_nested_fields.push((1, tlv::TlvItemValueEnc::String(x.clone())).into()); }
344        if let Some(inner) = inner.size {
345        let mut size_nested_fields = Vec::new();
346        // TODO: encoding for field width (double) not implemented
347        // TODO: encoding for field height (double) not implemented
348        if let Some(x) = inner.metric { size_nested_fields.push((2, tlv::TlvItemValueEnc::UInt8(x.to_u8())).into()); }
349        splash_nested_fields.push((2, tlv::TlvItemValueEnc::StructInvisible(size_nested_fields)).into());
350        }
351        branding_information_fields.push((4, tlv::TlvItemValueEnc::StructInvisible(splash_nested_fields)).into());
352        }
353        if let Some(inner) = branding_information.water_mark {
354        let mut water_mark_nested_fields = Vec::new();
355        if let Some(x) = inner.image_url { water_mark_nested_fields.push((0, tlv::TlvItemValueEnc::String(x.clone())).into()); }
356        if let Some(x) = inner.color { water_mark_nested_fields.push((1, tlv::TlvItemValueEnc::String(x.clone())).into()); }
357        if let Some(inner) = inner.size {
358        let mut size_nested_fields = Vec::new();
359        // TODO: encoding for field width (double) not implemented
360        // TODO: encoding for field height (double) not implemented
361        if let Some(x) = inner.metric { size_nested_fields.push((2, tlv::TlvItemValueEnc::UInt8(x.to_u8())).into()); }
362        water_mark_nested_fields.push((2, tlv::TlvItemValueEnc::StructInvisible(size_nested_fields)).into());
363        }
364        branding_information_fields.push((5, tlv::TlvItemValueEnc::StructInvisible(water_mark_nested_fields)).into());
365        }
366        tlv_fields.push((2, tlv::TlvItemValueEnc::StructInvisible(branding_information_fields)).into());
367    }
368    if let Some(playback_preferences) = playback_preferences {
369        // Encode struct PlaybackPreferencesStruct
370        let mut playback_preferences_fields = Vec::new();
371        if let Some(x) = playback_preferences.playback_position { playback_preferences_fields.push((0, tlv::TlvItemValueEnc::UInt64(x)).into()); }
372        if let Some(inner) = playback_preferences.text_track {
373        let mut text_track_nested_fields = Vec::new();
374        if let Some(x) = inner.language_code { text_track_nested_fields.push((0, tlv::TlvItemValueEnc::String(x.clone())).into()); }
375        if let Some(listv) = inner.characteristics { text_track_nested_fields.push((1, tlv::TlvItemValueEnc::StructAnon(listv.into_iter().map(|x| (0, tlv::TlvItemValueEnc::UInt8(x)).into()).collect())).into()); }
376        if let Some(x) = inner.audio_output_index { text_track_nested_fields.push((2, tlv::TlvItemValueEnc::UInt8(x)).into()); }
377        playback_preferences_fields.push((1, tlv::TlvItemValueEnc::StructInvisible(text_track_nested_fields)).into());
378        }
379        if let Some(listv) = playback_preferences.audio_tracks {
380        let inner_vec: Vec<_> = listv.into_iter().map(|inner| {
381        let mut nested_fields = Vec::new();
382        if let Some(x) = inner.language_code { nested_fields.push((0, tlv::TlvItemValueEnc::String(x.clone())).into()); }
383        if let Some(listv) = inner.characteristics { nested_fields.push((1, tlv::TlvItemValueEnc::StructAnon(listv.into_iter().map(|x| (0, tlv::TlvItemValueEnc::UInt8(x)).into()).collect())).into()); }
384        if let Some(x) = inner.audio_output_index { nested_fields.push((2, tlv::TlvItemValueEnc::UInt8(x)).into()); }
385        (0, tlv::TlvItemValueEnc::StructAnon(nested_fields)).into()
386        }).collect();
387        playback_preferences_fields.push((2, tlv::TlvItemValueEnc::Array(inner_vec)).into());
388        }
389        tlv_fields.push((3, tlv::TlvItemValueEnc::StructInvisible(playback_preferences_fields)).into());
390    }
391    let tlv = tlv::TlvItemEnc {
392        tag: 0,
393        value: tlv::TlvItemValueEnc::StructInvisible(tlv_fields),
394    };
395    Ok(tlv.encode()?)
396}
397
398// Attribute decoders
399
400/// Decode AcceptHeader attribute (0x0000)
401pub fn decode_accept_header(inp: &tlv::TlvItemValue) -> anyhow::Result<Vec<String>> {
402    let mut res = Vec::new();
403    if let tlv::TlvItemValue::List(v) = inp {
404        for item in v {
405            if let tlv::TlvItemValue::String(s) = &item.value {
406                res.push(s.clone());
407            }
408        }
409    }
410    Ok(res)
411}
412
413/// Decode SupportedStreamingProtocols attribute (0x0001)
414pub fn decode_supported_streaming_protocols(inp: &tlv::TlvItemValue) -> anyhow::Result<SupportedProtocols> {
415    if let tlv::TlvItemValue::Int(v) = inp {
416        Ok(*v as u8)
417    } else {
418        Err(anyhow::anyhow!("Expected Integer"))
419    }
420}
421
422
423// JSON dispatcher function
424
425/// Decode attribute value and return as JSON string
426///
427/// # Parameters
428/// * `cluster_id` - The cluster identifier
429/// * `attribute_id` - The attribute identifier
430/// * `tlv_value` - The TLV value to decode
431///
432/// # Returns
433/// JSON string representation of the decoded value or error
434pub fn decode_attribute_json(cluster_id: u32, attribute_id: u32, tlv_value: &crate::tlv::TlvItemValue) -> String {
435    // Verify this is the correct cluster
436    if cluster_id != 0x050A {
437        return format!("{{\"error\": \"Invalid cluster ID. Expected 0x050A, got {}\"}}", cluster_id);
438    }
439
440    match attribute_id {
441        0x0000 => {
442            match decode_accept_header(tlv_value) {
443                Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
444                Err(e) => format!("{{\"error\": \"{}\"}}", e),
445            }
446        }
447        0x0001 => {
448            match decode_supported_streaming_protocols(tlv_value) {
449                Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
450                Err(e) => format!("{{\"error\": \"{}\"}}", e),
451            }
452        }
453        _ => format!("{{\"error\": \"Unknown attribute ID: {}\"}}", attribute_id),
454    }
455}
456
457/// Get list of all attributes supported by this cluster
458///
459/// # Returns
460/// Vector of tuples containing (attribute_id, attribute_name)
461pub fn get_attribute_list() -> Vec<(u32, &'static str)> {
462    vec![
463        (0x0000, "AcceptHeader"),
464        (0x0001, "SupportedStreamingProtocols"),
465    ]
466}
467
468// Command listing
469
470pub fn get_command_list() -> Vec<(u32, &'static str)> {
471    vec![
472        (0x00, "LaunchContent"),
473        (0x01, "LaunchURL"),
474    ]
475}
476
477pub fn get_command_name(cmd_id: u32) -> Option<&'static str> {
478    match cmd_id {
479        0x00 => Some("LaunchContent"),
480        0x01 => Some("LaunchURL"),
481        _ => None,
482    }
483}
484
485pub fn get_command_schema(cmd_id: u32) -> Option<Vec<crate::clusters::codec::CommandField>> {
486    match cmd_id {
487        0x00 => Some(vec![
488            crate::clusters::codec::CommandField { tag: 0, name: "search", kind: crate::clusters::codec::FieldKind::Struct { name: "ContentSearchStruct" }, optional: false, nullable: false },
489            crate::clusters::codec::CommandField { tag: 1, name: "auto_play", kind: crate::clusters::codec::FieldKind::Bool, optional: false, nullable: false },
490            crate::clusters::codec::CommandField { tag: 2, name: "data", kind: crate::clusters::codec::FieldKind::String, optional: true, nullable: false },
491            crate::clusters::codec::CommandField { tag: 3, name: "playback_preferences", kind: crate::clusters::codec::FieldKind::Struct { name: "PlaybackPreferencesStruct" }, optional: true, nullable: false },
492            crate::clusters::codec::CommandField { tag: 4, name: "use_current_context", kind: crate::clusters::codec::FieldKind::Bool, optional: true, nullable: false },
493        ]),
494        0x01 => Some(vec![
495            crate::clusters::codec::CommandField { tag: 0, name: "content_url", kind: crate::clusters::codec::FieldKind::String, optional: false, nullable: false },
496            crate::clusters::codec::CommandField { tag: 1, name: "display_string", kind: crate::clusters::codec::FieldKind::String, optional: true, nullable: false },
497            crate::clusters::codec::CommandField { tag: 2, name: "branding_information", kind: crate::clusters::codec::FieldKind::Struct { name: "BrandingInformationStruct" }, optional: true, nullable: false },
498            crate::clusters::codec::CommandField { tag: 3, name: "playback_preferences", kind: crate::clusters::codec::FieldKind::Struct { name: "PlaybackPreferencesStruct" }, optional: true, nullable: false },
499        ]),
500        _ => None,
501    }
502}
503
504pub fn encode_command_json(cmd_id: u32, _args: &serde_json::Value) -> anyhow::Result<Vec<u8>> {
505    match cmd_id {
506        0x00 => Err(anyhow::anyhow!("command \"LaunchContent\" has complex args: use raw mode")),
507        0x01 => Err(anyhow::anyhow!("command \"LaunchURL\" has complex args: use raw mode")),
508        _ => Err(anyhow::anyhow!("unknown command ID: 0x{:02X}", cmd_id)),
509    }
510}
511
512#[derive(Debug, serde::Serialize)]
513pub struct LauncherResponse {
514    pub status: Option<Status>,
515    pub data: Option<String>,
516}
517
518// Command response decoders
519
520/// Decode LauncherResponse command response (02)
521pub fn decode_launcher_response(inp: &tlv::TlvItemValue) -> anyhow::Result<LauncherResponse> {
522    if let tlv::TlvItemValue::List(_fields) = inp {
523        let item = tlv::TlvItem { tag: 0, value: inp.clone() };
524        Ok(LauncherResponse {
525                status: item.get_int(&[0]).and_then(|v| Status::from_u8(v as u8)),
526                data: item.get_string_owned(&[1]),
527        })
528    } else {
529        Err(anyhow::anyhow!("Expected struct fields"))
530    }
531}
532
533// Typed facade (invokes + reads)
534
535/// Invoke `LaunchContent` command on cluster `Content Launcher`.
536pub async fn launch_content(conn: &crate::controller::Connection, endpoint: u16, search: ContentSearch, auto_play: bool, data: Option<String>, playback_preferences: Option<PlaybackPreferences>, use_current_context: Option<bool>) -> anyhow::Result<LauncherResponse> {
537    let tlv = conn.invoke_request2(endpoint, crate::clusters::defs::CLUSTER_ID_CONTENT_LAUNCHER, crate::clusters::defs::CLUSTER_CONTENT_LAUNCHER_CMD_ID_LAUNCHCONTENT, &encode_launch_content(search, auto_play, data, playback_preferences, use_current_context)?).await?;
538    decode_launcher_response(&tlv)
539}
540
541/// Invoke `LaunchURL` command on cluster `Content Launcher`.
542pub async fn launch_url(conn: &crate::controller::Connection, endpoint: u16, content_url: String, display_string: Option<String>, branding_information: Option<BrandingInformation>, playback_preferences: Option<PlaybackPreferences>) -> anyhow::Result<LauncherResponse> {
543    let tlv = conn.invoke_request2(endpoint, crate::clusters::defs::CLUSTER_ID_CONTENT_LAUNCHER, crate::clusters::defs::CLUSTER_CONTENT_LAUNCHER_CMD_ID_LAUNCHURL, &encode_launch_url(content_url, display_string, branding_information, playback_preferences)?).await?;
544    decode_launcher_response(&tlv)
545}
546
547/// Read `AcceptHeader` attribute from cluster `Content Launcher`.
548pub async fn read_accept_header(conn: &crate::controller::Connection, endpoint: u16) -> anyhow::Result<Vec<String>> {
549    let tlv = conn.read_request2(endpoint, crate::clusters::defs::CLUSTER_ID_CONTENT_LAUNCHER, crate::clusters::defs::CLUSTER_CONTENT_LAUNCHER_ATTR_ID_ACCEPTHEADER).await?;
550    decode_accept_header(&tlv)
551}
552
553/// Read `SupportedStreamingProtocols` attribute from cluster `Content Launcher`.
554pub async fn read_supported_streaming_protocols(conn: &crate::controller::Connection, endpoint: u16) -> anyhow::Result<SupportedProtocols> {
555    let tlv = conn.read_request2(endpoint, crate::clusters::defs::CLUSTER_ID_CONTENT_LAUNCHER, crate::clusters::defs::CLUSTER_CONTENT_LAUNCHER_ATTR_ID_SUPPORTEDSTREAMINGPROTOCOLS).await?;
556    decode_supported_streaming_protocols(&tlv)
557}
558