This morning the ADA P1 Meter woke me up and even brewed my coffee :)

In October, a new feature will appear in HMKE.APP! The ADA P1 Meter not only reads now but also controls. You configure the rules on the hmke.app interface, and the device executes them locally from its own memory – even if there's no internet at home. In this article, we detail the structure of the control, the format of the rules, the interpreter’s capabilities, and practical examples.


Summary – How is it structured?

1.      Configuration in hmke.app → rules are stored in a JSON file.

2.      The system encodes the JSON to Base64 and sends it to the device (via MQTT or HTTP).

3.      The ADA P1 stores the rules in /rules.enc on LittleFS and loads them into RAM.

4.      The interpreter evaluates conditions from current meter data and supplementary values (plugins) and performs HTTP calls.

5.      The rules run offline as well, whenever a full DSMR telegram arrives.

Key message: The control is not cloud-dependent. hmke.app is only a convenient editor/distributor; execution always happens locally on the ADA P1.

Dataflow and components

·         DSMR → JSON: from the meter's messages, the ADA P1 creates a detailed JSON (e.g., voltage, current, power, tariff, timestamp, ...).

·         Plugins: any custom sensor/value can be written to the ADA P1 at the POST /write endpoint (→ in the JSON as plugins..value).

·         Interpreter: evaluates the condition expressions of rules and executes the associated actions list (HTTP GET/POST).

·         State management: each rule has its own state (timer, whether it has run, cooldown, etc.).

Rules format

Rules travel inside a single JSON:

{
  "rules": [
    {
      "id": "coffee_on_0730",
      "enabled": true,
      "condition": "voltage_phase_l1 > 220 && time_hm == 730",
      "min_timer_seconds": 0,
      "repeat": true,
      "repeat_delay_seconds": 120,
      "actions": [
        { "method": "GET", "url": "http://192.168.31.185/relay/0?turn=on" }
      ]
    }
  ]
}

Fields

·         id (string, required): unique identifier.

·         enabled (bool, default: true): toggle rule on/off.

·         condition (string, required): expression to evaluate (see Interpreter syntax).

·         min_timer_seconds (int, default: 0): condition must be continuously true this many seconds before actions run.

·         repeat (bool, default: false): if true, the rule is repeatable.

·         repeat_delay_seconds (int, default: 0): repeat cooldown duration – no new run within this time.

·         actions (list): sequence of HTTP calls; fields: method (GET/POST), url, optional body (for POST).

One-time rule: if repeat: false, after successful run the rule is closed (completed) and no longer activates.

Interpreter – Syntax and Capabilities

The interpreter is located in rules_interpreter.cpp. Key facts:

Type Handling

· Converts any JSON value to a number (getNumericValue):

o  true → 1, false → 0

o  “true”/“false” strings → 1/0

o  other strings → toDouble()

Operators and Precedence

· Logical: ||, &&

· Relational: >=, <=, ==, !=, >, <

· Arithmetic: +, -, *, /

· Parentheses supported: ( ... )

· Unary minus handled at expression start.

The processor parses expressions recursively, so usual math precedence applies (multiplication/division before addition/subtraction).

Nested keys

· Use dotted access: e.g. plugins.pv_kw.value or some.nested.key.

· If any element in the chain is missing, the value is treated as 0.

Available variables (partial)

ADA P1 provides, among others (from DSMR and computed fields):

· Time/state: timestamp, time_hm (“HHMM” as number, e.g. 0730 → 730), rules_global_enabled.

· Voltage: voltage_phase_l1, voltage_phase_l2, voltage_phase_l3 (V)

· Current: current_phase_l1, current_phase_l2, current_phase_l3 (A)

· Power: instantaneous_power_import, instantaneous_power_export (kW)

· Frequency / PF: frequency, power_factor, power_factor_l1/l2/l3

· Tariff: current_tariff

· Energy meters: active_*_energy_* etc.

· Plugins: any key loaded via POST /write: plugins..value

TIP: use numeric comparisons for time windows, e.g. time_hm >= 700 && time_hm < 830.

Execution and State Machine

The device maintains state for every rule:

· Timer: if min_timer_seconds > 0, actions run only if the condition has been continuously true for that duration.

· Repeat: if repeat: true, after successful run the rule enters cooldown for repeat_delay_seconds.

· One-time run: if repeat: false, the first successful run closes the rule.

· Reset: when condition becomes false, the timer resets; for repeat rules, it can trigger again when true next time.

Execution triggers HTTP calls with 3 s timeout. The watchdog is reset after every rule run, ensuring the system runs reliably long term.

Persistent storage and updates

· Rules are stored in the filesystem at /rules.enc (Base64 encoded).

· Rules are loaded into RAM at startup and after upload.

Updates via HTTP

· Upload: POST /rules with body: { "rules_base64": "…" } → saves to file and loads into RAM.

· Query: GET /rules

· Delete: DELETE /rules

· Global enable/disable:

o GET /rules/enable / GET /rules/disable

o GET/POST /rules/global body: { "enabled": true|false }

Updates via MQTT

· Connection: the device subscribes the topic ada/cmd/_ (QoS1).

· Load rules: send a JSON with m: "RULES" and rules_base64 fields; the device saves and loads it.

· Global start/stop commands: listens on ACK channel (ada/ack/_) for messages:

o { "cmd": "ALL_START" } → global enable ON + restart all min_timers.

o { "cmd": "ALL_STOP" } → global enable OFF.

· Rules dump request: { "cmd": "RULES_GET" } → device returns Base64 encoded rules in chunks (~3000B) on the ACK channel.

