1use serde::{Deserialize, Serialize};
2
3pub const VERSION: &str = crate::node::settings::VERSION;
4pub const NODE_TYPE: &str = "camera";
5pub const APP_TARGET_PREFIX: &str = "/app/plugin/camera/";
6pub const RUNTIME_TARGET_PREFIX: &str = "/camera/app/";
7pub const DEVICE_LIST: &str = "device/list";
8pub const MANAGEMENT_SNAPSHOT: &str = "management/snapshot";
9pub const SOURCE_REMOVE: &str = "source/remove";
10pub const RUNTIME_SOURCE_REMOVE_TARGET: &str = "/camera/app/source/remove";
11pub const APP_SOURCE_REMOVE_TARGET: &str = "/app/plugin/camera/source/remove";
12pub const DEVICE_REMOVE: &str = "device/remove";
13pub const WATCH_STATUS: &str = "watch/status";
14pub const WATCH_SET: &str = "watch/set";
15pub const APP_DEVICE_LIST_TARGET: &str = "/app/plugin/camera/device/list";
16pub const APP_MANAGEMENT_SNAPSHOT_TARGET: &str = "/app/plugin/camera/management/snapshot";
17pub const APP_DEVICE_REMOVE_TARGET: &str = "/app/plugin/camera/device/remove";
18pub const APP_WATCH_STATUS_TARGET: &str = "/app/plugin/camera/watch/status";
19pub const APP_WATCH_SET_TARGET: &str = "/app/plugin/camera/watch/set";
20pub const RUNTIME_DEVICE_LIST_TARGET: &str = "/camera/app/device/list";
21pub const RUNTIME_MANAGEMENT_SNAPSHOT_TARGET: &str = "/camera/app/management/snapshot";
22pub const RUNTIME_DEVICE_REMOVE_TARGET: &str = "/camera/app/device/remove";
23pub const RUNTIME_WATCH_STATUS_TARGET: &str = "/camera/app/watch/status";
24pub const RUNTIME_WATCH_SET_TARGET: &str = "/camera/app/watch/set";
25pub const DEVICE_ADD_FLOW: &str = "device.add";
26pub const SETTINGS_STATUS: &str = crate::node::settings::STATUS;
27pub const SETTINGS_UPDATE: &str = crate::node::settings::UPDATE;
28pub const SETTINGS_REVERT: &str = crate::node::settings::REVERT;
29pub const SETTINGS_RETRY: &str = crate::node::settings::RETRY;
30pub const RECOGNITION_SECTION: &str = "recognition";
31
32pub type SettingsStatusRequest = crate::node::settings::StatusRequest;
33pub type RecognitionUpdateRequest = crate::node::settings::UpdateRequest<RecognitionSettings>;
34
35#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
36#[serde(rename_all = "camelCase", deny_unknown_fields)]
37pub struct EmptyRequest {}
38
39#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
40#[serde(rename_all = "camelCase", deny_unknown_fields)]
41pub struct SourceIdRequest {
42 pub source_id: String,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
46#[serde(rename_all = "camelCase", deny_unknown_fields)]
47pub struct DeviceIdRequest {
48 pub device_id: String,
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
52#[serde(rename_all = "camelCase", deny_unknown_fields)]
53pub struct WatchStatusRequest {
54 pub device_id: String,
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
58#[serde(rename_all = "camelCase", deny_unknown_fields)]
59pub struct WatchSetRequest {
60 pub device_id: String,
61 pub enabled: bool,
62}
63
64#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
65#[serde(rename_all = "snake_case")]
66pub enum CameraDeviceHealth {
67 Applying,
68 Ready,
69 Degraded,
70 Error,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
74#[serde(rename_all = "camelCase", deny_unknown_fields)]
75pub struct ProviderCatalogItem {
76 pub id: String,
77 pub display_name: String,
78 pub description: String,
79 pub supports_multiple_cameras: bool,
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
83#[serde(rename_all = "camelCase", deny_unknown_fields)]
84pub struct CameraDevice {
85 pub source_id: Option<String>,
86 pub resolution_status: CameraResolutionStatus,
87 pub id: String,
88 pub name: String,
89 #[serde(default, skip_serializing_if = "Option::is_none")]
90 pub manufacturer_name: Option<String>,
91 pub capabilities: Vec<String>,
92 pub health: CameraDeviceHealth,
93 #[serde(default, skip_serializing_if = "Option::is_none")]
94 pub last_error: Option<String>,
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
98#[serde(rename_all = "camelCase", deny_unknown_fields)]
99pub struct OnlineStateObservation {
100 pub id: String,
101 pub state: String,
102 #[serde(default, skip_serializing_if = "Option::is_none")]
103 pub observed_at: Option<i64>,
104 #[serde(default, skip_serializing_if = "Option::is_none")]
105 pub source: Option<String>,
106 #[serde(default, skip_serializing_if = "Option::is_none")]
107 pub reason: Option<String>,
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
111#[serde(rename_all = "camelCase", deny_unknown_fields)]
112pub struct WatchControlResponse {
113 pub ok: bool,
114 pub device_id: String,
115 pub enabled: bool,
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
119#[serde(rename_all = "snake_case")]
120pub enum CameraResolutionStatus {
121 Unresolved,
122 Ready,
123 Failed,
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
127#[serde(rename_all = "camelCase", deny_unknown_fields)]
128pub struct HomeAssistantSource {
129 pub id: String,
130 pub name: String,
131 pub device_count: usize,
132 pub last_error: Option<String>,
133 pub last_refreshed_at: Option<String>,
134}
135
136#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
137#[serde(rename_all = "camelCase", deny_unknown_fields)]
138pub struct ManagementSnapshot {
139 pub sources: Vec<HomeAssistantSource>,
140 pub providers: Vec<ProviderCatalogItem>,
141 pub devices: Vec<CameraDevice>,
142 pub online_states: Vec<OnlineStateObservation>,
143 pub watch_states: Vec<WatchControlResponse>,
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
147#[serde(rename_all = "camelCase", deny_unknown_fields)]
148pub struct DeviceListResponse {
149 pub devices: Vec<CameraDevice>,
150 pub online_states: Vec<OnlineStateObservation>,
151}
152
153#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
154#[serde(rename_all = "camelCase", deny_unknown_fields)]
155pub struct DeviceMutationResponse {
156 pub device_ids: Vec<String>,
157 pub changed: bool,
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize)]
161#[serde(rename_all = "camelCase", deny_unknown_fields)]
162pub struct RecognitionSettings {
163 pub enabled: bool,
164}
165
166pub type RecognitionCommandRequest = crate::node::settings::SectionCommandRequest;
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171
172 #[test]
173 fn manifest_and_rust_camera_contract_agree() {
174 let manifest: serde_json::Value =
175 serde_json::from_str(include_str!("../../../manifest/core.v1.json")).unwrap();
176 let camera = &manifest["protocols"]["cameraPlugin"];
177 assert_eq!(camera["version"], VERSION);
178 assert_eq!(camera["nodeType"], NODE_TYPE);
179 assert_eq!(camera["appTargetPrefix"], APP_TARGET_PREFIX);
180 assert_eq!(camera["runtimeTargetPrefix"], RUNTIME_TARGET_PREFIX);
181 assert_eq!(camera["routes"]["deviceList"], DEVICE_LIST);
182 assert_eq!(camera["routes"]["managementSnapshot"], MANAGEMENT_SNAPSHOT);
183 assert_eq!(camera["routes"]["deviceRemove"], DEVICE_REMOVE);
184 assert_eq!(camera["routes"]["sourceRemove"], SOURCE_REMOVE);
185 assert_eq!(camera["routes"]["watchStatus"], WATCH_STATUS);
186 assert_eq!(camera["routes"]["watchSet"], WATCH_SET);
187 assert_eq!(camera["flows"]["deviceAdd"], DEVICE_ADD_FLOW);
188 assert_eq!(
189 APP_DEVICE_LIST_TARGET,
190 format!("{APP_TARGET_PREFIX}{DEVICE_LIST}")
191 );
192 assert_eq!(
193 APP_MANAGEMENT_SNAPSHOT_TARGET,
194 format!("{APP_TARGET_PREFIX}{MANAGEMENT_SNAPSHOT}")
195 );
196 assert_eq!(
197 APP_DEVICE_REMOVE_TARGET,
198 format!("{APP_TARGET_PREFIX}{DEVICE_REMOVE}")
199 );
200 assert_eq!(
201 APP_WATCH_STATUS_TARGET,
202 format!("{APP_TARGET_PREFIX}{WATCH_STATUS}")
203 );
204 assert_eq!(
205 APP_WATCH_SET_TARGET,
206 format!("{APP_TARGET_PREFIX}{WATCH_SET}")
207 );
208 assert_eq!(
209 RUNTIME_DEVICE_LIST_TARGET,
210 format!("{RUNTIME_TARGET_PREFIX}{DEVICE_LIST}")
211 );
212 assert_eq!(
213 RUNTIME_MANAGEMENT_SNAPSHOT_TARGET,
214 format!("{RUNTIME_TARGET_PREFIX}{MANAGEMENT_SNAPSHOT}")
215 );
216 assert_eq!(
217 RUNTIME_DEVICE_REMOVE_TARGET,
218 format!("{RUNTIME_TARGET_PREFIX}{DEVICE_REMOVE}")
219 );
220 assert_eq!(
221 RUNTIME_WATCH_STATUS_TARGET,
222 format!("{RUNTIME_TARGET_PREFIX}{WATCH_STATUS}")
223 );
224 assert_eq!(
225 RUNTIME_WATCH_SET_TARGET,
226 format!("{RUNTIME_TARGET_PREFIX}{WATCH_SET}")
227 );
228 }
229
230 #[test]
231 fn management_requests_are_strict() {
232 let invalid = serde_json::json!({"deviceId": "camera-1", "enabled": true});
233 assert!(serde_json::from_value::<WatchStatusRequest>(invalid).is_err());
234 let request: WatchSetRequest = serde_json::from_value(serde_json::json!({
235 "deviceId": "camera-1",
236 "enabled": true
237 }))
238 .unwrap();
239 assert_eq!(request.device_id, "camera-1");
240 assert!(request.enabled);
241 }
242
243 #[test]
244 fn management_conformance_corpus_matches_schema() {
245 let schema: serde_json::Value = serde_json::from_str(include_str!(
246 "../../../schema/camera-plugin-management.v1.schema.json"
247 ))
248 .unwrap();
249 let corpus: serde_json::Value = serde_json::from_str(include_str!(
250 "../../../fixtures/camera-plugin-management.conformance.json"
251 ))
252 .unwrap();
253 let validator = jsonschema::validator_for(&schema).unwrap();
254 for example in corpus["valid"].as_array().unwrap() {
255 assert!(
256 validator.is_valid(&example["value"]),
257 "valid example was rejected: {}",
258 example["name"]
259 );
260 }
261 for example in corpus["invalid"].as_array().unwrap() {
262 assert!(
263 !validator.is_valid(&example["value"]),
264 "invalid example was accepted: {}",
265 example["name"]
266 );
267 }
268 }
269}