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=...`.
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`.
25///
26/// This deliberately does NOT request `meta=1`: a value read returns only the
27/// values, and the controller types them from the accessory database fetched
28/// via [`parse_accessories`](crate::parse_accessories). Per-read `meta=1`
29/// responses are unreliable across accessories — some shipping HomeKit firmware
30/// (e.g. LIFX) serializes the metadata fields as malformed JSON — so metadata is
31/// sourced from the well-formed `/accessories` tree instead.
32pub fn build_read_request(ids: &[(u64, u64)]) -> String {
33    let mut path = String::from("/characteristics?id=");
34    for (i, (aid, iid)) in ids.iter().enumerate() {
35        if i > 0 {
36            path.push(',');
37        }
38        // write! into a String cannot fail.
39        let _ = write!(path, "{aid}.{iid}");
40    }
41    path
42}
43
44// ---- read response ----
45
46#[derive(Debug, Deserialize)]
47struct WireReadResponse {
48    characteristics: Vec<WireReadEntry>,
49}
50
51#[derive(Debug, Deserialize)]
52struct WireReadEntry {
53    aid: u64,
54    iid: u64,
55    #[serde(default)]
56    value: Option<serde_json::Value>,
57    #[serde(default)]
58    status: Option<i64>,
59    #[serde(default)]
60    format: Option<CharFormat>,
61}
62
63/// Parse a `GET /characteristics` response body into `((aid,iid), CharValue)`
64/// pairs for the successful entries.
65///
66/// When `meta=1` was requested the accessory echoes each characteristic's
67/// `format`, which is used to decode the value. If `format` is absent the
68/// JSON value is inferred (bool→Bool, integer→Uint/Int, real→Float,
69/// string→Str); base64 strings cannot be distinguished from plain strings
70/// without a format, so callers needing tlv8/data must request `meta=1`.
71///
72/// # Errors
73/// - [`ModelError::Json`] for malformed JSON.
74/// - [`ModelError::CharacteristicStatus`] if any entry carries a non-zero
75///   `status`. (Entries with status 0 or no status are kept.)
76pub fn parse_read_response(json: &[u8]) -> Result<Vec<((u64, u64), CharValue)>> {
77    let wire: WireReadResponse = serde_json::from_slice(json)?;
78    let mut out = Vec::with_capacity(wire.characteristics.len());
79    for e in wire.characteristics {
80        if let Some(s) = e.status {
81            if s != 0 {
82                return Err(ModelError::CharacteristicStatus {
83                    aid: e.aid,
84                    iid: e.iid,
85                    status: s,
86                });
87            }
88        }
89        let Some(raw) = e.value else { continue };
90        let value = match e.format {
91            Some(fmt) => fmt.value_from_json(&raw)?,
92            None => infer_value(&raw)?,
93        };
94        out.push(((e.aid, e.iid), value));
95    }
96    Ok(out)
97}
98
99/// Decode a JSON value with no declared format (best-effort inference).
100fn infer_value(v: &serde_json::Value) -> Result<CharValue> {
101    use serde_json::Value as J;
102    match v {
103        J::Bool(b) => Ok(CharValue::Bool(*b)),
104        J::String(s) => Ok(CharValue::Str(s.clone())),
105        J::Number(n) => {
106            if let Some(u) = n.as_u64() {
107                Ok(CharValue::Uint(u))
108            } else if let Some(i) = n.as_i64() {
109                Ok(CharValue::Int(i))
110            } else if let Some(f) = n.as_f64() {
111                Ok(CharValue::Float(f))
112            } else {
113                Err(ModelError::ValueType {
114                    format: "unknown",
115                    detail: format!("uninterpretable number {n}"),
116                })
117            }
118        }
119        other => Err(ModelError::ValueType {
120            format: "unknown",
121            detail: format!("got JSON {other}"),
122        }),
123    }
124}
125
126// ---- write + subscribe ----
127
128/// Build the JSON body for `PUT /characteristics` writing each `(aid,iid)` its
129/// `CharValue`.
130pub fn build_write_request(writes: &[((u64, u64), CharValue)]) -> Vec<u8> {
131    let entries: Vec<serde_json::Value> = writes
132        .iter()
133        .map(|((aid, iid), v)| serde_json::json!({ "aid": aid, "iid": iid, "value": v.to_json() }))
134        .collect();
135    let body = serde_json::json!({ "characteristics": entries });
136    // serde_json::to_vec on an in-memory Value cannot fail.
137    serde_json::to_vec(&body).unwrap_or_default()
138}
139
140/// Build the JSON body for `PUT /characteristics` subscribing (`enable=true`)
141/// or unsubscribing (`enable=false`) to events for each `(aid,iid)`.
142pub fn build_subscribe_request(ids: &[(u64, u64)], enable: bool) -> Vec<u8> {
143    let entries: Vec<serde_json::Value> = ids
144        .iter()
145        .map(|(aid, iid)| serde_json::json!({ "aid": aid, "iid": iid, "ev": enable }))
146        .collect();
147    let body = serde_json::json!({ "characteristics": entries });
148    serde_json::to_vec(&body).unwrap_or_default()
149}
150
151/// Build the JSON body for `PUT /prepare`: a timed-write reservation.
152///
153/// `ttl_ms` is milliseconds; `pid` is the controller-chosen transaction id echoed
154/// by the subsequent timed write.
155pub fn build_prepare_request(ttl_ms: u64, pid: u64) -> Vec<u8> {
156    let body = serde_json::json!({ "ttl": ttl_ms, "pid": pid });
157    serde_json::to_vec(&body).unwrap_or_default()
158}
159
160/// Build a `PUT /characteristics` body for a timed write: every entry carries
161/// the `pid` from a preceding [`build_prepare_request`].
162pub fn build_timed_write_request(writes: &[((u64, u64), CharValue)], pid: u64) -> Vec<u8> {
163    let entries: Vec<serde_json::Value> = writes
164        .iter()
165        .map(|((aid, iid), v)| {
166            serde_json::json!({ "aid": aid, "iid": iid, "value": v.to_json(), "pid": pid })
167        })
168        .collect();
169    serde_json::to_vec(&serde_json::json!({ "characteristics": entries })).unwrap_or_default()
170}
171
172/// Build a `PUT /characteristics` body requesting the post-write value back
173/// (the HAP `r` flag).
174pub fn build_write_request_with_response(writes: &[((u64, u64), CharValue)]) -> Vec<u8> {
175    let entries: Vec<serde_json::Value> = writes
176        .iter()
177        .map(|((aid, iid), v)| {
178            serde_json::json!({ "aid": aid, "iid": iid, "value": v.to_json(), "r": true })
179        })
180        .collect();
181    serde_json::to_vec(&serde_json::json!({ "characteristics": entries })).unwrap_or_default()
182}