-
-
Notifications
You must be signed in to change notification settings - Fork 93
DPL option "Allow standby" for battery powered inverter #2355
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: development
Are you sure you want to change the base?
Conversation
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
Build ArtifactsFirmware built from this pull request's code:
Notice
|
WalkthroughThis 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
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()
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~28 minutes Key areas requiring attention:
Poem
Pre-merge checks and finishing touches✅ Passed checks (1 passed)
✨ Finishing touches🧪 Generate unit tests (beta)
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.
Example instruction:
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. Comment |
There was a problem hiding this 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 callsetAcOutput(_config.LowerPowerLimit)before returning 0The 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 (
BatteryStateenum used only ininclude/PowerLimiter.hwith no corresponding test cases). Given the complexity of the state machine—particularly the nested conditionals (lines 283-299), the_oneStopPerNightDoneflag 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
📒 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.vuewebapp/src/types/SystemStatus.tswebapp/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.cppsrc/main.cppsrc/PowerLimiterBatteryInverter.cppinclude/PowerLimiter.hsrc/WebApi_maintenance.cppsrc/WebApi_sysstatus.cppinclude/RuntimeData.hsrc/RuntimeData.cppsrc/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.cppinclude/PowerLimiter.hsrc/WebApi_maintenance.cppinclude/RuntimeData.hsrc/RuntimeData.cppsrc/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
_fromStartvia 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_savecountfield addition is correctly typed asstringto match the formatted output fromRuntimeData.getWriteCountAndTimeString()(which combines count and time). The placement betweencfgsavecountanduptimeis 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:
- Line 13:
getMaxReductionWatts- determines if full output can be reduced (standby)- Line 48:
applyReduction- first standby entry point when already at/below lower limit- Line 60:
applyReduction- second standby entry point when reduction exceeds available rangeAlso applies to: 48-51, 60-63
src/RuntimeData.cpp (7)
32-36: LGTM! Well-defined constants and singleton.The constants
RUNTIME_FILENAMEandRUNTIME_VERSIONare appropriately scoped and named. The singleton pattern withRuntimeDatais 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 viagetWriteTrigger().
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
cleanExitlambdaThe 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
_lastTriggerflag 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.his 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
serializeRTDanddeserializeRTDmethods only persist_fromStart(which direction the system entered the stop-start zone), not the full battery state. This is the correct approach because:
- The full state (
_batteryState) is recalculated each loop iteration based on current conditions_fromStartis a persistent flag that affects state transitions and needs to survive restarts- The deserialization provides a sensible default (
false) if the value is missingNote on thread safety (lines 989-990, 996-997): The comments acknowledge the lack of mutex protection. Since
_fromStartis a boolean and the PowerLimiter runs on a single task, this is acceptable. However, if concurrent access becomes possible in the future, consider usingstd::atomic<bool>as the comment suggests.Based on learnings
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.
"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:
These standby events are not affected or modified by this option!
This PR is based on PRs #2262 and #2340.