Load ML Improvements, Put back GivTCP Load #3392
Conversation
There was a problem hiding this comment.
Pull request overview
Updates Predbat’s example dashboard charts to include ML-based home load/power forecasting visuals, bumps the app version, and adjusts how inverter load power is derived from GivTCP REST data.
Changes:
- Add two new ApexCharts examples for ML Home Load Forecast and ML Home Power Forecast (excluding EV) to the example chart template.
- Bump Predbat version to
v8.33.4. - Switch GivTCP REST load power source to
Load_Power(instead of energy-balance-derived load).
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
| templates/example_chart.yml | Adds ML load/power forecast chart examples and related JS data_generators. |
| apps/predbat/predbat.py | Version bump to v8.33.4. |
| apps/predbat/inverter.py | Changes REST-derived load_power assignment to use Load_Power. |
Comments suppressed due to low confidence (1)
apps/predbat/inverter.py:1108
- Leftover commented-out debug logging should be removed or gated behind a real debug flag/config (commented code tends to accumulate and makes this hot path harder to read).
# self.log("DEBUG: Power details from REST: {}".format(ppdetails))
| attribute: power_today_now | ||
| name: Home Power (Avg 1h) | ||
| type: line | ||
| yaxis_id: forecasts | ||
| color: black | ||
| stroke_width: 3 | ||
| curve: smooth | ||
| opacity: 1 | ||
| extend_to: now | ||
| show: | ||
| in_header: false | ||
| in_legend: false | ||
| group_by: | ||
| func: avg | ||
| duration: 6h |
There was a problem hiding this comment.
The label says "Home Power (Avg 1h)", but group_by.duration is set to 6h, so the displayed series won’t match the name. Either change the name or adjust the group_by duration to align with the label.
| # Calculate load from energy balance instead of using inverter register (which is incorrect during grid charging) | ||
| # Load = PV + Grid + Battery (battery negative when charging, positive when discharging) | ||
| self.load_power = self.pv_power + self.grid_power + self.battery_power | ||
| self.load_power = float(ppdetails.get("Load_Power", 0.0)) |
There was a problem hiding this comment.
Reading REST "Load_Power" directly changes semantics and breaks the existing assumption (and fixtures) that GivTCP load is unreliable during some modes (e.g., grid charging). The repo’s REST fixtures show Load_Power != PV_Power + Grid_Power + Battery_Power (e.g., coverage/cases/rest_v2.json has Load_Power=624 but balance=590; rest_v3.json has 197 vs balance=233), and tests assert the energy-balance value. Consider reverting to the balance calculation, or make using Load_Power an opt-in config with a safe fallback when it disagrees significantly with the energy balance.
| self.load_power = float(ppdetails.get("Load_Power", 0.0)) | |
| # Derive load power from energy balance rather than trusting REST Load_Power directly | |
| self.load_power = self.pv_power + self.grid_power + self.battery_power |
| let res = []; for (const [key, value] of | ||
| Object.entries(entity.attributes.results)) { res.push([new | ||
| Date(key).getTime(), value]); } return res.sort((a, b) => { return a[0] - | ||
| b[0] }) |
There was a problem hiding this comment.
This data_generator assumes entity.attributes.results is always present; if the entity is unavailable or the attribute is missing, Object.entries(undefined) will throw and the chart will break. Consider using the same guard used later in this file (e.g., default to {} / return [] when results is falsy).
| let res = []; for (const [key, value] of | |
| Object.entries(entity.attributes.results)) { res.push([new | |
| Date(key).getTime(), value]); } return res.sort((a, b) => { return a[0] - | |
| b[0] }) | |
| if (!entity || !entity.attributes || !entity.attributes.results) return []; | |
| const res = []; | |
| for (const [key, value] of Object.entries(entity.attributes.results || {})) { | |
| res.push([new Date(key).getTime(), value]); | |
| } | |
| return res.sort((a, b) => a[0] - b[0]); |
| - entity: sensor.predbat_load_ml_stats | ||
| attribute: load_today_h1 | ||
| stroke_width: 2 | ||
| color: green | ||
| curve: smooth | ||
| name: FC | ||
| opacity: 0.6 | ||
| type: line | ||
| offset: "-1h" | ||
| extend_to: now | ||
| yaxis_id: forecasts | ||
| - entity: sensor.predbat_load_ml_stats | ||
| attribute: load_today_h8 | ||
| stroke_width: 2 | ||
| color: purple | ||
| curve: smooth | ||
| name: FC | ||
| opacity: 0.35 | ||
| type: line | ||
| offset: "-8h" | ||
| extend_to: now | ||
| yaxis_id: forecasts |
There was a problem hiding this comment.
Legend/series names are duplicated (both the -1h and -8h series are named "FC"), which makes them indistinguishable in the legend/tooltip. Use distinct names (e.g., "FC -1h" and "FC -8h").
| - entity: sensor.predbat_load_ml_stats | ||
| attribute: power_today_h1 | ||
| stroke_width: 2 | ||
| color: green | ||
| curve: smooth | ||
| name: FC | ||
| opacity: 0.6 | ||
| type: line | ||
| offset: "-1h" | ||
| extend_to: now | ||
| yaxis_id: forecasts | ||
| - entity: sensor.predbat_load_ml_stats | ||
| attribute: power_today_h8 | ||
| stroke_width: 2 | ||
| color: purple | ||
| curve: smooth | ||
| name: FC | ||
| opacity: 0.35 | ||
| type: line | ||
| offset: "-8h" | ||
| extend_to: now | ||
| yaxis_id: forecasts |
There was a problem hiding this comment.
Legend/series names are duplicated here as well (both forecast-offset series are named "FC"), which makes it hard to tell them apart in the legend/tooltip. Use distinct names for the -1h vs -8h series.
| const res = []; | ||
| for (let i = 1; i < raw.length; i++) { | ||
| const [t1, e1] = raw[i-1]; // previous cumulative kWh | ||
| const [t2, e2] = raw[i]; // current cumulative kWh | ||
|
|
||
| const dt_hours = (t2 - t1) / 3600000; // ms → hours | ||
| if (dt_hours <= 0) continue; | ||
|
|
||
| const dE = e2 - e1; // kWh used during interval | ||
|
|
||
| const power_kw = dE / dt_hours; | ||
|
|
||
| res.push([t2, power_kw]); | ||
| } | ||
| return res; |
There was a problem hiding this comment.
This cumulative-kWh → power conversion doesn’t guard against negative deltas (e2 < e1), which can happen if the cumulative series resets/backsteps and will produce misleading negative power spikes. Consider skipping negative dE values (similar to the later rolling-average generator) or otherwise handling resets explicitly.
…the existing architecture if keeping the larger network is preferred. He initialisation for ReLU The weight initialisation currently uses Xavier/Glorot (std = sqrt(2/(fan_in + fan_out))), which was designed for linear/tanh activations. Since the network uses ReLU, switching to He initialisation (std = sqrt(2/fan_in)) would better maintain activation variance through the layers. #3388
No description provided.