matter_interaction/status.rs
1//! Interaction Model status codes — Matter Core Spec §8.10 (Status Codes).
2
3#![forbid(unsafe_code)]
4
5use crate::error::ImError;
6use crate::{expect_message_struct, skip_container};
7use matter_codec::{Element, Tag, TlvReader, Value};
8
9/// Parse a bare `StatusResponseMessage` (`{ 0: Status (u8), 0xFF: IMRev }`) into
10/// its status code, or `None` if the message is **not** a bare status response.
11///
12/// This disambiguates a message-level `StatusResponse` from a `WriteResponse`
13/// (whose tag 0 is the `WriteResponses` array) and an `InvokeResponse` (whose tag
14/// 0 is the `SuppressResponse` bool): only a `StatusResponse` carries a **scalar
15/// uint** at context tag 0. The write/invoke verbs use this to detect a
16/// `NEEDS_TIMED_INTERACTION (0xc6)` rejection, which the device returns as a
17/// message-level `StatusResponse` rather than a per-path status.
18///
19/// # Errors
20///
21/// Returns [`ImError`] if `bytes` is not a valid IM message struct, or the
22/// status value exceeds a single octet ([`ImError::InvalidStatusCode`]).
23pub fn parse_status_response(bytes: &[u8]) -> Result<Option<u8>, ImError> {
24 let mut r = TlvReader::new(bytes);
25 expect_message_struct(&mut r)?;
26 loop {
27 match r.next()? {
28 // A bare StatusResponse carries Status as a scalar uint at ctx 0.
29 Some(Element::Scalar {
30 tag: Tag::Context(0),
31 value: Value::Uint(n),
32 }) => {
33 let code = u8::try_from(n).map_err(|_| ImError::InvalidStatusCode { code: n })?;
34 return Ok(Some(code));
35 }
36 // Not a bare status response: end of message, or tag 0 is a bool
37 // (InvokeResponse SuppressResponse) / a container (WriteResponse array).
38 None
39 | Some(
40 Element::ContainerEnd
41 | Element::Scalar {
42 tag: Tag::Context(0),
43 ..
44 }
45 | Element::ContainerStart {
46 tag: Tag::Context(0),
47 ..
48 },
49 ) => return Ok(None),
50 Some(Element::ContainerStart { .. }) => skip_container(&mut r)?,
51 Some(_) => {}
52 }
53 }
54}
55
56/// An Interaction Model status, as carried by a `StatusIB`.
57#[derive(Copy, Clone, Debug, PartialEq, Eq)]
58#[non_exhaustive]
59pub enum ImStatus {
60 /// `SUCCESS` (0x00).
61 Success,
62 /// Any non-zero IM status code (e.g. 0x01 `FAILURE`, 0x86
63 /// `UNSUPPORTED_ATTRIBUTE`, 0x88 `INVALID_ACTION`). The raw code is
64 /// preserved so callers can log or branch on it.
65 Failure(u8),
66}
67
68impl ImStatus {
69 /// Map a raw IM status byte to [`ImStatus`].
70 #[must_use]
71 pub fn from_u8(code: u8) -> Self {
72 if code == 0x00 {
73 Self::Success
74 } else {
75 Self::Failure(code)
76 }
77 }
78
79 /// The raw IM status byte: `0x00` for [`Success`](Self::Success), the
80 /// preserved code for [`Failure`](Self::Failure). Inverse of [`Self::from_u8`].
81 #[must_use]
82 pub fn to_u8(self) -> u8 {
83 match self {
84 Self::Success => 0x00,
85 Self::Failure(code) => code,
86 }
87 }
88
89 /// `true` iff this is [`ImStatus::Success`].
90 #[must_use]
91 pub fn is_success(self) -> bool {
92 matches!(self, Self::Success)
93 }
94}
95
96#[cfg(test)]
97mod tests {
98 #![allow(clippy::unwrap_used)] // Test code: CLAUDE.md test-code carve-out.
99 use super::*;
100
101 #[test]
102 fn maps_success_and_failure_codes() {
103 assert_eq!(ImStatus::from_u8(0x00), ImStatus::Success);
104 assert!(matches!(ImStatus::from_u8(0x01), ImStatus::Failure(0x01)));
105 assert!(matches!(ImStatus::from_u8(0x88), ImStatus::Failure(0x88)));
106 }
107
108 #[test]
109 fn is_success_only_for_zero() {
110 assert!(ImStatus::from_u8(0x00).is_success());
111 assert!(!ImStatus::from_u8(0x01).is_success());
112 }
113
114 #[test]
115 fn parse_status_response_reads_bare_status() {
116 let bytes = crate::build_status_response(0xc6);
117 assert_eq!(parse_status_response(&bytes).unwrap(), Some(0xc6));
118 let ok = crate::build_status_response(0x00);
119 assert_eq!(parse_status_response(&ok).unwrap(), Some(0x00));
120 }
121
122 #[test]
123 fn parse_status_response_none_for_write_response() {
124 // A WriteRequest's tag 0 is a bool (SuppressResponse); a WriteResponse's
125 // tag 0 is the WriteResponses array. Neither is a bare status response.
126 let write = crate::build_write_request(&[]);
127 assert_eq!(parse_status_response(&write).unwrap(), None);
128 }
129
130 #[test]
131 fn parse_status_response_none_for_invoke_request() {
132 // InvokeRequest tag 0 is a bool (SuppressResponse) ⇒ not a status response.
133 let inv = crate::build_invoke_request(
134 crate::CommandPath {
135 endpoint: 1,
136 cluster: 0x06,
137 command: 0x02,
138 },
139 &[0x15, 0x18],
140 );
141 assert_eq!(parse_status_response(&inv).unwrap(), None);
142 }
143}