Repository navigation
feat: debounce writable Modbus commands - #707
nickcmaynard wants to merge 3 commits into
Conversation
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
|
Please point your agent to the project rules |
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
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. |
|
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? |
There was a problem hiding this comment.
🟡 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_DEBOUNCEand per-sensor.debouncesettings, 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
| pending = [ | ||
| (sensor, value) | ||
| for sensor, value in ist.write_queue.items() |
| 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) |
| 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) | ||
| ): |
|
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:
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:
Other, likely irrelevant detail follows. Other settings written:
Predbat also spams |

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