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: String, playback_preferences: PlaybackPreferences, use_current_context: 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            // Encode struct PlaybackPreferencesStruct
258            let mut playback_preferences_fields = Vec::new();
259            if let Some(x) = playback_preferences.playback_position { playback_preferences_fields.push((0, tlv::TlvItemValueEnc::UInt64(x)).into()); }
260            if let Some(inner) = playback_preferences.text_track {
261                let mut text_track_nested_fields = Vec::new();
262                if let Some(x) = inner.language_code { text_track_nested_fields.push((0, tlv::TlvItemValueEnc::String(x.clone())).into()); }
263                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()); }
264                if let Some(x) = inner.audio_output_index { text_track_nested_fields.push((2, tlv::TlvItemValueEnc::UInt8(x)).into()); }
265                playback_preferences_fields.push((1, tlv::TlvItemValueEnc::StructInvisible(text_track_nested_fields)).into());
266            }
267            if let Some(listv) = playback_preferences.audio_tracks {
268                let inner_vec: Vec<_> = listv.into_iter().map(|inner| {
269                    let mut nested_fields = Vec::new();
270                        if let Some(x) = inner.language_code { nested_fields.push((0, tlv::TlvItemValueEnc::String(x.clone())).into()); }
271                        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()); }
272                        if let Some(x) = inner.audio_output_index { nested_fields.push((2, tlv::TlvItemValueEnc::UInt8(x)).into()); }
273                    (0, tlv::TlvItemValueEnc::StructAnon(nested_fields)).into()
274                }).collect();
275                playback_preferences_fields.push((2, tlv::TlvItemValueEnc::Array(inner_vec)).into());
276            }
277    let tlv = tlv::TlvItemEnc {
278        tag: 0,
279        value: tlv::TlvItemValueEnc::StructInvisible(vec![
280        (0, tlv::TlvItemValueEnc::StructInvisible(search_fields)).into(),
281        (1, tlv::TlvItemValueEnc::Bool(auto_play)).into(),
282        (2, tlv::TlvItemValueEnc::String(data)).into(),
283        (3, tlv::TlvItemValueEnc::StructInvisible(playback_preferences_fields)).into(),
284        (4, tlv::TlvItemValueEnc::Bool(use_current_context)).into(),
285        ]),
286    };
287    Ok(tlv.encode()?)
288}
289
290/// Encode LaunchURL command (0x01)
291pub fn encode_launch_url(content_url: String, display_string: String, branding_information: BrandingInformation, playback_preferences: PlaybackPreferences) -> anyhow::Result<Vec<u8>> {
292            // Encode struct BrandingInformationStruct
293            let mut branding_information_fields = Vec::new();
294            if let Some(x) = branding_information.provider_name { branding_information_fields.push((0, tlv::TlvItemValueEnc::String(x.clone())).into()); }
295            if let Some(inner) = branding_information.background {
296                let mut background_nested_fields = Vec::new();
297                if let Some(x) = inner.image_url { background_nested_fields.push((0, tlv::TlvItemValueEnc::String(x.clone())).into()); }
298                if let Some(x) = inner.color { background_nested_fields.push((1, tlv::TlvItemValueEnc::String(x.clone())).into()); }
299                if let Some(inner) = inner.size {
300                    let mut size_nested_fields = Vec::new();
301                    // TODO: encoding for field width (double) not implemented
302                    // TODO: encoding for field height (double) not implemented
303                    if let Some(x) = inner.metric { size_nested_fields.push((2, tlv::TlvItemValueEnc::UInt8(x.to_u8())).into()); }
304                    background_nested_fields.push((2, tlv::TlvItemValueEnc::StructInvisible(size_nested_fields)).into());
305                }
306                branding_information_fields.push((1, tlv::TlvItemValueEnc::StructInvisible(background_nested_fields)).into());
307            }
308            if let Some(inner) = branding_information.logo {
309                let mut logo_nested_fields = Vec::new();
310                if let Some(x) = inner.image_url { logo_nested_fields.push((0, tlv::TlvItemValueEnc::String(x.clone())).into()); }
311                if let Some(x) = inner.color { logo_nested_fields.push((1, tlv::TlvItemValueEnc::String(x.clone())).into()); }
312                if let Some(inner) = inner.size {
313                    let mut size_nested_fields = Vec::new();
314                    // TODO: encoding for field width (double) not implemented
315                    // TODO: encoding for field height (double) not implemented
316                    if let Some(x) = inner.metric { size_nested_fields.push((2, tlv::TlvItemValueEnc::UInt8(x.to_u8())).into()); }
317                    logo_nested_fields.push((2, tlv::TlvItemValueEnc::StructInvisible(size_nested_fields)).into());
318                }
319                branding_information_fields.push((2, tlv::TlvItemValueEnc::StructInvisible(logo_nested_fields)).into());
320            }
321            if let Some(inner) = branding_information.progress_bar {
322                let mut progress_bar_nested_fields = Vec::new();
323                if let Some(x) = inner.image_url { progress_bar_nested_fields.push((0, tlv::TlvItemValueEnc::String(x.clone())).into()); }
324                if let Some(x) = inner.color { progress_bar_nested_fields.push((1, tlv::TlvItemValueEnc::String(x.clone())).into()); }
325                if let Some(inner) = inner.size {
326                    let mut size_nested_fields = Vec::new();
327                    // TODO: encoding for field width (double) not implemented
328                    // TODO: encoding for field height (double) not implemented
329                    if let Some(x) = inner.metric { size_nested_fields.push((2, tlv::TlvItemValueEnc::UInt8(x.to_u8())).into()); }
330                    progress_bar_nested_fields.push((2, tlv::TlvItemValueEnc::StructInvisible(size_nested_fields)).into());
331                }
332                branding_information_fields.push((3, tlv::TlvItemValueEnc::StructInvisible(progress_bar_nested_fields)).into());
333            }
334            if let Some(inner) = branding_information.splash {
335                let mut splash_nested_fields = Vec::new();
336                if let Some(x) = inner.image_url { splash_nested_fields.push((0, tlv::TlvItemValueEnc::String(x.clone())).into()); }
337                if let Some(x) = inner.color { splash_nested_fields.push((1, tlv::TlvItemValueEnc::String(x.clone())).into()); }
338                if let Some(inner) = inner.size {
339                    let mut size_nested_fields = Vec::new();
340                    // TODO: encoding for field width (double) not implemented
341                    // TODO: encoding for field height (double) not implemented
342                    if let Some(x) = inner.metric { size_nested_fields.push((2, tlv::TlvItemValueEnc::UInt8(x.to_u8())).into()); }
343                    splash_nested_fields.push((2, tlv::TlvItemValueEnc::StructInvisible(size_nested_fields)).into());
344                }
345                branding_information_fields.push((4, tlv::TlvItemValueEnc::StructInvisible(splash_nested_fields)).into());
346            }
347            if let Some(inner) = branding_information.water_mark {
348                let mut water_mark_nested_fields = Vec::new();
349                if let Some(x) = inner.image_url { water_mark_nested_fields.push((0, tlv::TlvItemValueEnc::String(x.clone())).into()); }
350                if let Some(x) = inner.color { water_mark_nested_fields.push((1, tlv::TlvItemValueEnc::String(x.clone())).into()); }
351                if let Some(inner) = inner.size {
352                    let mut size_nested_fields = Vec::new();
353                    // TODO: encoding for field width (double) not implemented
354                    // TODO: encoding for field height (double) not implemented
355                    if let Some(x) = inner.metric { size_nested_fields.push((2, tlv::TlvItemValueEnc::UInt8(x.to_u8())).into()); }
356                    water_mark_nested_fields.push((2, tlv::TlvItemValueEnc::StructInvisible(size_nested_fields)).into());
357                }
358                branding_information_fields.push((5, tlv::TlvItemValueEnc::StructInvisible(water_mark_nested_fields)).into());
359            }
360            // Encode struct PlaybackPreferencesStruct
361            let mut playback_preferences_fields = Vec::new();
362            if let Some(x) = playback_preferences.playback_position { playback_preferences_fields.push((0, tlv::TlvItemValueEnc::UInt64(x)).into()); }
363            if let Some(inner) = playback_preferences.text_track {
364                let mut text_track_nested_fields = Vec::new();
365                if let Some(x) = inner.language_code { text_track_nested_fields.push((0, tlv::TlvItemValueEnc::String(x.clone())).into()); }
366                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()); }
367                if let Some(x) = inner.audio_output_index { text_track_nested_fields.push((2, tlv::TlvItemValueEnc::UInt8(x)).into()); }
368                playback_preferences_fields.push((1, tlv::TlvItemValueEnc::StructInvisible(text_track_nested_fields)).into());
369            }
370            if let Some(listv) = playback_preferences.audio_tracks {
371                let inner_vec: Vec<_> = listv.into_iter().map(|inner| {
372                    let mut nested_fields = Vec::new();
373                        if let Some(x) = inner.language_code { nested_fields.push((0, tlv::TlvItemValueEnc::String(x.clone())).into()); }
374                        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()); }
375                        if let Some(x) = inner.audio_output_index { nested_fields.push((2, tlv::TlvItemValueEnc::UInt8(x)).into()); }
376                    (0, tlv::TlvItemValueEnc::StructAnon(nested_fields)).into()
377                }).collect();
378                playback_preferences_fields.push((2, tlv::TlvItemValueEnc::Array(inner_vec)).into());
379            }
380    let tlv = tlv::TlvItemEnc {
381        tag: 0,
382        value: tlv::TlvItemValueEnc::StructInvisible(vec![
383        (0, tlv::TlvItemValueEnc::String(content_url)).into(),
384        (1, tlv::TlvItemValueEnc::String(display_string)).into(),
385        (2, tlv::TlvItemValueEnc::StructInvisible(branding_information_fields)).into(),
386        (3, tlv::TlvItemValueEnc::StructInvisible(playback_preferences_fields)).into(),
387        ]),
388    };
389    Ok(tlv.encode()?)
390}
391
392// Attribute decoders
393
394/// Decode AcceptHeader attribute (0x0000)
395pub fn decode_accept_header(inp: &tlv::TlvItemValue) -> anyhow::Result<Vec<String>> {
396    let mut res = Vec::new();
397    if let tlv::TlvItemValue::List(v) = inp {
398        for item in v {
399            if let tlv::TlvItemValue::String(s) = &item.value {
400                res.push(s.clone());
401            }
402        }
403    }
404    Ok(res)
405}
406
407/// Decode SupportedStreamingProtocols attribute (0x0001)
408pub fn decode_supported_streaming_protocols(inp: &tlv::TlvItemValue) -> anyhow::Result<SupportedProtocols> {
409    if let tlv::TlvItemValue::Int(v) = inp {
410        Ok(*v as u8)
411    } else {
412        Err(anyhow::anyhow!("Expected Integer"))
413    }
414}
415
416
417// JSON dispatcher function
418
419/// Decode attribute value and return as JSON string
420///
421/// # Parameters
422/// * `cluster_id` - The cluster identifier
423/// * `attribute_id` - The attribute identifier
424/// * `tlv_value` - The TLV value to decode
425///
426/// # Returns
427/// JSON string representation of the decoded value or error
428pub fn decode_attribute_json(cluster_id: u32, attribute_id: u32, tlv_value: &crate::tlv::TlvItemValue) -> String {
429    // Verify this is the correct cluster
430    if cluster_id != 0x050A {
431        return format!("{{\"error\": \"Invalid cluster ID. Expected 0x050A, got {}\"}}", cluster_id);
432    }
433
434    match attribute_id {
435        0x0000 => {
436            match decode_accept_header(tlv_value) {
437                Ok(value) => serde_json::to_string(&value).unwrap_or_else(|_| "null".to_string()),
438                Err(e) => format!("{{\"error\": \"{}\"}}", e),
439            }
440        }
441        0x0001 => {
442            match decode_supported_streaming_protocols(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        _ => format!("{{\"error\": \"Unknown attribute ID: {}\"}}", attribute_id),
448    }
449}
450
451/// Get list of all attributes supported by this cluster
452///
453/// # Returns
454/// Vector of tuples containing (attribute_id, attribute_name)
455pub fn get_attribute_list() -> Vec<(u32, &'static str)> {
456    vec![
457        (0x0000, "AcceptHeader"),
458        (0x0001, "SupportedStreamingProtocols"),
459    ]
460}
461
462// Command listing
463
464pub fn get_command_list() -> Vec<(u32, &'static str)> {
465    vec![
466        (0x00, "LaunchContent"),
467        (0x01, "LaunchURL"),
468    ]
469}
470
471pub fn get_command_name(cmd_id: u32) -> Option<&'static str> {
472    match cmd_id {
473        0x00 => Some("LaunchContent"),
474        0x01 => Some("LaunchURL"),
475        _ => None,
476    }
477}
478
479pub fn get_command_schema(cmd_id: u32) -> Option<Vec<crate::clusters::codec::CommandField>> {
480    match cmd_id {
481        0x00 => Some(vec![
482            crate::clusters::codec::CommandField { tag: 0, name: "search", kind: crate::clusters::codec::FieldKind::Struct { name: "ContentSearchStruct" }, optional: false, nullable: false },
483            crate::clusters::codec::CommandField { tag: 1, name: "auto_play", kind: crate::clusters::codec::FieldKind::Bool, optional: false, nullable: false },
484            crate::clusters::codec::CommandField { tag: 2, name: "data", kind: crate::clusters::codec::FieldKind::String, optional: true, nullable: false },
485            crate::clusters::codec::CommandField { tag: 3, name: "playback_preferences", kind: crate::clusters::codec::FieldKind::Struct { name: "PlaybackPreferencesStruct" }, optional: true, nullable: false },
486            crate::clusters::codec::CommandField { tag: 4, name: "use_current_context", kind: crate::clusters::codec::FieldKind::Bool, optional: true, nullable: false },
487        ]),
488        0x01 => Some(vec![
489            crate::clusters::codec::CommandField { tag: 0, name: "content_url", kind: crate::clusters::codec::FieldKind::String, optional: false, nullable: false },
490            crate::clusters::codec::CommandField { tag: 1, name: "display_string", kind: crate::clusters::codec::FieldKind::String, optional: true, nullable: false },
491            crate::clusters::codec::CommandField { tag: 2, name: "branding_information", kind: crate::clusters::codec::FieldKind::Struct { name: "BrandingInformationStruct" }, optional: true, nullable: false },
492            crate::clusters::codec::CommandField { tag: 3, name: "playback_preferences", kind: crate::clusters::codec::FieldKind::Struct { name: "PlaybackPreferencesStruct" }, optional: true, nullable: false },
493        ]),
494        _ => None,
495    }
496}
497
498pub fn encode_command_json(cmd_id: u32, _args: &serde_json::Value) -> anyhow::Result<Vec<u8>> {
499    match cmd_id {
500        0x00 => Err(anyhow::anyhow!("command \"LaunchContent\" has complex args: use raw mode")),
501        0x01 => Err(anyhow::anyhow!("command \"LaunchURL\" has complex args: use raw mode")),
502        _ => Err(anyhow::anyhow!("unknown command ID: 0x{:02X}", cmd_id)),
503    }
504}
505
506#[derive(Debug, serde::Serialize)]
507pub struct LauncherResponse {
508    pub status: Option<Status>,
509    pub data: Option<String>,
510}
511
512// Command response decoders
513
514/// Decode LauncherResponse command response (02)
515pub fn decode_launcher_response(inp: &tlv::TlvItemValue) -> anyhow::Result<LauncherResponse> {
516    if let tlv::TlvItemValue::List(_fields) = inp {
517        let item = tlv::TlvItem { tag: 0, value: inp.clone() };
518        Ok(LauncherResponse {
519                status: item.get_int(&[0]).and_then(|v| Status::from_u8(v as u8)),
520                data: item.get_string_owned(&[1]),
521        })
522    } else {
523        Err(anyhow::anyhow!("Expected struct fields"))
524    }
525}
526
527// Typed facade (invokes + reads)
528
529/// Invoke `LaunchContent` command on cluster `Content Launcher`.
530pub async fn launch_content(conn: &crate::controller::Connection, endpoint: u16, search: ContentSearch, auto_play: bool, data: String, playback_preferences: PlaybackPreferences, use_current_context: bool) -> anyhow::Result<LauncherResponse> {
531    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?;
532    decode_launcher_response(&tlv)
533}
534
535/// Invoke `LaunchURL` command on cluster `Content Launcher`.
536pub async fn launch_url(conn: &crate::controller::Connection, endpoint: u16, content_url: String, display_string: String, branding_information: BrandingInformation, playback_preferences: PlaybackPreferences) -> 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_LAUNCHURL, &encode_launch_url(content_url, display_string, branding_information, playback_preferences)?).await?;
538    decode_launcher_response(&tlv)
539}
540
541/// Read `AcceptHeader` attribute from cluster `Content Launcher`.
542pub async fn read_accept_header(conn: &crate::controller::Connection, endpoint: u16) -> anyhow::Result<Vec<String>> {
543    let tlv = conn.read_request2(endpoint, crate::clusters::defs::CLUSTER_ID_CONTENT_LAUNCHER, crate::clusters::defs::CLUSTER_CONTENT_LAUNCHER_ATTR_ID_ACCEPTHEADER).await?;
544    decode_accept_header(&tlv)
545}
546
547/// Read `SupportedStreamingProtocols` attribute from cluster `Content Launcher`.
548pub async fn read_supported_streaming_protocols(conn: &crate::controller::Connection, endpoint: u16) -> anyhow::Result<SupportedProtocols> {
549    let tlv = conn.read_request2(endpoint, crate::clusters::defs::CLUSTER_ID_CONTENT_LAUNCHER, crate::clusters::defs::CLUSTER_CONTENT_LAUNCHER_ATTR_ID_SUPPORTEDSTREAMINGPROTOCOLS).await?;
550    decode_supported_streaming_protocols(&tlv)
551}
552