1use clasp_core::{SignalType, Value};
4use serde::{Deserialize, Serialize};
5use std::time::Duration;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct Rule {
10 pub id: String,
12 pub name: String,
14 pub enabled: bool,
16 pub trigger: Trigger,
18 #[serde(default, skip_serializing_if = "Vec::is_empty")]
20 pub conditions: Vec<Condition>,
21 pub actions: Vec<RuleAction>,
23 #[serde(default, skip_serializing_if = "Option::is_none")]
25 pub cooldown: Option<Duration>,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
30pub enum Trigger {
31 OnChange { pattern: String },
33 OnThreshold {
35 address: String,
36 #[serde(default, skip_serializing_if = "Option::is_none")]
37 above: Option<f64>,
38 #[serde(default, skip_serializing_if = "Option::is_none")]
39 below: Option<f64>,
40 },
41 OnEvent { pattern: String },
43 OnInterval { seconds: u64 },
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct Condition {
50 pub address: String,
52 pub op: CompareOp,
54 pub value: Value,
56}
57
58#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
60pub enum CompareOp {
61 Eq,
63 Ne,
65 Gt,
67 Gte,
69 Lt,
71 Lte,
73}
74
75impl CompareOp {
76 pub fn evaluate(&self, left: &Value, right: &Value) -> bool {
78 match (left, right) {
79 (Value::Float(a), Value::Float(b)) => match self {
80 CompareOp::Eq => (a - b).abs() < f64::EPSILON,
81 CompareOp::Ne => (a - b).abs() >= f64::EPSILON,
82 CompareOp::Gt => a > b,
83 CompareOp::Gte => a >= b,
84 CompareOp::Lt => a < b,
85 CompareOp::Lte => a <= b,
86 },
87 (Value::Int(a), Value::Int(b)) => match self {
88 CompareOp::Eq => a == b,
89 CompareOp::Ne => a != b,
90 CompareOp::Gt => a > b,
91 CompareOp::Gte => a >= b,
92 CompareOp::Lt => a < b,
93 CompareOp::Lte => a <= b,
94 },
95 (Value::Bool(a), Value::Bool(b)) => match self {
96 CompareOp::Eq => a == b,
97 CompareOp::Ne => a != b,
98 _ => false,
99 },
100 (Value::String(a), Value::String(b)) => match self {
101 CompareOp::Eq => a == b,
102 CompareOp::Ne => a != b,
103 _ => false,
104 },
105 _ => false,
106 }
107 }
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize)]
112pub enum RuleAction {
113 Set { address: String, value: Value },
115 Publish {
117 address: String,
118 signal: SignalType,
119 #[serde(default, skip_serializing_if = "Option::is_none")]
120 value: Option<Value>,
121 },
122 SetFromTrigger {
124 address: String,
125 #[serde(default)]
126 transform: Transform,
127 },
128 Delay { milliseconds: u64 },
130}
131
132#[derive(Debug, Clone, Default, Serialize, Deserialize)]
134pub enum Transform {
135 #[default]
137 Identity,
138 Scale { scale: f64, offset: f64 },
140 Clamp { min: f64, max: f64 },
142 Threshold { value: f64 },
144 Invert { min: f64, max: f64 },
146}
147
148impl Transform {
149 pub fn apply(&self, value: &Value) -> Value {
151 match self {
152 Transform::Identity => value.clone(),
153 Transform::Scale { scale, offset } => match value {
154 Value::Float(f) => Value::Float(f * scale + offset),
155 Value::Int(i) => Value::Float(*i as f64 * scale + offset),
156 _ => value.clone(),
157 },
158 Transform::Clamp { min, max } => match value {
159 Value::Float(f) => Value::Float(f.clamp(*min, *max)),
160 Value::Int(i) => Value::Float((*i as f64).clamp(*min, *max)),
161 _ => value.clone(),
162 },
163 Transform::Threshold { value: threshold } => match value {
164 Value::Float(f) => Value::Bool(*f > *threshold),
165 Value::Int(i) => Value::Bool(*i as f64 > *threshold),
166 _ => value.clone(),
167 },
168 Transform::Invert { min, max } => match value {
169 Value::Float(f) => Value::Float(max - (f - min)),
170 Value::Int(i) => Value::Float(max - (*i as f64 - min)),
171 _ => value.clone(),
172 },
173 }
174 }
175}
176
177impl Trigger {
178 pub fn pattern(&self) -> Option<&str> {
180 match self {
181 Trigger::OnChange { pattern } => Some(pattern),
182 Trigger::OnThreshold { address, .. } => Some(address),
183 Trigger::OnEvent { pattern } => Some(pattern),
184 Trigger::OnInterval { .. } => None,
185 }
186 }
187
188 pub fn matches(&self, address: &str, signal_type: SignalType) -> bool {
190 match self {
191 Trigger::OnChange { pattern } => {
192 signal_type == SignalType::Param
193 && clasp_core::address::glob_match(pattern, address)
194 }
195 Trigger::OnThreshold { address: addr, .. } => {
196 signal_type == SignalType::Param && addr == address
197 }
198 Trigger::OnEvent { pattern } => {
199 signal_type == SignalType::Event
200 && clasp_core::address::glob_match(pattern, address)
201 }
202 Trigger::OnInterval { .. } => false,
203 }
204 }
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210
211 #[test]
212 fn test_transform_identity() {
213 let t = Transform::Identity;
214 assert_eq!(t.apply(&Value::Float(0.5)), Value::Float(0.5));
215 }
216
217 #[test]
218 fn test_transform_scale() {
219 let t = Transform::Scale {
220 scale: 2.0,
221 offset: 1.0,
222 };
223 assert_eq!(t.apply(&Value::Float(0.5)), Value::Float(2.0));
224 }
225
226 #[test]
227 fn test_transform_clamp() {
228 let t = Transform::Clamp { min: 0.0, max: 1.0 };
229 assert_eq!(t.apply(&Value::Float(1.5)), Value::Float(1.0));
230 assert_eq!(t.apply(&Value::Float(-0.5)), Value::Float(0.0));
231 assert_eq!(t.apply(&Value::Float(0.5)), Value::Float(0.5));
232 }
233
234 #[test]
235 fn test_transform_threshold() {
236 let t = Transform::Threshold { value: 0.5 };
237 assert_eq!(t.apply(&Value::Float(0.8)), Value::Bool(true));
238 assert_eq!(t.apply(&Value::Float(0.3)), Value::Bool(false));
239 }
240
241 #[test]
242 fn test_transform_invert() {
243 let t = Transform::Invert { min: 0.0, max: 1.0 };
244 assert_eq!(t.apply(&Value::Float(0.25)), Value::Float(0.75));
245 assert_eq!(t.apply(&Value::Float(0.0)), Value::Float(1.0));
246 }
247
248 #[test]
249 fn test_compare_op() {
250 assert!(CompareOp::Gt.evaluate(&Value::Float(1.0), &Value::Float(0.5)));
251 assert!(!CompareOp::Gt.evaluate(&Value::Float(0.5), &Value::Float(1.0)));
252 assert!(CompareOp::Eq.evaluate(&Value::Int(42), &Value::Int(42)));
253 assert!(CompareOp::Ne.evaluate(
254 &Value::String("a".to_string()),
255 &Value::String("b".to_string())
256 ));
257 }
258
259 #[test]
260 fn test_trigger_matches() {
261 let trigger = Trigger::OnChange {
262 pattern: "/lights/**".to_string(),
263 };
264 assert!(trigger.matches("/lights/room1/brightness", SignalType::Param));
265 assert!(!trigger.matches("/audio/volume", SignalType::Param));
266 assert!(!trigger.matches("/lights/room1/brightness", SignalType::Event));
267 }
268
269 #[test]
270 fn test_threshold_trigger_matches() {
271 let trigger = Trigger::OnThreshold {
272 address: "/sensor/temp".to_string(),
273 above: Some(30.0),
274 below: None,
275 };
276 assert!(trigger.matches("/sensor/temp", SignalType::Param));
277 assert!(!trigger.matches("/sensor/humidity", SignalType::Param));
278 }
279}