Skip to content

Latest commit

 

History

History
210 lines (168 loc) · 7.98 KB

File metadata and controls

210 lines (168 loc) · 7.98 KB

Runtime State

data/runtime-state.json stores temporary mutable operator state in new generated configs.

config.json
= static installation, safety flags, IPs, serial numbers, technical defaults

data/runtime-state.json
= temporary local runtime/operator data

data/ems_state.sqlite
= durable core EMS lifecycle state such as battery full-charge assist tracking

The EMS creates the runtime state file on first start from config defaults. The file is ignored by Git and is recreated automatically if missing. Older root-level runtime-state.json files from previous setups are no longer required after switching to data/runtime-state.json and may be removed manually.

Battery full-charge assist does not store lifecycle state in runtime-state.json. It uses the core SQLite database configured by battery_full_charge_assist.state_database_path, so it can recover active assist and restore state after restart without overwriting operator runtime intent.

Example:

{
  "system": {
    "enabled": true,
    "max_total_power": 800,
    "loop_interval": 5,
    "min_output_limit": 35
  },
  "ha": {
    "enabled": false,
    "control_enabled": false
  },
  "winter": {
    "enabled": true
  },
  "devices": {
    "WR1": {
      "enabled": true,
      "max_power": 800,
      "offgrid_socket_mode": "off",
      "pv_priority_factor": 1.0
    },
    "WR2": {
      "enabled": true,
      "max_power": 800,
      "offgrid_socket_mode": "off",
      "pv_priority_factor": 1.0
    }
  }
}

System Fields

Field Meaning
enabled Enables or disables EMS output writes
max_total_power Runtime total power limit
loop_interval Runtime loop interval
min_output_limit Runtime guard against very low enabled outputLimit writes

min_output_limit=35 is the current template default and is useful on installations where outputLimit=0 behaves like a stop, idle, or sleep state. The guard is applied before deadband handling and only while EMS control is enabled.

The same value is also used as the standby/wakeup outputLimit for strict night/minSoc idle. When all active online devices report exactly no PV, no charge/discharge flow, no home output, and a blocked battery at minSoc, the EMS parks each device at min_output_limit once if needed. It then suppresses further outputLimit writes until PV telemetry becomes positive again.

Device Fields

Field Meaning
enabled Skip writes for this device when false
max_power Runtime per-device power limit
offgrid_socket_mode Operator intent for Zendure offgrid socket mode
pv_priority_factor Runtime PV-first allocation weight override
identity Stable hardware identity (serial) used to track a device across renames; managed by the EMS

pv_priority_factor defaults from config.json and can be changed at runtime:

python3 emsctl.py device WR1 pv-priority-factor 1.3
python3 emsctl.py device WR2 pv-priority-factor 0.7

Values above 1.0 increase the device's PV-first share, values below 1.0 reduce it. The setting only changes allocation weight. It does not create additional PV power and does not override real PV availability, device state, SOC logic, or configured power limits.

Offgrid mapping:

off      -> gridOffMode=2
eco      -> gridOffMode=1
standard -> gridOffMode=0

The CLI and Home Assistant only change the intent. The EMS is the only component that writes gridOffMode to hardware.

Device Lifecycle Reconciliation

The EMS is authoritative for runtime device state. On every load it reconciles the device entries against the configured (controllable) devices, so the file stays clean as devices are added, renamed, or removed in config.json. The Admin Console never edits runtime-state.json to reconcile the device lifecycle (add/rename/remove) — that stays EMS-authoritative. It does mirror a small, whitelisted set of overlapping scalar values into runtime-state on Apply; see Config → runtime convergence.

Devices are matched to their config entry by a stable identity (serial), so:

  • Add — a new controllable config device gets default runtime settings.
  • Rename — when a device's name changes in config but its identity is unchanged, the operator's runtime settings (max_power, pv_priority_factor, ac_charge_power_w, offgrid_socket_mode, …) migrate to the new name instead of being lost.
  • Remove — a device no longer in config is pruned.

Telemetry-only Zendure MQTT devices carry no controllable runtime settings and deliberately have no runtime-state entry (they are not flagged as missing by diagnose).

Reconciliation is fail-closed: if the configured device list is empty or the config is unreadable, nothing is pruned. Before any destructive change (rename/prune) the EMS writes a one-step backup to runtime-state.json.bak and logs an audit event (event=runtime_device_renamed, event=runtime_device_pruned, event=runtime_device_added). Identities are backfilled onto pre-existing entries by name on first load, so subsequent renames migrate settings.

Config → runtime convergence (Admin maintenance)

A few scalar values are writable in both config.json (the Admin console) and runtime-state.json (the Dashboard control tab and emsctl). For these keys the live EMS resolves the runtime value first, with the config value as the fallback, so an Admin edit to config.json alone would not take effect until a restart — and the runtime value would keep winning even then.

To keep the two stores coherent, Admin maintenance Apply mirrors the keys it actually changed into runtime-state, one-directionally (config/Admin → runtime; runtime is never merged back into config). The overlapping keys are exactly the Dashboard runtime-write whitelist:

  • system.enabled, system.max_total_power, system.loop_interval, system.min_output_limit
  • winter.enabled (and ha.enabled / ha.control_enabled)
  • per device: enabled, max_power, pv_priority_factor, offgrid_socket_mode

The mirror reuses the same validated whitelist writers the Dashboard uses (so the safety property — only whitelisted keys ever reach runtime-state — is preserved) and is best-effort: config.json is already written when it runs, so a value above the runtime power ceiling, a device not yet present in runtime (newly added or renamed), or a read-only/absent runtime file is reported as a per-key skip and never fails the Apply. New devices are seeded from config by the EMS on its next start. The maintenance editor also shows a per-field provenance badge (installed config value vs. live Dashboard override) and a Put live values back action that writes the installed config values back into runtime-state (it always writes the value; it never just clears the override, because the EMS loads config.json once per process).

HA Fields

Field Meaning
enabled Enables or disables HA publishing in the EMS loop
control_enabled Enables or disables HA helper sync

These fields can only affect HA when HA is statically configured and the EMS has an HA client. They do not edit HA URL or token.

The template default is standalone operation, so both HA runtime fields start as false unless Home Assistant is enabled in config.json.

Winter Fields

Field Meaning
enabled Enables or disables winter mode at runtime

Winter months, SOC limits, ramp step, adjustment hour, and AC charge power stay static in config.json.

Home Assistant Sync

Home Assistant helper values can act as a UI over runtime state. The EMS avoids writing HA every cycle and avoids interpreting its own HA writes as user changes.

Helpers are optional. If a helper is missing, the EMS continues with local runtime-state values.

HA sync failures do not block the EMS loop. The EMS logs the failed sync and continues with the previous local runtime-state values so telemetry fetch, safety decisions, and output-control decisions can still run.