Skip to content

feat: debounce writable Modbus commands - #707

Open
nickcmaynard wants to merge 3 commits into
kellerza:mainfrom
nickcmaynard:debounce
Open

nickcmaynard wants to merge 3 commits into
kellerza:mainfrom
nickcmaynard:debounce

Conversation

@nickcmaynard

Copy link
Copy Markdown

Add optional trailing-edge write debounce with last-value-wins behavior for writable sensors. Support global WRITE_DEBOUNCE configuration and per-sensor .debounce overrides, including zero to disable the global setting.

Skip writes when the requested register value is already confirmed, update edge and multi add-on configuration and documentation, and add regression coverage for global fallback, local overrides, coalescing, and duplicate writes.

Closes #706

Add optional trailing-edge write debounce with last-value-wins behavior for
writable sensors. Support global WRITE_DEBOUNCE configuration and per-sensor
.debounce overrides, including zero to disable the global setting.

Skip writes when the requested register value is already confirmed, update
edge and multi add-on configuration and documentation, and add regression
coverage for global fallback, local overrides, coalescing, and duplicate
writes.

Closes kellerza#706
@kellerza

kellerza commented Nov 6, 2026

Copy link
Copy Markdown
Owner

Please point your agent to the project rules

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@nickcmaynard

Copy link
Copy Markdown
Author

Apologies @kellerza - I didn't see the rules before initially submitting! I read the README, but not the agent rules. Every day's a school day...

Hopefully the additional commit aligns this PR with the rules better.

@kellerza

kellerza commented Nov 7, 2026 •

Copy link
Copy Markdown
Owner

How many settings do you control from predbat?

What is the impact on the UI. Does it update when changed, or only after the debounce?

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Critical queue races, alias ordering, and stale-cache checks can lose newer commands or suppress required writes, leaving device registers inconsistent with the latest requests.

3 open issues
What changed in this PR

Adds optional trailing-edge debounce to writable Modbus commands, aiming to reduce unnecessary inverter writes as requested in #706.

Changes:

  • Adds global WRITE_DEBOUNCE and per-sensor .debounce settings, including zero to bypass the global delay.
  • Queues the latest sensor command and adds register-cache comparison to suppress unchanged writes.
  • Adds regression coverage, edge add-on configuration, and user documentation.
File Description
www/​docs/​reference/​multi-options.md Documents global debounce and sensor overrides.
src/​tests/​sunsynk/​test_sunsynk.py Tests duplicate-write suppression.
src/​tests/​sunsynk/​test_sensors.py Tests the debounce override attribute.
src/​tests/​ha_addon_sunsynk_multi/​test_sensor_callback.py Tests coalescing, timing, global fallback, and local overrides.
src/​tests/​ha_addon_sunsynk_multi/​test_options.py Tests global debounce option parsing.
src/​sunsynk/​sunsynk.py Skips writes matching cached register values.
src/​sunsynk/​rwsensors.py Adds the optional sensor debounce attribute.
src/​ha_addon_sunsynk_multi/​sensor_callback.py Flushes queued commands after their debounce window.
src/​ha_addon_sunsynk_multi/​options.py Adds the global debounce setting.
src/​ha_addon_sunsynk_multi/​a_sensor.py Records command timestamps on enqueue.
src/​ha_addon_sunsynk_multi/​a_inverter.py Stores per-sensor enqueue timestamps.
hass-addon-sunsynk-edge/​translations/​en.yaml Adds debounce labels and configuration help.
hass-addon-sunsynk-edge/​config.yaml Exposes the default and bounded debounce schema.

Review effort: Balanced

Comment on lines +90 to +92
pending = [
(sensor, value)
for sensor, value in ist.write_queue.items()
Comment on lines +101 to 106
for sensor, value in pending:
ist.write_queue.pop(sensor, None)
ist.write_queue_at.pop(sensor, None)
if not isinstance(sensor, RWSensor):
continue
await asyncio.sleep(0.1)
Comment thread src/sunsynk/sunsynk.py
Comment on lines +175 to +178
if sensor.address and all(
addr in self.state.registers and self.state.registers[addr] == value
for addr, value in zip(sensor.address, regs, strict=True)
):
@nickcmaynard

Copy link
Copy Markdown
Author

Good questions. Let me give some explanation on why I've got here.

The ultimate root of this problem is that I'm trying to work around register writes sometimes not persisting properly. This is intermittent.

For Sunsynk inverters (and others without custom Predbat classes) Predbat provides procedural hooks fired by internal state transitions, which can invoke HA services; used to set the inverter settings. We use this to say "sell" or "charge" etc.

Arguably, Predbat should do this via a desired state reconciliation pattern, but it doesn't provide that.

Because the settings sometimes don't take, my approach (and others with other inverter models) has been to ask Predbat to "repeat" these service invocations, to ensure the inverter ends up in the right state.

Sadly, its implementation of this "repeat" feature (arguably misguided - I'm working on a PR for that project) has the effect of, every five minutes, performing this flip-flop pattern:

  1. Set register A to X (intermediary) (ie. charge_stop_service)
  2. Set register A to Y (desired) (ie. discharge_start_service)

This app's behaviour is thus to send two register writes for what is in effect (if we just waited a few seconds) a noop of "set register A to Y", and which this app would resolve to zero registry writes.

These are the settings that likely need debouncing for my use case:

  • prog[1-6]_charge
  • prog[1-6]_sell
  • load_limit
    Possibly also:
  • prog[1-6]_capacity

Other, likely irrelevant detail follows.

Other settings written:

  • battery_max_discharge_current
  • battery_max_charge_current OR grid_charge_battery_current (depending on config)

Predbat also spams battery_max_charge_current OR grid_charge_battery_current (depending on config) when doing a "low power charge" - again, a register write every five minutes as it chases a charge curve, slowly increasing the rate. At the moment I'm fudging that in Node-RED (via a throttling proxy number) until I understand it well enough to engage constructively on the problem.

This branch has not been deployed

No deployments
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.

Feature request: Configurable Modbus register write debounce / coalescing to protect inverter flash memory

3 participants