Repository navigation
fix(gateway): never enable discharge/export after a failed window write - #5532
Merged
Merged
Conversation
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
…hecks 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
…o 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
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
…edule 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
Resolve the run_gateway_tests conflict with #5496 by registering both TestGatewayDischargeWindow and TestGatewayDisplayOnly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
springfall2008
pushed a commit
that referenced
this pull request
Oct 14, 2026
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
pushed a commit
that referenced
this pull request
Oct 14, 2026
) * fix(gateway): send direct battery charge windows as paired slots * fix(gateway): separate coordinator staging from async publication failure * fix(gateway): detect coordinator staging before charge ownership checks 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> --------- Co-authored-by: CI <ci@example.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Bug
For a direct Hub (gateway) battery,
adjust_force_exportwrites the discharge start and end as two separateset_discharge_slotcommands, then turns onscheduled_discharge_enableand callsadjust_inverter_mode(export mode) regardless of whether those endpoint writes were applied.schedule_write_okonly withheld the later commit record.When the Hub accepts the start but refuses the end, it is left enabled on a stale window. In the reproduction below that window is 02:55–00:01, which wraps for about 21 hours, and export mode is active.
Reproduction
This runs the actual
Inverter.adjust_force_export,GatewayMQTTcommand path and ACK cache. The transport is synthetic: it accepts the start and refuses anyset_discharge_slotcarryingend(modbus_write_failed).{start:255},{end:301}(refused),set_discharge_enable true{start:255,end:301}(refused)02:55:00 – 00:01:0000:00:00 – 00:01:00(unchanged)Fix
This mirrors the charge-window fix in #5531.
GatewayMQTT.update_discharge_windowsends both endpoints in one full-serialset_discharge_slot. It refuses read-only mode, non-slot1 or unpaired entities, mismatched serials, coordinators and unparsable times.Trueonly means the command was published.GatewayMQTT.discharge_window_uses_stagingpicks the coordinator path synchronously, before the async bridge. A swallowed exception in therun_asyncthread therefore returnsNone, which counts as a publish failure. It never sends the caller to the legacy individual writes.Inverter.write_and_poll_gateway_discharge_windowruns when both endpoint entities are owned bygateway.adjust_force_export, any failure returns before the discharge target, the enable write, export mode and the commit, and leavesexport_windowcommit pending. Coordinators (GATEWAY_COORDINATOR_TYPES), other providers, the disable path and midnight windows keep their existing sequence.Tests
apps/predbat/tests/test_gateway_discharge_window.pyadds 15 tests, registered inrun_gateway_tests. They cover:Results:
unit_test.py -t gateway: PASSED (434 OK).-t inverter_write_polland-t inverter_multi: PASSED.-t inverterstops on the same pre-existingadjust_target50write_and_poll_switchunpack error locally, with and without this change. Theadjust_force_export*cases before it pass.Notes
gateway.py,inverter.pyand therun_gateway_testsimport). The resolution is to keep both.adjust_inverter_mode(False). An unconfirmed write (slow or missing ACK, telemetry lag) may have applied, so the running export is left alone and the pending commit retries. After a refusal, execute.py still reports "Exporting" for that cycle.🤖 Generated with Claude Code
https://claude.ai/code/session_018BaJjLpuMEtQYfvmTRcFJf