Skip to main content

hap_model/
characteristics.rs

1//! Building and parsing `/characteristics` requests and responses.
2
3use crate::error::{ModelError, Result};
4use crate::format::{CharFormat, CharValue};
5use serde::Deserialize;
6use std::fmt::Write as _;
7
8/// One decoded entry from a `GET /characteristics` response.
9#[derive(Debug, Clone, PartialEq)]
10pub struct CharRead {
11    /// Accessory instance id.
12    pub aid: u64,
13    /// Characteristic instance id.
14    pub iid: u64,
15    /// The decoded value, if the entry carried one.
16    pub value: Option<CharValue>,
17    /// The HAP status code, if present (0 = success).
18    pub status: Option<i64>,
19}
20
21/// Build the path+query for `GET /characteristics?id=...&meta=1`.
22///
23/// `ids` is a list of `(aid, iid)` pairs. The query is rendered in the order
24/// given, comma-joined, e.g. `/characteristics?id=1.9,1.10&meta=1`.
25pub fn build_read_request(ids: &[(u64, u64)]) -> String {
26    let mut path = String::from("/characteristics?id=");
27    for (i, (aid, iid)) in ids.iter().enumerate() {
28        if i > 0 {
29            path.push(',');
30        }
31        // write! into a String cannot fail.
32        let _ = write!(path, "{aid}.{iid}");
33    }
34    path.push_str("&meta=1");
35    path
36}
37
38// ---- read response ----
39
40#[derive(Debug, Deserialize)]
41struct WireReadResponse {
42    characteristics: Vec<WireReadEntry>,
43}
44
45#[derive(Debug, Deserialize)]
46struct WireReadEntry {
47    aid: u64,
48    iid: u64,
49    #[serde(default)]
50    value: Option<serde_json::Value>,
51    #[serde(default)]
52    status: Option<i64>,
53    #[serde(default)]
54    format: Option<CharFormat>,
55}
56
57/// Parse a `GET /characteristics` response body into `((aid,iid), CharValue)`
58/// pairs for the successful entries.
59///
60/// When `meta=1` was requested the accessory echoes each characteristic's
61/// `format`, which is used to decode the value. If `format` is absent the
62/// JSON value is inferred (bool→Bool, integer→Uint/Int, real→Float,
63/// string→Str); base64 strings cannot be distinguished from plain strings
64/// without a format, so callers needing tlv8/data must request `meta=1`.
65///
66/// # Errors
67/// - [`ModelError::Json`] for malformed JSON.
68/// - [`ModelError::CharacteristicStatus`] if any entry carries a non-zero
69///   `status`. (Entries with status 0 or no status are kept.)
70pub fn parse_read_response(json: &[u8]) -> Result<Vec<((u64, u64), CharValue)>> {
71    let wire: WireReadResponse = serde_json::from_slice(json)?;
72    let mut out = Vec::with_capacity(wire.characteristics.len());
73    for e in wire.characteristics {
74        if let Some(s) = e.status {
75            if s != 0 {
76                return Err(ModelError::CharacteristicStatus {
77                    aid: e.aid,
78                    iid: e.iid,
79                    status: s,
80                });
81            }
82        }
83        let Some(raw) = e.value else { continue };
84        let value = match e.format {
85            Some(fmt) => fmt.value_from_json(&raw)?,
86            None => infer_value(&raw)?,
87        };
88        out.push(((e.aid, e.iid), value));
89    }
90    Ok(out)
91}
92
93/// Decode a JSON value with no declared format (best-effort inference).
94fn infer_value(v: &serde_json::Value) -> Result<CharValue> {
95    use serde_json::Value as J;
96    match v {
97        J::Bool(b) => Ok(CharValue::Bool(*b)),
98        J::String(s) => Ok(CharValue::Str(s.clone())),
99        J::Number(n) => {
100            if let Some(u) = n.as_u64() {
101                Ok(CharValue::Uint(u))
102            } else if let Some(i) = n.as_i64() {
103                Ok(CharValue::Int(i))
104            } else if let Some(f) = n.as_f64() {
105                Ok(CharValue::Float(f))
106            } else {
107                Err(ModelError::ValueType {
108                    format: "unknown",
109                    detail: format!("uninterpretable number {n}"),
110                })
111            }
112        }
113        other => Err(ModelError::ValueType {
114            format: "unknown",
115            detail: format!("got JSON {other}"),
116        }),
117    }
118}
119
120// ---- write + subscribe ----
121
122/// Build the JSON body for `PUT /characteristics` writing each `(aid,iid)` its
123/// `CharValue`.
124pub fn build_write_request(writes: &[((u64, u64), CharValue)]) -> Vec<u8> {
125    let entries: Vec<serde_json::Value> = writes
126        .iter()
127        .map(|((aid, iid), v)| serde_json::json!({ "aid": aid, "iid": iid, "value": v.to_json() }))
128        .collect();
129    let body = serde_json::json!({ "characteristics": entries });
130    // serde_json::to_vec on an in-memory Value cannot fail.
131    serde_json::to_vec(&body).unwrap_or_default()
132}
133
134/// Build the JSON body for `PUT /characteristics` subscribing (`enable=true`)
135/// or unsubscribing (`enable=false`) to events for each `(aid,iid)`.
136pub fn build_subscribe_request(ids: &[(u64, u64)], enable: bool) -> Vec<u8> {
137    let entries: Vec<serde_json::Value> = ids
138        .iter()
139        .map(|(aid, iid)| serde_json::json!({ "aid": aid, "iid": iid, "ev": enable }))
140        .collect();
141    let body = serde_json::json!({ "characteristics": entries });
142    serde_json::to_vec(&body).unwrap_or_default()
143}