# clasp-rules
Server-side reactive automation engine for CLASP routers.
## Features
- **Reactive Triggers** - Fire on state change, threshold crossing, events, or intervals
- **Conditional Execution** - Guard rules with comparisons against live state
- **Transform Pipeline** - Scale, clamp, invert, or threshold values on the fly
- **Loop Prevention** - Automatic origin tagging prevents rule feedback loops
- **Cooldowns** - Per-rule minimum time between firings
- **JSON Rules** - Define rules in JSON for runtime loading
## Installation
```toml
[dependencies]
clasp-rules = "3.5"
```
## Usage
### Motion Sensor to Lights (OnChange)
```rust
use clasp_rules::{Rule, Trigger, RuleAction, RulesEngine};
use clasp_core::Value;
use std::time::Duration;
let rule = Rule {
id: "motion-lights".to_string(),
name: "Motion activates lights".to_string(),
enabled: true,
trigger: Trigger::OnChange {
pattern: "/sensors/*/motion".to_string(),
},
conditions: vec![],
actions: vec![RuleAction::Set {
address: "/lights/hallway/brightness".to_string(),
value: Value::Float(1.0),
}],
cooldown: Some(Duration::from_secs(5)),
};
let mut engine = RulesEngine::new();
engine.add_rule(rule)?;
```
### Threshold Alert
```rust
use clasp_rules::{Trigger, Condition, CompareOp, RuleAction};
use clasp_core::{Value, SignalType};
let rule = Rule {
id: "temp-alert".to_string(),
name: "High temperature alert".to_string(),
enabled: true,
trigger: Trigger::OnThreshold {
address: "/sensors/room1/temperature".to_string(),
above: Some(30.0),
below: None,
},
conditions: vec![Condition {
address: "/system/alerts/enabled".to_string(),
op: CompareOp::Eq,
value: Value::Bool(true),
}],
actions: vec![RuleAction::Publish {
address: "/alerts/temperature".to_string(),
signal: SignalType::Event,
value: Some(Value::String("High temperature in room 1".into())),
}],
cooldown: Some(Duration::from_secs(60)),
};
```
### SetFromTrigger with Scale Transform
```rust
use clasp_rules::{RuleAction, Transform};
// Map a 0-1 slider to 0-255 DMX range
let rule = Rule {
id: "slider-to-dmx".to_string(),
name: "Scale slider to DMX".to_string(),
enabled: true,
trigger: Trigger::OnChange {
pattern: "/controls/slider1".to_string(),
},
conditions: vec![],
actions: vec![RuleAction::SetFromTrigger {
address: "/dmx/1/channel/1".to_string(),
transform: Transform::Scale {
scale: 255.0,
offset: 0.0,
},
}],
cooldown: None,
};
```
### Periodic Heartbeat (OnInterval)
```rust
let rule = Rule {
id: "heartbeat".to_string(),
name: "Periodic heartbeat".to_string(),
enabled: true,
trigger: Trigger::OnInterval { seconds: 10 },
conditions: vec![],
actions: vec![RuleAction::Publish {
address: "/system/heartbeat".to_string(),
signal: SignalType::Event,
value: None,
}],
cooldown: None,
};
```
### Evaluate Rules
```rust
let actions = engine.evaluate(
"/sensors/room1/motion", // address that changed
&Value::Bool(true), // new value
SignalType::Param, // signal type
None, // origin (None = from client)
|addr| { // state lookup function
// Return current value for an address
Some(Value::Bool(true))
},
);
for action in actions {
println!("Rule {} fires: {:?}", action.rule_id, action.action);
// Execute action.action against the router
}
// For interval rules
let intervals = engine.interval_rules(); // Vec<(rule_id, seconds)>
let actions = engine.evaluate_interval("heartbeat", |addr| None);
```
### JSON Rule Definition
Rules are fully serializable for runtime configuration:
```json
{
"id": "motion-lights",
"name": "Motion activates lights",
"enabled": true,
"trigger": {
"OnChange": {
"pattern": "/sensors/*/motion"
}
},
"conditions": [],
"actions": [
{
"Set": {
"address": "/lights/hallway/brightness",
"value": 1.0
}
}
],
"cooldown": {
"secs": 5,
"nanos": 0
}
}
```
## Configuration Reference
### Rule
| `id` | `String` | required | Unique rule identifier |
| `name` | `String` | required | Human-readable name |
| `enabled` | `bool` | required | Whether the rule is active |
| `trigger` | `Trigger` | required | What triggers the rule |
| `conditions` | `Vec<Condition>` | `[]` | Additional conditions (all must be true) |
| `actions` | `Vec<RuleAction>` | required | Actions to execute when the rule fires |
| `cooldown` | `Option<Duration>` | `None` | Minimum time between firings |
### Trigger Variants
| `OnChange` | `pattern: String` | Fires when a param matching the pattern changes |
| `OnThreshold` | `address: String`, `above: Option<f64>`, `below: Option<f64>` | Fires when a value crosses a threshold |
| `OnEvent` | `pattern: String` | Fires when an event matching the pattern is published |
| `OnInterval` | `seconds: u64` | Fires periodically |
### Condition
| `address` | `String` | CLASP address to check |
| `op` | `CompareOp` | `Eq`, `Ne`, `Gt`, `Gte`, `Lt`, `Lte` |
| `value` | `Value` | Value to compare against |
### RuleAction Variants
| `Set` | `address`, `value` | Set a parameter to a fixed value |
| `Publish` | `address`, `signal`, `value?` | Publish an event |
| `SetFromTrigger` | `address`, `transform` | Copy trigger's value with optional transform |
| `Delay` | `milliseconds` | Delay before the next action |
### Transform Variants
| `Identity` | -- | Pass through unchanged |
| `Scale` | `scale`, `offset` | `output = input * scale + offset` |
| `Clamp` | `min`, `max` | Clamp to range `[min, max]` |
| `Threshold` | `value` | `true` if input > value, else `false` |
| `Invert` | `min`, `max` | `output = max - (input - min)` |
## Loop Prevention
Actions produced by rules carry an `origin` field (`"rule:{id}"` or `"interval:{id}"`). The engine skips evaluation when the origin starts with `"rule:"`, preventing infinite feedback loops.
## License
Licensed under either of Apache License, Version 2.0 or MIT license at your option.
---
Maintained by [LumenCanvas](https://lumencanvas.studio)