core_api/node/contracts/
matter_device_sources.rs1use serde::{Deserialize, Serialize};
3pub const MATTER_DEVICE_SOURCES_TARGET: &str = "/matter/device/sources";
4#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
5#[serde(rename_all = "camelCase", deny_unknown_fields)]
6pub struct MatterDeviceSourcesRequest {}
7#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
8#[serde(rename_all = "camelCase")]
9pub enum MatterSourceAvailability {
10 Available,
11 Unavailable,
12}
13#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
14#[serde(rename_all = "camelCase", deny_unknown_fields)]
15pub struct MatterDeviceSourcesSnapshot {
16 pub availability: MatterSourceAvailability,
17 pub bridges: Vec<MatterSourceBridge>,
18 pub devices: Vec<MatterDeviceSource>,
19}
20#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
21#[serde(rename_all = "camelCase", deny_unknown_fields)]
22pub struct MatterSourceBridge {
23 pub node_id: String,
24 pub display_name: String,
25 pub reachable: bool,
26}
27#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
28#[serde(rename_all = "camelCase", deny_unknown_fields)]
29pub struct MatterDeviceSource {
30 pub node_id: String,
31 pub availability: MatterSourceAvailability,
32 pub bridge_node_id: Option<String>,
33}
34impl MatterDeviceSourcesSnapshot {
35 pub fn validate(&self) -> Result<(), String> {
36 use MatterSourceAvailability::*;
37 if self.availability == Unavailable
38 && (!self.bridges.is_empty() || !self.devices.is_empty())
39 {
40 return Err("Unavailable Matter inventory must be empty".into());
41 }
42 let mut bridges = std::collections::HashSet::new();
43 for bridge in &self.bridges {
44 if bridge.node_id.trim().is_empty()
45 || bridge.display_name.trim().is_empty()
46 || !bridges.insert(bridge.node_id.as_str())
47 {
48 return Err("Invalid or duplicate Matter bridge".into());
49 }
50 }
51 let mut devices = std::collections::HashSet::new();
52 for device in &self.devices {
53 if device.node_id.trim().is_empty() || !devices.insert(device.node_id.as_str()) {
54 return Err("Invalid or duplicate Matter source".into());
55 }
56 if let Some(bridge) = &device.bridge_node_id {
57 if device.availability != Available
58 || !bridges.contains(bridge.as_str())
59 || bridge != &device.node_id
60 {
61 return Err("Invalid Matter bridge reference".into());
62 }
63 }
64 }
65 for bridge in &self.bridges {
66 if !self.devices.iter().any(|d| {
67 d.node_id == bridge.node_id
68 && d.bridge_node_id.as_deref() == Some(bridge.node_id.as_str())
69 }) {
70 return Err("Matter bridge has no source mapping".into());
71 }
72 }
73 Ok(())
74 }
75}
76#[cfg(test)]
77mod tests {
78 use super::*;
79 use serde_json::json;
80 #[test]
81 fn validates_direct_bridged_and_unknown_sources() {
82 let value = json!({"availability":"available","bridges":[{"nodeId":"1","displayName":"Bridge","reachable":false}],"devices":[{"nodeId":"1","availability":"available","bridgeNodeId":"1"},{"nodeId":"2","availability":"available","bridgeNodeId":null},{"nodeId":"3","availability":"unavailable","bridgeNodeId":null}]});
83 let snapshot: MatterDeviceSourcesSnapshot = serde_json::from_value(value.clone()).unwrap();
84 assert!(snapshot.validate().is_ok());
85 assert_eq!(serde_json::to_value(&snapshot).unwrap(), value);
86 let mut invalid = snapshot.clone();
87 invalid.devices[0].bridge_node_id = Some("missing".into());
88 assert!(invalid.validate().is_err());
89 let mut invalid = snapshot.clone();
90 invalid.availability = MatterSourceAvailability::Unavailable;
91 assert!(invalid.validate().is_err());
92 let mut invalid = snapshot.clone();
93 invalid.devices[0].availability = MatterSourceAvailability::Unavailable;
94 assert!(invalid.validate().is_err());
95 let mut invalid = snapshot.clone();
96 invalid.devices.push(invalid.devices[0].clone());
97 assert!(invalid.validate().is_err());
98 let mut invalid = value;
99 invalid["bridges"][0]["endpoints"] = json!([]);
100 assert!(serde_json::from_value::<MatterDeviceSourcesSnapshot>(invalid).is_err());
101 assert!(
102 serde_json::from_value::<MatterDeviceSourcesRequest>(json!({"discover":true})).is_err()
103 );
104 }
105}