Skip to content

Rules Engine ​

window.lcards.core.rulesManager: Evaluates conditions and hot-patches card styles at runtime.


Overview ​

RulesEngine extends BaseService. It maintains a compiled index of rules, tracks which entities each rule depends on, and re-evaluates dirty rules on every HASS state push. Matched rules produce patches: plain style objects that are merged onto target overlays without those overlays needing to know the rule exists.


Key Classes ​

ClassFileRole
RulesEnginecore/rules/RulesEngine.jsEvaluation loop, dependency index, patch distribution
compileConditionscore/rules/compileConditions.jsCompiles rule when DSL to optimised predicate functions
RuleTraceBuffercore/rules/RuleTraceBuffer.jsRing buffer of evaluation events for the Debug Panel

Compilation ​

When RulesEngine is constructed it immediately compiles every rule's when block into an optimised predicate tree via compileRule() in compileConditions.js. The result is stored in this.compiledRules (a Map<ruleId, { tree, deps }>):

  • tree: a recursive node structure (all, any, not, leaf comparisons, javascript, jinja2) that evalCompiled() walks during evaluation.
  • deps: sets of entity IDs, performance-metric keys, and feature flags that the rule references, used to build the dependency index.

Compilation happens once per RulesEngine instance (at construction). If rules are replaced, a new RulesEngine instance is created rather than mutating the existing one. Compilation issues (e.g. a plain string condition without [[[ ]]] or {{ }}) are logged as warnings and the affected rule falls back to an always-true node.

javascript
// compileConditions.js shape
{ tree: {
    type: 'all',           // 'all' | 'any' | 'not' | 'leaf' | 'javascript' | 'jinja2' | 'always'
    nodes: [ ... ]
  },
  deps: {
    entities: Set<string>,
    perf:     Set<string>,
    flags:    Set<string>
  }
}

Evaluation Flow ​

HASS state push
    → mark affected rules dirty  (dependency index O(1) lookup)
    → evaluate dirty rules only  (evalCompiled() walks compiled tree)
    → for each matched rule: compute patches
    → distribute patches to registered overlay listeners
    → overlays call requestUpdate()

Rule Schema ​

yaml
rules:
  - id: high_temp
    priority: 10             # higher number = evaluated first
    stop: false
    enabled: true
    when:
      entity: sensor.cpu_temp
      above: 75
    apply:
      overlays:
        tag:temp_widget:     # tag selector - all overlays tagged 'temp_widget'
          style:
            color: var(--lcars-red)
      animations:
        - tag: temp_widget
          preset: pulse
          loop: true

  - id: motion_active
    when:
      entity: binary_sensor.front_door
      state: "on"
    apply:
      overlays:
        front-door-button:   # direct overlay ID
          style:
            color: var(--lcars-orange)

Condition Operators ​

OperatorValue typeMeaning
statestringExact state match (alias for equals)
equalsstring/numberExact equality
not_equalsstring/numberInequality
abovenumbernumeric_state > value (strictly greater)
at_leastnumbernumeric_state >= value (inclusive)
belownumbernumeric_state < value (strictly less)
at_mostnumbernumeric_state <= value (inclusive)
inarrayState is one of the listed values
not_inarrayState is not in the listed values
regexstringState matches regular expression
attributestringAttribute name: pair with comparison operator
allcondition[]AND: all nested conditions must match
anycondition[]OR: at least one nested condition must match
notconditionNegate a nested condition
conditionstringJS [[[code]]] or Jinja2 {{template}}: truthy return
jinja2stringExplicit Jinja2 template
javascriptstringExplicit JavaScript expression
time_between"HH:MM-HH:MM"True when current time is in range
weekday_instring[]True when today is one of mon…sun
sun_elevation{ above?, at_least?, below?, at_most? }True when sun elevation matches
perf_metric{ key, above?, at_least?, below?, at_most? }Internal performance metric comparison
random_chancenumber 0–1True with given probability each evaluation

The 4 numeric operators (above/at_least/below/at_most) AND-combine when more than one is present on the same condition, so a range is expressed by combining two of them:

yaml
when:
  entity: sensor.cpu_temp
  at_least: 20   # inclusive range: 20 <= value <= 80
  at_most: 80

The condition-group editor's operator dropdown offers this as ready-made "between" / "between (exclusive)" options for entity and entity-attribute conditions. sun_elevation/perf_metric already expose all 4 bound fields at once, so a range needs no special operator there; just fill in two of the four fields.


Targeting ​

The keys of apply.overlays are selectors. Each maps to a patch fragment:

Selector formMatches
overlay-idExact overlay ID (direct match)
tag:tagnameAll overlays with tag tagname
type:typenameAll overlays of type typename
pattern:regexAll overlays whose ID matches the regex
allEvery registered overlay
excludeArray of IDs to exclude from bulk selectors

Multiple selectors in one rule are merged; later selectors' style keys override earlier ones for the same overlay.


Card Integration ​

javascript
// Register overlay (in _handleFirstUpdate)
this._registerOverlayForRules({ id: `btn-${this._cardGuid}`, type: 'button' });

// Receive patches
_onRulePatchesChanged(patches) {
  this._resolveStyle();   // merge config.style + patches, then requestUpdate()
}

Priority & Stop Processing ​

Higher priority number wins when multiple rules target the same overlay property (default 0). Rules are evaluated in descending priority order.

Set stop: true on a rule to prevent lower-priority rules from running at all once this rule matches.


Public API ​

Property / MethodReturnsDescription
rulesRule[]All registered rules in evaluation order
rulesByIdMap<id, Rule>Rules keyed by ID for fast lookup
getAllRules()Rule[]Array of all registered rule objects
getTrace()ObjectDetailed evaluation trace with per-overlay match history
getRecentMatches(windowMs)Match[]All rule matches within the last N milliseconds
getRuleTrace(ruleId, limit?)Object[]Per-rule match history with optional result count limit

Console Access ​

javascript
window.lcards.debug.singleton('rulesManager')
// → { type: 'RulesEngine', rulesCount: 3, dirtyRules: 0, evalCounts: {...}, trace: {...} }
javascript
const rm = window.lcards.core.rulesManager

rm.rules                            // full rules array
rm.rulesById                        // Map<id, rule>
rm.getTrace()                       // detailed evaluation trace
rm.getRecentMatches(30000)          // matches in the last 30s
rm.getRuleTrace('my-rule-id', 20)   // history for one rule
rm.getAllRules()                     // array of all registered rules

See Also ​