Skip to content

Releases: sm26449/multi-bus-gateway

Multi-Bus Gateway 3.92.0

Choose a tag to compare

@sm26449 sm26449 released this 23 Oct 10:21

Upgrade notes:

  • The Device Builder is gone (breaking). ESP flashing, ESPHome builds
    and the node list moved to the separate multi-bus-gateway-esphome
    service. /api/builder/*, the USB flasher, the ESPHOME_* variables and
    the bundled esphome compose service are removed; an esphome: block in
    config.yaml is ignored and dropped by the next save. ESPHome LAN
    discovery stays.
  • InfluxDB measurement of unclassified fields. A field the gateway
    cannot classify used to land in measurement janitza on every device; it
    now lands in its row's category, or other. Bucket and field names
    do not change. A Flux query of your own that filters
    _measurement == "janitza" on a device other than the UMG needs the new
    name.
  • Saving through the API keeps what it was not told. A PUT that leaves
    a key out keeps the stored value; null clears it; an unchanged save
    writes nothing and restarts nothing. A client that relied on a missing key
    meaning "back to the default" must now send null.
  • Deleting is refused while something uses the object (409 with what
    uses it): a device, an installation or a template that a rule, a sequence,
    a formula, a virtual meter or another device's meter link names. Disable
    it instead, or change those first.
  • Yes/no fields take only JSON true or false. enabled and the
    other switches (the write master switch, raw writes, anonymous OPC UA, a
    rule's enabled) now refuse strings and numbers such as "true",
    "false", 1 or "on" with 422 and name the field. Before, any
    non-empty string, "false" too, counted as true. Turning a rule or a
    command on or off needs enabled in the body. A command's lease_s is a
    whole number from 0 to 86400, and a bad one is refused before anything is
    written.

Control

  • Rules: a schedule kind, timers on every kind (for_s, minimum on/off
    time, maximum starts per hour), forcing a rule for a while, time and
    per-rule counters in expressions, several rules on one target as layers
    (safety, manual, schedule, optimize), and sequences — commands in order
    across devices, undone on failure. A new rule runs in shadow until
    armed; disarming or disabling an armed rule puts its target back to its
    safe values first (unless the rule says on_disable: hold). One rule
    that cannot be read in rules.yaml no longer stops the others: it is
    shown in error, alerts, and can be fixed or deleted. Opt-in per-rule
    keys wait for the read-back after a restart, resume the step the device
    holds and resend an urgent want sooner. A rule that leaves armed never
    sends a write it still had queued, and a command's lease reverts to safe
    even after the command was turned off.
  • Commands over MQTT — a device read over MQTT runs its template's
    commands; a new Victron GX template brings the ESS commands.
  • Meters with roles — a device measured by other meters: consumption, or
    input and output (a UPS, a charger), with power, energy, loss, efficiency
    and load published only when the readings can be trusted. A device with no
    connection of its own can be added, measured by its meters alone.

Devices and templates

  • Components — the batteries and sensors a Victron GX holds, each its
    own device on the GX's single connection, found on the device.
  • Asset attributes — location, grid connection limits, PV inputs,
    ratings, battery limits, from the gateway's catalog; the sun's position and
    attr() in formulas and rules.
  • Formulas: fresh, count_fresh, age, absent, steps, ema,
    hysteresis; a copy/paste character is named before saving.
  • Templates: notes for users, setup words written at connect, rule
    examples, pauses between writes, single-register writes; new maps for a
    Samsung heat pump, a Delta VFD-EL drive and the Victron GX.
  • Devices found over MQTT can be adopted from Devices; topic variables
    per device; TLS to a device with its own certificate.

Outputs

  • OPC UA server for SCADA and HMI panels: every device and value with its
    quality and read time, logins with the gateway's accounts, encryption on by
    default, chosen per device; tested with FUXA (guide).
  • A device may publish on its own rhythm; a device's Outputs tab lists every
    output with its state.

Pages

  • Devices: kinds, search and filters; the device page in three groups
    (Live, Configure, Diagnose); one connection form for adding and editing;
    Used by and Disable instead of a refused delete.
  • Bridges say how busy each bus really was in the last minute.
  • Installations: a save restarts only the units it changes.
  • A page left open across an update says so and offers Reload.
  • The whole UI in English and Romanian, in one vocabulary, with the
    language's plural forms and dates.

Operations

  • High-availability hooks: the gateway's role (active / standby), state
    and configuration mirroring for a standby, behind a load balancer.
  • Service tokens for programs (Authorization: Bearer …), with a role.
  • Device alarms declared by a template become alerts.
  • Dependencies: certifi 2026.10.19 (PYSEC-2026-4320); asyncua for the
    OPC UA server.

Multi-Bus Gateway 3.88.0

Choose a tag to compare

@sm26449 sm26449 released this 07 Oct 19:45

Nothing to do when upgrading.

2026-10-08 — canonical names beyond the grid, and your own

The canonical dictionary — the names that give a quantity the same MQTT
topic, InfluxDB measurement and unit on every device — covered only grid
and PV quantities: a battery, a water meter or a weather station had none.

  • 95 new built-in names across six domains:

    • battery: soc, soh, cycles, pack_voltage, battery_current
      and battery_power (positive while charging), capacities, energy
      stored/to full, charge limits, cell_1…cell_32, cell temperatures
      and their min/max/avg;
    • environment: temperature, humidity, irradiance, module
      temperature, wind, pressure, CO₂;
    • water and gas: volume and flow;
    • heat: energy, power, flow, supply/return temperature;
    • EV charging: power, session and lifetime energy, current limit,
      state;
    • tank levels.

    Names BMS maps already used are kept, so the bundled Seplos map is
    canonical as it is.

  • Counters are declared, not guessed: the battery's energy_remaining
    and energy_to_full are not counters although they start with energy_;
    a water or gas volume is one.

  • Your own canonical fields on Templates → Canonical fields: name,
    category (InfluxDB measurement), unit, MQTT topic leaf, counter or not.
    They are canonical everywhere a built-in one is (topic, measurement, unit
    contract, editor guidance). Built-in names cannot be redefined; once a
    device reads a field, its category and topic stay put. A template that
    names them carries their definitions when exported and creates them on
    the gateway it is uploaded to.

  • InfluxDB: a battery unit's energy_remaining / energy_to_full
    are now written under the battery measurement instead of
    energy_active, where the unit heuristic had filed them. Nothing else
    moves: rows with an explicit measurement keep it, and MQTT topics of
    existing devices do not change (a stored topic, or the flat name). New
    devices get the hierarchical topics.

2026-10-08 — a device template anyone can write, rules that travel

Writing a template for a new device needed JSON for the things that matter
most, and a few traps cost values silently. Now the whole path is in the UI
and documented:

  • Template editor — How the device is read: the ways to reach it
    (Modbus TCP/RTU, RTU listen-only, HTTP, MQTT), the word order of
    32/64-bit values (ABCD / CDAB / BADC / DCBA, explained), the default unit
    ID, the read size, and every poll group's interval. Rows take FC1 coils
    and FC2 discrete inputs
    , any data type (string:N, sign-magnitude…),
    and a json_path / topic column when HTTP or MQTT is ticked.
  • A changed word order takes effect at once. It used to be read only
    when a device's client was made — a corrected template kept the device
    reading garbage until the container restarted. Saving a template whose
    protocol changed restarts the devices reading with it (and says which).
  • Diagnostics → Probe decodes a register all four ways side by side;
    each column now has a button that writes that order into the device's
    template
    .
  • CSV / YAML import take Read over and Word order (a YAML file may
    declare byte_order/transports itself, and a re-imported template keeps
    its whole protocol block — it used to be dropped); CSV takes FC1/FC2
    aliases (coil, DO, DI…); an import whose addresses all look like the
    1-based 40001/30001 notation warns instead of reading every value
    from the wrong place.
  • Rules export and import. Rules → Export (all, or one from its card)
    as YAML; Import checks each rule against this installation first —
    new, replaces, already here, cannot import with the reason — then
    saves the valid ones. Imported rules always arrive in shadow, and an
    armed rule is never replaced by an import.
  • A viewer no longer sees the rule controls it cannot use (arm, clamp,
    pause, edit, delete).
  • Found by walking the whole path as a new user (a new end-to-end test with
    simulated Modbus TCP, RTU, HTTP and MQTT devices and a virtual meter):
    • Add Device → Modbus RTU over the network: Test connection wrote its
      result into a hidden field — nothing appeared.
    • Diagnostics → Probe did not list RTU-over-network devices.
    • Clicking the Devices tab with a device open stayed on that device;
      it returns to the list now.
    • The editor showed no interval for rows without a poll group (they read
      on normal); an HTTP/MQTT map no longer carries Modbus settings.
    • Refusing an HTTP device URL said "SSRF guard"; it now says why:
      127.0.0.1 is the gateway itself (inside Docker, the container) or
      not on your local network.
    • Virtual Meters, first visit: it said "use Add instance" while that
      button was disabled — it now says to make a template first, with a link.
  • New guides: device-templates.md — from a
    manual's register table to live values: addresses, word order with a worked
    float, scale, the editor, CSV/YAML/JSON, HTTP and MQTT examples, testing,
    changing and sharing, troubleshooting, every field.
    rules.md — the safe way to bring a rule in, five recipes
    (over-voltage, over-frequency, battery full and exporting, grid lost, a
    value from another system), import/export, what each card state means.

Multi-Bus Gateway 3.87.0

Choose a tag to compare

@sm26449 sm26449 released this 07 Oct 17:41

Upgrade notes: the dashboard no longer colours a current or a power
by itself. Its implicit 90/100 "percent" defaults were compared with raw
amps and VA (any load above 100 VA showed red), and the threshold editor
filled them in — check thresholds saved that way (warning 90 / danger 100
on a power or a current) and set real ones or switch them off. For a bank
on seplos_bms_v3_rtu_tap, the installation page announces the template
update — review and apply it with the new calculated fields ticked (the new
warning_count); until then the bank's warnings are not counted.

2026-10-07 — a template update is announced, reviewed, then applied

The Update from template button was always there and said nothing about
what it would do — an operator could not tell whether to press it. Now:

  • The installation page shows The template has been updated only when
    the template has something the units lack (the devices list carries a
    template update badge); otherwise there is nothing to press.
  • Review and apply opens a review computed with the very rule the apply
    uses: every change with its old and new value and on how many units, the
    calculated fields the template gained (tick to add them), what stays as
    you set it — then applies it to every unit at once.
  • The Templates page says where an update from each map is waiting
    (Update waiting in: Battery bank, with the review a click away), and
    counts a map as in use by the units that reach it through a source too.
  • A unit whose template is missing (deleted, renamed, failed to load)
    says so — Template not available on its page, a template missing
    badge in the list — instead of quietly never being updatable; it keeps
    reading with its own copy.
  • GET /api/endpoints/{id}/template-changes (and /api/devices/{id}/…)
    return that review without writing anything; /api/endpoints carries a
    short template_update summary.
  • A unit created from a template now starts identical to it (its rows carry
    the template's descriptions and sections), so a new installation is never
    "out of date"; filling a section or a description an older unit never had
    is done with the next update but announces nothing (the page reads them
    from the template anyway); the log says how many rows changed and fields
    were added.

2026-10-07 — thresholds that know when they apply

  • onlyWhen on a register's thresholds: judge them only while another
    field of the same device is above a value — a current's THD at
    near-zero load is noise, so {field: current_l1, above: 2}. Set in the
    threshold editor (Only judge while … is above …); applied alike on the
    dashboard, the fleet's alarm count and value alerts, and an alarm it was
    in clears with an event.
  • No implicit current/power limits: a limit for those is a share of a
    rating (a breaker, an inverter) the gateway cannot know. Voltage,
    frequency, power factor and THD keep their grid defaults.
  • Update from template can add what the template gained (ticked in
    the review, or ?add_new=true): the template's calculated fields a unit
    lacks; existing rows and formulas stay as they are.
  • seplos_bms_v3_rtu_tap 1.9.0: warning_count (the alarm flags without
    the protections) is the declared warning, so one protection event is one
    danger, not a warning and a danger.
  • fronius_solar_api_site: the load reads Load seen by Fronius
    (− consuming)
    — the datalogger's figure is negative while consuming and
    leaves out a battery or an inverter behind the meter it does not know.

Multi-Bus Gateway 3.86.1

Choose a tag to compare

@sm26449 sm26449 released this 07 Oct 15:58

Fixes from the post-release audit of 3.86.0; nothing to do when upgrading.

2026-10-07 — audit fixes

  • Home Assistant: Update from template withdrew an installation's HA
    entities (stopping the units does) and never published them again until
    the next broker reconnect. It republishes them now, and a device refresh
    pushes its new labels to HA.
  • Alarms count the same everywhere: the fleet now ignores a value gone
    stale, as the installation page did (a silent pack no longer alarms in
    the fleet forever); a field the template declares as an alarm is not
    counted a second time through a threshold on it; "0.0", NaN and —
    read as nothing active, on the server as in the browser.
  • History of a grouped installation: a total's chart mixed the points of
    every group that has a total of that name. /api/history reads the first
    group by default and takes group= for the others.
  • Update from template keeps a row's unit when the operator changed its
    scale or offset (a W register read as kW stays kW).
  • Cell grid: an unused or unread slot (0 V) is no longer marked as the
    weakest cell; the marking is also announced to screen readers. A bitmask
    read from a signed register lists its real bits; masks up to 53 bits.
  • The dashboard rebuilds its sections when the template's labels, categories
    or display block change (after an Update from template, or switching
    between two devices whose registers share addresses).
  • A tiles pattern must be plain syntax both Python and the browser read —
    (?i) or (?P<…>) are refused at load instead of failing silently.
  • Hardening: a template's category name and a total's unit are escaped
    where they reach the page.
  • A viewer no longer sees Test, Test push, Delete source or a
    command's Run / Test (the server already refused them).
  • The alarm pill says what it means to a screen reader on a phone; two
    section names missing from the translations added; a validation error from
    Update from template reads as text, not [object Object].
  • GET /api/endpoints keeps aggregate_fields for a one-unit installation
    (empty since 3.86.0).

Multi-Bus Gateway 3.86.0

Choose a tag to compare

@sm26449 sm26449 released this 07 Oct 15:40

Upgrade notes (no config change needed): an installation total is no
longer computed twice — a field whose template says how it combines is not
also combined by the name rules. For a bank on the bundled
seplos_bms_v3_rtu_tap map, energy_remaining (same as
pack_energy_remaining) stops at once; power_active_total (same as
pack_total_power) stops after Update from template on the
installation, since an existing unit keeps the totals its rows were seeded
with. Their last retained MQTT message stays on the broker until you clear
it; read the pack_* names. The serial-bridge image now takes Debian security updates at build
time (the scan flagged perl-base and libpcre2).

2026-10-07 — the template decides how a unit is shown

An audit of a real battery bank, through a viewer account, found the UI
choosing what to show from lists hardcoded by role and by name. A battery
fit neither: its units showed "—" for power, its totals were raw names
without units, its headline read "Producing now", its cells read "3.29 V"
sixteen times, and 34 alarm flags at zero filled a section. What a unit of
a kind shows now comes from its template's display block — read live,
so improving a template improves every unit at once:

  • Names and columns: unit_label, icon, glance (the columns of a
    unit row: power and SOC for a pack), hero (its fleet row), headline
    (the installation's top line, from its totals, with a hint such as the
    sign of a battery's power).
  • Alarms (alarms: [{field, severity}]): fields whose non-zero value
    is an alarm — a BMS's alarm, protection and failure counts. The fleet
    counts them (a pack in alarm no longer shows 0), each installation unit
    carries alarms, the installation units_alarming; the unit row gets a
    pill, the unit page lists what is active.
  • Sections (sections: {category: {widget}}): grid turns a
    section into tiles with the lowest and highest marked — a pack's weak
    cell at a glance — with tiles (which fields) and decimals (read to the
    millivolt, summaries included); active_only shows only what is active,
    or Nothing active.
  • Bitmasks (bitmasks: {field: "Cell {n}"}): a mask reads as what is
    set — Cell 3, Cell 7, — when clear.

It is validated at load: the fields it names against the template's own
(headline names totals, so only its shape), section keys against the
template's categories. Without a block, the role decides as before. Reference:
config-reference display:, MANUAL §5.3.

  • Totals speak: each inherits its source field's label and unit
    (SOC (average) %); a total pooled over several fields is named after
    itself; the grid lists the headline first, then the template's order,
    folds after 12, and reads percentages and temperatures to one decimal.
    New operation mode — the majority value, text included: a bank's
    one-word status (text, so it has no history series).
  • Template categories reach the register list, so the dashboard and
    the unit page group a vendor's fields as its template files them.
  • Update from template (admin, on the installation page; a standalone
    device through the API): carries
    improved labels, units, descriptions, categories and totals over to the
    rows a unit already has; the selection, flags, sinks, thresholds and
    formulas stay yours.
  • A viewer sees no control it cannot use; the unit table fits a phone;
    the tap's wire health (frames, CRC errors, resyncs) shows under How it
    is read
    .

seplos_bms_v3_rtu_tap 1.8.1 declares all of the above, with clear labels
for the BMS current limits and the signs of power and current, and a bank
pack_status.

Fixed along the way:

  • Formulas loaded at boot only: a device or installation created or edited
    at runtime ran no calculated field until the next restart.
  • An installation's history charts were always empty: its totals are
    written as fields named after the total, and the history query looked for
    a name tag. It now reads them as they are written, and the picker offers
    them.
  • The login no longer says "Session expired" on a first visit; a unit page
    no longer shows "No values yet" over live values.

A post-release audit found a few issues (Home Assistant entities after Update from template, alarm counting, grouped history); they are fixed in 3.86.1 — upgrade to that.

Multi-Bus Gateway 3.84.2

Choose a tag to compare

@sm26449 sm26449 released this 03 Oct 18:33

2026-10-03 — direct serial that works, MQTT input that speaks BMS

  • Direct serial mode could never open its port: the documented mode
    (docs/rtu-serial.md §1) maps /dev/ttyUSBx into the container, whose
    node keeps the host's root:dialout 660 — and the mbg user had no
    dialout membership, while the entrypoint's setpriv --init-groups
    drops any docker group_add. The image user joins dialout; the doc
    gains a tip for stable /dev/serial/by-id mappings. Found moving a
    Fronius Smart Meter 65A-3 from the serial-bridge to direct mode.
  • The MQTT input ingests what BMS collectors actually publish: an
    enum register accepts its LABELS (ON, Standby) via reverse lookup
    — symmetric with the HA write face — and a string:N register carries
    free text (bounded) into the store; unknown labels drop. Register
    source topics support a relative ~/leaf form resolved against the
    device's base topic, which is what lets ONE template serve N units.
  • New built-in template seplos_bms_mqtt: the full 58-field map of
    one seplos-bms-mqtt
    battery pack (battery / cells / temperature / alarms / diagnostics),
    register topics relative — set the device's input topic to
    seplos/battery_N/# and seed from the template. Per-register defaults
    keep MQTT/Influx re-publish off: the collector stays the writer, the
    gateway adds per-pack health, dashboards and thresholds.
  • The register span-overlap warning (a Modbus concept) no longer fires
    for topic/json_path registers — synthetic consecutive addresses are
    the norm there, and a Seplos pack printed 57 bogus lines per boot.

Multi-Bus Gateway 3.84.1

Choose a tag to compare

@sm26449 sm26449 released this 02 Oct 07:24

2026-10-02 — urllib3 2.8.0 (release gate caught PYSEC-2026-4175/76/77)

The v3.84.0 images were never published: between the audit work and the
tag, three urllib3 2.7.0 advisories landed (chunk-size buffering DoS,
a Deflate-stream infinite loop, and HTTPS-proxy TLS-context mix-ups) and
the release workflow's pip-audit/Trivy gate refused the build — exactly
what it exists for. 3.84.1 is 3.84.0 plus urllib3 2.8.0 in the lock.

3.84.0 (never published — see above)

2026-10-02 — dead code: the audit's pruning list

Everything below was grep-verified unreferenced (including from tests,
the UI's fetch calls and the docs) before deletion:

  • Python: MQTTPublisher.publish_status, InfluxDBPublisher.write_single
    (+ its _get_measurement/_get_tags delegation shims — the one test
    using them now calls the module-level function),
    RegisterParser.parse_registers, Config.get_registers_by_poll_group,
    RegisterEncoder.register_count (call sites read REGISTER_COUNTS
    directly), EsphomeClient.unarchive. The test-seam trio
    (_EndpointArbiter.turn, PqRecorder.ring, the _should_publish/
    _confirm_publish lock-wrappers) stays — tests exercise them.
  • JS: removeRegister (the live path is removeRegisterFromTable), the
    query-history feature remnant (renderQueryHistory, its unbounded
    state and its vanished #queryHistory element), renderStatusDetails
    (~70 lines re-rendered into a container deleted long ago, on every
    status poll while on Settings), updateSettingsStatusDots, and the
    null-guarded writes to the departed #registerCount, #dropzoneHint
    and #monitorTableEmpty elements.
  • CSS: 39 rule blocks whose classes appear nowhere in the HTML/JS — the
    old config-page layout (config-form*, connection-*, danger-zone*,
    poll-group-*, json-preview, apply-config-banner), threshold-grid*,
    widget-badge/tall, status-card*, btn-success/warning,
    text-accent/success/warning and friends (−237 lines). chart-area
    stays: chart-${...} class names are composed dynamically.
  • i18n: 63 keys no code references anymore, removed from en and ro
    together (1496 keys, parity intact).
  • datetime.utcnow() (deprecated) → datetime.now(timezone.utc) in the
    self-signed-cert validity window.

2026-10-02 — i18n: the hardcoded-English sweep

  • t() learned {name} interpolation, so the three sentences that were
    glued from fragments (lockout minutes, scan-result counts, rule
    confirmations) are single translatable keys with placeholders — word
    order is the translator's again.
  • 73 new keys cover what rendered in English even in Romanian: the
    statusbar, the enum-builder modal, the virtual-meter page and its
    instance/delete modals, the raw-config and customize-dashboard modals,
    the History page's hints/axis notes/empty states, the Monitor and
    Dashboard empty states, and the register editor's header and footer.
  • The phase-convention options carried Romanian wording in the ENGLISH
    base ("maro / negru / gri"); the English labels are English now and the
    Romanian wording lives in ro.json, where it belongs. Option values are
    untouched, so saved configs read back identically.
  • en.json and ro.json remain in full key parity (1559 keys).

2026-10-02 — infra: the serial-bridge joins CI, pins, teardown

  • The serial-bridge image is built and Trivy-scanned in CI — it never
    was: a broken Dockerfile or a debian CVE surfaced first on release day.
    Same HIGH/CRITICAL gate as the gateway image.
  • Grafana and ESPHome are pinned in docker-compose.yml (13.0.2 /
    2026.5.3) instead of :latest — a bad upstream release no longer lands
    on every compose pull; Dependabot's new docker-compose ecosystem
    keeps the pins moving (the docker ecosystem only watches Dockerfiles).
  • The gateway's entrypoint chowns the config volume only when it arrived
    with foreign ownership
    (a recursive chown over snapshots and buffer
    files on every start is slow and wears SD cards) and tolerates a
    read-only config mount instead of failing the boot; the serial-bridge
    entrypoint gets --no-new-privs for standalone docker run parity;
    init: true on the gateway service reaps would-be zombies.
  • Leaked rules threads are stopped between tests: the 9162777 flake's
    root cause was an earlier test's rules thread still sweeping its units
    under random ordering — that fix immunized one victim; an autouse
    teardown now stops every live runtime after each test.
  • --timeout=120 moved into pytest.ini addopts (a bare local pytest
    could hang forever); requirements.txt caps the next MAJOR of
    fastapi/uvicorn/pydantic for bare-metal installs (the image ships the
    hash-pinned lock regardless); rtu-serial.md states the control-API
    binding precisely (host-local publish, not an in-container bind).

2026-10-02 — UI: correctness and accessibility

The audit's frontend findings — first the bugs, then the access gaps:

  • One visit to the Status page froze the titlebar for the session. The
    page's 2 s refresh reused the app-wide _statusTimer handle, so leaving
    the page cleared the global 5 s status loop: health dots, the
    devices-online count and the connection-lost detection all stopped. The
    page has its own timer now; its refresh also got the last-wins sequence
    guard History/PQ already had, and it preserves keyboard focus across the
    re-render (the embedded Test button was unreachable — focus dropped
    every 2 s).
  • Settings → Alerts "Test" looked dead. Its result span shared an id
    with the Status banner's, so the outcome rendered into a hidden element.
    The handler resolves the span next to the button now.
  • Modal keyboard traps. Escape closed TWO stacked modals (the trap
    didn't stop propagation, and the global fallback closed the first modal
    in DOM order — now the top-most). Enter with focus on Cancel or ✕
    SAVED the edit in three modals; it now confirms only from a text field.
  • Stale shown as live: on a WebSocket loss the dashboard kept its last
    numbers looking live — it dims now, with a "values as of HH:MM" badge.
  • Rapid dashboard device switching could land a slow /api/values
    response in the wrong device's store (sequence guard); localStorage
    access can throw in private windows — boot survives it now; the status
    banner double-escaped device names (A&B showed as A&amp;B);
    setButtonLoading hard-coded "Saving..." in English and stripped the
    button's icon on restore.
  • Accessibility: the History measurement picker and its category
    headers are keyboard-operable (role, tabindex, Enter/Space — they were
    mouse-only, a keyboard user could not chart anything); the dashboard
    customize list gained move up/down buttons (reorder was drag-only) and
    its checkboxes/selects accessible names; status dots carry their state
    as text (title + aria-label) everywhere, not color alone; warning/danger
    TEXT uses the theme's --warning-text/--danger-text tokens (the
    hard-coded ambers were ≈2.2:1 on the light theme); focus rings restored
    where outline:none had no replacement, and the theme toggle got one
    plus an accessible name; 12 form labels got their for; duplicate
    aria-label attributes, a nested <label> and an empty spacer label
    fixed; secondary tab bars expose tablist semantics with
    aria-selected; JS-built tables set scope="col"; and a new
    data-i18n-aria pass translates aria-labels with the rest of the UI.

2026-10-02 — rules: clocks, stale semantics, threshold clears

The audit's remaining rules-layer findings, fixed with regression tests:

  • Rule timing runs on the monotonic clock now. The rate-limit,
    reassert and staleness windows were measured on the wall clock: a chrony
    step backward (the 2026-07-28 +138 s jump class) froze them — including
    the reassert that restores a Fronius limit after RvrtTms — and a step
    forward prematurely expired overrides and clamps. evaluate() takes the
    interval clock plus a separate wall for Decision timestamps and the
    wall-persisted absolutes (paused_until, clamp expiry), which the UI
    shows as dates and which must survive restarts as wall time.
  • Threshold clears are delivered. Alarm and "back to normal" share a
    rate-limit key, so within min_interval_s the clear was swallowed and
    alarm→normal→alarm delivered only the first alarm. Band transitions
    (edge-triggered by design) now bypass the per-key limit; and a
    danger-only threshold (no warning bound) gets the same clear-side
    deadband every other band has, instead of flapping on every harvest.
  • Rules persistence is crash-safe. rules_state.json (operator clamps
    — setpoint ceilings! — and overrides) was written non-atomically and a
    torn file silently loaded as empty; rules.yaml was replaced without
    fsync, so a power loss could leave it empty — and an unreadable
    rules.yaml silently disabled EVERY rule with only a log line. Both files
    write temp+fsync+replace now, an unreadable rules.yaml raises an alert,
    a corrupt state file warns that clamps were lost, and a hand-edited
    duplicate rule id says which copy wins.
  • Recovering from STALE straight into the dead band restored the pre-stale
    state only AFTER the desired-state computation had already produced a
    synthetic 'stale' state (severity 99, duplicate warn events,
    ping-ponging with the restore every tick); the restore runs first now.
  • The operator clamp caps on_stale fixed/safe wants too (they used to
    bypass it); a fast (emergency) step and a stale safe want no longer
    sit out the min_interval_s window a routine step opened moments
    earlier.
  • A command verify/guard rule against a non-numeric read-back (an enum
    decoded to text) answers a rejected verdict instead of raising a 500
    out of the face; the alert rate-limit memo is bounded (it grew one entry
    per key forever).

2026-10-02 — data path: the audit's correctness findings

The same audit's ...

Read more

Multi-Bus Gateway 3.83.1

Choose a tag to compare

@sm26449 sm26449 released this 24 Sep 19:44

Multi-Bus Gateway 3.83.1 fixes a UI regression introduced by the strict Content Security Policy of 3.82.0: the shared modal's Save/Run button (installations, groups, sources, command dialog) did nothing. Upgrade from 3.82.0, 3.82.1 or 3.83.0. Images: ghcr.io/sm26449/multi-bus-gateway:3.83.1 (amd64 + arm64, signed provenance).

2026-09-24 — the shared modal's Save button was dead under the strict CSP

  • The installation, group, source and command editors share one modal
    whose Save/Run button was repointed from script with a handler
    attribute. The script-src 'self' policy of 3.82.0 refuses such a
    handler exactly like a static one, so since 3.82.0 saving an
    installation, a group or a source, and running a command from its
    dialog, did nothing (the console said why; the page did not). The button
    is repointed through the delegated data-action now, and the CSP guard
    test also catches handlers set with setAttribute. Found by the
    commands end-to-end script during the 3.83.0 verification.

Multi-Bus Gateway 3.83.0

Choose a tag to compare

@sm26449 sm26449 released this 24 Sep 18:19

Multi-Bus Gateway 3.83.0 is a security release: every Critical, High and Medium finding of an independent second-pass audit (2026-09-24) is fixed, each with a regression test. Upgrade before running a command with a lease on a production inverter — 3.82.1 and earlier could un-curtail a plant to 1 % after a crash. Images: ghcr.io/sm26449/multi-bus-gateway:3.83.0 (amd64 + arm64, signed provenance).

2026-09-24 — SECURITY: findings of an independent second-pass audit

An independent audit of the public repository (Copilot, 2026-09-24) found
what ours had missed. Every Critical and High finding is fixed here, each
with a regression test that fails on 3.82.1. Upgrade before running a
command with a lease on a production inverter.

  • Critical — a crashed gateway un-curtailed a plant to 1 %. A dead-man
    lease taken by a command (power_limit with lease_s) was persisted
    with a raw envelope; after a crash, boot recovery wrote the bare word
    100 into WMaxLimPct, which a SunSpec scale factor of −2 turns into a
    1 % limit with the enable bit still set and no device-side timeout.
    Recovery now runs the command's own safe recipe, exactly as the live
    revert does.
  • High — raw /write on a scale-factored register. The declared write
    path took the template's scale but not scale_from, so writing
    power_limit_pct = 60 sent the word 60 (0.6 %) and the read-back
    "verified" it through the same wrong scale. A register with a live scale
    factor, or one a command writes first, is refused with 422 and pointed
    at the command — the Home Assistant face has always done this.
  • High — a lease was not armed when the frames went out but the
    read-back failed.
    An unverified or mismatch command left the limit
    on the device with no revert timer. The lease is armed whenever at least
    one frame was sent.
  • High — the API key doubled as the admin role. With API_KEY set,
    every operator must present the key on every write, and the key alone
    unlocked raw (unguarded) writes, secret exports and full snapshot
    downloads. With login on those are admin-only now; the key stands in
    only on a box without login. docs/API.md, SECURITY.md and the
    hardening checklist say so.
  • High — the "sanitized" export leaked nested credentials. Only
    connection.* and rest_push.* were stripped; a device's http.headers
    and mqtt_in.password, the credentials of multi-source devices and of
    installation sources went out in clear in a backup any viewer could
    download. Redaction now walks the whole devices[] and endpoints[]
    trees (secret-named fields dropped, every URL redacted) and the
    merge-import restores the live values for all of them.
  • High — an infinite value froze a virtual meter with its fail-safe
    disarmed.
    OverflowError and struct.error escaped the row guard,
    aborting the block rebuild; an MQTT publisher could trigger it. They are
    caught, and a non-finite source value is treated as missing rather than
    served as a float32 infinity.
  • High — a device whose template did not load polled anyway with
    byte_order: big.
    A little-endian meter would have published
    word-swapped garbage as if it were fine. The source stays idle and the
    log says which template is missing.
  • High — round(x, -10**8) hung the poller or the rules thread.
    CPython computes 10**|n| for it. The second argument of round() is
    bounded (±15) at save time and at evaluation time, and a result that is
    not finite or wider than 64 bits never leaves the expression engine.

Medium findings fixed in the same release:

  • override_s (the seconds a controller may pause the rule that owns a
    command) is bounded to 24 h and must be a finite number; the rule is
    paused only once the command has passed every gate, so a rejected
    command no longer silences a safety rule (F-09).
  • The Home Assistant number/select set path honours the device write
    lock, like the HTTP and command faces (F-13).
  • Every /api/ write body is capped at 8 MB before it is read (import
    keeps its own 25 MB cap); a chunked body without a length is refused
    (F-19).
  • The bundled broker refuses to start with the .env.example placeholder
    secret or with a mosquitto.conf that still allows anonymous clients;
    .env.example ships the four required secrets commented out instead of
    with placeholder values (F-22, F-23).
  • GET /api/commands/history is admin-only while login is on, as
    documented — it is the audit log filtered (F-33).
  • Documentation: the primary device is write-locked by default, not
    "always read-only" (F-34); the Romanian manual's stack paragraph
    described the pre-3.81.0 anonymous broker and an always-on MQTT
    Explorer (F-35).

More Medium findings fixed in the same release:

  • Command guard and verify reads use each register's own function code —
    a guard on an input register was read with FC3 and could never hold
    (F-14).
  • Renaming or removing an account revokes every session and deletes the
    old name's passkeys (F-16); a passkey credential id belongs to one
    account — re-registering it under another is refused with 409 instead of
    replacing the owner's passkey (F-17).
  • validate_template rejects boolean addresses, non-finite scales,
    offsets and write bounds, string registers longer than 125 words, and a
    write_safe outside its own envelope (F-21).
  • The boot last-known-good seatbelt restores only when the config cannot
    be parsed or validated; an OSError, ImportError or any other failure is
    re-raised instead of rolling credentials and settings back (F-26).
  • The session store keeps at most 256 live sessions, oldest first (F-27).
  • A clean shutdown reverts every live dead-man lease before the devices
    disconnect; a revert that fails stays on disk for boot recovery (F-28).
  • Rule definitions reject NaN and ±inf in on_stale, step values and
    thresholds (F-30).
  • A source stamp up to one second ahead of the virtual meter's rebuild is a
    thread race, not a corrupted stamp: it no longer causes a one-tick
    meter stop (F-31). A virtual meter whose server cannot start (port in
    use, bad interface) retries with backoff and logs on the first and every
    tenth failure instead of flooding the event ring every tick; bind must
    be an IP address (F-32).
  • VENDOR-DATA.md says what the bundled maps actually carry — the register's
    short functional label, not the manual's prose (F-36).
  • The import route's disk and Modbus work runs off the event loop (F-38);
    the .good/.bad self-heal copies of register selections are written
    0600 and atomically (F-39).
  • A rule's release-to-safe reports the command's verdict: a safe recipe
    that did not write or verify is logged and shown as FAILED, not as
    "released to safe" (F-10).
  • Dead-man reverts are consistent on both paths: a revert runs even if
    writes were disabled or the device locked after the limit was placed
    (otherwise the limit would stay forever), and the raw-register revert is
    audited like every other write (F-11).
  • Commands meet the written registers' own write_* envelope, refuse to
    write input or discrete registers, and refuse a value the encoder would
    have to clamp; the raw /write route refuses the clamp case too and its
    audit record carries the words that went out (F-12, F-15).
  • A backup import or snapshot restore is checked with the live routes'
    rules before a byte is written: allowlist and trusted-proxy entries must
    parse, the webhook must be http(s), write flags must be booleans, HTTP
    sources must be on the LAN unless allowed, rules must validate. A live
    allowlist entry that does not parse now denies every client until fixed
    instead of silently admitting everyone (F-18).
  • Saving a user template removes any other file carrying the same id, and
    deleting one removes every file carrying it, so a hand-copied twin can no
    longer shadow a tightened envelope or resurrect a deleted template (F-20).
  • The release workflow runs the test suite, lint, the vendor checksum and
    pip-audit on the tagged commit before it builds, and publishes a
    Sigstore-signed build provenance for both images, verifiable with
    gh attestation verify (F-25).

Low and informational findings fixed in the same release:

  • The serial-bridge container gets the gateway's privilege model
    (cap_drop: ALL + the entrypoint's five capabilities, no-new-privileges,
    digest-pinned base image) and its unauthenticated RS-485 endpoints sit
    on a private internal network shared only with the gateway (F-24).
  • Virtual meters cap client connections per meter (transport. max_connections, default 16; the surplus is closed) so a peer holding
    sockets open cannot exhaust file descriptors (F-60); transport. strict_unit_id: true makes a meter answer only its configured unit id
    (F-61, opt-in — the default still answers any id, as before); the
    hold window counts from the moment a row went stale, not from the
    value's own timestamp, so rows whose freshness bound exceeded the hold
    time hold at last (F-62).
  • MQTT-in sources cap the payload at 64 KB, survive a deliberately deep
    document, and skip one hostile field instead of dropping the whole
    message (F-63).
  • The last-known-good timer, unit freshness in installation aggregates
    and the MQTT heartbeat use the monotonic clock, so an NTP step no longer
    marks every unit offline or short-circuits the LKG deadline (F-58).
  • A failing lease revert logs one ERROR, then retries at debug level and
    logs its recovery, instead of twenty ERROR lines a minute (F-59).
  • mbg/rules/<id>/set may disable a rule but never enable one (enabling
    an armed rule is arming it, which stays with the API); a clamp must be a
    finite non-negative number with an expiry of at most a week (F-74).
  • Command leases and raw-write leases live in separate namespaces, so a
    raw leased write can never replace a command's recipe revert (F-75).
  • /api/query/batch is capped a...
Read more

Multi-Bus Gateway 3.82.1

Choose a tag to compare

@sm26449 sm26449 released this 24 Sep 09:54

Multi-Bus Gateway 3.82.1 is a documentation release for the public repository: a README that says what the gateway is and where it runs, screenshots recaptured from the current UI, a regenerated device catalog, and one lint fix. Images: ghcr.io/sm26449/multi-bus-gateway:3.82.1 (amd64 + arm64).

2026-09-24 — documentation for the public release

  • README (English and Romanian): the opening says what the gateway is and
    where it runs — the author's own PV site since the 3.0.0 release, with
    the hardware named and the less-exercised parts called out — and the
    Janitza ancestry moved to a History section at the end.
  • Device catalog regenerated: it had been generated before the three
    Fronius Solar API maps existed, so it listed 13 of the 16 bundled
    templates; the READMEs quoted 11. One number everywhere now.
  • Screenshots recaptured from the current UI (installations, unit
    workspace, commands, rules); the visual guide gained sections for the
    pages that did not exist when it was written, and the capture script
    (tools/e2e/capture_docs_screenshots.mjs) is in the repository so the
    images can be regenerated consistently.
  • The plant-wizard e2e script selected a button by its former inline
    handler; it uses the data-action attribute now.
  • The Templates page showed an amber "non-canonical field names" warning
    for the bundled Fronius SunSpec inverter map: three scale-factor
    registers that no hardware in the field implements are deliberately
    left out of the curated set, and the lint mistook "not curated" for
    "routed". Registers without defaults are skipped by the lint now.