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}
196
197fn default_true() -> bool {
198 true
199}
200
201#[derive(Debug, Clone, Serialize, Deserialize)]
203pub struct Route {
204 pub input: String,
205 pub intent: String,
206 #[serde(default)]
207 pub params: BTreeMap<String, serde_json::Value>,
208}
209
210#[derive(Debug, Clone, Serialize, Deserialize)]
212pub struct FeedbackRule {
213 pub state: String,
214 pub feedback_type: String,
215 pub mapping: serde_json::Value,
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221
222 #[test]
223 fn server_to_edge_config_full_roundtrip() {
224 let msg = ServerToEdge::ConfigFull {
225 config: EdgeConfig {
226 edge_id: "living-room".into(),
227 mappings: vec![Mapping {
228 mapping_id: Uuid::nil(),
229 edge_id: "living-room".into(),
230 device_type: "nuimo".into(),
231 device_id: "C3:81:DF:4E:FF:6A".into(),
232 service_type: "roon".into(),
233 service_target: "zone-1".into(),
234 routes: vec![Route {
235 input: "rotate".into(),
236 intent: "volume_change".into(),
237 params: BTreeMap::from([("damping".into(), serde_json::json!(80))]),
238 }],
239 feedback: vec![],
240 active: true,
241 }],
242 glyphs: vec![Glyph {
243 name: "play".into(),
244 pattern: " * \n ** ".into(),
245 builtin: false,
246 }],
247 },
248 };
249 let json = serde_json::to_string(&msg).unwrap();
250 assert!(json.contains("\"type\":\"config_full\""));
251 assert!(json.contains("\"edge_id\":\"living-room\""));
252
253 let parsed: ServerToEdge = serde_json::from_str(&json).unwrap();
254 match parsed {
255 ServerToEdge::ConfigFull { config } => {
256 assert_eq!(config.edge_id, "living-room");
257 assert_eq!(config.mappings.len(), 1);
258 }
259 _ => panic!("wrong variant"),
260 }
261 }
262
263 #[test]
264 fn edge_to_server_state_with_optional_output_id() {
265 let msg = EdgeToServer::State {
266 service_type: "roon".into(),
267 target: "zone-1".into(),
268 property: "volume".into(),
269 output_id: Some("output-1".into()),
270 value: serde_json::json!(50),
271 };
272 let json = serde_json::to_string(&msg).unwrap();
273 assert!(json.contains("\"output_id\":\"output-1\""));
274
275 let msg2 = EdgeToServer::State {
276 service_type: "roon".into(),
277 target: "zone-1".into(),
278 property: "playback".into(),
279 output_id: None,
280 value: serde_json::json!("playing"),
281 };
282 let json2 = serde_json::to_string(&msg2).unwrap();
283 assert!(!json2.contains("output_id"));
284 }
285}