Skip to content

Conversation

@SW-Niko
Copy link

@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!

This PR is based on PRs #2262 and #2340.

grafik

read() must be done after all other components have been initialized
Refactored getBatteryPower() to use a state machine approach for better clarity and maintainability.
- Now we have 4 different battery states: STOP, NO_DISCHARGE, DISCHARGE_ALLOWED and DISCHARGE_NIGHT
- Renamed some variables for better understanding
- Improved comments and documentation
- State stored in runtime file
Add option AllowStandby to battery powered inverter
@github-actions
Copy link

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 f42ebc4.

@coderabbitai
Copy link

coderabbitai bot commented Nov 20, 2025

Walkthrough

This pull request introduces a new runtime data management system for persisting PowerLimiter state to persistent storage, refactors battery discharge control into a state machine, tightens standby permission logic, and exposes runtime save statistics in the web UI.

Changes

Cohort / File(s) Summary
Core Runtime Data Management
include/RuntimeData.h, src/RuntimeData.cpp
Introduces RuntimeClass for serializing/deserializing runtime data to LittleFS with mutex-protected read/write operations, write throttling, and daily persistence triggers scheduled via task loop.
PowerLimiter Battery State Machine
include/PowerLimiter.h, src/PowerLimiter.cpp
Replaces boolean discharge flags with BatteryState enum (STOP, DISCHARGE_ALLOWED, NO_DISCHARGE, DISCHARGE_NIGHT), refactors battery determination logic, and adds serializeRTD/deserializeRTD methods for runtime data integration.
Battery Inverter Standby Control
src/PowerLimiterBatteryInverter.cpp
Adds LogHelper include and guards standby allowance checks with both allowStandby parameter and configuration flag.
Application Initialization
src/main.cpp
Adds RuntimeData header and initializes/reads RuntimeData after all components including Battery are set up.
Web API Integration
src/WebApi_firmware.cpp, src/WebApi_maintenance.cpp, src/WebApi_sysstatus.cpp
Adds RuntimeData persistence calls on firmware update and reboot; exposes runtime save count via getWriteCountAndTimeString in system status response.
Frontend UI & Data Types
webapp/src/components/FirmwareInfo.vue, webapp/src/types/SystemStatus.ts, webapp/src/views/PowerLimiterAdminView.vue
Adds runtime\_savecount display row in FirmwareInfo, extends SystemStatus interface with runtime\_savecount field, and broadens AllowStandby visibility to Battery power source.
Localization
webapp/src/locales/de.json, webapp/src/locales/en.json
Adds RuntimeSaveCount translations and updates AllowStandbyHint text.

Sequence Diagram(s)

sequenceDiagram
    participant App as Application
    participant RT as RuntimeData
    participant PL as PowerLimiter
    participant FS as LittleFS

    App->>RT: init(scheduler)
    Note over RT: Register minute-loop task

    App->>RT: read()
    RT->>FS: Load /runtime.json
    RT->>PL: deserializeRTD(data)
    Note over PL: Restore battery state

    loop Periodic / On-demand
        RT->>RT: loop() triggered
        RT->>PL: serializeRTD(json)
        Note over PL: Capture current state
        RT->>FS: Write /runtime.json
        Note over RT: Update write_count, write_epoch
    end

    Note over RT: Daily trigger (00:05-00:10)<br/>or requestWriteOnNextTaskLoop()
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~28 minutes

Key areas requiring attention:

  • Thread-safety validation in RuntimeData: verify mutex coverage around shared state (_readOK, _writeOK, _writeNow, _writeCount, _writeEpoch)
  • Battery state machine logic in PowerLimiter.cpp: confirm state transitions (STOP → DISCHARGE_ALLOWED/NO_DISCHARGE/DISCHARGE_NIGHT) handle edge cases and nightly buffering correctly
  • Persistence timing: ensure write freezing logic (freezeMinutes parameter) and daily trigger window (00:05–00:10) align with system expectations
  • Standby guard conditions in PowerLimiterBatteryInverter.cpp: verify both allowStandby parameter and _config.AllowStandby are properly checked in all paths
  • Frontend integration: confirm runtime_savecount displays correctly in UI and SystemStatus TypeScript interface aligns with backend API

Poem

A rabbit hops through battery states so bright,
STOP, DISCHARGE, NIGHT—what a sight!
Runtime data saved with mutex care,
From LittleFS, persistence everywhere. 🐰💾
The inverter now standby-wise and lean,
Best battery logic you've ever seen! ⚡

Pre-merge checks and finishing touches

✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The PR description clearly explains the feature changes: making 'Allow Standby' option available for battery-powered inverters, detailing behavior when enabled/disabled, default settings, and important limitations.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

📝 Customizable high-level summaries are now available in beta!

You can now customize how CodeRabbit generates the high-level summary in your pull requests — including its content, structure, tone, and formatting.

  • Provide your own instructions using the high_level_summary_instructions setting.
  • Format the summary however you like (bullet lists, tables, multi-section layouts, contributor stats, etc.).
  • Use high_level_summary_in_walkthrough to move the summary from the description to the walkthrough section.

Example instruction:

"Divide the high-level summary into five sections:

  1. 📝 Description — Summarize the main change in 50–60 words, explaining what was done.
  2. 📓 References — List relevant issues, discussions, documentation, or related PRs.
  3. 📦 Dependencies & Requirements — Mention any new/updated dependencies, environment variable changes, or configuration updates.
  4. 📊 Contributor Summary — Include a Markdown table showing contributions:
    | Contributor | Lines Added | Lines Removed | Files Changed |
  5. ✔️ Additional Notes — Add any extra reviewer context.
    Keep each section concise (under 200 words) and use bullet or numbered lists for clarity."

Note: This feature is currently in beta for Pro-tier users, and pricing will be announced later.


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

@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

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.

1 participant