9 stable releases
| new 4.5.0 | Mar 29, 2026 |
|---|---|
| 4.4.0 | Mar 29, 2026 |
| 4.0.3 | Feb 22, 2026 |
#100 in Value formatting
Used in clasp-router
225KB
5.5K
SLoC
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
[dependencies]
clasp-rules = "3.5"
Usage
Motion Sensor to Lights (OnChange)
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
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
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)
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
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:
{
"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
| Field | Type | Default | Description |
|---|---|---|---|
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
| Variant | Fields | Description |
|---|---|---|
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
| Field | Type | Description |
|---|---|---|
address |
String |
CLASP address to check |
op |
CompareOp |
Eq, Ne, Gt, Gte, Lt, Lte |
value |
Value |
Value to compare against |
RuleAction Variants
| Variant | Fields | Description |
|---|---|---|
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
| Variant | Fields | Description |
|---|---|---|
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
Dependencies
~8–12MB
~139K SLoC