|
7 | 7 | //unit conversion (`units: { energy: 'kWh' }` normalises Wh/kWh/MWh server-side). The only math here is |
8 | 8 | //kWh-per-bucket / bucket-duration = average watts, so where HA has a number the card shows the same number. |
9 | 9 |
|
10 | | -import { CHANGE_REFRESH_MS, COARSE_PROBE_MS, DENSE_FRACTION, COARSE_MAX_SPREAD_BUCKETS, COARSE_REGULARITY, HOUR_MS, DAY_MS } from '../../core/config/constants'; |
| 10 | +import { CHANGE_REFRESH_MS, COARSE_PROBE_MS, DENSE_FRACTION, COARSE_MAX_SPREAD_BUCKETS, HOUR_MS, DAY_MS } from '../../core/config/constants'; |
11 | 11 | import { callWS } from '../ha-gateway'; |
12 | 12 | import { RequestCache } from '../request-cache'; |
13 | 13 | import { loadDurable, saveDurable } from '../durable-cache'; |
@@ -187,36 +187,33 @@ export function outlierCapKwh(buckets: ChangeBucket[] | null): number |
187 | 187 | return mags[idx] * OUTLIER_CAP_FACTOR; |
188 | 188 | } |
189 | 189 |
|
190 | | -//Coarse-meter smoothing on the binned per-bucket energy (sums/hit). A meter that reports its cumulative energy |
191 | | -//less often than a store bucket lands each delta in one bucket and leaves 0 in the buckets between reports, which |
192 | | -//draws a sawtooth (production looks spiky while a dense grid meter stays smooth). When the non-zero buckets are |
193 | | -//REGULARLY spaced (a real report cadence, not intermittent flow), spread each delta back over its report interval, |
194 | | -//capped so a night-long gap before the first daytime reading is never smeared. Energy-conserving: the sum over each |
195 | | -//interval is unchanged, so totals still match the Energy dashboard. No-op for dense meters (cadence 1) and for |
196 | | -//irregular series (export that only flows sometimes). |
| 190 | +//Floor the reconstructed power to the meter's real report cadence, on the binned per-bucket energy (sums/hit). A |
| 191 | +//meter that reports its cumulative energy less often than a store bucket lands each delta in one bucket and leaves 0 |
| 192 | +//in the buckets between reports; dividing that lump by the (shorter) bucket then reads an average power ABOVE what |
| 193 | +//the site actually produced (a 2.5 kW-capped array can plot >2.5 kW), and draws a sawtooth. So spread each report |
| 194 | +//back over the interval since the previous report, its true accumulation window: no bucket can then hold more than |
| 195 | +//one interval's energy, so no bucket over-reads. Capped so a long genuine gap (overnight before the first daytime |
| 196 | +//reading) is never smeared. Energy-conserving (each report's total is unchanged, so totals still match the Energy |
| 197 | +//dashboard) and inert for dense meters (consecutive reports are one bucket apart, so span 1 leaves the value as-is). |
197 | 198 | function smoothCoarseReports(sums: number[], hit: boolean[]): void |
198 | 199 | { |
199 | 200 | const reports: number[] = []; |
200 | 201 | for (let i = 0; i < sums.length; i++) |
201 | 202 | { |
202 | 203 | if (hit[i] && sums[i] !== 0) { reports.push(i); } |
203 | 204 | } |
204 | | - if (reports.length < 3) { return; } |
205 | | - const gaps: number[] = []; |
206 | | - for (let k = 1; k < reports.length; k++) { gaps.push(reports[k] - reports[k - 1]); } |
207 | | - const cadence = [...gaps].sort((a, b) => a - b)[Math.floor(gaps.length / 2)]; //median gap |
208 | | - if (cadence <= 1 || cadence > COARSE_MAX_SPREAD_BUCKETS) { return; } |
209 | | - const regular = gaps.filter(g => Math.abs(g - cadence) <= 1).length / gaps.length; |
210 | | - if (regular < COARSE_REGULARITY) { return; } |
211 | | - //Spread each report back over its own interval (capped at the cadence), evenly. Left to right; a report's range |
212 | | - //never reaches the previous report (span <= cadence <= gap), so the in-place writes never collide, and the |
213 | | - //genuine-gap buckets before a capped range keep their zero. |
214 | | - let prev = -1; |
215 | | - for (const i of reports) |
| 205 | + //Need two reports to know an interval; a lone spike (fully intermittent flow) is left untouched. |
| 206 | + if (reports.length < 2) { return; } |
| 207 | + //The first report keeps its own bucket (its accumulation start is unknown, so it is never spread backward); |
| 208 | + //every later report spreads over the capped gap to its predecessor. span <= gap means a report's range never |
| 209 | + //reaches the previous report, so the in-place writes never collide and genuine-gap buckets keep their zero. |
| 210 | + let prev = reports[0]; |
| 211 | + for (let r = 1; r < reports.length; r++) |
216 | 212 | { |
217 | | - const span = prev < 0 ? cadence : Math.min(i - prev, cadence); |
218 | | - const start = Math.max(0, i - span + 1); |
219 | | - const share = sums[i] / (i - start + 1); |
| 213 | + const i = reports[r]; |
| 214 | + const span = Math.min(i - prev, COARSE_MAX_SPREAD_BUCKETS); |
| 215 | + const start = i - span + 1; |
| 216 | + const share = sums[i] / span; |
220 | 217 | for (let j = start; j <= i; j++) |
221 | 218 | { |
222 | 219 | sums[j] = share; |
|
0 commit comments