1use serde::{Deserialize, Serialize};
8use std::collections::BTreeMap;
9use uuid::Uuid;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13#[serde(tag = "type", rename_all = "snake_case")]
14pub enum ServerToEdge {
15 ConfigFull { config: EdgeConfig },
17 ConfigPatch {
19 mapping_id: Uuid,
20 op: PatchOp,
21 mapping: Option<Mapping>,
22 },
23 TargetSwitch {
25 mapping_id: Uuid,
26 service_target: String,
27 },
28 GlyphsUpdate { glyphs: Vec<Glyph> },
30 Ping,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
36#[serde(tag = "type", rename_all = "snake_case")]
37pub enum EdgeToServer {
38 Hello {
40 edge_id: String,
41 version: String,
42 capabilities: Vec<String>,
43 },
44 State {
46 service_type: String,
47 target: String,
48 property: String,
49 #[serde(skip_serializing_if = "Option::is_none")]
50 output_id: Option<String>,
51 value: serde_json::Value,
52 },
53 DeviceState {
55 device_type: String,
56 device_id: String,
57 property: String,
58 value: serde_json::Value,
59 },
60 Pong,
62 SwitchTarget {
68 mapping_id: Uuid,
69 service_target: String,
70 },
71}
72
73#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
74#[serde(rename_all = "snake_case")]
75pub enum PatchOp {
76 Upsert,
77 Delete,
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct EdgeConfig {
83 pub edge_id: String,
84 pub mappings: Vec<Mapping>,
85 #[serde(default)]
90 pub glyphs: Vec<Glyph>,
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct Glyph {
98 pub name: String,
99 #[serde(default)]
100 pub pattern: String,
101 #[serde(default)]
102 pub builtin: bool,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
107#[serde(tag = "type", rename_all = "snake_case")]
108pub enum UiFrame {
109 Snapshot { snapshot: UiSnapshot },
111 EdgeOnline { edge: EdgeInfo },
113 EdgeOffline { edge_id: String },
115 ServiceState {
117 edge_id: String,
118 service_type: String,
119 target: String,
120 property: String,
121 #[serde(skip_serializing_if = "Option::is_none")]
122 output_id: Option<String>,
123 value: serde_json::Value,
124 },
125 DeviceState {
127 edge_id: String,
128 device_type: String,
129 device_id: String,
130 property: String,
131 value: serde_json::Value,
132 },
133 MappingChanged {
135 mapping_id: Uuid,
136 op: PatchOp,
137 mapping: Option<Mapping>,
138 },
139 GlyphsChanged { glyphs: Vec<Glyph> },
141}
142
143#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct UiSnapshot {
147 pub edges: Vec<EdgeInfo>,
148 pub service_states: Vec<ServiceStateEntry>,
149 pub device_states: Vec<DeviceStateEntry>,
150 pub mappings: Vec<Mapping>,
151 pub glyphs: Vec<Glyph>,
152}
153
154#[derive(Debug, Clone, Serialize, Deserialize)]
156pub struct EdgeInfo {
157 pub edge_id: String,
158 pub online: bool,
159 pub version: String,
160 pub capabilities: Vec<String>,
161 pub last_seen: String,
163}
164
165#[derive(Debug, Clone, Serialize, Deserialize)]
166pub struct ServiceStateEntry {
167 pub edge_id: String,
168 pub service_type: String,
169 pub target: String,
170 pub property: String,
171 #[serde(skip_serializing_if = "Option::is_none")]
172 pub output_id: Option<String>,
173 pub value: serde_json::Value,
174 pub updated_at: String,
176}
177
178#[derive(Debug, Clone, Serialize, Deserialize)]
179pub struct DeviceStateEntry {
180 pub edge_id: String,
181 pub device_type: String,
182 pub device_id: String,
183 pub property: String,
184 pub value: serde_json::Value,
185 pub updated_at: String,
186}
187
188#[derive(Debug, Clone, Serialize, Deserialize)]
192pub struct Mapping {
193 pub mapping_id: Uuid,
194 pub edge_id: String,
195 pub device_type: String,
196 pub device_id: String,
197 pub service_type: String,
198 pub service_target: String,
199 pub routes: Vec<Route>,
200 #[serde(default)]
201 pub feedback: Vec<FeedbackRule>,
202 #[serde(default = "default_true")]
203 pub active: bool,
204 #[serde(default)]
207 pub target_candidates: Vec<TargetCandidate>,
208 #[serde(default, skip_serializing_if = "Option::is_none")]
216 pub target_switch_on: Option<String>,
217}
218
219#[derive(Debug, Clone, Serialize, Deserialize)]
223pub struct TargetCandidate {
224 pub target: String,
226 #[serde(default)]
228 pub label: String,
229 pub glyph: String,
232}
233
234fn default_true() -> bool {
235 true
236}
237
238#[derive(Debug, Clone, Serialize, Deserialize)]
240pub struct Route {
241 pub input: String,
242 pub intent: String,
243 #[serde(default)]
244 pub params: BTreeMap<String, serde_json::Value>,
245}
246
247#[derive(Debug, Clone, Serialize, Deserialize)]
249pub struct FeedbackRule {
250 pub state: String,
251 pub feedback_type: String,
252 pub mapping: serde_json::Value,
253}
254
255#[cfg(test)]
256mod tests {
257 use super::*;
258
259 #[test]
260 fn server_to_edge_config_full_roundtrip() {
261 let msg = ServerToEdge::ConfigFull {
262 config: EdgeConfig {
263 edge_id: "living-room".into(),
264 mappings: vec![Mapping {
265 mapping_id: Uuid::nil(),
266 edge_id: "living-room".into(),
267 device_type: "nuimo".into(),
268 device_id: "C3:81:DF:4E:FF:6A".into(),
269 service_type: "roon".into(),
270 service_target: "zone-1".into(),
271 routes: vec![Route {
272 input: "rotate".into(),
273 intent: "volume_change".into(),
274 params: BTreeMap::from([("damping".into(), serde_json::json!(80))]),
275 }],
276 feedback: vec![],
277 active: true,
278 target_candidates: vec![],
279 target_switch_on: None,
280 }],
281 glyphs: vec![Glyph {
282 name: "play".into(),
283 pattern: " * \n ** ".into(),
284 builtin: false,
285 }],
286 },
287 };
288 let json = serde_json::to_string(&msg).unwrap();
289 assert!(json.contains("\"type\":\"config_full\""));
290 assert!(json.contains("\"edge_id\":\"living-room\""));
291
292 let parsed: ServerToEdge = serde_json::from_str(&json).unwrap();
293 match parsed {
294 ServerToEdge::ConfigFull { config } => {
295 assert_eq!(config.edge_id, "living-room");
296 assert_eq!(config.mappings.len(), 1);
297 }
298 _ => panic!("wrong variant"),
299 }
300 }
301
302 #[test]
303 fn edge_to_server_state_with_optional_output_id() {
304 let msg = EdgeToServer::State {
305 service_type: "roon".into(),
306 target: "zone-1".into(),
307 property: "volume".into(),
308 output_id: Some("output-1".into()),
309 value: serde_json::json!(50),
310 };
311 let json = serde_json::to_string(&msg).unwrap();
312 assert!(json.contains("\"output_id\":\"output-1\""));
313
314 let msg2 = EdgeToServer::State {
315 service_type: "roon".into(),
316 target: "zone-1".into(),
317 property: "playback".into(),
318 output_id: None,
319 value: serde_json::json!("playing"),
320 };
321 let json2 = serde_json::to_string(&msg2).unwrap();
322 assert!(!json2.contains("output_id"));
323 }
324}