Derive charging state from the wallbox instead of assuming it - #74
Merged
BorisBrock merged 3 commits intoAug 4, 2026
Merged
Conversation
Register 261 holds the charging current limit: 0 A blocks charging, 6-16 A permits it. It keeps its value when this bridge restarts. Two defects let the bridge's RAM state drift away from it, and the result was a reported state of "charging enabled" while the wallbox delivered nothing, which the Home Assistant switch could not correct. Init() never read register 261. It configured 262, 258 and 257, so mChargingEnabled started at the header default of true regardless of what the wallbox was doing. If charging had been disabled when the bridge restarted -- a PV surplus controller pausing overnight, then an OTA update or a power blip -- the flag claimed enabled while the register held 0 A. Init() now reads the register and seeds both the flag and the requested limit from it. If that read fails the old assumption remains, but the mismatch is no longer permanent, since any later enable or disable rewrites the register. GetChargingCurrentLimit() overwrote the setpoint. It is telemetry, called from the publish rotation and from the Modbus TCP server, but it assigned its reading to the same member that held the desired limit. A single read of 0 A destroyed the value to be applied next, so enabling afterwards restored 0 A. Intent and observation now live in separate members, and the getter only touches the observed one. Because intent now survives a disable, mPreviousChargingCurrentLimitA has nothing left to do and is removed. SetChargingEnabled writes the requested limit or 0 on every call rather than only on a transition, since a request that matches our flag must still reach a wallbox that may disagree. Requests are clamped to the 0 or 6-16 A the wallbox accepts, which also removes an undefined float-to-uint16_t conversion for negative payloads. Writing 0 to stop charging still works, so Daheimladen clients are unaffected. Two deliberate behaviour changes: - After a client stops charging by writing 0 A rather than by disabling, enabling now starts charging at the configured default, where the old transition gate made it a no-op. It does not remember a pre-zero limit: "8 A, 0 A, enable" gives the default, "8 A, disable, enable" gives 8 A. - GetChargingCurrentLimit() returns 0 A rather than a stale 16 A before the first successful read. DummyWallbox keeps the previous logic, since its register is RAM that dies with the process and this class of desync cannot occur there. Happy to port it if you would rather the two implementations match. Tested on a Heidelberg Energy Control with an ESP32. The sequence that used to fail -- set 8 A, disable, wait for telemetry to read 0 A repeatedly, enable -- now restores 8 A, verified by reading register 261 over Modbus TCP rather than over MQTT, since the firmware only publishes that value within 6-16 A.
sadilek
added a commit
to sadilek/HeidelBridge
that referenced
this pull request
Aug 4, 2026
…rock#75, BorisBrock#76) into kupa5 Our three fixes are now upstream, so the fork no longer carries them. Boris made two edits when merging: ClampToWallboxRange and WriteCurrentLimitRegister moved from a file-local anonymous namespace to private members of HeidelbergWallbox, and InitialChargingCurrentLimitA is now defined as MaxChargingCurrentA instead of repeating 16.0f. Conflicts resolved in favour of upstream throughout, except for two items that stay fork-specific in MQTTManager.cpp: - The Enable Charging unique_id keeps its underscore. Upstream still has "%control_enable_charging"; reverting ours would orphan the existing entity in Home Assistant and break the automation that references it by entity id. - The diagnostic echo to {DeviceName}/internal/last_command is kept. It is not upstream and is deliberately not proposed, since it adds public topic surface. Also adopted from upstream: the discovery publish retry (publish() can silently return 0 when the TCP send buffer is full) and the Last Will plus availability_topic on every entity, which together complement the retained discovery from BorisBrock#73.
sadilek
added a commit
to sadilek/HeidelBridge
that referenced
this pull request
Aug 4, 2026
All three fixes are upstream now, so the "candidates for a PR" framing was stale. Replaced with a short history keyed to BorisBrock#73/BorisBrock#74/BorisBrock#75, plus Boris's edits when merging: the two helpers became private members, and InitialChargingCurrentLimitA is defined as MaxChargingCurrentA. Added an explicit list of what kupa5 still diverges on, so a future upstream merge does not discard it: the Enable Charging unique_id underscore (reverting would orphan the Home Assistant entity the automation references) and the diagnostic echo topic.
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.
Register 261 holds the charging current limit: 0 A blocks charging, 6-16 A permits it. It keeps its value when this bridge restarts. Two defects let the bridge's RAM state drift away from it, and the result was a reported state of "charging enabled" while the wallbox delivered nothing, which the Home Assistant switch could not correct.
Init() never read register 261. It configured 262, 258 and 257, so mChargingEnabled started at the header default of true regardless of what the wallbox was doing. If charging had been disabled when the bridge restarted -- a PV surplus controller pausing overnight, then an OTA update or a power blip -- the flag claimed enabled while the register held 0 A. Init() now reads the register and seeds both the flag and the requested limit from it. If that read fails the old assumption remains, but the mismatch is no longer permanent, since any later enable or disable rewrites the register.
GetChargingCurrentLimit() overwrote the setpoint. It is telemetry, called from the publish rotation and from the Modbus TCP server, but it assigned its reading to the same member that held the desired limit. A single read of 0 A destroyed the value to be applied next, so enabling afterwards restored 0 A. Intent and observation now live in separate members, and the getter only touches the observed one.
Because intent now survives a disable, mPreviousChargingCurrentLimitA has nothing left to do and is removed. SetChargingEnabled writes the requested limit or 0 on every call rather than only on a transition, since a request that matches our flag must still reach a wallbox that may disagree.
Requests are clamped to the 0 or 6-16 A the wallbox accepts, which also removes an undefined float-to-uint16_t conversion for negative payloads. Writing 0 to stop charging still works, so Daheimladen clients are unaffected.
Two deliberate behaviour changes:
DummyWallbox keeps the previous logic, since its register is RAM that dies with the process and this class of desync cannot occur there. Happy to port it if you would rather the two implementations match.
Tested on a Heidelberg Energy Control with an ESP32. The sequence that used to fail -- set 8 A, disable, wait for telemetry to read 0 A repeatedly, enable -- now restores 8 A, verified by reading register 261 over Modbus TCP rather than over MQTT, since the firmware only publishes that value within 6-16 A.