Skip to content

fix(gateway): apply direct battery charge windows as paired slots - #5531

Merged
springfall2008 merged 4 commits into
mainfrom
fix/hub-charge-slot-pair
Oct 14, 2026
Merged

springfall2008 merged 4 commits into
mainfrom
fix/hub-charge-slot-pair

Conversation

@mgazza

@mgazza mgazza commented Oct 13, 2026 •

Copy link
Copy Markdown
Collaborator

When a direct Hub battery's new charge start equals the old charge end, separate start/end commands briefly form a zero-length slot. The Hub refuses the first command, then accepts the new end with the old start. A midnight-spanning window can consequently retain an earlier start while the engine logs the intended pair.

Send both endpoints in one existing set_charge_slot command for direct battery inverters and verify both endpoint entities through the normal applied-ACK/telemetry path. Refusal, staged or incomplete ACKs, timeout and publication errors stop the update before a success log or enable write. Track both endpoints in the control ledger, preserve read-only and entity ownership checks, and retain the existing EMS/Gateway coordinator staging sequence and other provider paths.

Validation: Gateway, inverter and inverter-write-poll suites pass, including nine joined engine/JSON/ACK regressions covering the observed failure shapes, endpoint swap, errors, read-only, ownership, ledger, repeat-cycle and coordinator behavior. Changed-file pre-commit checks pass. Fresh GitNexus analysis reports four expected files, medium risk and two indexed affected flows.

Focused review found and verified the correction for async bridge exceptions: select coordinator staging synchronously and require explicit publication success, so a swallowed coroutine exception cannot trigger individual fallback writes. Deeper independent review traced scheduling, entity ownership, full-serial routing, ACK cache, retry/backoff, ledger and coordinator compatibility and found no additional confident regression. All nine new tests independently pass. Normal CI38331810602 passes at08d96daf1a071e67eda05f2283e7fa67d00eab3d. Ready for review; physical validation remains outstanding. MQTT transport, inverter state and ACK delivery are synthetic in these tests. No physical hardware validation, deployment, firmware release or customer configuration change.

@mgazza
mgazza marked this pull request as ready for review October 13, 2026 01:28
springfall2008 pushed a commit that referenced this pull request Oct 14, 2026
…te (#5532)

* fix(gateway): never enable discharge/export after a failed window write

For a direct Hub battery, adjust_force_export wrote the discharge start
and end as two separate set_discharge_slot commands, then enabled the
scheduled discharge and switched to export mode regardless of whether
the endpoint writes were applied. schedule_write_ok only withheld the
later commit record. When the end write was refused after the start was
accepted, the Hub was left enabled with a stale wrapping window
(e.g. 02:55-00:01) and actively exporting.

Send both endpoints in one full-serial set_discharge_slot for direct Hub
batteries and verify both endpoint read-backs before continuing. Any
refusal, staged or partial ACK, timeout, publication error or swallowed
async-bridge exception returns before the enable write, export mode and
commit, leaving the commit pending for the next cycle to retry. Both
endpoints are tracked in the control ledger and share one backoff key.
Read-only, entity ownership and serial checks refuse before publishing.
Coordinators keep their staged separate-endpoint sequence; other
providers, disable and midnight windows are unchanged.

Mirrors the charge-window fix in #5531.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018BaJjLpuMEtQYfvmTRcFJf

* fix(gateway): detect coordinator staging before discharge ownership checks

A coordinator whose discharge end entity was not (yet) owned by the
gateway was refused with an ownership error instead of keeping its
staged separate-endpoint sequence. Identify coordinators first, as that
decision only needs the entity names and discovery record.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018BaJjLpuMEtQYfvmTRcFJf

* fix(gateway): stop an armed export when its window move is refused; no re-poll of a refusal

- When the paired window write fails while scheduled export is already
  enabled, turn the schedule off and return to Eco instead of leaving
  the previous window exporting on a plan that has moved away from it
  (skipped in read-only mode).
- A refusal of the exact paired command ends the attempt immediately:
  the poll stops when it arrives, and a retry inside the ack window is
  not published (_send_control would suppress it anyway), so a refused
  write no longer holds the engine for the full poll timeout per retry.
- Drop the unreachable end-serial comparison (the end entity name is
  derived from the start, so it carries the same serial) and name the
  test case after what it exercises.
- Document that an exception inside a running event loop reaches the
  caller as None via run_async's thread, not as an exception.
- Test the write/moved counters for the paired path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018BaJjLpuMEtQYfvmTRcFJf

* test(gateway): a successful move of an exporting schedule stays enabled

The fail-safe disable applies only to a failed window write.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018BaJjLpuMEtQYfvmTRcFJf

* fix(gateway): only an explicit Hub refusal turns off an exporting schedule

The fail-safe disable fired on any unverified window write, including a
slow but successful one (ACK outside the poll, older firmware without
an applied block, telemetry not yet landed). That stopped a correctly
re-windowed export for a cycle and toggled enable/mode. Take the
disable path only when the Hub refused the exact paired command; an
unconfirmed write leaves the running export alone and keeps the commit
pending for the next cycle. Document that execute.py still reports
"Exporting" for a cycle whose window was refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018BaJjLpuMEtQYfvmTRcFJf

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: CI <ci@example.com>
CI and others added 2 commits October 14, 2026 08:09
Resolve the run_gateway_tests conflict with #5496 and #5532 by registering
TestGatewayChargeWindow, TestGatewayDischargeWindow and TestGatewayDisplayOnly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A coordinator whose charge end entity was not (yet) owned by the gateway
was refused with an ownership error, so its charge window was never
written, instead of keeping its staged separate-endpoint sequence.
Identify coordinators first, as that decision only needs the entity names
and discovery record. Same ordering as the discharge side in #5532.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@springfall2008
springfall2008 merged commit 3b236e8 into main Oct 14, 2026
2 checks passed
@springfall2008
springfall2008 deleted the fix/hub-charge-slot-pair branch October 14, 2026 07:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants