Skip to content

Enable full power range of battery-powered HM-xxx inverter on 24VDC#2372

Draft
SW-Niko wants to merge 26 commits intohoylabs:developmentfrom
SW-Niko:DPLuseATF
Draft

Enable full power range of battery-powered HM-xxx inverter on 24VDC#2372
SW-Niko wants to merge 26 commits intohoylabs:developmentfrom
SW-Niko:DPLuseATF

Conversation

@SW-Niko
Copy link
Copy Markdown

@SW-Niko SW-Niko commented Dec 21, 2025

This PR is specifically for HM-xxx inverters powered by a 24VDC battery.
See also #1601

  • Replaces the faulty inverter AC measurement with an external power meter (Inverter-Meter).
  • Replaces the faulty linear conversion from limit to power and power to limit (ATF).
  • The Inverter-Meter can be configured and assigned to an inverter in the WebUI.
  • The DPL offers the AFT option for battery-powered inverters.
  • In the live view, the power from the Inverter-Meter is automatically used to calculate the "Inverter Total Power".
  • The ATF values ​​are automatically displayed in the inverter live view.

Snapshot with an inverter (ATF and inverter meter active)
(1) Measurement from the inverter meter
(2) ATF calculated values ​​(limit and power)
(3) Values ​​measured by the inverter

grafik

Limitations:
ATF is only available for battery-powered inverters.
Currently, only one inverter can be selected for ATF/inverter meters.

Note:
This PR is based on PRs: #2355, #2262 and #2340

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
Copy Markdown

github-actions Bot commented Dec 21, 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 f80b9be.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Dec 21, 2025

Walkthrough

This PR introduces an Adaptive Transfer Function (ATF) system for inverter power correction, external inverter meter support via multiple protocols (MQTT, HTTP JSON/SML, Serial SDM, UDP Victron), a battery state machine for discharge management, and runtime data persistence to LittleFS with corresponding web API and UI updates.

Changes

Cohort / File(s) Summary
Core ATF Support
include/InverterATF.h, src/InverterATF.cpp
New thread-safe InverterATF class managing adaptive power correction tables with activation, data updates, interpolation, serialization, and reporting
Configuration & Data Structures
include/Configuration.h, src/Configuration.cpp
Added UseATF boolean field to PowerLimiter inverter config; added InverterMeterConfig struct with multiple nested protocol configs (MQTT, SDM, HTTP JSON/SML, UDP Victron); full round-trip JSON serialization
Inverter Meter Controller
include/invertermeter/Controller.h, src/invertermeter/Controller.cpp
New InverterMeters::Controller class providing unified power query interface across multiple meter protocols with scheduled polling and thread-safe access
Runtime Data Persistence
include/RuntimeData.h, src/RuntimeData.cpp
New RuntimeClass managing persistent JSON storage of runtime state on LittleFS with versioning, write count tracking, and scheduled write/read operations
PowerLimiter Integration
include/PowerLimiter.h, src/PowerLimiter.cpp, include/PowerLimiterInverter.h, src/PowerLimiterInverter.cpp, src/PowerLimiterBatteryInverter.cpp
Extended PowerLimiter with ATF activation/deactivation, battery state machine (STOP, NO_DISCHARGE, DISCHARGE_ALLOWED, DISCHARGE_NIGHT), external meter power override, ATF-aware limit calculations, and RTD persistence hooks
Web API Extensions
include/WebApi.h, include/WebApi_invertermeter.h, src/WebApi.cpp, src/WebApi_invertermeter.cpp
New WebApiInverterMeterClass exposing REST endpoints for inverter meter config (GET/POST), status queries, and protocol-specific test endpoints (HTTP JSON, HTTP SML)
System Integration
src/WebApi_firmware.cpp, src/WebApi_maintenance.cpp, src/WebApi_sysstatus.cpp, src/WebApi_ws_live.cpp, src/Datastore.cpp, src/main.cpp
Added RuntimeData initialization and persistence triggers on firmware update/reboot; extended status/live data endpoints; integrated InverterMeter into power calculations; added initialization sequencing
Frontend Configuration & Types
webapp/src/types/InverterMeterConfig.ts, webapp/src/types/PowerLimiterConfig.ts, webapp/src/types/SystemStatus.ts
New TypeScript interfaces for inverter meter configuration schema with per-protocol nested structures; added use_atf and runtime_savecount fields
Frontend UI & Routing
webapp/src/router/index.ts, webapp/src/views/InverterMeterAdminView.vue, webapp/src/views/PowerLimiterAdminView.vue, webapp/src/components/NavBar.vue, webapp/src/components/FirmwareInfo.vue
New router entry and dedicated settings view for inverter meter with protocol-specific nested forms; extended PowerLimiter UI with ATF checkbox and isATFFull computed property; added runtime save count display
Localization
webapp/src/locales/en.json, webapp/src/locales/de.json
Added translation keys for inverter meter settings, ATF options, runtime save count, and conditional AllowStandby hint text

Sequence Diagrams

sequenceDiagram
    participant Config
    participant PowerLimiter
    participant InverterATF
    participant PowerLimiterInv as PowerLimiterInverter
    participant ATFState as ATF State

    Config->>PowerLimiter: init() with inverter config
    note over PowerLimiter: _checkATF = true
    
    rect rgb(220, 240, 255)
    note over PowerLimiter,ATFState: ATF Activation Phase
    PowerLimiter->>PowerLimiter: loop() detects _checkATF
    PowerLimiter->>PowerLimiter: initATF()
    PowerLimiter->>PowerLimiterInv: activateATF(nomPower)
    PowerLimiterInv->>InverterATF: activateATF(nomPower)
    InverterATF->>ATFState: Initialize correction table<br/>allocate _realPower array
    InverterATF-->>PowerLimiterInv: return success
    PowerLimiterInv-->>PowerLimiter: ATF active
    end

    rect rgb(240, 255, 240)
    note over PowerLimiter,ATFState: Runtime Power Limiting
    PowerLimiter->>PowerLimiterInv: update(target_watts)
    PowerLimiterInv->>InverterATF: setATFData(power, limit)
    InverterATF->>ATFState: Validate & update<br/>correction table
    PowerLimeterInv->>InverterATF: getATFLimit(power)
    InverterATF->>ATFState: Interpolate correction
    InverterATF-->>PowerLimeterInv: newLimitPercent
    PowerLimeterInv-->>PowerLimiter: updated limit
    end

    rect rgb(255, 240, 240)
    note over PowerLimiter,ATFState: Persistence Phase
    PowerLimiter->>PowerLimiter: loop() serializes
    PowerLimiter->>InverterATF: serializeATFData(json)
    InverterATF->>ATFState: Read correction table<br/>& state under mutex
    InverterATF-->>PowerLimiter: JSON object
    PowerLimiter->>RuntimeData: write(RTD with ATF data)
    end
Loading
sequenceDiagram
    participant Config
    participant InverterMeterCtrl as InverterMeters::Controller
    participant Provider
    participant PowerLimiter
    participant PowerLimiterInv as PowerLimiterInverter
    participant ExternalMeter

    Config->>Config: Load InverterMeter config<br/>(source, endpoint, auth)
    
    rect rgb(240, 255, 240)
    note over Config,InverterMeterCtrl: Meter Initialization
    Config->>InverterMeterCtrl: updateSettings()
    InverterMeterCtrl->>Provider: Create provider<br/>(HTTP_JSON/SML/MQTT/SDM)
    Provider->>Provider: init(settings)
    Provider-->>InverterMeterCtrl: ready
    end

    rect rgb(220, 240, 255)
    note over InverterMeterCtrl,ExternalMeter: Polling Cycle
    InverterMeterCtrl->>InverterMeterCtrl: loop() via scheduler
    InverterMeterCtrl->>Provider: loop() (advance polling)
    Provider->>ExternalMeter: HTTP/MQTT/Serial request
    ExternalMeter-->>Provider: power data
    Provider->>Provider: Update last_update,<br/>cache power value
    end

    rect rgb(255, 245, 240)
    note over PowerLimiter,InverterMeterCtrl: Power Limiter Integration
    PowerLimiter->>InverterMeterCtrl: getPower(inverter_serial)
    InverterMeterCtrl->>Provider: Query cached power
    Provider-->>InverterMeterCtrl: float power (optional)
    InverterMeterCtrl-->>PowerLimiter: std::optional<float>
    PowerLimiter->>PowerLimiterInv: setCurrentOutputAcWatts(power)
    PowerLimiterInv->>PowerLimiterInv: Override internal<br/>power reading
    end
Loading
sequenceDiagram
    participant Main
    participant RuntimeData
    participant Scheduler
    participant LittleFS
    participant PowerLimiter
    participant InverterATF

    Main->>RuntimeData: init(scheduler)
    RuntimeData->>Scheduler: Create periodic loop task<br/>(minute interval)
    
    rect rgb(220, 240, 255)
    note over RuntimeData,Scheduler: Startup Read
    RuntimeData->>Main: read(START_UP)
    RuntimeData->>LittleFS: Load RTD JSON file
    LittleFS-->>RuntimeData: JSON {battery_from_start, atf_data}
    RuntimeData->>PowerLimiter: deserializeRTD(data)
    RuntimeData->>InverterATF: deserializeATFData(atf)
    end

    rect rgb(240, 255, 240)
    note over RuntimeData,Scheduler: Periodic Writes
    Scheduler->>RuntimeData: loop() triggers
    RuntimeData->>RuntimeData: getWriteTrigger()<br/>(check min interval)
    RuntimeData->>PowerLimiter: serializeRTD(json)
    RuntimeData->>InverterATF: serializeATFData(json)
    RuntimeData->>LittleFS: Write RTD JSON<br/>{version, save_count,<br/>save_epoch, data}
    end

    rect rgb(255, 240, 240)
    note over RuntimeData,LittleFS: Persistence Events
    Main->>RuntimeData: write(freezeMinutes) on reboot
    RuntimeData->>RuntimeData: Check last write<br/>vs freeze time
    RuntimeData->>LittleFS: Persist current state
    LittleFS-->>RuntimeData: Write complete
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 A rabbit hops through code so bright,
ATF tables correcting might,
Meters now gather power's tale,
Runtime data won't fail,
Battery states dance through the night! 🔋✨

Pre-merge checks and finishing touches

✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The PR description is clearly related to the changeset, describing the implementation of external inverter power metering and ATF (Adaptive Transfer Function) for battery-powered HM-xxx inverters.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Dec 21, 2025

Walkthrough

Introduces three major features: ATF (Adaptive Thermal Framework) for inverter power correction, InverterMeter for reading power data via multiple protocols, and RuntimeData for persistent runtime state management. Extends PowerLimiter configuration, web APIs, and frontend UI to support these features alongside battery state machine refactoring.

Changes

Cohort / File(s) Summary
Core ATF Framework
include/InverterATF.h, src/InverterATF.cpp
Introduces thread-safe InverterATF class with activation, data management, interpolation-based power/limit conversion, serialization, and reporting. Implements correction table mapping and cache mechanism.
PowerLimiter ATF Integration
include/PowerLimiter.h, src/PowerLimiter.cpp
Adds ATF initialization, serialization/deserialization, and per-inverter ATF management. Introduces BatteryState enum replacing discrete boolean flags. Adds status value InverterPowerMeterPending and new public API for RTD/ATF operations.
PowerLimiterInverter ATF Support
include/PowerLimiterInverter.h, src/PowerLimiterInverter.cpp
Extends PowerLimiterInverter to inherit from InverterATF. Adds external meter power caching, ATF-aware limit calculation, and AC power update method.
InverterMeter Controller
include/invertermeter/Controller.h, src/invertermeter/Controller.cpp
New InverterMeters::Controller class managing power meter data via pluggable provider backends (MQTT, SDM, HTTP JSON, SML, UDP Victron). Implements thread-safe accessors and lifecycle management.
InverterMeter Web API
include/WebApi_invertermeter.h, src/WebApi_invertermeter.cpp
New WebApiInverterMeterClass exposing REST endpoints for status, configuration, and testing HTTP JSON/SML providers. Validates and persists inverter meter settings.
RuntimeData Persistence
include/RuntimeData.h, src/RuntimeData.cpp
Introduces RuntimeClass for managing runtime state with scheduled background writes to LittleFS. Supports read/write control, serialization/deserialization, and daily write trigger.
Configuration Structure
include/Configuration.h, src/Configuration.cpp
Adds InverterMeterConfig nested struct to CONFIG_T with source and protocol-specific settings. Adds UseATF boolean flag to PowerLimiter inverter config. Extends serialization/deserialization.
Web API Integration
include/WebApi.h, src/WebApi.cpp, src/WebApi_firmware.cpp, src/WebApi_maintenance.cpp, src/WebApi_sysstatus.cpp, src/WebApi_ws_live.cpp
Integrates InverterMeter web API component. Adds RuntimeData persistence calls post-firmware-update and post-reboot. Exposes runtime save count in system status. Adds ATF power computation in live inverter response.
Core Application Boot
src/main.cpp, src/Datastore.cpp, src/PowerLimiterBatteryInverter.cpp
Initializes InverterMeter and RuntimeData during setup. Datastore now queries InverterMeter.getPower() for AC power when enabled. Standby logic refined with AllowStandby configuration check.
Frontend Routing & Navigation
webapp/src/router/index.ts, webapp/src/components/NavBar.vue
Adds router entry and nav link for new InverterMeter settings page.
Frontend Type Definitions
webapp/src/types/PowerLimiterConfig.ts, webapp/src/types/InverterMeterConfig.ts, webapp/src/types/SystemStatus.ts
Defines TypeScript interfaces for InverterMeterConfig (MQTT, SDM, HTTP JSON, SML, UDP Victron sub-configs), adds use_atf field to PowerLimiterInverterConfig, and runtime_savecount to SystemStatus.
Frontend Views & Forms
webapp/src/views/InverterMeterAdminView.vue, webapp/src/views/PowerLimiterAdminView.vue
New InverterMeterAdminView component with dynamic protocol-specific forms, config fetch/save, and HTTP test endpoints. PowerLimiterAdminView extended with UseATF toggle and isATFFull logic.
Frontend Localization
webapp/src/locales/en.json, webapp/src/locales/de.json, webapp/src/components/FirmwareInfo.vue
Adds InverterMeterSettings UI label, invertermeteradmin translations, UseATF/ATFHint keys, and runtime save count display in firmware info.

Sequence Diagram(s)

sequenceDiagram
    participant UI as Browser / UI
    participant API as Web API
    participant IL as PowerLimiter
    participant INV as PowerLimiterInverter
    participant ATF as InverterATF
    participant RD as RuntimeData
    participant FS as LittleFS
    
    UI->>API: POST /api/invertermeter/config
    API->>IL: updateInverterMeterSettings()
    IL->>IL: setATFData(power, limit)
    IL->>INV: via PowerLimiterInverter
    INV->>ATF: setATFData(power, limit)
    ATF->>ATF: Update correction table
    
    Note over IL,ATF: ATF activation/management
    IL->>INV: activateATF(nomPower)
    INV->>ATF: activateATF(nomPower)
    ATF->>ATF: Initialize power array
    
    Note over RD,FS: Runtime data persistence
    API->>RD: write(freezeMinutes)
    RD->>RD: Serialize ATF + config state
    RD->>FS: Write /runtime.json
    FS->>RD: Success
    RD->>API: Update complete
    
    Note over IL,ATF: Power limit computation with ATF
    IL->>INV: getCurrentLimitWatts()
    INV->>ATF: isATFActive()?
    alt ATF Active
        INV->>ATF: getATFLimit(power)
        ATF->>ATF: Interpolate correction table
        ATF->>INV: Return corrected limit
    else ATF Inactive
        INV->>INV: Use linear calculation
    end
    INV->>IL: Return limit
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Areas requiring extra attention:

  • Thread safety in InverterATF: Verify mutable shared_mutex usage, lock scoping, and atomic flag operations across all public methods (activateATF, setATFData, getATFPower, getATFLimit).
  • PowerLimiter battery state machine: Transition logic between BatteryState enum values (STOP, NO_DISCHARGE, DISCHARGE_ALLOWED, DISCHARGE_NIGHT) in updateInvertersLimits and calcPowerBusUsage to ensure consistency.
  • InverterMeter provider polymorphism: Validate correct provider instantiation and fallback behavior for each source type (MQTT, SDM, HTTP JSON, SML, UDP Victron) in Controller.cpp.
  • RuntimeData persistence timing: Confirm write throttling (freezeMinutes), daily trigger window (00:05–00:10), and atomic flag coordination in loop().
  • ATF data serialization/deserialization: Ensure correct JSON structure and version handling in PowerLimiter::serializeATFtoRTD and deserializeRTDtoATF paths.
  • Web API validation: Check HTTP request validation, header parsing, and error responses in WebApi_invertermeter.cpp for both config and test endpoints.
  • Frontend form state binding: Verify udpVictronPollIntervalSeconds computed property and dynamic section rendering based on inverterMeterSourceList in InverterMeterAdminView.vue.

Poem

🐰 A new meter hops into view,
ATF learns power anew,
Runtime data saved with care,
Limits adjust everywhere—
The system now has power to spare!

Pre-merge checks and finishing touches

✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The pull request description clearly explains the purpose and scope of changes: replacing faulty inverter AC measurement with an external power meter and implementing an Adaptive Transfer Function (ATF) for battery-powered HM-xxx inverters.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
webapp/src/views/PowerLimiterAdminView.vue (1)

741-750: Missing default value for use_atf in new inverter config.

When creating a default config for new inverters in tidyUpInverterConfigs, the use_atf property is not initialized. This may cause issues or warnings when the UI tries to bind to inv.use_atf.

🔎 Proposed fix
                 newInv.power_source = 0; // battery
                 newInv.use_overscaling_to_compensate_shading = false;
                 newInv.allow_standby = true;
+                newInv.use_atf = false;
                 inverters.push(newInv);
🧹 Nitpick comments (22)
include/PowerLimiterInverter.h (1)

96-97: Address or remove the TODO comment.

This method is marked for deletion after testing. Consider tracking this in an issue or removing it before merging if testing is complete.

Do you want me to open an issue to track removing getATFConfigPower() after testing is completed?

src/RuntimeData.cpp (3)

10-10: Typo: "Threadsave" should be "Thread-safe".

- * - Threadsave access to the data is provided by a mutex.
+ * - Thread-safe access to the data is provided by a mutex.

180-180: Minor: Unnecessary close on potentially unopened file.

fRuntime.close() is called unconditionally, but if LittleFS.open() failed (line 154), fRuntime would be invalid. While this is safe (calling close() on an invalid File is a no-op on ESP32), it's clearer to close only when open succeeded.

🔎 Suggested fix
-    if (fRuntime) { fRuntime.close(); }
+    fRuntime.close();  // safe even if not opened, but consider moving inside the if(fRuntime) block above for clarity

Or restructure to close within the success block at line 161.


224-225: Consider clarifying the intent of the getLocalTime check.

The getLocalTime(&time, 5) call on line 224 populates time, but line 225 immediately overwrites it with localtime_r(&_writeEpoch, &time). The intent appears to be checking if time synchronization has occurred before converting the epoch. Consider using a clearer pattern:

