Skip to content

Feature: DPL: make standby configurable for battery-powered inverters#2355

Merged
AndreasBoehm merged 1 commit intohoylabs:developmentfrom
SW-Niko:DPLAllowStandby
Apr 10, 2026
Merged

Feature: DPL: make standby configurable for battery-powered inverters#2355
AndreasBoehm merged 1 commit intohoylabs:developmentfrom
SW-Niko:DPLAllowStandby

Conversation

@SW-Niko
Copy link
Copy Markdown

@SW-Niko SW-Niko commented Nov 20, 2025

This PR makes the "Allow Standby" option, already available for smart battery-powered inverters, available for battery-powered inverters as well. More precise... make the deactivation of "Allow Standby" configurable.

  • When "Allow Standby" is enabled, the inverter will enter standby mode when the minimum inverter power is undershot.
  • When "Allow Standby" is disabled, the inverter will be set to its minimum inverter power when the minimum inverter power is undershot.

"Allow Standby" is enabled by default. After a software update, the inverters' behavior will not initially change.
Only actively modifying the DPL configuration can alter the inverters' behavior.
This option is particularly useful for problems like "124 Switched off by remote control." Further information can be found here: #2321

Disabling "Allow Standby" is not recommended, if the minimum inverter output is higher than the minimum power consumption.
For systems with multiple inverters, you should also consider whether it is wise to disable the option for all inverters.

Important:
There are other events that can put battery inverters into standby mode, such as:

  • Disabling DPL
  • Battery below stop threshold

These standby events are not affected or modified by this option!

grafik

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Nov 20, 2025

Build Artifacts

Firmware built from this pull request's code:

Notice

  • These artifacts are ZIP files containing the factory update binary as well as the OTA update binary.
    Extract the binaries from the ZIP files first. Do not use the ZIP files themselves to perform an update.
  • These links point to artifacts of the latest successful build run.
  • The linked artifacts were built from 1ef968c.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Nov 20, 2025

Warning

Rate limit exceeded

@AndreasBoehm has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 11 minutes and 23 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 11 minutes and 23 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 093bf980-d0dc-4cb4-8165-1cc2a68fba14

📥 Commits

Reviewing files that changed from the base of the PR and between 23390ee and 9d28d11.

📒 Files selected for processing (5)
  • src/PowerLimiter.cpp
  • src/PowerLimiterBatteryInverter.cpp
  • webapp/src/locales/de.json
  • webapp/src/locales/en.json
  • webapp/src/views/PowerLimiterAdminView.vue

Walkthrough

Update enforces immediate standby for battery-powered inverters when _batteryState == BatteryState::STOP and gates standby behavior in battery inverter logic behind both an allowStandby parameter and _config.AllowStandby. UI shows AllowStandby for an additional power source and English/German hints clarify fallback to minimum power limit.

Changes