Note: the topicKey = _ is sanitized (replacing “+/#” characters), so can safely be used as topic key.

Local endpoints (HTTP)

· GET /json → latest compiled JSON (meter + plugins + meta)

· GET /telegram → raw DSMR telegram

· GET /acklog → latest ACK entries (MQTT)

· GET /rules | POST /rules | DELETE /rules → manage rules (Base64 encoded)

· GET /rules/enable | GET /rules/disable | GET/POST /rules/global → global enable

· POST /write → write plugins values (→ plugins..value)

· GET /check_version → get current firmware version

· GET /restart | GET /factoryreset → reboot / factory reset

** Example:**

POST /write
{
"device": "MyGateway",
"values": {
"pv_kw": 3.25,
"coffee": "ready"
}
}

→ in JSON accessed as: plugins.pv_kw.value, plugins.coffee.value

Practical examples

1) Timed coffee maker (only if there is mains voltage)

{
"id": "coffee_on_0730",
"enabled": true,
"condition": "voltage_phase_l1 > 220 && time_hm == 730",
"min_timer_seconds": 0,
"repeat": true,
"repeat_delay_seconds": 120,
"actions": [
{ "method": "GET", "url": "http://192.168.31.185/relay/0?turn=on" }
]
}

2) Connect a load when overproduction occurs

{
"id": "dump_load_on_export",
"enabled": true,
"condition": "instantaneous_power_export > 0.2",
"min_timer_seconds": 15,
"repeat": true,
"repeat_delay_seconds": 60,
"actions": [
{ "method": "GET", "url": "http://192.168.31.120/relay/0?turn=on" }
]
}

Over 0.2 kW export for 15 s → switch on a load.

3) Charging at cheap tariff (example)

{
"id": "cheap_tariff_charge",
"enabled": true,
"condition": "current_tariff == 2 && time_hm >= 2300 || time_hm < 600",
"min_timer_seconds": 30,
"repeat": true,
"repeat_delay_seconds": 300,
"actions": [
{ "method": "POST", "url": "http://192.168.31.140/api/charge", "body": "{\"mode\":\"on\"}" }
]
}

Example: for tariff 2 plus nighttime window, send command to local charger.

4) Undervoltage protection

{
"id": "undervoltage_cutoff",
"enabled": true,
"condition": "voltage_phase_l1 < 200 || voltage_phase_l2 < 200 || voltage_phase_l3 < 200",
"min_timer_seconds": 5,
"repeat": true,
"repeat_delay_seconds": 30,
"actions": [
{ "method": "GET", "url": "http://192.168.31.185/relay/0?turn=off" }
]
}

5) Plugin-based logic (external gateway reports PV)

{
"id": "pv_based_boost",
"enabled": true,
"condition": "plugins.pv_kw.value >= 2.5",
"min_timer_seconds": 60,
"repeat": true,
"repeat_delay_seconds": 180,
"actions": [
{ "method": "POST", "url": "http://192.168.31.150/heater", "body": "{\"power\":\"eco\"}" }
]
}

Example series – complete rules.json

{
"rules": [
{
"id": "coffee_on_0730",
"enabled": true,
"condition": "voltage_phase_l1 > 220 && time_hm == 730",
"min_timer_seconds": 0,
"repeat": true,
"repeat_delay_seconds": 120,
"actions": [
{ "method": "GET", "url": "http://192.168.31.185/relay/0?turn=on" }
]
},
{
"id": "coffee_off_0750",
"enabled": true,
"condition": "voltage_phase_l1 > 220 && time_hm == 750",
"min_timer_seconds": 0,
"repeat": true,
"repeat_delay_seconds": 120,
"actions": [
{ "method": "GET", "url": "http://192.168.31.185/relay/0?turn=off" }
]
},
{
"id": "dump_load_on_export",
"enabled": true,
"condition": "instantaneous_power_export > 0.2",
"min_timer_seconds": 15,
"repeat": true,
"repeat_delay_seconds": 60,
"actions": [
{ "method": "GET", "url": "http://192.168.31.120/relay/0?turn=on" }
]
}
]
}

MQTT – rule update example

Message to send on topic: ada/cmd/_

{
"m": "RULES",
"rules_base64": "eyJydWxlcyI6IFsgLi4uICBdIH0="
}

The rules_base64 is the above rules.json encoded in Base64.

Rules query on the ACK channel:

{ "cmd": "RULES_GET" }

→ response comes chunked Base64 rules_base64.

Global start/stop (ACK):

{ "cmd": "ALL_START" }
{ "cmd": "ALL_STOP" }

Security and Recommendations

· Calls should be on the local network, preferably in VLAN/isolation.

· Use token-based or IP-filtered endpoints on controlled devices if possible.

· The rules.enc file is Base64, not encrypted – do not store sensitive URLs/passwords in plain text.

Debugging and Device Operation

· /json: check values of keys used in conditions.

· /acklog: brief log of all MQTT ACK messages.

· /rules/global: quickly toggle the rule engine.

· LED blinking: status on DSMR processing.

· Watchdog: run safety; auto restart on hang.

FAQ

Does it really work without internet? Yes. Rules are on /rules.enc and loaded into RAM; evaluation and execution happen locally.

How fast is response? Evaluation runs after every complete DSMR telegram. Timers (min_timer_seconds) avoid momentary spikes.

Does it support complex expressions? Yes: parentheses, logical and relational operators, nested keys, and arithmetic all work.

How to add custom values to conditions? POST /write → results in plugins..value in JSON.

Conclusion

The ADA P1 Meter’s control engine is designed to be flexible, reliable, and locally autonomous. hmke.app is only a convenient “brain center” – the decisions and actions happen on the device. This keeps your household energy management truly in your hands.

If you have questions or want to see new examples, write to us!

Comments

No comments yet. Be the first!