🔎 Suggested clarification
-    if ((_writeEpoch != 0) && (getLocalTime(&time, 5))) {
-        localtime_r(&_writeEpoch, &time);
+    struct tm unused;
+    if ((_writeEpoch != 0) && (getLocalTime(&unused, 5))) {
+        localtime_r(&_writeEpoch, &time);

Or add a comment explaining that getLocalTime is used as a time-sync availability check.

include/InverterATF.h (1)

58-64: Clarify locking comments for getATFPower and getATFLimit.

The comments state "use a shared/unique lock when calling this method", but looking at the implementation in src/InverterATF.cpp, these methods acquire locks internally (shared lock first, then upgrade to unique if cache miss). The current comments may mislead callers into thinking they need to hold a lock before calling.

🔎 Suggested clarification
-        // returns the power for the given limit according to the ATF
-        // use a shared/unique lock when calling this method
+        // returns the power for the given limit according to the ATF
+        // internally manages locking (shared lock with upgrade to unique on cache miss)
         uint16_t getATFPower(float const limit) const;

-        // returns the limit for the given power according to the ATF
-        // use a shared/unique lock when calling this method
+        // returns the limit for the given power according to the ATF
+        // internally manages locking (shared lock with upgrade to unique on cache miss)
         float getATFLimit(uint16_t const power) const;
webapp/src/components/FirmwareInfo.vue (1)

86-89: Consider applying consistent number formatting.

Line 84 uses $n(systemStatus.cfgsavecount, 'decimal') for ConfigSaveCount, but line 88 displays runtime_savecount without number formatting. For consistency, consider applying the same formatting:

🔎 Suggested fix
                     <tr>
                         <th>{{ $t('firmwareinfo.RuntimeSaveCount') }}</th>
-                        <td>{{ systemStatus.runtime_savecount }}</td>
+                        <td>{{ $n(systemStatus.runtime_savecount, 'decimal') }}</td>
                     </tr>
src/PowerLimiterBatteryInverter.cpp (2)

52-52: TODO comment noted.

The question about whether it would be safer to set the lower limit again as a fallback is valid. While not blocking, this defensive measure could prevent unexpected states if the standby path is not taken.

Would you like me to propose an implementation that sets the lower limit as a fallback in this case?


2-2: Remove the unused LogHelper.h include from line 2.

The #include "LogHelper.h" doesn't have any corresponding logging calls in the file. If logging will be added later, this include can be restored at that time.

include/WebApi_invertermeter.h (1)

6-7: Consider removing unused includes from header.

ArduinoJson.h and Configuration.h appear to be unused in this header file. They may only be needed in the implementation file (WebApi_invertermeter.cpp). Moving them there would reduce compilation dependencies.

🔎 Suggested change
 #include <ESPAsyncWebServer.h>
 #include <TaskSchedulerDeclarations.h>
-#include <ArduinoJson.h>
-#include "Configuration.h"

Move these includes to src/WebApi_invertermeter.cpp if they're needed there.

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

542-552: Consider simplifying with Array.some().

The isATFFull computed property correctly limits ATF to one inverter. A minor simplification using Array.some() would be more idiomatic:

🔎 Suggested simplification
         isATFFull(): boolean {
-            let full = false;
-            for (const inv of this.powerLimiterConfigList.inverters) {
-                if (inv.use_atf) {
-                    // currently ATF is limited to 1 inverter only
-                    full = true;
-                    break;
-                }
-            }
-            return full;
+            // currently ATF is limited to 1 inverter only
+            return this.powerLimiterConfigList.inverters?.some((inv) => inv.use_atf) ?? false;
         },
src/PowerLimiterInverter.cpp (2)

195-201: ATF‑aware limit/power calculations integrate cleanly; consider optional safety clamp

Using isATFActive() to switch between getATFLimit(...)/getATFPower(...) and the existing linear calculation keeps the old behavior as a fallback and localizes ATF logic well. As a small hardening step, you might consider clamping newRelativeLimit (and the ATF‑derived power) to a sane range (e.g. 0–100% / 0–getInverterMaxPowerWatts()) before logging and sending it on the wire, to guard against unexpected ATF table contents or upstream data glitches.

Also applies to: 354-364


318-320: Tighten external inverter‑meter handling and align style

Functionality-wise this works (external AC power is preferred when recent and the inverter is eligible), but a few refinements would help:

  • The new function body style (...timestamp) {) and the missing space in timestamp == 0|| are inconsistent with the rest of the file and may trip cpplint; consider moving { to the next line and fixing spacing.
  • Since _oInverterMeterPower is a float but getCurrentOutputAcWatts() returns uint16_t, an explicit std::lround/clamp to [0, getInverterMaxPowerWatts()] would make the narrowing intentional and avoid surprising truncation if a slightly out‑of‑range value is ever passed in.
  • If false from setCurrentOutputAcWatts is meant to indicate “wait for fresher meter data before acting”, it’s worth double‑checking the caller logic to ensure that path really does defer DPL decisions as intended.

Also applies to: 464-481

src/WebApi_invertermeter.cpp (1)

219-260: HTTP test endpoints are useful; add a small guard around empty results

The two test endpoints follow the existing PowerMeter pattern: parse a one‑off config, construct a provider, init() + poll(), and return either a formatted success string or the provider’s error string. This is a good debugging aid.

For onTestHttpJsonRequest, you currently assume that DataPointContainer is non‑empty whenever poll() returns the variant’s “values” alternative:

auto const& vals = std::get<values_t>(res);
auto iter = vals.cbegin();
auto pos = snprintf(..., iter->second.getValueText().c_str());

If the provider implementation ever returns an empty container on success, dereferencing iter would be undefined. Adding a quick if (vals.empty()) branch that returns a “no values” message (or treats it as an error) before dereferencing would make this bullet‑proof without changing the normal success path. The SML test endpoint already handles its “no data” case via an explicit isEmpty() check, so this would just align the JSON path.

Also applies to: 263-295

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

19-25: Inconsistent interface naming: PowerMeterHttpJsonValue should be InverterMeterHttpJsonValue.

All other interfaces use the InverterMeter prefix (e.g., InverterMeterMqttValue, InverterMeterMqttConfig), but this interface uses PowerMeter. Consider renaming for consistency.

🔎 Proposed fix
-export interface PowerMeterHttpJsonValue {
+export interface InverterMeterHttpJsonValue {
     http_request: HttpRequestConfig;
     enabled: boolean;
     json_path: string;
     unit: number;
     sign_inverted: boolean;
 }

And update line 30 accordingly:

-    values: Array<PowerMeterHttpJsonValue>;
+    values: Array<InverterMeterHttpJsonValue>;
src/InverterATF.cpp (1)

46-49: Redundant nullptr check after std::make_unique.

std::make_unique throws std::bad_alloc on allocation failure rather than returning nullptr. This check will never trigger in standard C++.

🔎 Proposed fix
     // allocate memory for the ATF data array
     _realPower = std::make_unique<float[]>(_size);
-    if (_realPower == nullptr) {
-        DTU_LOGE("Unable to allocate memory for ATF data array");
-        return false;
-    }

If you need to handle allocation failures gracefully without exceptions, consider using std::unique_ptr<float[]>(_size, std::nothrow) with appropriate error handling, or wrap in a try-catch block.

src/invertermeter/Controller.cpp (2)

52-79: Add a default case to the switch statement.

If imcfg.Source contains an unexpected value, the switch silently falls through without setting _upProvider. Consider adding a default case to log a warning or handle the unknown source type explicitly.

🔎 Proposed fix
         case PowerMeters::Provider::Type::MODBUS_UDP_VICTRON:
             _upProvider = std::make_unique<::PowerMeters::Modbus::Udp::Victron::Provider>(imcfg.UdpVictron);
             break;
+        default:
+            // Unknown source type - _upProvider remains nullptr
+            break;
     }

93-107: Potential TOCTOU: Configuration read before mutex acquisition.

Configuration.get().InverterMeter.Serial is read at line 96 before acquiring the mutex at line 98. If configuration can be updated concurrently, this could lead to inconsistent behavior. Consider moving the configuration read inside the lock, or document that configuration changes require explicit synchronization.

🔎 Proposed fix
 std::optional<float> Controller::getPower(uint64_t inverterID) const
 {
-    // for now we only support one inverter meter
-    auto serial = Configuration.get().InverterMeter.Serial;
-
     std::lock_guard<std::mutex> l(_mutex);
 
+    // for now we only support one inverter meter
+    auto serial = Configuration.get().InverterMeter.Serial;
+
     if (inverterID != serial) { return std::nullopt; }
include/RuntimeData.h (1)

22-22: Document the freezeMinutes behavior.

The write method's freezeMinutes parameter throttles writes, but the default of 10 minutes may surprise callers. Consider adding a brief doc comment or ensuring this is documented in the implementation.

include/invertermeter/Controller.h (1)

4-13: Locking policy comment is misleading.

The comment at line 6 mentions "shared lock" but std::mutex (line 47) only provides exclusive locking. For actual shared/exclusive locking, std::shared_mutex with std::shared_lock would be needed. Consider updating the comment to reflect that all locks are exclusive, or switch to std::shared_mutex if reader-writer semantics are intended.

src/PowerLimiter.cpp (1)

1049-1062: Remove commented-out debug code.

Lines 1052 and 1054 contain commented-out code that appears to be debug/testing remnants. Clean up for production readiness.

Proposed cleanup
             auto maxPower = upInv->getInverterMaxPowerWatts();
-            //if (maxPower == 0) { maxPower = upInv->getATFConfigPower(); } // todo: delete after testing
             if (maxPower == 0) {
-                //DTU_LOGW("cannot initialize ATF: inverter max power unknown");
                 allDone = false;
                 continue;
             }
             if (!upInv->activateATF(maxPower)) {
-                //DTU_LOGW("cannot initialize ATF: activate data structure");
                 allDone = false;
                 continue;
             }
webapp/src/views/InverterMeterAdminView.vue (2)

350-358: Missing error handling for fetch operations.

getInverterMeterConfig() doesn't handle network errors or rejected promises. If the fetch fails, dataLoading remains true indefinitely, leaving the UI stuck in a loading state. Add a .catch() handler or use try/catch with async/await.

Proposed fix
         getInverterMeterConfig() {
             this.dataLoading = true;
             fetch('/api/invertermeter/config', { headers: authHeader() })
                 .then((response) => handleResponse(response, this.$emitter, this.$router))
                 .then((data) => {
                     this.inverterMeterConfigList = data;
                     this.dataLoading = false;
-                });
+                })
+                .catch((error) => {
+                    this.dataLoading = false;
+                    this.alertMessage = error.message || 'Failed to load configuration';
+                    this.alertType = 'danger';
+                    this.showAlert = true;
+                });
         },

378-401: Hardcoded strings should use i18n.

Lines 380 and 404 use hardcoded English strings "Triggering HTTP request..." instead of this.$t(). For consistency with the rest of the component, these should be localized.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0dc7b6c and 7d996ef.

📒 Files selected for processing (33)
  • include/Configuration.h (2 hunks)
  • include/InverterATF.h (1 hunks)
  • include/PowerLimiter.h (3 hunks)
  • include/PowerLimiterInverter.h (4 hunks)
  • include/RuntimeData.h (1 hunks)
  • include/WebApi.h (2 hunks)
  • include/WebApi_invertermeter.h (1 hunks)
  • include/invertermeter/Controller.h (1 hunks)
  • src/Configuration.cpp (4 hunks)
  • src/Datastore.cpp (2 hunks)
  • src/InverterATF.cpp (1 hunks)
  • src/PowerLimiter.cpp (15 hunks)
  • src/PowerLimiterBatteryInverter.cpp (3 hunks)
  • src/PowerLimiterInverter.cpp (4 hunks)
  • src/RuntimeData.cpp (1 hunks)
  • src/WebApi.cpp (1 hunks)
  • src/WebApi_firmware.cpp (2 hunks)
  • src/WebApi_invertermeter.cpp (1 hunks)
  • src/WebApi_maintenance.cpp (2 hunks)
  • src/WebApi_sysstatus.cpp (2 hunks)
  • src/WebApi_ws_live.cpp (2 hunks)
  • src/invertermeter/Controller.cpp (1 hunks)
  • src/main.cpp (2 hunks)
  • webapp/src/components/FirmwareInfo.vue (1 hunks)
  • webapp/src/components/NavBar.vue (1 hunks)
  • webapp/src/locales/de.json (4 hunks)
  • webapp/src/locales/en.json (4 hunks)
  • webapp/src/router/index.ts (2 hunks)
  • webapp/src/types/InverterMeterConfig.ts (1 hunks)
  • webapp/src/types/PowerLimiterConfig.ts (1 hunks)
  • webapp/src/types/SystemStatus.ts (1 hunks)
  • webapp/src/views/InverterMeterAdminView.vue (1 hunks)
  • webapp/src/views/PowerLimiterAdminView.vue (2 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
{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_sysstatus.cpp
  • src/PowerLimiterBatteryInverter.cpp
  • src/PowerLimiterInverter.cpp
  • src/WebApi_firmware.cpp
  • include/Configuration.h
  • src/WebApi_invertermeter.cpp
  • src/WebApi_maintenance.cpp
  • src/main.cpp
  • src/WebApi.cpp
  • include/WebApi.h
  • include/WebApi_invertermeter.h
  • src/WebApi_ws_live.cpp
  • src/InverterATF.cpp
  • src/invertermeter/Controller.cpp
  • include/PowerLimiter.h
  • src/PowerLimiter.cpp
  • src/RuntimeData.cpp
  • include/InverterATF.h
  • src/Datastore.cpp
  • include/invertermeter/Controller.h
  • src/Configuration.cpp
  • include/PowerLimiterInverter.h
  • include/RuntimeData.h
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/types/PowerLimiterConfig.ts
  • webapp/src/router/index.ts
  • webapp/src/components/FirmwareInfo.vue
  • webapp/src/types/SystemStatus.ts
  • webapp/src/types/InverterMeterConfig.ts
  • webapp/src/views/PowerLimiterAdminView.vue
  • webapp/src/components/NavBar.vue
  • webapp/src/views/InverterMeterAdminView.vue
🧠 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/WebApi_firmware.cpp
  • src/WebApi_maintenance.cpp
  • src/main.cpp
  • include/PowerLimiter.h
  • src/PowerLimiter.cpp
  • src/RuntimeData.cpp
  • include/RuntimeData.h
🧬 Code graph analysis (8)
src/PowerLimiterBatteryInverter.cpp (4)
src/PowerLimiterInverter.cpp (4)
  • getCurrentOutputAcWatts (316-320)
  • getCurrentOutputAcWatts (316-316)
  • getCurrentLimitWatts (354-364)
  • getCurrentLimitWatts (354-354)
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)
src/WebApi_invertermeter.cpp (1)
src/WebApi.cpp (2)
  • init (18-53)
  • init (18-18)
webapp/src/types/InverterMeterConfig.ts (1)
webapp/src/types/HttpRequestConfig.ts (1)
  • HttpRequestConfig (1-9)
include/WebApi_invertermeter.h (2)
src/WebApi.cpp (2)
  • init (18-53)
  • init (18-18)
src/WebApi_invertermeter.cpp (12)
  • init (18-29)
  • init (18-18)
  • onStatus (32-68)
  • onStatus (32-32)
  • onAdminGet (71-78)
  • onAdminGet (71-71)
  • onAdminPost (81-216)
  • onAdminPost (81-81)
  • onTestHttpJsonRequest (219-260)
  • onTestHttpJsonRequest (219-219)
  • onTestHttpSmlRequest (263-295)
  • onTestHttpSmlRequest (263-263)
include/PowerLimiter.h (1)
src/PowerLimiter.cpp (12)
  • serializeRTD (1015-1020)
  • serializeRTD (1015-1015)
  • deserializeRTD (1022-1027)
  • deserializeRTD (1022-1022)
  • initATF (1036-1074)
  • initATF (1036-1036)
  • serializeATFtoRTD (1079-1091)
  • serializeATFtoRTD (1079-1079)
  • deserializeRTDtoATF (1096-1111)
  • deserializeRTDtoATF (1096-1096)
  • getATFInverterPower (1117-1127)
  • getATFInverterPower (1117-1117)
include/InverterATF.h (1)
src/InverterATF.cpp (18)
  • activateATF (37-69)
  • activateATF (37-37)
  • deactivateATF (75-84)
  • deactivateATF (75-75)
  • setATFData (91-178)
  • setATFData (91-91)
  • deserializeATFData (341-369)
  • deserializeATFData (341-341)
  • serializeATFData (325-335)
  • serializeATFData (325-325)
  • printATFReport (375-429)
  • printATFReport (375-375)
  • getATFPower (215-254)
  • getATFPower (215-215)
  • getATFLimit (261-319)
  • getATFLimit (261-261)
  • makeAveragePower (184-208)
  • makeAveragePower (184-184)
include/invertermeter/Controller.h (1)
src/invertermeter/Controller.cpp (18)
  • init (31-39)
  • init (31-31)
  • updateSettings (41-84)
  • updateSettings (41-41)
  • getPowerTotal (86-91)
  • getPowerTotal (86-86)
  • getLastUpdate (122-127)
  • getLastUpdate (122-122)
  • isDataValid (129-134)
  • isDataValid (129-129)
  • getPower (93-107)
  • getPower (93-93)
  • getTime (109-120)
  • getTime (109-109)
  • getRequestTime (136-148)
  • getRequestTime (136-136)
  • loop (150-157)
  • loop (150-150)
include/PowerLimiterInverter.h (2)
src/PowerLimiterInverter.cpp (5)
  • PowerLimiterInverter (35-46)
  • setCurrentOutputAcWatts (464-481)
  • setCurrentOutputAcWatts (464-464)
  • getCurrentOutputAcWatts (316-320)
  • getCurrentOutputAcWatts (316-316)
include/InverterATF.h (1)
  • InverterATF (21-85)
🪛 GitHub Actions: cpplint
src/InverterATF.cpp

[error] 399-399: cpplint: Do not use variable-length arrays. Use an appropriately named ('k' followed by CamelCase) compile-time constant for the size. [runtime/arrays]

🔇 Additional comments (50)
webapp/src/types/PowerLimiterConfig.ts (1)

32-32: LGTM!

The use_atf boolean field is correctly added to the interface, following the existing snake_case naming convention used for other fields like is_governed and is_behind_power_meter.

include/PowerLimiterInverter.h (2)

78-94: LGTM!

The ATF integration methods are well-designed:

  • Clear separation between isATFEnabled() (config flag) and isATFActive() (runtime state from base class)
  • setATFData() correctly pairs current output power with the current limit percentage
  • The comment on line 89-90 helpfully documents the synchronization requirement

162-164: LGTM!

The _oInverterMeterPower optional member correctly stores the external meter value. Using std::optional<float> allows distinguishing between "no value available" and "value is zero", which is appropriate for sensor data that may be unavailable.

src/RuntimeData.cpp (1)

78-140: LGTM!

The write() method is well-structured:

  • Uses a clean exit lambda for consistent logging and state updates
  • Properly guards shared state with mutex
  • Only commits new state (_writeVersion, _writeEpoch, _writeCount) after successful file write
  • File handle is properly closed in all code paths
include/InverterATF.h (2)

21-28: LGTM!

The class is correctly marked as non-copyable and non-movable (deleted copy/move constructors and assignments), which is appropriate since it manages unique resources (_realPower unique_ptr) and contains synchronization primitives (_mutex).


70-79: LGTM!

Good use of std::atomic<bool> for _useATF to allow lock-free reads for quick eligibility checks. The mutable qualifiers on _cache and _mutex correctly enable their modification in const member functions. The fixed size of 101 elements supports direct percentage indexing (0-100%).

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

87-87: No action required. Both the translation key RuntimeSaveCount and the type field runtime_savecount are properly defined and implemented.

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

34-34: LGTM!

The new runtime_savecount field is correctly typed as a string and logically placed alongside cfgsavecount. This aligns with the backend API changes in WebApi_sysstatus.cpp that populate this field via RuntimeData.getWriteCountAndTimeString().

src/WebApi_sysstatus.cpp (2)

17-17: LGTM!

The include is necessary for the RuntimeData usage on line 83.


83-83: LGTM!

The runtime save count is correctly exposed via the system status API, aligning with the frontend SystemStatus type definition. The placement after cfgsavecount is logical and maintains consistency.

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

89-93: LGTM!

The new Inverter Meter Settings navigation item follows the established pattern and is logically placed after Power Meter Settings. The routing and translation key structure are consistent with existing menu items.

src/WebApi_maintenance.cpp (2)

11-11: LGTM!

The include is necessary for RuntimeData usage on line 49.


47-50: LGTM!

Persisting runtime data before reboot is appropriate. The placement after sending the response but before triggering the restart ensures the write operation completes. The 60-minute throttle prevents excessive writes while still capturing recent state changes.

src/WebApi.cpp (1)

44-44: LGTM!

The Inverter Meter web API initialization follows the established pattern and is logically positioned between Power Meter and Power Limiter. This aligns with the member declaration in include/WebApi.h.

src/PowerLimiterBatteryInverter.cpp (1)

13-13: LGTM - Strengthened standby condition.

The standby logic now requires both the allowStandby parameter AND the _config.AllowStandby flag to be true. This adds configuration-level control over standby behavior, which is appropriate for the ATF integration where standby must be more strictly controlled. The change is consistently applied across all three locations where standby is invoked.

Also applies to: 48-48, 60-60

include/WebApi.h (2)

23-23: LGTM!

The include is necessary for the WebApiInverterMeterClass member declaration on line 78 and follows the established pattern for web API component headers.


78-78: LGTM!

The Inverter Meter web API component is correctly declared and logically positioned between Power Meter and Power Limiter, matching the initialization order in src/WebApi.cpp.

src/WebApi_firmware.cpp (2)

13-13: LGTM!

The include is necessary for RuntimeData usage on line 53.


51-54: LGTM!

Persisting runtime data before the firmware update restart is appropriate and consistent with the reboot handling in WebApi_maintenance.cpp. The placement after sending the response ensures the user receives feedback, and the 60-minute throttle is reasonable for this infrequent operation.

src/WebApi_ws_live.cpp (2)

17-17: LGTM!

The inclusion of PowerLimiter.h is necessary for accessing getATFInverterPower().


276-281: LGTM!

The ATF power integration looks correct. The code properly uses std::optional pattern to check for ATF availability and falls back to the standard calculation when ATF is not active for this inverter.

webapp/src/router/index.ts (2)

15-15: LGTM!

Import follows the established naming convention and is appropriately placed alphabetically.


131-135: LGTM!

The new route is correctly defined and follows the existing route structure patterns. The route name and path are consistent with other settings routes.

src/Datastore.cpp (2)

8-8: LGTM!

Include for the InverterMeter controller is appropriately added.


95-106: Potential inconsistency: InverterMeter path ignores polling state.

When InverterMeter.getPower() returns a value, the power is accumulated regardless of whether polling is enabled for the inverter (lines 96-98). However, the fallback path (lines 100-104) only accumulates power when inv->getEnablePolling() is true.

This could lead to unexpected behavior where an inverter with polling disabled still contributes to _totalAcPowerEnabled when using InverterMeter. Please verify if this is intentional or if the InverterMeter path should also check inv->getEnablePolling().

include/Configuration.h (2)

152-152: LGTM!

The UseATF boolean flag is appropriately added to the inverter configuration structure.


441-450: LGTM!

The InverterMeterConfig struct follows the same pattern as PowerMeterConfig, reusing existing power meter configuration types for different providers. Serialization and deserialization are properly implemented in the corresponding read/write methods in Configuration.cpp, with all member fields (Enabled, Source, Serial, and nested configurations) correctly handled.

include/WebApi_invertermeter.h (1)

9-21: LGTM!

The class structure follows the established pattern for WebApi classes in the codebase. The public init() method and private handler methods are appropriately declared.

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

164-168: LGTM!

The condition is updated to show AllowStandby for both battery-powered (0) and SmartBuffer-powered (2) inverters, which aligns with the ATF feature requirements.


170-178: LGTM!

The UseATF input is correctly:

  • Shown only for battery-powered inverters (power_source == 0)
  • Disabled when ATF is already assigned to another inverter (isATFFull && !inv.use_atf)

This enforces the stated limitation that ATF is available for only one inverter at a time.

src/Configuration.cpp (1)

221-230: ATF flag and InverterMeter config wiring look consistent and backward compatible

use_atf is serialized and deserialized symmetrically with a safe default (false), and the new invertermeter section reuses the existing powermeter (de)serialization helpers so legacy configs will naturally fall back to defaults without special migration. No issues from my side here.

Also applies to: 433-452, 665-679, 924-932

src/main.cpp (1)

32-32: InverterMeter and RuntimeData initialization order is correct

Including the new controllers and placing InverterMeter.init(...) before PowerLimiter/GridCharger/Battery, then RuntimeData.init(...) and RuntimeData.read() afterwards, respects the documented requirement that RuntimeData be initialized after these components but before entering the main loop. Based on learnings, this order looks right.

Also applies to: 40-40, 150-162

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

15-16: DE localization for Inverter Meter, RuntimeData, and ATF matches backend/EN keys

The added German strings (menu entry, firmware runtime counter, invertermeteradmin block, and ATF‑related descriptions) line up with the English keys and backend expectations, and the terminology is consistent with existing DPL wording. No changes needed.

Also applies to: 286-287, 678-684, 740-743

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

15-16: EN localization additions for Inverter Meter and ATF are coherent with the new features

The new English keys for the Inverter Meter menu/admin view, runtime save counter, and ATF options are clear, technically accurate, and consistent with existing DPL terminology. They also match the corresponding DE keys, which keeps the locales in sync.

Also applies to: 286-287, 678-684, 740-743

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

1-53: Well-structured type definitions for InverterMeter configuration.

The interfaces correctly model the configuration hierarchy for various meter sources (MQTT, SDM, HTTP JSON/SML, UDP Victron) and properly import the shared HttpRequestConfig type.

src/InverterATF.cpp (2)

37-69: Proper mutex usage and initialization logic.

The activateATF function correctly uses std::unique_lock for exclusive access, initializes the correction table with linear values, and handles the already-initialized case. The state management with atomic _useATF flag is appropriate.


215-254: The _cache member is properly declared as mutable in the header file (include/InverterATF.h, line 78), allowing both getATFPower and getATFLimit to modify it despite being const methods. The implementation is correct.

src/invertermeter/Controller.cpp (1)

31-39: Clean initialization with TaskScheduler integration.

The controller properly initializes the loop task with the scheduler, binds the callback, and calls updateSettings() to configure the provider. Good use of TASK_FOREVER for continuous operation.

include/PowerLimiter.h (3)

61-68: Well-designed RTD/ATF public interface.

The new public methods for runtime data and ATF functionality are cleanly organized:

  • Serialization/deserialization pairs for RTD and ATF
  • initATF returning bool for initialization status
  • getATFInverterPower returning std::optional for safe value retrieval

87-90: Clear battery state machine design.

The BatteryState enum with explicit states (STOP, NO_DISCHARGE, DISCHARGE_ALLOWED, DISCHARGE_NIGHT) provides better readability and type safety compared to raw booleans. The associated state variables _fromStart and _oneStopPerNightDone support the logic well.


37-37: New status enumerator for inverter power meter pending state.

InverterPowerMeterPending extends the status enum to reflect the additional state when waiting for inverter meter data, aligning with the new InverterMeter integration.

include/RuntimeData.h (3)

10-17: Proper non-copyable, non-movable semantics.

The class correctly deletes copy and move constructors/assignment operators, which is appropriate for a singleton-like runtime data manager that owns scheduler tasks and file handles.


37-45: Good use of atomics and mutex for thread safety.

The design appropriately uses:

  • std::atomic<bool> for flags that are read/written from different contexts (_readOK, _writeOK, _readNow, _writeNow)
  • mutable std::mutex for protecting shared state that may be accessed from const methods

48-48: RuntimeData initialization order is correct. The code initializes SolarCharger, PowerMeter, PowerLimiter, GridCharger, and Battery before RuntimeData, then calls RuntimeData.init() and RuntimeData.read() before the main loop. The initialization sequence matches the required ordering and is properly documented with inline comments.

include/invertermeter/Controller.h (1)

22-49: LGTM!

The Controller class is well-structured with a clean public interface and appropriate thread-safety via mutex. The implementation in Controller.cpp correctly uses std::lock_guard for all public accessors.

src/PowerLimiter.cpp (4)

60-73: LGTM!

The new InverterPowerMeterPending status is properly added with the frozen::map size updated to 12 to match the entry count.


676-682: LGTM!

The early exit to standby all battery-powered inverters when BatteryState::STOP is active prevents further battery discharge below the stop threshold.


1117-1127: Consider whether zero power should return std::nullopt.

Line 1124 returns std::nullopt if power == 0. If zero is a valid ATF power output (e.g., inverter producing nothing), the UI might want to display "0 W" rather than omitting the value. Verify this is the intended behavior for the live view display.


211-222: No actionable issues found. The code correctly handles unavailable InverterMeter data: when getPower() returns std::nullopt, the fallback to 0.0f triggers the power <= 0.0f check inside setCurrentOutputAcWatts(), which returns true and allows the system to proceed using internal inverter measurements, as intended.

Likely an incorrect or invalid review comment.

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

282-335: LGTM!

Component structure follows existing patterns in the codebase. Data types are properly defined, and the component correctly uses existing UI components like BasePage, CardElement, and InputElement.

Comment thread include/invertermeter/Controller.h
Comment thread src/InverterATF.cpp Outdated
Comment thread src/InverterATF.cpp Outdated
Comment thread src/PowerLimiter.cpp
Comment thread src/PowerLimiter.cpp
Comment on lines +1015 to +1027
void PowerLimiterClass::serializeRTD(JsonObject const& obj) const
{
// Note: Up to now the PowerLimiterClass does not use a mutex
// Use of atomic<bool> would be an solution but I want to avoid the overhead
obj["battery_from_start"] = _fromStart;
}

void PowerLimiterClass::deserializeRTD(JsonObject const& obj)
{
// Note: Up to now the PowerLimiterClass does not use a mutex
// Use of atomic<bool> would be an solution but I want to avoid the overhead
_fromStart = obj["battery_from_start"] | false;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find call sites of serializeRTD and deserializeRTD
echo "=== serializeRTD/deserializeRTD call sites ==="
rg -n "serializeRTD|deserializeRTD" --type cpp -B 2 -A 2

# Find all _fromStart accesses
echo -e "\n=== _fromStart variable accesses ==="
rg -n "_fromStart" --type cpp -B 1 -A 1

# Check for threading/task creation in PowerLimiter
echo -e "\n=== Loop function and task context ==="
rg -n "void.*loop|xTaskCreate|xTaskCreatePinnedToCore" src/PowerLimiter.cpp

Repository: hoylabs/OpenDTU-OnBattery

Length of output: 4099


🏁 Script executed:

#!/bin/bash
# Check where RuntimeData functions are called from (to see if different task)
echo "=== RuntimeData function calls ==="
rg -n "RuntimeData\." --type cpp -B 2 -A 2 | head -50

# Look for task creation related to RuntimeData or file I/O
echo -e "\n=== Task creation patterns ==="
rg -n "xTaskCreate|xTaskCreatePinnedToCore|Thread" --type cpp | grep -E "(RuntimeData|File|SPIFFS)" | head -20

# Check if RuntimeData runs on different core/task
echo -e "\n=== RuntimeData.cpp file to understand execution context ==="
head -50 src/RuntimeData.cpp

Repository: hoylabs/OpenDTU-OnBattery

Length of output: 4332


🏁 Script executed:

#!/bin/bash
# Check RuntimeData's mutex protection and how it calls serializeRTD/deserializeRTD
echo "=== RuntimeData.write() and read() with mutex context ==="
sed -n '100,200p' src/RuntimeData.cpp

# Also check the lock/mutex handling
echo -e "\n=== Mutex usage in RuntimeData ==="
rg -n "mutex|lock" src/RuntimeData.cpp -i -B 2 -A 2

Repository: hoylabs/OpenDTU-OnBattery

Length of output: 6137


Fix unprotected access to _fromStart in deserializeRTD.

The deserializeRTD method writes to _fromStart without mutex protection, while loop() continuously reads and writes this variable. Although RuntimeData.write() correctly protects serializeRTD with a mutex lock, the corresponding deserializeRTD call at RuntimeData.cpp:174 executes outside the mutex scope, contradicting the code comment at lines 171-172 which instructs to "protect the shared data with a local mutex."

This creates a data race between the scheduled RuntimeData.read() task (running once per minute) and PowerLimiter.loop(). Protect _fromStart with either: (1) a mutex lock in deserializeRTD, (2) wrapping the deserializeRTD call in RuntimeData.read() with the same mutex used for serializeRTD, or (3) using std::atomic<bool> for _fromStart if performance is a concern.

🤖 Prompt for AI Agents
In src/PowerLimiter.cpp around lines 1015 to 1027, deserializeRTD writes to
_fromStart without the mutex used to protect serializeRTD, causing a data race
with loop(); fix by protecting _fromStart consistently — either make _fromStart
std::atomic<bool> and leave methods lock-free, or (preferred) acquire the same
local mutex used by RuntimeData::write()/serializeRTD before assigning
_fromStart in deserializeRTD so reads/writes are synchronized; ensure you use
the identical mutex object and scope as serializeRTD to fully eliminate the
race.

Comment on lines +81 to +216
void WebApiInverterMeterClass::onAdminPost(AsyncWebServerRequest* request)
{
if (!WebApi.checkCredentials(request)) {
return;
}

AsyncJsonResponse* response = new AsyncJsonResponse();
JsonDocument root;
if (!WebApi.parseRequestData(request, response, root)) {
return;
}

auto& retMsg = response->getRoot();

if (!(root["enabled"].is<bool>() && root["source"].is<uint32_t>())) {
retMsg["message"] = "Values are missing!";
response->setLength();
request->send(response);
return;
}

auto checkHttpConfig = [&](JsonObject const& cfg) -> bool {

if (!cfg["url"].is<String>()
|| (!cfg["url"].as<String>().startsWith("http://")
&& !cfg["url"].as<String>().startsWith("https://"))) {
retMsg["message"] = "URL must either start with http:// or https://!";
response->setLength();
request->send(response);
return false;
}

if ((cfg["auth_type"].as<uint8_t>() != HttpRequestConfig::Auth::None)
&& (cfg["username"].as<String>().length() == 0 || cfg["password"].as<String>().length() == 0)) {
retMsg["message"] = "Username or password must not be empty!";
response->setLength();
request->send(response);
return false;
}

if (!cfg["timeout"].is<uint16_t>()
|| cfg["timeout"].as<uint16_t>() <= 0) {
retMsg["message"] = "Timeout must be greater than 0 ms!";
response->setLength();
request->send(response);
return false;
}

return true;
};

if (static_cast<::PowerMeters::Provider::Type>(root["source"].as<uint8_t>()) == ::PowerMeters::Provider::Type::HTTP_JSON) {
JsonObject httpJson = root["http_json"];
JsonArray valueConfigs = httpJson["values"];
for (uint8_t i = 0; i < valueConfigs.size(); i++) {
JsonObject valueConfig = valueConfigs[i].as<JsonObject>();

if (i > 0 && !valueConfig["enabled"].as<bool>()) {
continue;
}

if (i == 0 || httpJson["individual_requests"].as<bool>()) {
if (!checkHttpConfig(valueConfig["http_request"].as<JsonObject>())) {
return;
}
}

if (!valueConfig["json_path"].is<String>()
|| valueConfig["json_path"].as<String>().length() == 0) {
retMsg["message"] = "Json path must not be empty!";
response->setLength();
request->send(response);
return;
}
}
}

if (static_cast<::PowerMeters::Provider::Type>(root["source"].as<uint8_t>()) == ::PowerMeters::Provider::Type::HTTP_SML) {
JsonObject httpSml = root["http_sml"];
if (!checkHttpConfig(httpSml["http_request"].as<JsonObject>())) {
return;
}
}

if (static_cast<::PowerMeters::Provider::Type>(root["source"].as<uint8_t>()) == ::PowerMeters::Provider::Type::MODBUS_UDP_VICTRON) {
JsonObject udpVictron = root["udp_victron"];
if (!udpVictron["ip_address"].is<String>()
|| udpVictron["ip_address"].as<String>().length() == 0) {
retMsg["message"] = "IP address must not be empty!";
response->setLength();
request->send(response);
return;
}

if (!udpVictron["polling_interval_ms"].is<uint32_t>()
|| udpVictron["polling_interval_ms"].as<uint32_t>() <= 0) {
retMsg["message"] = "Polling interval must be greater than 0 ms!";
response->setLength();
request->send(response);
return;
}
}

{
auto guard = Configuration.getWriteGuard();
auto& config = guard.getConfig();
config.InverterMeter.Enabled = root["enabled"].as<bool>();
config.InverterMeter.Source = root["source"].as<uint8_t>();

// Interpret the string as a hex value and convert it to uint64_t
const uint64_t serial = strtoll(root["serial"].as<String>().c_str(), NULL, 16);
config.InverterMeter.Serial = serial;

Configuration.deserializePowerMeterMqttConfig(root["mqtt"].as<JsonObject>(),
config.InverterMeter.Mqtt);

Configuration.deserializePowerMeterSerialSdmConfig(root["serial_sdm"].as<JsonObject>(),
config.InverterMeter.SerialSdm);

Configuration.deserializePowerMeterHttpJsonConfig(root["http_json"].as<JsonObject>(),
config.InverterMeter.HttpJson);

Configuration.deserializePowerMeterHttpSmlConfig(root["http_sml"].as<JsonObject>(),
config.InverterMeter.HttpSml);

Configuration.deserializePowerMeterUdpVictronConfig(root["udp_victron"].as<JsonObject>(),
config.InverterMeter.UdpVictron);
}

WebApi.writeConfig(retMsg);

WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);

InverterMeter.updateSettings();

}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Config POST flow mirrors PowerMeter; consider requiring serial explicitly

The admin POST handler is structured well: credentials check, basic enabled/source validation, per‑provider HTTP/UDP validation, guarded config update via getWriteGuard(), then a writeConfig + InverterMeter.updateSettings().

One small robustness gap: you don’t currently validate the presence/format of root["serial"] before calling strtoll(...). If the client omits serial or sends an empty string, this silently writes 0 into config.InverterMeter.Serial, which is also your “unused” sentinel elsewhere. Requiring a non‑empty hex string and failing fast would make misconfigurations more obvious to users.

A minimal adjustment would be to extend the initial Values are missing! check (or add a dedicated block) to assert root["serial"].is<String>() && root["serial"].as<String>().length() > 0 and return a clear error message if it’s not met.

🤖 Prompt for AI Agents
In src/WebApi_invertermeter.cpp around lines 81 to 216, add validation for
root["serial"] before it is parsed: ensure root["serial"].is<String>() &&
root["serial"].as<String>().length() > 0 (optionally also validate it contains
only hex digits) and if not set retMsg["message"] = "Serial must be a non-empty
hex string!", call response->setLength(); request->send(response); and return;
then only call strtoll(...) after this check; place the check either by
extending the initial "Values are missing!" validation or as a small dedicated
block immediately before the strtoll call so an omitted/empty serial no longer
silently writes 0.

Comment on lines +49 to +275
<template v-if="inverterMeterConfigList.enabled">
<!-- yarn linter wants us to not combine v-if with v-for, so we need to wrap the CardElements //-->
<template v-if="inverterMeterConfigList.source === 0">
<CardElement
v-for="(mqtt, index) in inverterMeterConfigList.mqtt.values"
v-bind:key="index"
:text="$t('powermeteradmin.MqttValue', { valueNumber: index + 1 })"
textVariant="text-bg-primary"
add-space
>
<InputElement
:label="$t('powermeteradmin.MqttTopic')"
v-model="mqtt.topic"
type="text"
maxlength="256"
wide
/>

<InputElement
:label="$t('powermeteradmin.mqttJsonPath')"
v-model="mqtt.json_path"
type="text"
maxlength="256"
:tooltip="$t('powermeteradmin.valueJsonPathDescription')"
wide
/>

<div class="row mb-3">
<label for="mqtt_power_unit" class="col-sm-4 col-form-label">
{{ $t('powermeteradmin.valueUnit') }}
</label>
<div class="col-sm-8">
<select id="mqtt_power_unit" class="form-select" v-model="mqtt.unit">
<option v-for="u in unitTypeList" :key="u.key" :value="u.key">
{{ u.value }}
</option>
</select>
</div>
</div>

<InputElement
:label="$t('powermeteradmin.valueSignInverted')"
v-model="mqtt.sign_inverted"
:tooltip="$t('powermeteradmin.valueSignInvertedHint')"
type="checkbox"
wide
/>
</CardElement>
</template>

<CardElement
v-if="inverterMeterConfigList.source === 1 || inverterMeterConfigList.source === 2"
:text="$t('powermeteradmin.SDM')"
textVariant="text-bg-primary"
add-space
>
<InputElement
:label="$t('powermeteradmin.pollingInterval')"
v-model="inverterMeterConfigList.serial_sdm.polling_interval"
type="number"
min="1"
max="15"
:postfix="$t('powermeteradmin.seconds')"
wide
/>

<InputElement
:label="$t('powermeteradmin.sdmaddress')"
v-model="inverterMeterConfigList.serial_sdm.address"
type="number"
wide
/>
</CardElement>

<template v-if="inverterMeterConfigList.source === 3">
<CardElement :text="$t('invertermeteradmin.HTTP')" textVariant="text-bg-primary" add-space>
<InputElement
:label="$t('powermeteradmin.httpIndividualRequests')"
v-model="inverterMeterConfigList.http_json.individual_requests"
type="checkbox"
wide
/>

<InputElement
:label="$t('powermeteradmin.pollingInterval')"
v-model="inverterMeterConfigList.http_json.polling_interval"
type="number"
min="1"
max="15"
:postfix="$t('powermeteradmin.seconds')"
wide
/>
</CardElement>

<CardElement
v-for="(httpJson, index) in inverterMeterConfigList.http_json.values"
:key="index"
:text="$t('powermeteradmin.httpValue', { valueNumber: index + 1 })"
textVariant="text-bg-primary"
add-space
>
<InputElement
v-if="index > 0"
:label="$t('powermeteradmin.httpEnabled')"
v-model="httpJson.enabled"
type="checkbox"
wide
/>

<template v-if="httpJson.enabled || index == 0">
<HttpRequestSettings
v-model="httpJson.http_request"
v-if="index == 0 || inverterMeterConfigList.http_json.individual_requests"
/>

<InputElement
:label="$t('powermeteradmin.valueJsonPath')"
v-model="httpJson.json_path"
type="text"
maxlength="256"
:tooltip="$t('powermeteradmin.valueJsonPathDescription')"
wide
/>

<div class="row mb-3">
<label for="power_unit" class="col-sm-4 col-form-label">
{{ $t('powermeteradmin.valueUnit') }}
</label>
<div class="col-sm-8">
<select id="power_unit" class="form-select" v-model="httpJson.unit">
<option v-for="u in unitTypeList" :key="u.key" :value="u.key">
{{ u.value }}
</option>
</select>
</div>
</div>

<InputElement
:label="$t('powermeteradmin.valueSignInverted')"
v-model="httpJson.sign_inverted"
:tooltip="$t('powermeteradmin.valueSignInvertedHint')"
type="checkbox"
wide
/>
</template>
</CardElement>

<CardElement
:text="$t('powermeteradmin.testHttpJsonHeader')"
textVariant="text-bg-primary"
add-space
>
<div class="text-center mb-3">
<button type="button" class="btn btn-primary" @click="testHttpJsonRequest()">
{{ $t('powermeteradmin.testHttpJsonRequest') }}
</button>
</div>

<BootstrapAlert
v-model="testHttpJsonRequestAlert.show"
dismissible
:variant="testHttpJsonRequestAlert.type"
>
{{ testHttpJsonRequestAlert.message }}
</BootstrapAlert>
</CardElement>
</template>

<template v-if="inverterMeterConfigList.source === 6">
<CardElement :text="$t('powermeteradmin.HTTP_SML')" textVariant="text-bg-primary" add-space>
<InputElement
:label="$t('powermeteradmin.pollingInterval')"
v-model="inverterMeterConfigList.http_sml.polling_interval"
type="number"
min="1"
max="15"
:postfix="$t('powermeteradmin.seconds')"
wide
/>

<HttpRequestSettings v-model="inverterMeterConfigList.http_sml.http_request" />
</CardElement>

<CardElement
:text="$t('powermeteradmin.testHttpSmlHeader')"
textVariant="text-bg-primary"
add-space
>
<div class="text-center mb-3">
<button type="button" class="btn btn-primary" @click="testHttpSmlRequest()">
{{ $t('powermeteradmin.testHttpSmlRequest') }}
</button>
</div>

<BootstrapAlert
v-model="testHttpSmlRequestAlert.show"
dismissible
:variant="testHttpSmlRequestAlert.type"
>
{{ testHttpSmlRequestAlert.message }}
</BootstrapAlert>
</CardElement>
</template>

<template v-if="inverterMeterConfigList.source === 7">
<CardElement :text="$t('powermeteradmin.UDP_VICTRON')" textVariant="text-bg-primary" add-space>
<InputElement
:label="$t('powermeteradmin.pollingInterval')"
v-model="udpVictronPollIntervalSeconds"
type="number"
min="0.5"
max="15.0"
step="0.1"
:postfix="$t('powermeteradmin.seconds')"
wide
/>

<InputElement
:label="$t('powermeteradmin.ipAddress')"
v-model="inverterMeterConfigList.udp_victron.ip_address"
type="text"
maxlength="15"
wide
/>
</CardElement>
</template>
</template>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing configuration UI for SML (source 4) and SMAHM2 (source 5).

The source dropdown includes "SML" (key 4) and "SMAHM2" (key 5) options, but there are no corresponding <template v-if="..."> sections to configure them. Users selecting these sources will see no configuration fields. Either add UI sections for these sources or remove them from the dropdown if they're not supported for InverterMeter.

🤖 Prompt for AI Agents
In webapp/src/views/InverterMeterAdminView.vue around lines 49 to 275, the
component renders configuration blocks for many inverterMeterConfigList.source
values but is missing UI sections for source 4 (SML) and source 5 (SMAHM2); add
corresponding <template v-if="inverterMeterConfigList.source === 4"> and
<template v-if="inverterMeterConfigList.source === 5"> blocks (similar to the
existing HTTP/HTTP_SML/SDM/UDP sections) that render CardElement wrappers and
InputElement fields bound to the appropriate inverterMeterConfigList.sml and
inverterMeterConfigList.sma_hm2 properties (e.g., polling interval,
request/settings, address/IP, enable flags, JSON path/unit/sign_inverted as
applicable), or alternatively remove/disable the 4 and 5 options from the source
dropdown and related translations if those sources aren’t supported for
InverterMeter; ensure v-model bindings match the data model and reuse existing
components (HttpRequestSettings, InputElement, unitTypeList) for consistency.

Comment on lines +76 to +87
<div class="row mb-3">
<label for="mqtt_power_unit" class="col-sm-4 col-form-label">
{{ $t('powermeteradmin.valueUnit') }}
</label>
<div class="col-sm-8">
<select id="mqtt_power_unit" class="form-select" v-model="mqtt.unit">
<option v-for="u in unitTypeList" :key="u.key" :value="u.key">
{{ u.value }}
</option>
</select>
</div>
</div>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Duplicate id attributes in v-for loop.

Line 81 uses a static id="mqtt_power_unit" inside a v-for loop, creating duplicate IDs when multiple MQTT values exist. This violates HTML standards and can cause accessibility issues with label associations.

Proposed fix
-                        <label for="mqtt_power_unit" class="col-sm-4 col-form-label">
+                        <label :for="`mqtt_power_unit_${index}`" class="col-sm-4 col-form-label">
                             {{ $t('powermeteradmin.valueUnit') }}
                         </label>
                         <div class="col-sm-8">
-                            <select id="mqtt_power_unit" class="form-select" v-model="mqtt.unit">
+                            <select :id="`mqtt_power_unit_${index}`" class="form-select" v-model="mqtt.unit">
🤖 Prompt for AI Agents
In webapp/src/views/InverterMeterAdminView.vue around lines 76 to 87, the select
and its label use a static id/for ("mqtt_power_unit") inside a v-for, producing
duplicate IDs; change both to bound attributes that include a unique suffix
(e.g., the loop index or the mqtt object id) so each instance has a unique id
and the label's for points to that id (use :id and :for with a template string
like `mqtt_power_unit_${index}` or `mqtt_power_unit_${mqtt.id}`).

Comment on lines +173 to +184
<div class="row mb-3">
<label for="power_unit" class="col-sm-4 col-form-label">
{{ $t('powermeteradmin.valueUnit') }}
</label>
<div class="col-sm-8">
<select id="power_unit" class="form-select" v-model="httpJson.unit">
<option v-for="u in unitTypeList" :key="u.key" :value="u.key">
{{ u.value }}
</option>
</select>
</div>
</div>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Duplicate id attributes in v-for loop.

Same issue as the MQTT section - line 178 uses static id="power_unit" inside a v-for loop for HTTP JSON values.

Proposed fix
-                            <label for="power_unit" class="col-sm-4 col-form-label">
+                            <label :for="`power_unit_${index}`" class="col-sm-4 col-form-label">
                                 {{ $t('powermeteradmin.valueUnit') }}
                             </label>
                             <div class="col-sm-8">
-                                <select id="power_unit" class="form-select" v-model="httpJson.unit">
+                                <select :id="`power_unit_${index}`" class="form-select" v-model="httpJson.unit">

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In webapp/src/views/InverterMeterAdminView.vue around lines 173 to 184, the
select element uses a static id="power_unit" inside a v-for context which
creates duplicate id attributes; update the template to generate unique ids (for
example bind :id to include the loop index or the unit key) and update the
corresponding label's for attribute to match that bound id so each select/label
pair has a unique identifier.

Comment on lines +339 to +348
computed: {
udpVictronPollIntervalSeconds: {
get(): number {
return this.inverterMeterConfigList.udp_victron.polling_interval_ms / 1000;
},
set(value: number) {
this.inverterMeterConfigList.udp_victron.polling_interval_ms = value * 1000;
},
},
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Potential runtime error accessing nested property on empty object.

inverterMeterConfigList is initialized as {} (line 305). The computed property at line 342 accesses this.inverterMeterConfigList.udp_victron.polling_interval_ms, which will throw a TypeError before data is loaded. Add a guard or provide a default structure.

Proposed fix
     computed: {
         udpVictronPollIntervalSeconds: {
             get(): number {
+                if (!this.inverterMeterConfigList.udp_victron) return 0;
                 return this.inverterMeterConfigList.udp_victron.polling_interval_ms / 1000;
             },
             set(value: number) {
+                if (!this.inverterMeterConfigList.udp_victron) return;
                 this.inverterMeterConfigList.udp_victron.polling_interval_ms = value * 1000;
             },
         },
     },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
computed: {
udpVictronPollIntervalSeconds: {
get(): number {
return this.inverterMeterConfigList.udp_victron.polling_interval_ms / 1000;
},
set(value: number) {
this.inverterMeterConfigList.udp_victron.polling_interval_ms = value * 1000;
},
},
},
computed: {
udpVictronPollIntervalSeconds: {
get(): number {
if (!this.inverterMeterConfigList.udp_victron) return 0;
return this.inverterMeterConfigList.udp_victron.polling_interval_ms / 1000;
},
set(value: number) {
if (!this.inverterMeterConfigList.udp_victron) return;
this.inverterMeterConfigList.udp_victron.polling_interval_ms = value * 1000;
},
},
},
🤖 Prompt for AI Agents
In webapp/src/views/InverterMeterAdminView.vue around lines 339-348, the
computed getter/setter directly access
this.inverterMeterConfigList.udp_victron.polling_interval_ms which will throw if
inverterMeterConfigList or udp_victron is not set; update the getter to return a
safe default (e.g., 0) when the nested path is missing using a guard/optional
chaining and nullish coalescing, and update the setter to ensure
inverterMeterConfigList.udp_victron exists (create an object) before assigning
polling_interval_ms = value * 1000 so the ms↔s conversion remains correct
without runtime errors.

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: 7

♻️ Duplicate comments (8)
webapp/src/views/InverterMeterAdminView.vue (4)

76-87: Duplicate id attributes in v-for loop.

The id="mqtt_power_unit" is static inside a v-for loop, creating duplicate IDs when multiple MQTT values exist. This violates HTML standards and causes accessibility issues.

Proposed fix
-                        <label for="mqtt_power_unit" class="col-sm-4 col-form-label">
+                        <label :for="`mqtt_power_unit_${index}`" class="col-sm-4 col-form-label">
                             {{ $t('powermeteradmin.valueUnit') }}
                         </label>
                         <div class="col-sm-8">
-                            <select id="mqtt_power_unit" class="form-select" v-model="mqtt.unit">
+                            <select :id="`mqtt_power_unit_${index}`" class="form-select" v-model="mqtt.unit">

173-184: Duplicate id attributes in v-for loop.

Same issue as the MQTT section - id="power_unit" is static inside a v-for loop for HTTP JSON values.

Proposed fix
-                            <label for="power_unit" class="col-sm-4 col-form-label">
+                            <label :for="`power_unit_${index}`" class="col-sm-4 col-form-label">
                                 {{ $t('powermeteradmin.valueUnit') }}
                             </label>
                             <div class="col-sm-8">
-                                <select id="power_unit" class="form-select" v-model="httpJson.unit">
+                                <select :id="`power_unit_${index}`" class="form-select" v-model="httpJson.unit">

339-348: Potential runtime error accessing nested property on empty object.

inverterMeterConfigList is initialized as {} (line 305). The computed property accesses this.inverterMeterConfigList.udp_victron.polling_interval_ms, which will throw a TypeError before data is loaded.

Proposed fix
     computed: {
         udpVictronPollIntervalSeconds: {
             get(): number {
+                if (!this.inverterMeterConfigList.udp_victron) return 0;
                 return this.inverterMeterConfigList.udp_victron.polling_interval_ms / 1000;
             },
             set(value: number) {
+                if (!this.inverterMeterConfigList.udp_victron) return;
                 this.inverterMeterConfigList.udp_victron.polling_interval_ms = value * 1000;
             },
         },
     },

306-314: Missing configuration UI for SML (source 4) and SMAHM2 (source 5).

The source dropdown includes "SML" (key 4) and "SMAHM2" (key 5) options, but there are no corresponding <template v-if="..."> sections to configure them. Users selecting these sources will see no configuration fields.

include/invertermeter/Controller.h (1)

15-19: Missing <optional> header for std::optional return type.

Line 35 uses std::optional<float> but the <optional> header is not included. This may compile on some implementations due to transitive includes but is not portable.

Proposed fix
 #include <powermeter/Provider.h>
 #include <TaskSchedulerDeclarations.h>
 #include <memory>
 #include <mutex>
+#include <optional>
src/WebApi_invertermeter.cpp (1)

189-192: Missing validation for serial field before parsing.

The code calls strtoll(root["serial"].as<String>().c_str(), NULL, 16) without validating that serial exists or is a valid non-empty hex string. If omitted or empty, this silently writes 0 which is the "unused" sentinel value.

Proposed fix

Add validation before parsing the serial:

+    if (!root["serial"].is<String>() || root["serial"].as<String>().length() == 0) {
+        retMsg["message"] = "Serial must be a non-empty hex string!";
+        response->setLength();
+        request->send(response);
+        return;
+    }
+
     // Interpret the string as a hex value and convert it to uint64_t
     const uint64_t serial = strtoll(root["serial"].as<String>().c_str(), NULL, 16);
src/PowerLimiter.cpp (2)

306-318: Unreachable fallback code remains.

Lines 314-317 are unreachable because the if-else at lines 307-312 exhaustively covers both day values (true and false). This was previously flagged - please remove the dead code.


1015-1027: Data race on _fromStart between serialization methods and loop().

Both serializeRTD and deserializeRTD access _fromStart without synchronization while loop() reads and writes the same variable via getBatteryState(). The comment acknowledges this but dismisses std::atomic<bool> due to "overhead" concerns.

For bool on ESP32/Xtensa, std::atomic<bool> has negligible overhead (typically compiles to the same instructions) and provides correctness guarantees. Consider using std::atomic<bool> for _fromStart to eliminate the data race without measurable performance impact.

🧹 Nitpick comments (12)
include/InverterATF.h (2)

34-64: Clarify locking documentation on public methods.

The locking policy header (lines 4-11) states that public mutating methods take locks internally, but the comments on individual public methods like activateATF, deactivateATF, setATFData, etc. say "use a unique lock when calling this method" or "use a shared lock when calling this method". This is contradictory and may confuse maintainers.

If the methods do acquire locks internally (as per the header policy), the per-method comments should be removed or rephrased (e.g., "acquires an exclusive lock internally").


72-72: Consider documenting why size must remain fixed.

The emphatic comment "never NEVER change the size!" is important but doesn't explain why. Since ATF data is persisted to runtime storage, changing the array size would corrupt deserialized data. A brief clarifying comment would help future maintainers.

🔎 Suggested clarification
-        static constexpr uint8_t _size = 101;           // Fixed size of the ATF data array, never NEVER change the size!
+        static constexpr uint8_t _size = 101;           // Fixed size of the ATF data array (0-100%), persisted to storage; changing breaks deserialization
src/PowerLimiterBatteryInverter.cpp (2)

2-2: Unused include.

LogHelper.h is included but no logging calls are visible in this file. Consider removing it if not needed.

🔎 Proposed fix
 #include "PowerLimiterBatteryInverter.h"
-#include "LogHelper.h"

52-52: Address the TODO comment.

The TODO raises a valid point about whether setting the lower limit would be safer than returning 0 when standby is not allowed. Returning 0 means no reduction is applied, which may leave the inverter at its current limit. If this is intentional, consider documenting the rationale; otherwise, setting the lower limit as a fallback could provide more predictable behavior.

Would you like me to help analyze the implications of each approach or open an issue to track this?

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

161-168: ATF UI wiring is consistent; consider a safer and more focused isATFFull implementation

  • The extended AllowStandby condition and the new UseATF checkbox for power_source == 0 match the backend config and PR objectives.
  • isATFFull currently iterates over this.powerLimiterConfigList.inverters without null‑guarding and counts every inverter, governed or not.

To make this more robust and aligned with the “only one ATF inverter” constraint, you could:

isATFFull(): boolean {
    const inverters = this.powerLimiterConfigList?.inverters || [];
    // Optionally restrict to governed battery-powered inverters if that matches backend limits:
    // const inverters = this.governedBatteryPoweredInverters;
    return inverters.some((inv: PowerLimiterInverterConfig) => inv.use_atf);
}

This avoids any risk if inverters is temporarily undefined and makes the capacity rule explicit.

Also applies to: 170-178, 542-552

src/PowerLimiterInverter.cpp (2)

316-320: External inverter meter override for output power is reasonable; consider an explicit cast

Preferring _oInverterMeterPower when present and falling back to inverter TYPE_AC/CH0/FLD_PAC stats implements the intended override cleanly. For clarity and to make the narrowing explicit, you might cast the float to uint16_t:

if (_oInverterMeterPower.has_value()) {
    return static_cast<uint16_t>(_oInverterMeterPower.value());
}
return _spInverter->Statistics()->getChannelFieldValue(TYPE_AC, CH0, FLD_PAC);

464-481: setCurrentOutputAcWatts logic looks sound; verify call-site threading model

The function correctly:

  • Rejects obviously invalid meter values (non-positive power, zero timestamp, missing stats, or ineligible inverter).
  • Ensures the meter timestamp is not more than ~2s older than the latest stats (statsMillis - timestamp > 2000 path).
  • Clears _oInverterMeterPower when data is stale and only accepts fresh samples.

Two follow-ups to consider:

  1. If power readings of exactly 0.0f from the external meter should be treated as valid (e.g., explicit standby confirmation), you may want to relax power <= 0.0f to < 0.0f.
  2. If setCurrentOutputAcWatts can be called from a different context than update()/getCurrentOutputAcWatts, protect _oInverterMeterPower with the same mutex used for stats/limits to avoid data races.
src/RuntimeData.cpp (1)

194-232: getWriteCountAndTimeString is fine; minor clarity tweak optional

The helper correctly formats <count> / <dd>-<mon> <hh>:<mm> when _writeEpoch is non-zero and getLocalTime succeeds, and otherwise uses " / no time". You might simplify the logic slightly by separating “time available” from the formatting, but the current implementation is clear and thread-safe thanks to the mutex.

src/InverterATF.cpp (1)

44-49: Redundant null check after std::make_unique.

std::make_unique throws std::bad_alloc on allocation failure; it never returns nullptr. The check at line 46 is unreachable dead code.

Proposed fix
     // allocate memory for the ATF data array
     _realPower = std::make_unique<float[]>(_size);
-    if (_realPower == nullptr) {
-        DTU_LOGE("Unable to allocate memory for ATF data array");
-        return false;
-    }

If you want to handle allocation failure gracefully, use std::make_unique in a try-catch block or use std::unique_ptr with new (std::nothrow) instead.

src/invertermeter/Controller.cpp (1)

52-79: Missing default case in switch statement.

If a new PowerMeters::Provider::Type is added but not handled here, _upProvider remains unset from the previous reset() call without any warning. Consider adding a default case to log an error or assert.

Proposed fix
         case PowerMeters::Provider::Type::MODBUS_UDP_VICTRON:
             _upProvider = std::make_unique<::PowerMeters::Modbus::Udp::Victron::Provider>(imcfg.UdpVictron);
             break;
+        default:
+            // Unknown provider type - log warning and leave provider null
+            break;
     }
webapp/src/types/InverterMeterConfig.ts (1)

19-25: Naming inconsistency: PowerMeterHttpJsonValue vs InverterMeter* pattern.

All other interfaces use the InverterMeter prefix, but this one uses PowerMeter. Consider renaming to InverterMeterHttpJsonValue for consistency.

Proposed fix
-export interface PowerMeterHttpJsonValue {
+export interface InverterMeterHttpJsonValue {
     http_request: HttpRequestConfig;
     enabled: boolean;
     json_path: string;
     unit: number;
     sign_inverted: boolean;
 }

Also update the reference in InverterMeterHttpJsonConfig:

 export interface InverterMeterHttpJsonConfig {
     polling_interval: number;
     individual_requests: boolean;
-    values: Array<PowerMeterHttpJsonValue>;
+    values: Array<InverterMeterHttpJsonValue>;
 }
src/PowerLimiter.cpp (1)

1036-1074: Clean up commented-out code and consider early loop exit.

  1. Lines 1052 and 1054 contain commented-out code that should be removed.
  2. Line 1042: Using continue after counter >= 1 causes the loop to iterate through remaining inverters without doing anything. Consider break for efficiency, or restructure if the intent is to deactivate ATF on non-selected inverters (which currently happens at lines 1068-1070).
Proposed cleanup
         if (upInv->isATFEnabled() && !upInv->isATFActive()) {

             auto maxPower = upInv->getInverterMaxPowerWatts();
-            //if (maxPower == 0) { maxPower = upInv->getATFConfigPower(); } // todo: delete after testing
             if (maxPower == 0) {
-                //DTU_LOGW("cannot initialize ATF: inverter max power unknown");
                 allDone = false;
                 continue;
             }
             if (!upInv->activateATF(maxPower)) {
-                //DTU_LOGW("cannot initialize ATF: activate data structure");
                 allDone = false;
                 continue;
             }
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

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

📒 Files selected for processing (33)
  • include/Configuration.h
  • include/InverterATF.h
  • include/PowerLimiter.h
  • include/PowerLimiterInverter.h
  • include/RuntimeData.h
  • include/WebApi.h
  • include/WebApi_invertermeter.h
  • include/invertermeter/Controller.h
  • src/Configuration.cpp
  • src/Datastore.cpp
  • src/InverterATF.cpp
  • src/PowerLimiter.cpp
  • src/PowerLimiterBatteryInverter.cpp
  • src/PowerLimiterInverter.cpp
  • src/RuntimeData.cpp
  • src/WebApi.cpp
  • src/WebApi_firmware.cpp
  • src/WebApi_invertermeter.cpp
  • src/WebApi_maintenance.cpp
  • src/WebApi_sysstatus.cpp
  • src/WebApi_ws_live.cpp
  • src/invertermeter/Controller.cpp
  • src/main.cpp
  • webapp/src/components/FirmwareInfo.vue
  • webapp/src/components/NavBar.vue
  • webapp/src/locales/de.json
  • webapp/src/locales/en.json
  • webapp/src/router/index.ts
  • webapp/src/types/InverterMeterConfig.ts
  • webapp/src/types/PowerLimiterConfig.ts
  • webapp/src/types/SystemStatus.ts
  • webapp/src/views/InverterMeterAdminView.vue
  • webapp/src/views/PowerLimiterAdminView.vue
🧰 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/types/SystemStatus.ts
  • webapp/src/types/PowerLimiterConfig.ts
  • webapp/src/components/NavBar.vue
  • webapp/src/router/index.ts
  • webapp/src/components/FirmwareInfo.vue
  • webapp/src/views/PowerLimiterAdminView.vue
  • webapp/src/types/InverterMeterConfig.ts
  • webapp/src/views/InverterMeterAdminView.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.cpp
  • src/WebApi_sysstatus.cpp
  • src/WebApi_ws_live.cpp
  • include/Configuration.h
  • src/PowerLimiterInverter.cpp
  • src/Datastore.cpp
  • src/WebApi_firmware.cpp
  • src/WebApi_invertermeter.cpp
  • src/main.cpp
  • include/invertermeter/Controller.h
  • src/invertermeter/Controller.cpp
  • src/WebApi_maintenance.cpp
  • src/PowerLimiterBatteryInverter.cpp
  • include/RuntimeData.h
  • include/PowerLimiter.h
  • include/WebApi.h
  • include/WebApi_invertermeter.h
  • src/Configuration.cpp
  • src/RuntimeData.cpp
  • src/PowerLimiter.cpp
  • src/InverterATF.cpp
  • include/InverterATF.h
  • include/PowerLimiterInverter.h
🧠 Learnings (1)
📚 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/RuntimeData.h
  • include/PowerLimiter.h
  • src/RuntimeData.cpp
  • src/PowerLimiter.cpp
🧬 Code graph analysis (7)
src/PowerLimiterInverter.cpp (1)
include/PowerLimiterInverter.h (1)
  • getInverterMaxPowerWatts (35-74)
webapp/src/types/InverterMeterConfig.ts (1)
webapp/src/types/HttpRequestConfig.ts (1)
  • HttpRequestConfig (1-9)
src/WebApi_invertermeter.cpp (1)
src/WebApi.cpp (2)
  • init (18-53)
  • init (18-18)
include/invertermeter/Controller.h (3)
src/invertermeter/Controller.cpp (16)
  • init (31-39)
  • init (31-31)
  • updateSettings (41-84)
  • updateSettings (41-41)
  • getPowerTotal (86-91)
  • getPowerTotal (86-86)
  • getLastUpdate (122-127)
  • getLastUpdate (122-122)
  • getPower (93-107)
  • getPower (93-93)
  • getTime (109-120)
  • getTime (109-109)
  • getRequestTime (136-148)
  • getRequestTime (136-136)
  • loop (150-157)
  • loop (150-150)
lib/Frozen/frozen/bits/pmh.h (2)
  • std (128-142)
  • private (151-169)
include/battery/zendure/Stats.h (1)
  • optional (65-144)
include/WebApi_invertermeter.h (2)
src/WebApi.cpp (2)
  • init (18-53)
  • init (18-18)
src/WebApi_invertermeter.cpp (12)
  • init (18-29)
  • init (18-18)
  • onStatus (32-68)
  • onStatus (32-32)
  • onAdminGet (71-78)
  • onAdminGet (71-71)
  • onAdminPost (81-216)
  • onAdminPost (81-81)
  • onTestHttpJsonRequest (219-260)
  • onTestHttpJsonRequest (219-219)
  • onTestHttpSmlRequest (263-295)
  • onTestHttpSmlRequest (263-263)
src/RuntimeData.cpp (2)
src/WebApi_firmware.cpp (2)
  • init (15-29)
  • init (15-15)
src/WebApi_maintenance.cpp (2)
  • init (13-18)
  • init (13-13)
include/PowerLimiterInverter.h (1)
include/InverterATF.h (1)
  • InverterATF (21-85)
🔇 Additional comments (35)
include/Configuration.h (2)

146-158: LGTM!

The UseATF field addition to POWERLIMITER_INVERTER_CONFIG_T is correctly placed and follows the existing naming and type conventions in the struct.


441-451: LGTM!

The InverterMeterConfig struct appropriately reuses existing PowerMeter sub-config types and adds the Serial field to associate the meter with a specific inverter. This is consistent with the PR objective of allowing inverter meter configuration and assignment via WebUI.

src/WebApi_ws_live.cpp (2)

17-17: LGTM!

The include is necessary for accessing PowerLimiter.getATFInverterPower().


276-281: LGTM!

The ATF power integration correctly prioritizes ATF-calculated values when available while maintaining the linear fallback calculation. The optional handling with has_value() and value() is correct.

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

89-93: LGTM!

The new menu item follows the established pattern for navigation items, uses proper i18n, and is logically positioned in the settings dropdown near related power/meter settings.

src/WebApi_sysstatus.cpp (1)

17-17: LGTM!

The runtime save count is appropriately added to system status, following the existing pattern of cfgsavecount. This provides useful diagnostic information about runtime data persistence.

Also applies to: 83-83

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

34-34: LGTM!

The runtime_savecount field is correctly typed as string to match the backend's formatted string output, and is logically placed next to cfgsavecount.

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

32-32: LGTM!

The use_atf field correctly mirrors the backend's UseATF configuration option and follows the snake_case naming convention used throughout this TypeScript interface.

src/WebApi_maintenance.cpp (1)

47-50: LGTM!

Persisting runtime data before an intentional reboot is good practice. The 60-minute threshold prevents excessive flash wear while ensuring recent data is saved. The implementation is safe: RuntimeData.write() is a synchronous blocking operation that completes before returning, and RestartHelper.triggerRestart() schedules a restart via the task scheduler with approximately 1 second delay, providing ample time for the write to finish before the actual restart occurs.

include/WebApi.h (1)

23-23: LGTM!

The new WebApi_invertermeter.h include and WebApiInverterMeterClass member follow the established pattern and are consistently placed alongside related power meter modules.

Also applies to: 78-78

src/WebApi_firmware.cpp (1)

51-54: LGTM!

Persisting runtime data before an OTA-triggered reboot ensures no data loss. The 60-minute threshold prevents excessive flash writes while the placement after request->send() ensures the response is sent before any blocking operation.

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

15-15: LGTM!

The new localization entries follow existing patterns and provide clear, descriptive text for the Inverter Meter settings, runtime save count display, and ATF configuration options.

Also applies to: 286-286, 678-684, 740-742

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

86-89: LGTM!

The new RuntimeSaveCount row correctly displays the pre-formatted string from systemStatus.runtime_savecount. Since the backend provides this as a combined "count / time" string (via getWriteCountAndTimeString()), direct rendering without numeric formatting is appropriate.

webapp/src/router/index.ts (1)

15-15: LGTM!

The new route for Inverter Meter Settings follows the established routing conventions and is logically placed after the Inverter Settings route.

Also applies to: 131-135

src/WebApi.cpp (1)

44-44: LGTM!

The inverter meter Web API initialization is correctly placed in the initialization sequence, maintaining consistency with the header declaration order and logical grouping of related modules.

src/main.cpp (1)

32-32: LGTM!

The initialization sequence correctly places:

  • InverterMeter.init() after PowerMeter.init() and before PowerLimiter.init()
  • RuntimeData.init() and RuntimeData.read() after all other components

This aligns with the established requirement that RuntimeData must be initialized after the components it serves. Based on learnings, this ordering ensures RuntimeData acts as a persistence service accessed during runtime operations rather than initialization.

Also applies to: 40-40, 150-161

src/PowerLimiterBatteryInverter.cpp (1)

13-13: LGTM!

The addition of && _config.AllowStandby correctly enforces the user's configuration setting. Previously, the allowStandby runtime parameter alone controlled standby behavior, ignoring the per-inverter AllowStandby config option. This fix ensures both conditions must be satisfied.

Also applies to: 48-48, 60-60

src/Datastore.cpp (1)

8-8: InverterMeter fast-path for AC power looks correct; just confirm interaction with polling and controller thread-safety

Using InverterMeter.getPower(inv->serial()) with an std::optional and falling back to per‑channel TYPE_AC stats preserves previous behavior when no meter is configured, and ensures we don’t double‑count. The digits minimum of 1 when the meter is used is a reasonable fallback. Please just verify that:

  • InverterMeter.getPower() is cheap/safe to call inside this locked loop, and
  • It’s intended that AC power is summed even if inv->getEnablePolling() is false, as long as the external meter provides a value.

Also applies to: 95-106

src/PowerLimiterInverter.cpp (1)

195-201: ATF-relative-limit integration in updateLimit is consistent with existing flow

Switching newRelativeLimit to use getATFLimit(*_oTargetPowerLimitWatts) when isATFActive() and falling back to the previous linear conversion keeps the rest of the update logic (deviation checks, logging, retries) intact. This is a clean, local change; just ensure getATFLimit() gracefully handles edge cases (e.g., limits near 0/100%) so we never send obviously out-of-range percentages.

src/Configuration.cpp (1)

215-230: PowerLimiter use_atf and InverterMeter config round-trip look correct; watch defaults

  • Adding t["use_atf"] = s.UseATF; and inv.UseATF = s["use_atf"] | false; makes the new flag fully round-trippable with a sane default of false.
  • The new invertermeter block mirrors the existing powermeter handling (enabled/source/serial + MQTT, SDM, HTTP JSON/SML, UDP Victron) and read/write paths are symmetric.

Two small points to keep in mind:

  1. On read(), if older config files don’t contain an invertermeter section at all, the deserializePowerMeter*Config calls will see empty JsonObjects and rely on their internal | default behavior. That matches how powermeter is already handled but is worth keeping in mind when debugging migrations.
  2. config.InverterMeter.Source defaults to POWERMETER_SOURCE when absent; verify that this default makes sense for inverter meters (instead of e.g. a dedicated INVERTERMETER_SOURCE_DEFAULT), to avoid misconfiguration if the section is partially missing.

Also applies to: 433-452, 638-679, 924-933

src/RuntimeData.cpp (4)

33-40: RuntimeData singleton and loop scheduling fit the intended usage

Creating RuntimeClass RuntimeData{RUNTIME_VERSION}; and scheduling its loop() every minute is consistent with the design of a background persistence service and with the initialization ordering described for this project (RuntimeData after the major components, then used at runtime). The loop simply reacts to _writeNow / _readNow flags and the daily trigger, so there’s no unexpected behavior here.

Based on learnings, RuntimeData is being used as intended in the lifecycle.

Also applies to: 43-51


78-140: Write throttling and commit-after-success semantics are solid

The write(freezeMinutes) implementation:

  • Enforces a minimum interval between writes when freezeMinutes > 0.
  • Uses a local JsonDocument, fills info.version/save_count/save_epoch, then delegates to PowerLimiter.serializeRTD() and serializeATFtoRTD() while holding the mutex.
  • Only updates _writeVersion, _writeEpoch, _writeCount after a successful file write.

This is a good pattern for durability and protects against partial updates if a write fails. Just ensure all contributors to runtime data (currently only PowerLimiter) follow the comment and use their own internal locking as needed when touching shared state during serialization.


148-188: Read path and START_UP vs ON_DEMAND split look correct

The read(ReadMode mode) function:

  • Attempts to open and deserialize /runtime.json but still initializes _writeVersion, _writeCount, _writeEpoch from whatever is present (or defaults) even on error.
  • For START_UP, calls PowerLimiter.deserializeRTD(doc["power_limiter"]);
  • For ON_DEMAND, calls PowerLimiter.deserializeRTDtoATF(doc.as<JsonVariant>());

This matches the intended two-phase initialization (configurable data at startup, ATF data on demand) and fails over to default values when the file is missing or invalid. That’s appropriate for runtime-only data.


238-255: Daily write trigger window logic behaves as intended

getWriteTrigger():

  • Waits for valid local time.
  • Returns true once between 00:05 and 00:10 per day, using _lastTrigger to edge-detect the minute range.
  • Resets _lastTrigger outside the window.

This is a straightforward and efficient way to guarantee at most one scheduled write per day in addition to explicit triggers.

include/PowerLimiter.h (2)

61-68: LGTM! Well-structured ATF and RTD API additions.

The new public methods for serialization/deserialization and ATF functionality follow a consistent pattern and provide clear separation of concerns. The const qualifiers and return types are appropriate.


87-90: Clean state machine design for battery discharge management.

The BatteryState enum with clear state names (STOP, NO_DISCHARGE, DISCHARGE_ALLOWED, DISCHARGE_NIGHT) provides a well-defined state machine. The related tracking flags _fromStart and _oneStopPerNightDone appropriately support the state transitions.

include/invertermeter/Controller.h (1)

4-13: Excellent locking policy documentation.

The detailed comments explaining the locking strategy (shared vs exclusive locks, public vs private method conventions, atomic flag usage) provide valuable guidance for maintainers and help prevent concurrency bugs.

src/WebApi_invertermeter.cpp (1)

219-260: LGTM! Test endpoint implementation is well-structured.

The onTestHttpJsonRequest handler properly validates credentials, parses request data, creates a scoped provider via std::make_unique, polls for data, and formats results appropriately. Error handling distinguishes between success and failure cases.

include/RuntimeData.h (1)

10-46: Well-designed runtime data persistence class.

The design demonstrates good practices:

  • Deleted copy/move semantics prevent accidental duplication of the singleton-like resource
  • Atomic flags for _readNow/_writeNow enable lock-free signaling between threads
  • Mutable mutex correctly supports locking in const methods
  • Version parameter enables future format migrations
src/InverterATF.cpp (2)

219-254: Thread-safe cache with double-checked locking pattern.

The getATFPower method correctly implements double-checked locking: first checking the cache under a shared lock, then upgrading to an exclusive lock and re-checking before computing. This optimizes for the common cache-hit case while maintaining thread safety.


394-399: VLA issue resolved with named constant.

The buffer size is now defined using a properly named compile-time constant kTableWidth, which satisfies cpplint requirements and ensures the array size is known at compile time.

src/invertermeter/Controller.cpp (1)

31-39: LGTM! Clean initialization flow.

The initialization properly sets up the task scheduler callback, configures infinite iterations, enables the task, and calls updateSettings() to set up the initial provider based on configuration.

src/PowerLimiter.cpp (3)

402-415: LGTM!

The periodic ATF data update and reporting logic is clean and appropriately gated by the 60-second interval check.


676-682: LGTM!

Early exit for battery stop state correctly puts all battery-powered inverters into standby before attempting limit calculations.


214-222: Distinguish between unconfigured and unavailable meter data to prevent silent 0.0f power usage.

When InverterMeter.getPower() returns std::nullopt, the code defaults to 0.0f without distinguishing between two cases: (1) the meter is not configured for this specific inverter, or (2) the meter is configured but data is temporarily unavailable. Both conditions result in the same silent fallback to zero power. If the meter is configured but data is stale, the system should return InverterPowerMeterPending to wait for fresh data rather than proceeding with zero power, which could cause incorrect power calculations.

Comment on lines +8 to 11
#include "InverterATF.h"

class PowerLimiterInverter {
class PowerLimiterInverter : public InverterATF {
public:
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add a virtual destructor and clean up ATF helper surface

The new ATF integration and external meter plumbing are conceptually fine, but there are a couple of structural issues:

  1. Non-virtual base destructor with polymorphic use

    PowerLimiterInverter is used polymorphically (std::unique_ptr<PowerLimiterInverter> holding derived instances), but it has no virtual destructor. This is undefined behavior when deleting derived objects via a base pointer. Please add:

    class PowerLimiterInverter : public InverterATF {
    public:
        virtual ~PowerLimiterInverter() = default;
        // ...
    };
  2. Inline ATF helpers couple the header tightly to implementation details

    • setATFData() calls into InverterATF::setATFData(getCurrentOutputAcWatts(), _spInverter->SystemConfigPara()->getLimitPercent()); directly from the header. That’s OK, but it makes every inclusion of this header depend on these internals; consider moving the definition to the .cpp if compile times become an issue.
    • getATFConfigPower() is marked “todo: delete after testing”. It’s better to remove or #ifdef test-only APIs before merging to avoid public API creep.
  3. External meter state

    _oInverterMeterPower as std::optional<float> aligns with the new override semantics; just ensure any concurrent access is guarded consistently with how the rest of the class is used.

Also applies to: 78-99, 162-164

Comment thread include/WebApi_invertermeter.h
Comment on lines +136 to +148
uint32_t Controller::getRequestTime() const
{
std::lock_guard<std::mutex> l(_mutex);
if (_upProvider) {
if (Configuration.get().InverterMeter.Source == static_cast<uint32_t>(PowerMeters::Provider::Type::HTTP_JSON)) {
return Configuration.get().InverterMeter.HttpJson.PollingInterval + 200;
}
if (Configuration.get().InverterMeter.Source == static_cast<uint32_t>(PowerMeters::Provider::Type::HTTP_SML)) {
return Configuration.get().InverterMeter.HttpSml.PollingInterval + 200;
}
}
return 2000;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check the Controller.cpp file around the getRequestTime method
# Look at line 40 comment about milliseconds and lines 136-148

# First find the file
find . -name "Controller.cpp" -path "*/invertermeter/*" -type f

# Then examine line 40 and the getRequestTime method
if [ -f "src/invertermeter/Controller.cpp" ]; then
  echo "=== Line 40 and surrounding context ==="
  sed -n '35,45p' src/invertermeter/Controller.cpp
  
  echo ""
  echo "=== getRequestTime method (lines 136-148) ==="
  sed -n '136,148p' src/invertermeter/Controller.cpp
fi

Repository: hoylabs/OpenDTU-OnBattery

Length of output: 976


🏁 Script executed:

#!/bin/bash
# Search for PollingInterval usage and definition
echo "=== PollingInterval usages in C++ files ==="
rg -n "PollingInterval" --type cpp -C 2 | head -100

echo ""
echo "=== Check InverterMeterAdminView.vue for polling interval config ==="
find . -name "InverterMeterAdminView.vue" -type f -exec head -300 {} \;

Repository: hoylabs/OpenDTU-OnBattery

Length of output: 20497


🏁 Script executed:

#!/bin/bash
# Check how PollingInterval is defined in configuration structs
rg -n "PollingInterval" --type cpp -A 1 -B 1 | grep -E "(^\s*(uint|int|double|float|long).*PollingInterval|PollingInterval.*=|struct|class)" | head -50

Repository: hoylabs/OpenDTU-OnBattery

Length of output: 1113


🏁 Script executed:

#!/bin/bash
# Find POWERMETER_POLLING_INTERVAL definition
echo "=== POWERMETER_POLLING_INTERVAL definition ==="
rg -n "POWERMETER_POLLING_INTERVAL\s*=" --type cpp -B 1 -A 1

echo ""
echo "=== Look at Configuration.h struct definitions ==="
rg -n "POWERMETER_HTTP_JSON_CONFIG_T|POWERMETER_HTTP_SML_CONFIG_T" include/Configuration.h -A 8

echo ""
echo "=== How getRequestTime() is used ==="
rg -n "getRequestTime" --type cpp -B 2 -A 2

Repository: hoylabs/OpenDTU-OnBattery

Length of output: 1506


🏁 Script executed:

#!/bin/bash
# Check header file for more details on the structs and polling interval fields
echo "=== Check full struct definitions in Configuration.h ==="
sed -n '120,160p' include/Configuration.h

echo ""
echo "=== Find all polling interval related constants ==="
rg -n "POLLING_INTERVAL" include/ --type cpp | head -30

Repository: hoylabs/OpenDTU-OnBattery

Length of output: 1683


🏁 Script executed:

#!/bin/bash
# Double-check how the return value of getRequestTime is used to confirm it expects milliseconds
echo "=== Search for calls/usage of getRequestTime() result ==="
rg -n "getRequestTime()" --type cpp -B 2 -A 3 | head -50

Repository: hoylabs/OpenDTU-OnBattery

Length of output: 826


🏁 Script executed:

#!/bin/bash
# Look at what happens with the 2000ms default return value to understand intent
echo "=== Look at full getRequestTime method context and surrounding code ==="
sed -n '130,155p' src/invertermeter/Controller.cpp

Repository: hoylabs/OpenDTU-OnBattery

Length of output: 967


Multiply PollingInterval by 1000 on lines 141 and 144.

The method returns milliseconds (per the comment at line 40), but PollingInterval is in seconds (UI range 1-15 seconds, default 10). Lines 141 and 144 must convert to milliseconds:

return Configuration.get().InverterMeter.HttpJson.PollingInterval * 1000 + 200;
return Configuration.get().InverterMeter.HttpSml.PollingInterval * 1000 + 200;

This is consistent with how POWERMETER_UDP_VICTRON_CONFIG_T explicitly names its field PollingIntervalMs and Configuration.cpp line 574 multiplies POWERMETER_POLLING_INTERVAL by 1000 for that type.

🤖 Prompt for AI Agents
In src/invertermeter/Controller.cpp around lines 136 to 148, the function
getRequestTime returns milliseconds but uses PollingInterval (which is in
seconds) directly; change the two return expressions to multiply PollingInterval
by 1000 before adding 200 so the value is in milliseconds (e.g., use
PollingInterval * 1000 + 200 for both HTTP_JSON and HTTP_SML branches). Ensure
locking/logic remains unchanged and only adjust the arithmetic to convert
seconds to ms.

Comment thread src/PowerLimiter.cpp
Comment on lines +1117 to +1127
std::optional<uint16_t> PowerLimiterClass::getATFInverterPower(uint64_t inverterSerial, float limit) const {
for (auto& upInv : _inverters) {
if (!upInv->isATFActive()) { continue; }
if (upInv->getSerial() != inverterSerial) { continue; }

// found matching inverter, get power from its ATF
auto power = upInv->getATFPower(limit);
if (power != 0) { return power; }
}
return std::nullopt;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Zero power treated as invalid may cause incorrect behavior.

Line 1124 returns std::nullopt when power == 0, but 0W could be a legitimate ATF calculation (e.g., when the inverter should produce no power). This conflates "no data available" with "calculated power is zero."

Consider returning the value unconditionally once a matching active inverter is found:

Proposed fix
         // found matching inverter, get power from its ATF
         auto power = upInv->getATFPower(limit);
-        if (power != 0) { return power; }
+        return power;
     }
     return std::nullopt;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
std::optional<uint16_t> PowerLimiterClass::getATFInverterPower(uint64_t inverterSerial, float limit) const {
for (auto& upInv : _inverters) {
if (!upInv->isATFActive()) { continue; }
if (upInv->getSerial() != inverterSerial) { continue; }
// found matching inverter, get power from its ATF
auto power = upInv->getATFPower(limit);
if (power != 0) { return power; }
}
return std::nullopt;
}
std::optional<uint16_t> PowerLimiterClass::getATFInverterPower(uint64_t inverterSerial, float limit) const {
for (auto& upInv : _inverters) {
if (!upInv->isATFActive()) { continue; }
if (upInv->getSerial() != inverterSerial) { continue; }
// found matching inverter, get power from its ATF
auto power = upInv->getATFPower(limit);
return power;
}
return std::nullopt;
}
🤖 Prompt for AI Agents
In src/PowerLimiter.cpp around lines 1117-1127 the code treats a computed ATF
power of 0 as "no data" and returns std::nullopt; instead, once an active
inverter with matching serial is found return its computed power unconditionally
(even if zero). Remove the "if (power != 0)" branch and return the calculated
value wrapped in std::optional<uint16_t> (perform the appropriate
conversion/cast from float to uint16_t), leaving std::nullopt only for the case
where no matching active inverter is found.

Comment on lines 354 to +363
uint16_t PowerLimiterInverter::getCurrentLimitWatts() const
{
auto currentLimitPercent = _spInverter->SystemConfigPara()->getLimitPercent();
return static_cast<uint16_t>(currentLimitPercent * getInverterMaxPowerWatts() / 100);

// get the power from ATF or from the linear calculation
if (isATFActive()) {
return getATFPower(currentLimitPercent); // ATF
} else {
return static_cast<uint16_t>(currentLimitPercent * getInverterMaxPowerWatts() / 100); // linear
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

find . -name "PowerLimiterInverter.cpp" -o -name "PowerLimiterInverter.h" | head -20

Repository: hoylabs/OpenDTU-OnBattery

Length of output: 132


🏁 Script executed:

cat -n src/PowerLimiterInverter.cpp | sed -n '354,363p'

Repository: hoylabs/OpenDTU-OnBattery

Length of output: 549


🏁 Script executed:

# Search for getATFPower implementation
grep -n "getATFPower" src/PowerLimiterInverter.cpp | head -5

Repository: hoylabs/OpenDTU-OnBattery

Length of output: 128


🏁 Script executed:

# Search for getEligibility implementation
grep -n "getEligibility" src/PowerLimiterInverter.cpp | head -5

Repository: hoylabs/OpenDTU-OnBattery

Length of output: 278


🏁 Script executed:

# Search for isATFActive implementation
grep -n "isATFActive" src/PowerLimiterInverter.cpp | head -5

Repository: hoylabs/OpenDTU-OnBattery

Length of output: 130


🏁 Script executed:

# Get getEligibility() implementation
cat -n src/PowerLimiterInverter.cpp | sed -n '48,80p'

Repository: hoylabs/OpenDTU-OnBattery

Length of output: 1849


🏁 Script executed:

# Search for getATFPower implementation
grep -n "uint16_t.*getATFPower" src/PowerLimiterInverter.cpp

Repository: hoylabs/OpenDTU-OnBattery

Length of output: 51


🏁 Script executed:

# Get more context on the getEligibility logic - check for CurrentLimitUnknown
grep -n "CurrentLimitUnknown\|Eligibility::" src/PowerLimiterInverter.cpp | head -20

Repository: hoylabs/OpenDTU-OnBattery

Length of output: 1074


🏁 Script executed:

# Find where Eligibility enum is defined
grep -rn "enum.*Eligibility\|CurrentLimitUnknown" include/PowerLimiterInverter.h

Repository: hoylabs/OpenDTU-OnBattery

Length of output: 145


🏁 Script executed:

# Search for getATFPower implementation
grep -n "getATFPower" src/PowerLimiterInverter.cpp

Repository: hoylabs/OpenDTU-OnBattery

Length of output: 128


🏁 Script executed:

# Search in header file
grep -n "getATFPower" include/PowerLimiterInverter.h

Repository: hoylabs/OpenDTU-OnBattery

Length of output: 51


🏁 Script executed:

# Look for ATF-related implementations
grep -rn "getATFPower" --include="*.cpp" --include="*.h"

Repository: hoylabs/OpenDTU-OnBattery

Length of output: 396


🏁 Script executed:

# Get InverterATF::getATFPower implementation
cat -n src/InverterATF.cpp | sed -n '215,240p'

Repository: hoylabs/OpenDTU-OnBattery

Length of output: 1204


🏁 Script executed:

# Get more context
cat -n src/InverterATF.cpp | sed -n '210,260p'

Repository: hoylabs/OpenDTU-OnBattery

Length of output: 2116


🏁 Script executed:

# Search for _useATF usage and initialization
grep -n "_useATF" src/InverterATF.cpp | head -20

Repository: hoylabs/OpenDTU-OnBattery

Length of output: 377


🏁 Script executed:

# Search for ATF learning phase
grep -n "learning\|_useATF\|ATF" src/InverterATF.cpp | head -30

Repository: hoylabs/OpenDTU-OnBattery

Length of output: 1637


🏁 Script executed:

# Check header for _useATF member
grep -n "_useATF" include/InverterATF.h

Repository: hoylabs/OpenDTU-OnBattery

Length of output: 256


🏁 Script executed:

# Find when activateATF is called
grep -rn "activateATF" --include="*.cpp" --include="*.h"

Repository: hoylabs/OpenDTU-OnBattery

Length of output: 479


🏁 Script executed:

# Check PowerLimiterInverter constructor and initialization
grep -n "isATFActive\|activateATF" src/PowerLimiterInverter.cpp

Repository: hoylabs/OpenDTU-OnBattery

Length of output: 130


🏁 Script executed:

# Search for ATF enable/initialization logic
grep -n "isATFActive\|ATF" include/PowerLimiterInverter.h | head -20

Repository: hoylabs/OpenDTU-OnBattery

Length of output: 762


🏁 Script executed:

# Get context around the activateATF call in PowerLimiter.cpp
cat -n src/PowerLimiter.cpp | sed -n '1050,1075p'

Repository: hoylabs/OpenDTU-OnBattery

Length of output: 1097


🏁 Script executed:

# Check if there's any scenario where getATFPower returns 0 after activation
# Look at setATFData implementation
cat -n src/InverterATF.cpp | sed -n '91,120p'

Repository: hoylabs/OpenDTU-OnBattery

Length of output: 1697


🏁 Script executed:

# Check how the _realPower array is initialized in activateATF
cat -n src/InverterATF.cpp | sed -n '37,73p'

Repository: hoylabs/OpenDTU-OnBattery

Length of output: 1411


🏁 Script executed:

# Check cpplint configuration
find . -name ".cpplint*" -o -name "cpplint*" | grep -v node_modules

Repository: hoylabs/OpenDTU-OnBattery

Length of output: 100


🏁 Script executed:

# Check if there's a setup for cpplint or clang-format configuration
cat .cpplint 2>/dev/null || echo "No .cpplint file found"

Repository: hoylabs/OpenDTU-OnBattery

Length of output: 91


🏁 Script executed:

# Verify the actual behavior: with ATF initialized and valid limit, what gets returned?
# Let's trace the _realPower initialization more carefully
cat -n src/InverterATF.cpp | sed -n '60,65p'

Repository: hoylabs/OpenDTU-OnBattery

Length of output: 327


🏁 Script executed:

# Check the cpplint workflow to understand what rules are enforced
cat .github/workflows/cpplint.yml

Repository: hoylabs/OpenDTU-OnBattery

Length of output: 1035


🏁 Script executed:

# Let's check if there are any recent changes or TODOs in PowerLimiterInverter.cpp
grep -n "TODO\|FIXME\|BUG\|XXX" src/PowerLimiterInverter.cpp

Repository: hoylabs/OpenDTU-OnBattery

Length of output: 51


🏁 Script executed:

# Verify that _realPower is properly initialized before being used
cat -n src/InverterATF.cpp | sed -n '44,70p'

Repository: hoylabs/OpenDTU-OnBattery

Length of output: 1093


🏁 Script executed:

# Search for getLimitPercent to understand the range of values
grep -rn "getLimitPercent" --include="*.cpp" --include="*.h" | grep -E "getLimitPercent\(\)|return.*Limit"

Repository: hoylabs/OpenDTU-OnBattery

Length of output: 1779


🏁 Script executed:

# Check what values are valid for currentLimitPercent
grep -rn "LimitPercent\|currentLimitPercent" src/PowerLimiterInverter.cpp | head -10

Repository: hoylabs/OpenDTU-OnBattery

Length of output: 415


🏁 Script executed:

# Get the getLimitPercent implementation
cat -n lib/Hoymiles/src/parser/SystemConfigParaParser.cpp | sed -n '48,70p'

Repository: hoylabs/OpenDTU-OnBattery

Length of output: 969


🏁 Script executed:

# Search for where SystemConfigPara limit is set/initialized
grep -rn "SystemConfigPara\|setLimit" lib/Hoymiles/src/parser/SystemConfigParaParser.cpp | head -20

Repository: hoylabs/OpenDTU-OnBattery

Length of output: 1218


🏁 Script executed:

# Check if there's any handling for very low or zero limits
grep -n "limit.*0\|zero.*limit" src/InverterATF.cpp

Repository: hoylabs/OpenDTU-OnBattery

Length of output: 918


🏁 Script executed:

# Check the _size constant to understand the array dimensions
grep -n "_size\|size.*=" include/InverterATF.h | head -10

Repository: hoylabs/OpenDTU-OnBattery

Length of output: 193


🏁 Script executed:

# Let's verify the actual initialization values for a small inverter
cat -n include/InverterATF.h | sed -n '65,75p'

Repository: hoylabs/OpenDTU-OnBattery

Length of output: 804


🏁 Script executed:

# Check if there's any minimum value enforcement or special handling during learning
grep -n "State::DEFAULT_INIT\|State::RTD_INIT" src/InverterATF.cpp

Repository: hoylabs/OpenDTU-OnBattery

Length of output: 368


🏁 Script executed:

# Look at what happens during cache initialization and low limits
cat -n src/InverterATF.cpp | sed -n '215,254p'

Repository: hoylabs/OpenDTU-OnBattery

Length of output: 1788


🏁 Script executed:

# Check if there's any state-based logic that prevents returning 0 during learning
grep -n "_state\|DEFAULT_INIT" src/InverterATF.cpp | head -20

Repository: hoylabs/OpenDTU-OnBattery

Length of output: 396


🏁 Script executed:

# Verify that _cache is initialized to {0, 0.0f}
grep -n "_cache.*=" include/InverterATF.h

Repository: hoylabs/OpenDTU-OnBattery

Length of output: 191


🏁 Script executed:

# Check how currentLimitPercent flows through the system
cat -n src/PowerLimiterInverter.cpp | sed -n '354,365p'

Repository: hoylabs/OpenDTU-OnBattery

Length of output: 566


🏁 Script executed:

# Final confirmation: can getLimitPercent return exactly 0?
cat -n lib/Hoymiles/src/parser/SystemConfigParaParser.cpp | sed -n '48,58p'

Repository: hoylabs/OpenDTU-OnBattery

Length of output: 554


Ensure getATFPower() preserves non-zero results for all valid limits during ATF learning

The ATF path routing in getCurrentLimitWatts() is correct, but getATFPower() can return 0 for limits between 0% and ~1% due to rounding during interpolation (lines 240-253 in InverterATF.cpp). Since getEligibility() treats a return value of 0 as CurrentLimitUnknown (line 69 in PowerLimiterInverter.cpp), this edge case could keep ATF-enabled inverters ineligible longer than necessary during the initial learning phase. Consider ensuring that a valid, non-zero limit percentage always results in a non-zero power output, even during the DEFAULT_INIT state.

🤖 Prompt for AI Agents
In src/PowerLimiterInverter.cpp around lines 354 to 363, ensure ATF path does
not yield zero watts for tiny but valid non-zero limits during ATF learning:
after calling getATFPower(currentLimitPercent) (or by changing getATFPower
itself), if currentLimitPercent > 0 and the ATF result is 0, return a minimal
non-zero power (e.g., 1 watt or a small floor based on inverter max power) so
that getEligibility() does not treat the value as CurrentLimitUnknown; implement
the floor in the ATF branch only (or inside getATFPower) and ensure it respects
inverter max power and DEFAULT_INIT behavior.

"DeviceManager": "Hardware",
"SolarChargerSettings": "Solarladeregler",
"PowerMeterSettings": "Stromzähler",
"InverterMeterSettings": "Wechselrichter-Stromzähler",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

fd "de.json" --type f

Repository: hoylabs/OpenDTU-OnBattery

Length of output: 95


🏁 Script executed:

# Find the German locale file
find . -name "de.json" -type f | head -5

Repository: hoylabs/OpenDTU-OnBattery

Length of output: 97


🏁 Script executed:

# Also search for the implementation code related to ATF
rg "isATFFull|use_atf" -t js -t ts -t json --max-count 20

Repository: hoylabs/OpenDTU-OnBattery

Length of output: 483


🏁 Script executed:

# Search for powerlimiteradmin in the codebase
rg "powerlimiteradmin" -A 3 -B 3

Repository: hoylabs/OpenDTU-OnBattery

Length of output: 29246


🏁 Script executed:

# Read the German JSON file - check specific lines
sed -n '270,290p' webapp/src/locales/de.json

Repository: hoylabs/OpenDTU-OnBattery

Length of output: 1306


🏁 Script executed:

# Also check lines around 15, and the ATF-related section
sed -n '10,20p' webapp/src/locales/de.json
sed -n '675,710p' webapp/src/locales/de.json

Repository: hoylabs/OpenDTU-OnBattery

Length of output: 3084


🏁 Script executed:

# Find the isATFFull method implementation
sed -n '/isATFFull/,+10p' webapp/src/views/PowerLimiterAdminView.vue

Repository: hoylabs/OpenDTU-OnBattery

Length of output: 1010


🏁 Script executed:

# Search for UseATFHint in de.json
rg "UseATFHint" webapp/src/locales/de.json -A 1 -B 1

Repository: hoylabs/OpenDTU-OnBattery

Length of output: 706


🏁 Script executed:

# Also check English locale for comparison
rg "UseATFHint" webapp/src/locales/en.json -A 1 -B 1

Repository: hoylabs/OpenDTU-OnBattery

Length of output: 663


🏁 Script executed:

# Get line numbers for context
rg "UseATFHint|UseATF" webapp/src/locales/de.json --line-number

Repository: hoylabs/OpenDTU-OnBattery

Length of output: 483


Update ATF capacity text from 2 to 1 inverter in both German and English locales

The German locale at line 741 states: "Diese Option kann für maximal 2 Inverter aktiviert werden." However, the implementation in PowerLimiterAdminView.vue confirms ATF is limited to a single inverter—the isATFFull() method sets the flag true as soon as any one inverter has use_atf enabled. Update the German text to "maximal 1 Wechselrichter" and the English equivalent to "maximum of one inverter" to match the actual implementation limit.

🤖 Prompt for AI Agents
In webapp/src/locales/de.json around lines 15 (and specifically the entry that
reads "Diese Option kann für maximal 2 Inverter aktiviert werden." near line
~741), update the German text to read "Diese Option kann für maximal 1
Wechselrichter aktiviert werden."; also update the English locale in
webapp/src/locales/en.json to change "This option can be enabled for a maximum
of 2 inverters." (or similar phrasing) to "This option can be enabled for a
maximum of one inverter." so both locale strings match the actual ATF
implementation limit of a single inverter.

Comment on lines +43 to +53
export interface InverterMeterConfig {
enabled: boolean;
source: number;
serial: string;
interval: number;
mqtt: InverterMeterMqttConfig;
serial_sdm: InverterMeterSerialSdmConfig;
http_json: InverterMeterHttpJsonConfig;
http_sml: InverterMeterHttpSmlConfig;
udp_victron: InverterMeterUdpVictronConfig;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if 'interval' is serialized in the backend for InverterMeter
echo "=== Searching for 'interval' in WebApi_invertermeter.cpp ==="
rg -n '"interval"' src/WebApi_invertermeter.cpp 2>/dev/null || echo "No matches found"

echo ""
echo "=== Searching for 'Interval' in relevant backend files ==="
rg -n 'Interval' src/WebApi_invertermeter.cpp include/Configuration.h 2>/dev/null | grep -i invertermeter || echo "No matches found"

echo ""
echo "=== Broader search for interval in invertermeter files ==="
rg -n 'interval' src/ include/ 2>/dev/null | grep -i invertermeter || echo "No matches found"

Repository: hoylabs/OpenDTU-OnBattery

Length of output: 599


🏁 Script executed:

#!/bin/bash
# Find and examine the onStatus handler in WebApi_invertermeter.cpp
echo "=== Looking for onStatus function ==="
rg -n 'onStatus|onInverterMeterStatus' src/WebApi_invertermeter.cpp -A 30 | head -100

echo ""
echo "=== Checking what fields are serialized in the response ==="
rg -n 'root\[|insert|add' src/WebApi_invertermeter.cpp | grep -E '(enabled|source|serial|mqtt|http|udp|interval)' | head -20

Repository: hoylabs/OpenDTU-OnBattery

Length of output: 4692


Remove the unused interval field from the InverterMeterConfig interface.

The interval field is not serialized by the backend's onStatus handler in WebApi_invertermeter.cpp. The handler only returns: enabled, source, serial, mqtt, serial_sdm, http_json, http_sml, and udp_victron. Keeping this field will create a mismatch between the TypeScript types and the actual API response.

🤖 Prompt for AI Agents
In webapp/src/types/InverterMeterConfig.ts around lines 43 to 53, the interface
includes an unused field `interval` that the backend's onStatus handler does not
return; remove the `interval: number;` property from the InverterMeterConfig
interface so the TypeScript type matches the actual API response, and scan for
any local usages or tests referencing .interval to update or remove them
accordingly.

@AndreasBoehm AndreasBoehm self-assigned this Mar 30, 2026
@SW-Niko SW-Niko marked this pull request as draft April 1, 2026 20:26
@AndreasBoehm AndreasBoehm removed their assignment Apr 10, 2026
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