Cohort / File(s) Summary
Battery state handling
src/PowerLimiter.cpp
In PowerLimiterClass::updateInverterLimits add early-return: if matched inverter(s) are battery-powered and _batteryState == BatteryState::STOP, call standby() on each, log, and return 0, skipping diff/hysteresis adjustments.
Standby gating in inverter logic
src/PowerLimiterBatteryInverter.cpp
Require both call-site allowStandby and _config.AllowStandby to enable standby in getMaxReductionWatts(bool) and applyReduction(uint16_t, bool); otherwise fall through to non-standby reduction behavior.
UI visibility & translations
webapp/src/views/PowerLimiterAdminView.vue, webapp/src/locales/en.json, webapp/src/locales/de.json
AllowStandby input now renders when `inv.power_source == 2

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 In STOP the battery whispers, soft and low,
I tell the inverters it’s time to go.
Flags and configs decide the gentle rest,
UI sings hints so users know best—
A bunny hop to keep the grid aglow.

🚥 Pre-merge checks | ✅ 1
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The PR description clearly explains the changes: making the 'Allow Standby' option configurable for battery-powered inverters, with details on behavior when enabled/disabled and important caveats.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
src/PowerLimiterBatteryInverter.cpp (1)

52-52: Consider the TODO: should the lower limit be set as a safety measure?

The TODO raises a valid safety question: when standby is not allowed and the inverter is already at or below the lower power limit, should the code explicitly set the lower limit again to ensure the inverter stays at that boundary?

Current behavior: returns 0 (no action taken)
Alternative: explicitly call setAcOutput(_config.LowerPowerLimit) before returning 0

The alternative would be more defensive and ensure the inverter is explicitly commanded to the lower limit even if it should already be there. This could help recover from unexpected states.

Would you like me to propose a specific implementation for this safety improvement?

src/PowerLimiter.cpp (1)

240-305: Add unit tests to verify battery state machine transitions and edge cases.

Verification confirms no existing tests cover the battery state logic (BatteryState enum used only in include/PowerLimiter.h with no corresponding test cases). Given the complexity of the state machine—particularly the nested conditionals (lines 283-299), the _oneStopPerNightDone flag preventing oscillation (lines 267, 287, 295, 298), and the unreachable fallback code (line 302)—adding comprehensive unit tests is essential to verify all state transitions and prevent regressions.

The implementation appears sound for the documented scenarios, but test coverage will provide confidence that edge cases are correctly handled and prevent future breakage when state logic is modified.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0dc7b6c and 00f0b79.

📒 Files selected for processing (14)
  • include/PowerLimiter.h (2 hunks)
  • include/RuntimeData.h (1 hunks)
  • src/PowerLimiter.cpp (9 hunks)
  • src/PowerLimiterBatteryInverter.cpp (3 hunks)
  • src/RuntimeData.cpp (1 hunks)
  • src/WebApi_firmware.cpp (2 hunks)
  • src/WebApi_maintenance.cpp (2 hunks)
  • src/WebApi_sysstatus.cpp (2 hunks)
  • src/main.cpp (2 hunks)
  • webapp/src/components/FirmwareInfo.vue (1 hunks)
  • webapp/src/locales/de.json (2 hunks)
  • webapp/src/locales/en.json (2 hunks)
  • webapp/src/types/SystemStatus.ts (1 hunks)
  • webapp/src/views/PowerLimiterAdminView.vue (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
webapp/src/**/*.{js,jsx,ts,tsx,vue}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

webapp/src/**/*.{js,jsx,ts,tsx,vue}: Webapp source must pass ESLint (yarn lint)
Webapp source must be Prettier-formatted (yarn prettier --check src/)

Files:

  • webapp/src/components/FirmwareInfo.vue
  • webapp/src/types/SystemStatus.ts
  • webapp/src/views/PowerLimiterAdminView.vue
{src,include,lib/Hoymiles,lib/MqttSubscribeParser,lib/TimeoutHelper,lib/ResetReason}/**/*.{c,cc,cpp,cxx,h,hpp,hxx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

C/C++ code must pass cpplint with the specified filters

Files:

  • src/WebApi_firmware.cpp
  • src/main.cpp
  • src/PowerLimiterBatteryInverter.cpp
  • include/PowerLimiter.h
  • src/WebApi_maintenance.cpp
  • src/WebApi_sysstatus.cpp
  • include/RuntimeData.h
  • src/RuntimeData.cpp
  • src/PowerLimiter.cpp
🧠 Learnings (2)
📓 Common learnings
Learnt from: SW-Niko
Repo: hoylabs/OpenDTU-OnBattery PR: 2262
File: src/main.cpp:149-159
Timestamp: 2025-10-17T08:29:40.205Z
Learning: In the OpenDTU-OnBattery project (src/main.cpp), RuntimeData must be initialized after SolarCharger, PowerMeter, PowerLimiter, GridCharger, and Battery components have been initialized, but before the main loop. RuntimeData acts as a persistence service that these components access during their runtime operations (not during initialization).
📚 Learning: 2025-10-17T08:29:40.205Z
Learnt from: SW-Niko
Repo: hoylabs/OpenDTU-OnBattery PR: 2262
File: src/main.cpp:149-159
Timestamp: 2025-10-17T08:29:40.205Z
Learning: In the OpenDTU-OnBattery project (src/main.cpp), RuntimeData must be initialized after SolarCharger, PowerMeter, PowerLimiter, GridCharger, and Battery components have been initialized, but before the main loop. RuntimeData acts as a persistence service that these components access during their runtime operations (not during initialization).

Applied to files:

  • src/main.cpp
  • include/PowerLimiter.h
  • src/WebApi_maintenance.cpp
  • include/RuntimeData.h
  • src/RuntimeData.cpp
  • src/PowerLimiter.cpp
🧬 Code graph analysis (3)
src/PowerLimiterBatteryInverter.cpp (4)
src/PowerLimiterInverter.cpp (4)
  • getCurrentOutputAcWatts (310-313)
  • getCurrentOutputAcWatts (310-310)
  • getCurrentLimitWatts (347-351)
  • getCurrentLimitWatts (347-347)
src/PowerLimiterSmartBufferInverter.cpp (2)
  • standby (87-92)
  • standby (87-87)
src/PowerLimiterSolarInverter.cpp (2)
  • standby (122-128)
  • standby (122-122)
src/PowerLimiterOverscalingInverter.cpp (2)
  • setAcOutput (120-129)
  • setAcOutput (120-120)
include/PowerLimiter.h (1)
src/PowerLimiter.cpp (4)
  • serializeRTD (987-992)
  • serializeRTD (987-987)
  • deserializeRTD (994-999)
  • deserializeRTD (994-994)
include/RuntimeData.h (1)
src/RuntimeData.cpp (16)
  • init (42-49)
  • init (42-42)
  • read (137-172)
  • read (137-137)
  • write (70-131)
  • write (70-70)
  • getWriteCount (178-182)
  • getWriteCount (178-178)
  • getWriteEpochTime (188-192)
  • getWriteEpochTime (188-188)
  • getWriteCountAndTimeString (200-216)
  • getWriteCountAndTimeString (200-200)
  • loop (55-63)
  • loop (55-55)
  • getWriteTrigger (222-239)
  • getWriteTrigger (222-222)
🪛 Clang (14.0.6)
include/RuntimeData.h

[error] 4-4: 'ArduinoJson.h' file not found

(clang-diagnostic-error)

src/RuntimeData.cpp

[error] 20-20: 'Utils.h' file not found

(clang-diagnostic-error)

🔇 Additional comments (26)
include/PowerLimiter.h (2)

60-61: LGTM! Runtime data serialization hooks properly exposed.

The public serialization methods integrate cleanly with the RuntimeData persistence system introduced in this PR.


79-83: LGTM! State machine approach improves clarity.

The refactoring from boolean flags to a BatteryState enum with explicit states (STOP, NO_DISCHARGE, DISCHARGE_ALLOWED, DISCHARGE_NIGHT) improves code readability and maintainability. The persistence of only _fromStart via RTD serialization is appropriate for maintaining state across restarts.

include/RuntimeData.h (1)

1-43: LGTM! Well-designed runtime data persistence class.

The RuntimeClass design demonstrates good practices:

  • Non-copyable via deleted constructors/assignment operators
  • Thread-safe shared data access via mutex
  • Atomic flags for status tracking
  • Write throttling to prevent excessive disk I/O
  • Daily automatic persistence trigger

Based on learnings

webapp/src/components/FirmwareInfo.vue (1)

86-89: LGTM! Runtime save count properly displayed.

The new row correctly displays the runtime save count from the system status, consistent with the pattern used for ConfigSaveCount above.

src/WebApi_firmware.cpp (1)

13-13: LGTM! Runtime data persisted before firmware-induced restart.

Appropriate to persist runtime data before the restart triggered by firmware updates, with a sensible 60-minute freeze window to prevent excessive writes.

Also applies to: 51-53

webapp/src/locales/de.json (1)

285-285: LGTM! German translation added.

The translation for RuntimeSaveCount is appropriately added and maintains consistency with the English counterpart.

src/WebApi_sysstatus.cpp (1)

17-17: LGTM! Runtime save count exposed in system status.

The new runtime_savecount field is appropriately placed alongside cfgsavecount and uses the formatted string method to provide both count and timestamp information.

Also applies to: 83-83

webapp/src/views/PowerLimiterAdminView.vue (1)

164-164: LGTM! Allow Standby option extended to battery-powered inverters.

This change implements the core PR objective by making the "Allow Standby" option visible for both battery-powered (power_source == 0) and smart buffer (power_source == 2) inverters. Previously, this option was only available for smart buffer inverters.

src/main.cpp (1)

39-39: LGTM! RuntimeData initialization order is correct.

RuntimeData is properly initialized after all other components (SolarCharger, PowerMeter, PowerLimiter, GridCharger, and Battery) as required. The added section comments improve code clarity. This aligns with the learning that RuntimeData acts as a persistence service accessed during runtime operations, not during component initialization.

Based on learnings

Also applies to: 149-159

webapp/src/types/SystemStatus.ts (1)

34-34: LGTM!

The runtime_savecount field addition is correctly typed as string to match the formatted output from RuntimeData.getWriteCountAndTimeString() (which combines count and time). The placement between cfgsavecount and uptime is logical.

webapp/src/locales/en.json (2)

285-285: LGTM!

The translation string clearly describes the runtime data save count field with its time component.


732-732: LGTM!

The updated hint text provides clearer guidance by explicitly stating that the inverter will be set to the minimum power limit when standby is not allowed and the calculated power is below the minimum. This improves user understanding of the feature behavior.

src/WebApi_maintenance.cpp (1)

11-11: LGTM!

The runtime data persistence before reboot is correctly implemented with a 60-minute freeze interval to avoid excessive writes. The comment clearly explains the intent, and the operation sequence (send response → save data → reboot) is appropriate.

Based on learnings

Also applies to: 47-49

src/PowerLimiterBatteryInverter.cpp (1)

13-13: LGTM! Standby logic correctly gated by configuration.

The implementation properly checks both allowStandby (dynamic permission based on power calculation) AND _config.AllowStandby (user configuration) before allowing the inverter to enter standby. This ensures the new "Allow Standby" option is respected for battery-powered inverters.

The three locations where this check occurs are:

  1. Line 13: getMaxReductionWatts - determines if full output can be reduced (standby)
  2. Line 48: applyReduction - first standby entry point when already at/below lower limit
  3. Line 60: applyReduction - second standby entry point when reduction exceeds available range

Also applies to: 48-51, 60-63

src/RuntimeData.cpp (7)

32-36: LGTM! Well-defined constants and singleton.

The constants RUNTIME_FILENAME and RUNTIME_VERSION are appropriately scoped and named. The singleton pattern with RuntimeData is consistent with other components in the codebase.


42-63: LGTM! Clean initialization and loop implementation.

The minute-based task scheduling is appropriate for the use case, and the loop correctly handles both on-demand writes (_writeNow) and daily scheduled writes via getWriteTrigger().


70-131: LGTM! Robust write implementation with proper safeguards.

The write method demonstrates excellent defensive programming:

  • Thread safety via mutex guard (line 88)
  • Time availability check before writing (line 84)
  • Freeze interval enforcement (lines 91-93) to prevent excessive writes
  • JSON allocation verification (lines 109-111)
  • Atomic state update only after successful write (lines 124-126)
  • Comprehensive error handling with the cleanExit lambda

The integration with PowerLimiter via serializeRTD (line 105) is clean and extensible.

Based on learnings


137-172: LGTM! Read method with appropriate fallback behavior.

The read implementation correctly:

  • Tolerates missing or corrupted files by using default values (line 142 comment)
  • Protects shared state with mutex (lines 154-158)
  • Deserializes PowerLimiter RTD data (line 161)
  • Logs success/failure appropriately (lines 165-168)

The graceful degradation approach (returning false but continuing with defaults) is appropriate for runtime data that should not block system startup.


178-216: LGTM! Accessor methods are thread-safe and well-formatted.

All accessor methods properly protect reads with mutex guards. The getWriteCountAndTimeString() method's formatting with fallback to "no time" (lines 208-212) provides a user-friendly display even when time synchronization hasn't occurred yet.


222-239: LGTM! Daily trigger with oscillation prevention.

The getWriteTrigger() implementation correctly:

  • Returns true once per day during the 00:05–00:10 window (line 230)
  • Uses _lastTrigger flag to prevent multiple triggers within the window (lines 231-234)
  • Resets the flag outside the window (lines 235-237)
  • Guards shared state with mutex (line 229)

This prevents oscillation and ensures exactly one daily write as intended.


20-20: Note: Static analysis warning is a false positive.

The static analysis tool reports that Utils.h is not found (line 20), but this is expected to be a false positive in the build environment. The file is part of the existing codebase and is used correctly here (e.g., Utils::getEpoch, Utils::skipBom, Utils::checkJsonAlloc).

src/PowerLimiter.cpp (5)

338-339: LGTM! State is properly computed and cached per DPL loop iteration.

The battery state is calculated once per loop iteration and stored in _batteryState, then used consistently throughout the loop in multiple decision points (lines 554, 648-650, 750, 835). This ensures consistent behavior within each iteration.


648-654: LGTM! Battery-powered inverters correctly stopped when battery is below threshold.

This new logic properly handles the case where battery-powered inverters need to enter standby because the battery is below the stop threshold, regardless of power demand. The early return (line 653) prevents further processing for these inverters in this loop iteration.

The log message at line 652 clearly indicates why the inverters are being stopped.


750-753: LGTM! DC power bus usage correctly blocked when battery is in STOP state.

This check ensures that when the battery is below the stop threshold, no power is drawn from the DC power bus for battery-powered inverters. The log message clearly documents the reason for blocking.


835-835: LGTM! Battery discharge limit correctly returns 0 for STOP and NO_DISCHARGE states.

When the battery state is STOP (below stop threshold) or NO_DISCHARGE (solar passthrough only), the discharge limit is correctly set to 0 to prevent battery discharge.


987-999: LGTM! RTD serialization approach is appropriate.

The serializeRTD and deserializeRTD methods only persist _fromStart (which direction the system entered the stop-start zone), not the full battery state. This is the correct approach because:

  1. The full state (_batteryState) is recalculated each loop iteration based on current conditions
  2. _fromStart is a persistent flag that affects state transitions and needs to survive restarts
  3. The deserialization provides a sensible default (false) if the value is missing

Note on thread safety (lines 989-990, 996-997): The comments acknowledge the lack of mutex protection. Since _fromStart is a boolean and the PowerLimiter runs on a single task, this is acceptable. However, if concurrent access becomes possible in the future, consider using std::atomic<bool> as the comment suggests.

Based on learnings

@AndreasBoehm
Copy link
Copy Markdown
Member

@SW-Niko Can you rebase and reduce this PR to only contain the addition of standby settings for battery inverters? Thank you

@AndreasBoehm AndreasBoehm changed the title DPL option "Allow standby" for battery powered inverter Feature: DPL: make standby configurable for battery-powered inverters Apr 1, 2026
@SW-Niko
Copy link
Copy Markdown
Author

SW-Niko commented Apr 1, 2026

@AndreasBoehm
I assume we can close issue #1875 after the merge.

I had been using the standby option since last year without any problems but just tested with one inverter.
A wiki describing the standby option can be found here: Link

Comment thread src/PowerLimiter.cpp
@AndreasBoehm
Copy link
Copy Markdown
Member

I assume we can close issue #1875 after the merge.

After reading your wiki page i would agree with closing this issue afterwards.

A wiki describing the standby option can be found here: Link

This is amazing, thanks a lot!

@AndreasBoehm AndreasBoehm linked an issue Apr 10, 2026 that may be closed by this pull request
4 tasks
- WebApp, add option standby
- PowerLimiter, add standby on/off
- PowerLimiter, handle battery state OFF
- Improve standby tooltip
@AndreasBoehm AndreasBoehm merged commit 004a383 into hoylabs:development Apr 10, 2026
11 of 12 checks passed
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