RuleWay

RuleWay is a lightweight embeddable rule engine for Dart applications, device automation, and edge Linux devices. It is designed for workloads where rules are driven by device attributes, attribute changes, rule-level time windows, and explicit action handlers.

Features

  • Device attribute state and previous-state tracking.
  • device_expr and attr_change triggers.
  • Multi-device rules through targets and target_index.
  • duration and cooldown windows.
  • Rule-level multi-segment time_windows with weekday repeat.
  • time, date, and six-field second-level cron triggers.
  • Global actions and per-device device_actions with action parameters.
  • Action registration with built-in emit_alarm.
  • Optional SQLite rule storage using compact JSON columns.

Usage

import 'package:ruleway/ruleway.dart';

Future<void> main() async {
  final engine = RuleEngine(
    timeZone: 'Asia/Shanghai',
    rules: [
      Rule(
        targets: ['AC01'],
        triggers: [
          Trigger(
            type: TriggerType.deviceExpr,
            targetIndex: 0,
            attr: 'temperature',
            op: '>',
            value: 30,
          ),
        ],
        actions: [
          ActionConfig(type: 'emit_alarm', params: {'reason': 'too hot'}),
        ],
      ),
    ],
  );

  engine.onAlarm.listen((alarm) => print(alarm.toJson()));
  await engine.postAttrUpdate('AC01', {'temperature': '31'});
  engine.dispose();
}

postAttrUpdate() and tick() return futures because registered actions may be asynchronous. Await them when deterministic action completion is required.

Event Context and Actions

Attach request, session, or source metadata to an attribute update without storing it as device state:

engine.registerAction('notify', (action, context) async {
  final requestId = context.context['request_id'];
  await sendNotification(requestId, action.params);
});

await engine.postAttrUpdate(
  'AC01',
  {'temperature': 31},
  context: {'request_id': 'req-42'},
);

For rules with duration > 0, RuleWay preserves the context from the event that started the duration window. Calling tick() continues evaluating eligible duration rules even when no new attribute event arrives. attr_change rules still require a new edge event and are not completed by a timer tick.

Rule JSON

{
  "rules": [
    {
      "id": 1,
      "enabled": true,
      "duration": 0,
      "cooldown": 5,
      "targets": [{"id": "AC01"}, {"id": "Door01"}],
      "time_windows": [
        {"start": "08:00:00", "end": "09:00:00", "week_repeat": [0, 1, 2, 3, 4]},
        {"start": "20:00:00", "end": "21:00:00", "week_repeat": [0, 1, 2, 3, 4]}
      ],
      "triggers": [
        {
          "type": "attr_change",
          "target_index": 0,
          "attr": "has_person",
          "from": 0,
          "to": 1
        }
      ],
      "conditions": [
        {
          "type": "device_expr",
          "target_index": 1,
          "attr": "door_open",
          "op": "==",
          "value": true
        }
      ],
      "actions": [{"type": "emit_alarm", "reason": "person at open door"}],
      "device_actions": [
        {
          "target_index": 1,
          "actions": [
            {"type": "set_property", "power": true},
            {"type": "set_property", "brightness": 80}
          ]
        }
      ]
    }
  ]
}

week_repeat uses 0..6 for Monday..Sunday. Empty or missing means every day. time_windows is optional; if omitted, the rule is always active. All time_windows, time, date in_weekday, and cron calculations use the timezone passed to RuleEngine(timeZone: ...); they never use the host machine's local timezone. device_actions is optional and runs after global actions.

SQLite Storage

final storage = RuleStorage('/tmp/ruleway.db');
storage.init();
storage.importJsonFile('rules.json', reload: true);
final rules = storage.loadRules();

CLI

dart run ruleway:ruleway_cli --timezone Asia/Shanghai rules.json

Input events as:

AC01.temperature=31
Door01.door_open=true

Libraries

ruleway
Lightweight embeddable rule engine for Dart and edge devices.