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}
63
64#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
65#[serde(rename_all = "snake_case")]
66pub enum PatchOp {
67 Upsert,
68 Delete,
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct EdgeConfig {
74 pub edge_id: String,
75 pub mappings: Vec<Mapping>,
76 #[serde(default)]
81 pub glyphs: Vec<Glyph>,
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct Glyph {
89 pub name: String,
90 #[serde(default)]
91 pub pattern: String,
92 #[serde(default)]
93 pub builtin: bool,
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize)]
98#[serde(tag = "type", rename_all = "snake_case")]
99pub enum UiFrame {
100 Snapshot { snapshot: UiSnapshot },
102 EdgeOnline { edge: EdgeInfo },
104 EdgeOffline { edge_id: String },
106 ServiceState {
108 edge_id: String,
109 service_type: String,
110 target: String,
111 property: String,
112 #[serde(skip_serializing_if = "Option::is_none")]
113 output_id: Option<String>,
114 value: serde_json::Value,
115 },
116 DeviceState {
118 edge_id: String,
119 device_type: String,
120 device_id: String,
121 property: String,
122 value: serde_json::Value,
123 },
124 MappingChanged {
126 mapping_id: Uuid,
127 op: PatchOp,
128 mapping: Option<Mapping>,
129 },
130 GlyphsChanged { glyphs: Vec<Glyph> },
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize)]
137pub struct UiSnapshot {
138 pub edges: Vec<EdgeInfo>,
139 pub service_states: Vec<ServiceStateEntry>,
140 pub device_states: Vec<DeviceStateEntry>,
141 pub mappings: Vec<Mapping>,
142 pub glyphs: Vec<Glyph>,
143}
144
145#[derive(Debug, Clone, Serialize, Deserialize)]
147pub struct EdgeInfo {
148 pub edge_id: String,
149 pub online: bool,
150 pub version: String,
151 pub capabilities: Vec<String>,
152 pub last_seen: String,
154}
155
156#[derive(Debug, Clone, Serialize, Deserialize)]
157pub struct ServiceStateEntry {
158 pub edge_id: String,
159 pub service_type: String,
160 pub target: String,
161 pub property: String,
162 #[serde(skip_serializing_if = "Option::is_none")]
163 pub output_id: Option<String>,
164 pub value: serde_json::Value,
165 pub updated_at: String,
167}
168
169#[derive(Debug, Clone, Serialize, Deserialize)]
170pub struct DeviceStateEntry {
171 pub edge_id: String,
172 pub device_type: String,
173 pub device_id: String,
174 pub property: String,
175 pub value: serde_json::Value,
176 pub updated_at: String,
177}
178
179#[derive(Debug, Clone, Serialize, Deserialize)]
183pub struct Mapping {
184 pub mapping_id: Uuid,
185 pub edge_id: String,
186 pub device_type: String,
187 pub device_id: String,
188 pub service_type: String,
189 pub service_target: String,
190 pub routes: Vec<Route>,
191 #[serde(default)]
192 pub feedback: Vec<FeedbackRule>,
193 #[serde(default = "default_true")]
194 pub active: bool,
195 #[serde(default)]
198 pub target_candidates: Vec<TargetCandidate>,
199 #[serde(default, skip_serializing_if = "Option::is_none")]
207 pub target_switch_on: Option<String>,
208}
209
210#[derive(Debug, Clone, Serialize, Deserialize)]
214pub struct TargetCandidate {
215 pub target: String,
217 #[serde(default)]
219 pub label: String,
220 pub glyph: String,
223}
224
225fn default_true() -> bool {
226 true
227}
228
229#[derive(Debug, Clone, Serialize, Deserialize)]
231pub struct Route {
232 pub input: String,
233 pub intent: String,
234 #[serde(default)]
235 pub params: BTreeMap<String, serde_json::Value>,
236}
237
238#[derive(Debug, Clone, Serialize, Deserialize)]
240pub struct FeedbackRule {
241 pub state: String,
242 pub feedback_type: String,
243 pub mapping: serde_json::Value,
244}
245
246#[cfg(test)]
247mod tests {
248 use super::*;
249
250 #[test]
251 fn server_to_edge_config_full_roundtrip() {
252 let msg = ServerToEdge::ConfigFull {
253 config: EdgeConfig {
254 edge_id: "living-room".into(),
255 mappings: vec![Mapping {
256 mapping_id: Uuid::nil(),
257 edge_id: "living-room".into(),
258 device_type: "nuimo".into(),
259 device_id: "C3:81:DF:4E:FF:6A".into(),
260 service_type: "roon".into(),
261 service_target: "zone-1".into(),
262 routes: vec![Route {
263 input: "rotate".into(),
264 intent: "volume_change".into(),
265 params: BTreeMap::from([("damping".into(), serde_json::json!(80))]),
266 }],
267 feedback: vec![],
268 active: true,
269 target_candidates: vec![],
270 target_switch_on: None,
271 }],
272 glyphs: vec![Glyph {
273 name: "play".into(),
274 pattern: " * \n ** ".into(),
275 builtin: false,
276 }],
277 },
278 };
279 let json = serde_json::to_string(&msg).unwrap();
280 assert!(json.contains("\"type\":\"config_full\""));
281 assert!(json.contains("\"edge_id\":\"living-room\""));
282
283 let parsed: ServerToEdge = serde_json::from_str(&json).unwrap();
284 match parsed {
285 ServerToEdge::ConfigFull { config } => {
286 assert_eq!(config.edge_id, "living-room");
287 assert_eq!(config.mappings.len(), 1);
288 }
289 _ => panic!("wrong variant"),
290 }
291 }
292
293 #[test]
294 fn edge_to_server_state_with_optional_output_id() {
295 let msg = EdgeToServer::State {
296 service_type: "roon".into(),
297 target: "zone-1".into(),
298 property: "volume".into(),
299 output_id: Some("output-1".into()),
300 value: serde_json::json!(50),
301 };
302 let json = serde_json::to_string(&msg).unwrap();
303 assert!(json.contains("\"output_id\":\"output-1\""));
304
305 let msg2 = EdgeToServer::State {
306 service_type: "roon".into(),
307 target: "zone-1".into(),
308 property: "playback".into(),
309 output_id: None,
310 value: serde_json::json!("playing"),
311 };
312 let json2 = serde_json::to_string(&msg2).unwrap();
313 assert!(!json2.contains("output_id"));
314 }
315}