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