remainingDays was computed as days-from-today to period-end, while
actual-to-date consumption only ever reflects data up to the meter's
last real reading. With sparse readings the last reading sits days
before today, so that gap was counted in neither the actual nor the
projected total. Project from the last reading's date instead.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Reports previously used the raw delta between whichever real readings
happened to fall nearest a period's start/end, un-prorated — accurate
only when readings land close to calendar boundaries. With sparse
readings this misattributes days on either side to the neighbouring
period instead.
getPeriodConsumption() now interpolates the meter's value at each
boundary between the readings bracketing it (linear by day-count),
then diffs those — equivalent to a "daily rate carried forward until
the next reading" spreadsheet approach, generalised across however
many sparse-reading gaps a period spans. Falls back to the old
nearest-reading behaviour when a boundary can't be interpolated (period
starts before the first-ever reading or ends after the latest one).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
routes/meters.js already imported lib/mqtt.js and lib/history.js in an
earlier uncommitted change; a prior commit staged the whole meters.js
file (picking up those imports) without staging the modules themselves,
crashing production with ERR_MODULE_NOT_FOUND. Completing the commit:
package.json (mqtt dependency), index.js (wires connectMqtt/startScheduler,
both already error-guarded if the broker isn't reachable), and the three
lib files themselves.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Gas meters are read in raw volume (ft3 on our imperial meter, m3 on
newer metric ones), never kWh — but tariff rates and CCL/RAB levy are
all p/kWh. Add gas_volume_unit + gas_calorific_value to meters, and
convert reading deltas to kWh in cost-calc.js using the standard
UK gas-billing formula (matches the exact calculation printed on
supplier invoices) before any cost math runs.
Also fixes meter/readings pages showing raw gas dial readings
mislabeled as "kWh" — they now show the actual physical unit
(ft³/m³) via a new reading_unit_label, while computed consumption
stays correctly labeled kWh.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The June electricity bill showed £252.72 (7.5% of the bill) in charges the
cost model didn't account for: Nuclear RAB Levy, Metering Charge, and DUoS
Availability/Excess/Reactive. RAB levy and metering charge are modeled
directly (same shape as CCL / standing charge); the DUoS charges need kVA
capacity and kVArh reactive energy that only the supplier's own meter reads,
so they're covered by one adjustable "other network charges" £/month field
to update manually from each bill.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Rate windows were matched by substring (label.includes(key)), so
overlapping labels like "Day"/"Weekday" could silently cross-match to
the wrong rate. Switched to an exact match.
The "split must total 100%" check was advisory-only in the UI and
never validated server-side, letting a tariff save with a split that
doesn't sum to 100% and permanently mis-cost that meter's usage.
Enforced on both the API (POST/PATCH /api/tariffs) and the Save
button.
Also aligned the app's portal category to 'Hotel' (already correct in
auth/src/db.js) here and in stack-init/install-stack.sh, which had
both drifted to 'Operations'.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Rate changes are often scheduled ahead of time or confirmed by finance
after the fact and backdated — assign-tariff already supported any
effective_from date, but cost-calc previously priced a whole period at
a single tariff (whichever was in force on the period-end date),
silently mispricing the days on the other side of a change.
getTariffSegments() finds every tariff assignment overlapping a date
range; getMeterCostForPeriod/getMeterEstimateForPeriod now split
consumption pro-rata by day count across segments and price each
against its own tariff, summing to the period total. Estimates project
the remaining days across a future-scheduled change the same way,
rather than assuming today's tariff holds for the rest of the period.
Also consolidates internal.js's cost/estimate routes (previously their
own duplicate calc) onto the same shared functions reports.js and
estimates.js use, so the Directors' report can't drift from the in-app
numbers.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>