fix(units): store watts and 0-1 fractions in core - #962
Conversation
PV arrays use rated_w. SoC, planner bounds, and related settings are 0-1. kWp and 0-100 exist only at UI, HA, appproto, calendar titles, and the forecast.solar URL. Config load still folds the old keys. A units package plus consistency tests reject kWp and soc_pct leaking back into core structs.
Core stored EV SoC as 0-100 while the battery planner already used 0-1. That mix, not a missing clamp, is what let percent-scale values leak into watt math. Loadpoint state, schedules, boost leases, vehicle telemetry, calendar intents, V2X envelopes, and shadow SoC are now fractions. 0-100 remains only at UI, HA, appproto, calendar titles, Tesla verify, and optimizer protocol v1. The units harness fails if _pct names or kWp return to those core structs. Nameplate ceilings stay as physics gates. They are not the unit fix.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 57da5e0200
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| var GROUPS = [ | ||
| { title: 'Power & electrical', items: [ | ||
| { key: 'hp_energy_log_current_power_consumption', label: 'Total power now', fmt: fmtKW, info: "The whole heat pump's instantaneous electrical draw right now — compressor + fan + circulation pumps + electronics." }, | ||
| { key: 'hp_energy_log_current_power_consumption', label: 'Total power now', fmt: fmtPower, info: "The whole heat pump's instantaneous electrical draw right now — compressor + fan + circulation pumps + electronics." }, |
There was a problem hiding this comment.
Update the driver pin before reading bulk metrics as watts
The pinned drivers/BUNDLED_SOURCE.json commit is unchanged, and HostEnv.emitMetric still forwards metric values and units without conversion, so the currently bundled myUplink/NIBE drivers continue emitting these generic power metrics in kW. Switching the card to fmtPower therefore renders a real 2.5 kW reading as roughly 3 W, and the newly changed driver tests will fail against the populated pinned snapshot; update the source driver and pin, or normalize this boundary in core, before changing the consumer.
AGENTS.md reference: AGENTS.md:L12-L18
Useful? React with 👍 / 👎.
| function cell(wh) { | ||
| if (wh == null) return '<span class="ftw-hp-acc">accumulating</span>'; | ||
| var kwh = wh / 1000; | ||
| return (Math.round(kwh * 10) / 10).toLocaleString('en-US') + ' kWh'; |
There was a problem hiding this comment.
Normalize existing energy samples before subtracting Wh counters
On an upgraded site with stored NIBE history, existing samples under hp_energy_consumed_kwh/hp_energy_produced_kwh remain kWh, while the proposed driver behavior writes Wh under the same metric IDs; metricID only relabels the unit and does not transform old samples. Consequently energyDeltas subtracts an old kWh baseline from a new Wh lifetime counter, and this division reports a huge phantom period total—approximately the pump's lifetime consumption—until every requested period starts after the upgrade. Migrate or scale historical samples, or introduce new metric names instead of mixing scales in one series.
Useful? React with 👍 / 👎.
| const socPct = socPercent(sl.soc); | ||
| const lpSocPct = socPercent(sl.loadpoint_soc); |
There was a problem hiding this comment.
Preserve legacy SoC keys when rendering diagnostic history
For any diagnostic persisted before this upgrade, /api/mpc/diagnose/at returns the stored JSON verbatim, whose slots contain soc_pct and loadpoint_soc_pct rather than the new keys. Reading only sl.soc and sl.loadpoint_soc makes all historical SoC cells and chart points disappear; idle-EV snapshots are also misclassified because the new lpActive predicate ignores their legacy loadpoint SoC. Add a legacy-key fallback with percent-to-fraction conversion so the time-travel view remains usable across the upgrade.
Useful? React with 👍 / 👎.
Slot directives, live PV surplus caps, and the operator absorb cap already stored 0-1. The Pct suffix made them look like 0-100. Rename the fields, convert golden dump keys, and fail the units harness if those names return.
Pinned myUplink/NIBE Lua still emit vendor kilo-units. Convert at the host door so core stores W/Wh without waiting on a pin bump. Diagnose reads legacy soc_pct keys; heating period totals scale mixed kWh/Wh history.
Summary
Björn's 18.96 kW roof showed ~2046 kW PV because a v2 POA path treated kWp as watts. A later nameplate clamp hid the display. It did not store the right unit.
This PR makes core SI:
rated_w.kwpis a YAML load door only, then cleared._pct.80%), Tesla verify, optimizer protocol v1soc_pctJSON, forecast.solar URL.soc_pct,kwp) and folds them. New writes use the canonical names.go/internal/unitsplusconsistency_test.gofail the build if kWp orsoc_pctreturns to those core structs.HTTP contracts that change (same binary ships the web UI):
/api/loadpointsand schedule/boost/SoC writes:current_soc,target_soc,soc,min_battery_socas 0–1.soc0–1 (from the first commit).min_reserve_soc,departure_target_soc.Persisted loadpoint schedules and boost leases with
soc_pct: 80hydrate as 0.80 on load.Overlapping open PRs
These have files in common. They should rebase onto this after it lands, or say so:
SoCMinPct/ 0–100. Bot review asked it to stay draft.weather.js/ forecast. They still writekwp. This branch storesrated_w.target_soc_pctwill need the same 0–1 door.Test plan
make verify(pre-commit)make verify-all(pre-push, including linux/arm64)go test ./internal/units— harness: RatedW, no kWp on forecast.Array, loadpoint/plan JSON is 0–1, legacysoc_pct:80hydrates to 0.80go test ./internal/loadpoint ./internal/telemetry ./internal/calendar ./internal/mpc ./internal/apinpm test(374 pass), including weatherrated_wand planner 0–1 SoC fieldsWhy this is not another clamp
The evening POA for 18960 W at ~108 W/m² is ~2 kW. Treating 18960 as kWp yields megawatts. Storing watts makes the nameplate gate almost idle. The gate stays for stale SQLite rows and sensor glitches.