Skip to content

Outcomes and audit

Direct Connect Bridge. Mirrors the Modbus Bridge page Outcomes and audit section for section.

How you find out what a schedule did: the History view, the run queue, the dispatch record and notifications. Also the controls that act outside the schedule: Run now, Stop, and the API-only Pause and Resume.

Where to look

Where What
Scheduler → History every decision the scheduler made and every change to a schedule, newest first (GET /api/schedules/log)
Run queue (GET /api/schedules/runs, GET /api/schedules/stats) one row per run the scheduler expected: pending, running, ok, failed, missed, skipped, unknown, paused, stopped. No view in the UI.
Card on the Schedules view the last result with its date, Next:, ● active
Logs tab the application log: each fired schedule (info) and each missed run (warning)
Control-log (GET /api/control-log) Pause and Resume calls

Not in this bridge: a per-command control log with requester and hardware state before and after, and a dispatch-outcomes record (see below).

Activity Log results

The History view is this bridge's activity log. It loads the latest 200 rows. Times are in bridge-host time. The chips at the top filter by status (one chip per status present); battery shows the rows written by the force path and the VPP monitor.

Status Meaning
created, updated, enabled, disabled, deleted a change to the schedule ("created enabled", "settings changed", "toggled", "removed")
fired the action and the on-entry HA actions ran and were judged a success; the detail is what each returned, or "nothing to do"
gated the window was open but the entry conditions failed; up to two failing rows with live values; once a day per schedule and gateway
waiting conditions met and the hold is counting, or If busy = wait
deferred lost a same-tick contest, or If busy = defer
exit the window closed: "exit condition met" or "window closed"
error the action failed; with the attempt number and the next retry
unknown the outcome could not be told; not retried (see Retries)
missed the window closed with no successful run
coalesced an open run left by downtime was folded into the current one
manual Run now
stopped, paused, resumed Stop, or the Pause and Resume API calls
dispatch-start, dispatch-end a force started or ended (also the watchdog's duration and target-SoC releases); under battery
vpp-start, vpp-stop, orphan the VPP monitor; under battery
exception the scheduler tick itself raised an error (a bug, not a rule)

Changes to a schedule are logged with a short word only; the detail does not list which fields changed. Import creates schedules without a created row.

Through the API, GET /api/schedules/log filters by schedule_id, status and severity, newest first, limit 1–500.

Each row has a severity class: error for error, failed, missed and unknown; exception for exception; event for everything else. Each class has its own cap (2000, 1000 and 500 rows), so routine gated rows cannot push a failure out.

A run that did nothing still leaves a row: an HA-only schedule with no HA actions logs fired — "nothing to do".

The run queue (Direct Connect only)

Direct Connect only. Before it acts, the scheduler writes the run it expects into the occurrences table: one row per schedule, gateway and occurrence. The row records attempts, the outcome and the reason. GET /api/schedules/runs lists the rows, newest first; GET /api/schedules/stats counts runs, ok, missed, failed, skipped, stopped and paused per schedule, with the first, last and last failure. Finished rows are kept 90 days.

If several runs of one schedule on one gateway are still open, for example after downtime, the older ones are closed as skipped — "coalesced into the run for …" and logged coalesced. Only the newest proceeds; a backlog is never replayed.

Retries

A failed action does not use up the occurrence. The run is left open and tried again while the window is open:

  • Between ticks. After N failed attempts the next try waits 30 s × 2^(N−1), capped at 10 minutes: 30 s, 60 s, 2 min, 4 min, 8 min, then every 10 min. Each failure logs error — "… — attempt N; retrying in 60s while the window is open".
  • Within one tick. A device write (force, mode, smart circuit, off-grid) is also retried inside the call: up to 5 attempts for a force (waits of about 5, 10, 20 and 40 s) and 2 for the others, cut off at the window's end.

  • A success after failures logs fired and closes the run as ok. There is no "recovered after N attempts" wording.

  • A run that is retrying and then loses a same-tick priority contest is closed as skipped.

A long in-tick retry holds up every gateway

The in-call retries run inside the scheduler's tick. A force that keeps failing can hold the tick for over a minute, and no gateway is evaluated meanwhile. Whether this caused the missed heartbeat in #183 is unverified.

Some failures are recorded as fired

The scheduler decides success by reading the action's result text. A result counts as a failure only if it contains "failed", "error" or "not confirmed". So:

  • a force refused because the Modbus client (this bridge) is off, the Modbus TCP Server (aGate :502) does not answer, or SunSpec does not respond, is logged fired and the occurrence is used up ("force charge: This bridge's Modbus client is off …");
  • a Set operating mode or Go off-grid that the aGate answers with a non-zero result is logged fired ("mode -> tou: {'ok': False, 'result': 3}").

Measured with mocks on 2026-10-17. Read the detail of a fired row, not just its status. Issue #191.

An unknown outcome is not retried: if the scheduler is told the outcome is unknown (the write may have landed), it logs unknown, closes the run and does not send again.

In practice the scheduler never reports unknown

The action code never raises; a failure comes back as text, which the scheduler turns into a connection error that counts as "never arrived". So a failed write is always error and retried, never unknown. The test above reaches unknown only through a stand-in. From a code read. Issue #201.

The per-attempt audit rows are never written

The retry helper calls its audit hook in a form the scheduler's hook does not accept. The error is swallowed, so no per-attempt row reaches the History view; only the scheduler's own fired / error row does. From a code read. Issue #201.

A window that closes without success is recorded as missed. On every tick the scheduler closes runs still pending, running or failed whose window ended more than 2 minutes ago, and logs missed — "window closed with N attempt(s) and no success". There is no catch-up: a missed run is never fired late.

Not in this bridge: failure and recovery notifications for retries (see Notifications).

Supervision of a running command

Not in this bridge. After a force is accepted, the bridge does not check that the setpoint stays enabled, does not check that the battery moves, and never re-sends. The only watch on a running force is the watchdog's duration and target SoC (see Exit and release). Tracked in #162.

The dispatch record

Not in this bridge as a per-command outcome record. The closest thing is the dispatches table: one row per force the bridge sends successfully, from a schedule, from Run now or from the Battery Control widget. It records the direction, power, target SoC and window, and a status:

Status Meaning
active running, as far as the bridge knows
ended released by the watchdog, by a window-end or exit Release, or by the widget's Release
superseded a newer force was recorded for the same gateway and host
stopped, paused Stop or Pause
interrupted, released, resumed handled by the boot reconcile after a restart

A refused or failed force writes no row.

There is no REST endpoint for this table and no dispatch-outcomes endpoint (GET /api/gateways/{gw}/dispatch-outcomes does not exist here). It is used by If busy, Stop, Pause and the boot reconcile.

Overshoot guard

Not in this bridge. A force is not released because the battery ran faster than requested. Tracked in #162.

Notifications

Not in this bridge for scheduled actions. A failed, missed or unknown scheduled action sends no notification: the scheduler passes no notifier to its retry helper (#172). To be told, add a Notify HA action to the schedule; note it runs after the battery action on every attempt, whatever the outcome (see Actions).

Direct Connect only. After a restart that interrupted a scheduled force, the boot reconcile notifies your enabled notification devices ("FranklinWH — scheduled dispatch …") under the default policy notify. See Restarts and the clock.

Run now and Stop

The schedule list; Run now and Stop are also REST calls

Control Shown What it does
⋮ → Run now always (asks first) Runs the battery action and the on-entry HA actions now. POST /api/schedules/{id}/run
Stop while the card shows ● active Releases the force this schedule started. POST /api/schedules/{id}/stop
Pause, Resume API only See Pause and Resume.

● active (and so Stop) shows when the schedule is enabled, its window is open on the container's clock, and it has already fired in this occurrence (#161). An Always schedule never shows it.

Run now

  1. Open the card's ⋮ menu and choose Run now. Confirm.
  2. The bridge runs the action now. It ignores the window, the calendar and the entry conditions; each HA action's own guard still applies, and only On entry and Entry & exit HA actions run. It logs manual with what each step returned.

A Run now command differs from a scheduled run:

  • A force runs for the schedule's Duration (min); with 0 it has no time limit and runs until it is released or reaches its target SoC. A Recurring windows schedule has the hidden value (90 for a new schedule).

  • It ignores If busy: it replaces a running force.

  • It is not retried, has no exit conditions and no exit steps, and does not change the card's last result.

  • A force it sends is recorded under the schedule's id, so Stop can release it, and other schedules' If busy sees it.

  • Message placeholders read only the gateway's values and Home Assistant entities: tariff, weather and other sensors, and %run.*%, print ?.

Run now acts on the gateway selected in the top bar

With more than one gateway and one picked in the top bar, the Run now menu item sends that gateway with the request, and the bridge uses it instead of the schedule's own gateway. With Site selected, or one gateway, the schedule's gateway is used. From a code read; not tested in a browser. Issue #193.

Run now acts for real

Run now does exactly what the schedule does: a mode change, a smart circuit, off-grid, or a battery force. Use ⋮ → Test against live first.

Stop

  1. Press Stop on the card. Confirm.
  2. If the dispatches table has an active force recorded for this schedule, the bridge sends a Release, marks the rows stopped and logs stopped — "dispatch released by user". The schedule stays enabled.
  3. If not, it logs stopped — "no active dispatch to stop" and changes nothing.

Stop does not run the exit HA actions, does not restore the mode, and does not stop the schedule firing again: it fires at its next occurrence. A schedule that had not fired yet in this window (nothing to stop) can still fire later in the same window. To stop a schedule from firing at all, disable it: Turn off on the card.

On a multi-gateway site Stop releases the default gateway

Stop sends the Release to FWH_MODBUS_HOST or the default gateway's host, not to the host recorded for the schedule's force. For a schedule bound to another gateway, Stop releases the wrong gateway and leaves its own force running. Pause has the same problem. From a code read; not tested. Issue #192.

Pause and Resume (Direct Connect only)

Direct Connect only, API only (POST /api/schedules/{id}/pause and /resume).

  • Pause finds this schedule's open run (pending or failed, window still open), releases its recorded force if any, marks the run paused and logs paused.
  • Resume returns a paused run to pending, but only while its window is open; after that it answers 409 "window has closed — resume is only valid inside it".
  • A run still paused when the bridge restarts becomes stopped.

Pause does not hold a run

  • A run that succeeded is not "open", so Pause on a schedule whose force is running answers "no run is open for this schedule" and releases nothing.
  • A run that failed can be paused, but the scheduler does not check for paused: on its next attempt it runs the action again and the run becomes failed or ok.

Measured with a mock on 2026-10-17. Use Stop to release a schedule's force, and disable the schedule to keep it from firing. Issue #200.