@golemio/energetics 2.0.0-dev.2922626380 → 2.0.0-dev.2962537365

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (19) hide show
  1. package/db/example/09_enapo_consumption_history.sql +52 -11
  2. package/db/migrations/postgresql/20261013090000-enapo-ave-zero-core.js +53 -0
  3. package/db/migrations/postgresql/20261013090100-enapo-consumption-history-gas.js +53 -0
  4. package/db/migrations/postgresql/20261019090000-enapo-consumption-history-invoice-zero.js +53 -0
  5. package/db/migrations/postgresql/sqls/20261013090000-enapo-ave-zero-core-down.sql +1 -0
  6. package/db/migrations/postgresql/sqls/20261013090000-enapo-ave-zero-core-up.sql +7 -0
  7. package/db/migrations/postgresql/sqls/20261013090100-enapo-consumption-history-gas-down.sql +511 -0
  8. package/db/migrations/postgresql/sqls/20261013090100-enapo-consumption-history-gas-up.sql +492 -0
  9. package/db/migrations/postgresql/sqls/20261019090000-enapo-consumption-history-invoice-zero-down.sql +492 -0
  10. package/db/migrations/postgresql/sqls/20261019090000-enapo-consumption-history-invoice-zero-up.sql +495 -0
  11. package/dist/integration-engine/enapo/transformations/PPasMeasurementTransformation.d.ts +2 -2
  12. package/dist/integration-engine/enapo/transformations/PPasMeasurementTransformation.js +8 -5
  13. package/dist/integration-engine/enapo/transformations/PPasMeasurementTransformation.js.map +1 -1
  14. package/dist/output-gateway/routers/v2/V2EnoBuildingsRouter.d.ts +4 -1
  15. package/dist/output-gateway/routers/v2/V2EnoBuildingsRouter.js +17 -5
  16. package/dist/output-gateway/routers/v2/V2EnoBuildingsRouter.js.map +1 -1
  17. package/docs/implementation_documentation.md +7 -5
  18. package/docs/openapi-output.yaml +3 -1
  19. package/package.json +1 -1
@@ -0,0 +1,495 @@
1
+ DROP MATERIALIZED VIEW IF EXISTS mv_enapo_om_consumption;
2
+
3
+ CREATE MATERIALIZED VIEW mv_enapo_om_consumption AS
4
+ WITH bounds AS (
5
+ SELECT
6
+ ((date_trunc('month', now() AT TIME ZONE 'Europe/Prague') - interval '48 months')
7
+ AT TIME ZONE 'Europe/Prague') AS month_from_tstz,
8
+ (date_trunc('month', now() AT TIME ZONE 'Europe/Prague') - interval '48 months') AS month_from,
9
+ (date_trunc('month', now() AT TIME ZONE 'Europe/Prague')
10
+ AT TIME ZONE 'Europe/Prague') AS month_to_tstz,
11
+ date_trunc('month', now() AT TIME ZONE 'Europe/Prague') AS month_to
12
+ ),
13
+
14
+ point AS (
15
+ SELECT DISTINCT gid, place_id, id_type, commodity
16
+ FROM v_enapo_building_consumption_points
17
+ ),
18
+
19
+ electricity_metered AS (
20
+ SELECT
21
+ em.id AS place_id,
22
+ date_trunc('month', em."timestamp" AT TIME ZONE 'Europe/Prague')::date AS period_start,
23
+ sum(em.value) FILTER (WHERE em.var = 'EActi_VT_NT_DaySum') AS total_kwh,
24
+ sum(em.value) FILTER (WHERE em.var = 'EActi_VT_DaySum') AS vt_kwh,
25
+ sum(em.value) FILTER (WHERE em.var = 'EActi_NT_DaySum') AS nt_kwh,
26
+ -- Coverage is the max day-count across the three vars, not just the total's.
27
+ GREATEST(
28
+ count(*) FILTER (WHERE em.var = 'EActi_VT_NT_DaySum'),
29
+ count(*) FILTER (WHERE em.var = 'EActi_VT_DaySum'),
30
+ count(*) FILTER (WHERE em.var = 'EActi_NT_DaySum')
31
+ ) AS days_covered
32
+ FROM enapo_measurements em
33
+ CROSS JOIN bounds b
34
+ WHERE em.source = 'predi_input'
35
+ AND em.var IN ('EActi_VT_NT_DaySum', 'EActi_VT_DaySum', 'EActi_NT_DaySum')
36
+ AND em."timestamp" >= b.month_from_tstz
37
+ AND em."timestamp" < b.month_to_tstz
38
+ GROUP BY em.id, date_trunc('month', em."timestamp" AT TIME ZONE 'Europe/Prague')::date
39
+ ),
40
+
41
+ gas_core_month AS (
42
+ -- Cumulative 'core' reading (m3) at the first measured hour of each first-of-month day, kept per
43
+ -- (id, place_id): an id can carry several meters, one per PPAS place id, whose diffs are summed.
44
+ -- Not per device_serial_number - the integration stamps every refetched hour with the serial AVE
45
+ -- lists at fetch time, so the serial is unreliable for history and would split one meter in two.
46
+ SELECT DISTINCT ON (em.id, em.place_id, date_trunc('month', em."timestamp" AT TIME ZONE 'Europe/Prague'))
47
+ em.id,
48
+ em.place_id,
49
+ date_trunc('month', em."timestamp" AT TIME ZONE 'Europe/Prague')::date AS month_start,
50
+ em.value
51
+ FROM enapo_measurements em
52
+ CROSS JOIN bounds b
53
+ WHERE em.source = 'ppas_ave_api'
54
+ AND em.var = 'core'
55
+ AND (em."timestamp" AT TIME ZONE 'Europe/Prague')::date
56
+ = date_trunc('month', em."timestamp" AT TIME ZONE 'Europe/Prague')::date
57
+ AND em."timestamp" >= b.month_from_tstz
58
+ AND em."timestamp" < (b.month_to_tstz + interval '1 month')
59
+ ORDER BY em.id, em.place_id, date_trunc('month', em."timestamp" AT TIME ZONE 'Europe/Prague'), em."timestamp"
60
+ ),
61
+ gas_metered AS (
62
+ SELECT
63
+ cur.id AS place_id,
64
+ cur.month_start AS period_start,
65
+ -- A meter reading only grows, so a negative month total means a meter was exchanged or reset:
66
+ -- the month stays empty. Checked on the total, so a tiny drop on a small meter does not empty it.
67
+ (CASE WHEN sum(nxt.value - cur.value) < 0 THEN NULL ELSE sum(nxt.value - cur.value) END) AS m3_sum,
68
+ ((cur.month_start + interval '1 month')::date - cur.month_start) AS days_covered
69
+ FROM gas_core_month cur
70
+ INNER JOIN gas_core_month nxt
71
+ ON nxt.id = cur.id
72
+ AND nxt.place_id = cur.place_id
73
+ AND nxt.month_start = (cur.month_start + interval '1 month')::date
74
+ GROUP BY cur.id, cur.month_start
75
+ ),
76
+
77
+ distribution_device AS (
78
+ SELECT DISTINCT ON (ins.eic, d.place_id, d.device_serial_number, d.date_from, d.date_to, d.kind, d.unit)
79
+ ins.eic,
80
+ d.place_id,
81
+ d.invoice_id,
82
+ d.date_from,
83
+ d.date_to,
84
+ d.kind,
85
+ d.unit,
86
+ -- PPAS bills a month without consumption as an empty consumption with zero Nm3 and kWh.
87
+ COALESCE(d.consumption, 0) AS consumption,
88
+ d.normometers_nm3,
89
+ d.gas_consumption_kwh,
90
+ d.combustion_heat
91
+ FROM enapo_ppas_distribution_invoice_device d
92
+ INNER JOIN enapo_ppas_distribution_invoice i
93
+ ON i.id = d.invoice_id
94
+ INNER JOIN enapo_ppas_distribution_installation ins
95
+ ON ins.invoice_id = d.invoice_id
96
+ AND ins.customer_id = i.customer_id
97
+ AND ins.place_id = d.place_id
98
+ WHERE COALESCE(i.is_canceled, false) = false
99
+ AND d.date_to >= d.date_from
100
+ AND (d.consumption IS NOT NULL OR (d.normometers_nm3 = 0 AND d.gas_consumption_kwh = 0))
101
+ AND d.kind IN ('ZIZWC', 'ZIABN3')
102
+ AND d.reading_type = 'STANDARD'
103
+ AND NOT EXISTS (
104
+ SELECT 1 FROM enapo_ppas_distribution_invoice s
105
+ WHERE s.preceding_id = i.id AND COALESCE(s.is_canceled, false) = false
106
+ )
107
+ ORDER BY ins.eic, d.place_id, d.device_serial_number, d.date_from, d.date_to, d.kind, d.unit,
108
+ i.facts_doc_date DESC, i.id DESC
109
+ ),
110
+ commercial_device AS (
111
+ SELECT DISTINCT ON (ins.eic, d.place_id, d.device_serial_number, d.date_from, d.date_to, d.kind, d.unit)
112
+ ins.eic,
113
+ d.place_id,
114
+ d.invoice_id,
115
+ d.date_from,
116
+ d.date_to,
117
+ d.kind,
118
+ d.unit,
119
+ -- PPAS bills a month without consumption as an empty consumption with zero Nm3 and kWh.
120
+ COALESCE(d.consumption, 0) AS consumption,
121
+ d.normometers_nm3,
122
+ d.gas_consumption_kwh,
123
+ d.combustion_heat
124
+ FROM enapo_ppas_commercial_invoice_device d
125
+ INNER JOIN enapo_ppas_commercial_invoice i
126
+ ON i.id = d.invoice_id
127
+ INNER JOIN enapo_ppas_commercial_invoice_installations ins
128
+ ON ins.invoice_id = d.invoice_id
129
+ AND ins.customer_id = i.customer_id
130
+ AND ins.place_id = d.place_id
131
+ WHERE COALESCE(i.is_canceled, false) = false
132
+ AND d.date_to >= d.date_from
133
+ AND (d.consumption IS NOT NULL OR (d.normometers_nm3 = 0 AND d.gas_consumption_kwh = 0))
134
+ AND d.reading_type = 'STANDARD'
135
+ AND NOT EXISTS (
136
+ SELECT 1 FROM enapo_ppas_commercial_invoice s
137
+ WHERE s.preceding_id = i.id AND COALESCE(s.is_canceled, false) = false
138
+ )
139
+ ORDER BY ins.eic, d.place_id, d.device_serial_number, d.date_from, d.date_to, d.kind, d.unit,
140
+ i.facts_doc_date DESC, i.id DESC
141
+ ),
142
+
143
+ invoice_device_month AS (
144
+ SELECT
145
+ 'ppas_distribution_invoice' AS source,
146
+ d.eic AS place_id,
147
+ d.invoice_id,
148
+ gs.month::date AS period_start,
149
+ CASE WHEN d.unit = 'KWH' THEN NULL ELSE d.consumption END * o.share AS volume,
150
+ CASE WHEN d.unit = 'NM3' THEN 'Nm3' ELSE 'm3' END AS volume_unit,
151
+ d.normometers_nm3 * o.share AS nm3,
152
+ -- A zero kWh is a missing conversion input unless nothing was consumed: no consumption is 0 kWh.
153
+ CASE WHEN d.consumption = 0 THEN 0 ELSE NULLIF(d.gas_consumption_kwh, 0) END * o.share AS kwh,
154
+ (o.share < 1) AS is_estimated,
155
+ o.overlap_days AS days_covered
156
+ FROM distribution_device d
157
+ CROSS JOIN bounds b
158
+ CROSS JOIN LATERAL generate_series(
159
+ date_trunc('month', d.date_from::timestamp),
160
+ date_trunc('month', d.date_to::timestamp),
161
+ interval '1 month'
162
+ ) gs(month)
163
+ CROSS JOIN LATERAL (
164
+ SELECT
165
+ ov.overlap_days,
166
+ ov.overlap_days::numeric / NULLIF(d.date_to - d.date_from + 1, 0) AS share
167
+ FROM (
168
+ SELECT LEAST(d.date_to, (gs.month + interval '1 month - 1 day')::date)
169
+ - GREATEST(d.date_from, gs.month::date) + 1 AS overlap_days
170
+ ) ov
171
+ ) o
172
+ WHERE gs.month >= b.month_from AND gs.month < b.month_to
173
+
174
+ UNION ALL
175
+
176
+ SELECT
177
+ 'ppas_commercial_invoice',
178
+ d.eic,
179
+ d.invoice_id,
180
+ gs.month::date,
181
+ CASE WHEN d.unit = 'KWH' THEN NULL ELSE d.consumption END * o.share,
182
+ CASE WHEN d.unit = 'NM3' THEN 'Nm3' ELSE 'm3' END,
183
+ d.normometers_nm3 * o.share,
184
+ CASE WHEN d.consumption = 0 THEN 0 ELSE NULLIF(d.gas_consumption_kwh, 0) END * o.share,
185
+ (o.share < 1),
186
+ o.overlap_days
187
+ FROM commercial_device d
188
+ CROSS JOIN bounds b
189
+ CROSS JOIN LATERAL generate_series(
190
+ date_trunc('month', d.date_from::timestamp),
191
+ date_trunc('month', d.date_to::timestamp),
192
+ interval '1 month'
193
+ ) gs(month)
194
+ CROSS JOIN LATERAL (
195
+ SELECT
196
+ ov.overlap_days,
197
+ ov.overlap_days::numeric / NULLIF(d.date_to - d.date_from + 1, 0) AS share
198
+ FROM (
199
+ SELECT LEAST(d.date_to, (gs.month + interval '1 month - 1 day')::date)
200
+ - GREATEST(d.date_from, gs.month::date) + 1 AS overlap_days
201
+ ) ov
202
+ ) o
203
+ WHERE gs.month >= b.month_from AND gs.month < b.month_to
204
+ ),
205
+ invoice_month AS (
206
+ SELECT
207
+ source,
208
+ place_id,
209
+ period_start,
210
+ sum(volume) AS volume,
211
+ max(volume_unit) AS volume_unit,
212
+ sum(nm3) AS nm3,
213
+ sum(kwh) AS kwh,
214
+ bool_or(is_estimated) AS is_estimated,
215
+ -- Clamp to the month's length: sum() would exceed it when meters are billed in parallel.
216
+ LEAST(
217
+ sum(days_covered),
218
+ (period_start + interval '1 month')::date - period_start
219
+ ) AS days_covered,
220
+ array_agg(DISTINCT invoice_id::text ORDER BY invoice_id::text) AS invoice_ids
221
+ FROM invoice_device_month
222
+ GROUP BY source, place_id, period_start
223
+ ),
224
+ invoice_year AS (
225
+ SELECT
226
+ source,
227
+ place_id,
228
+ date_trunc('year', period_start)::date AS period_start,
229
+ array_agg(DISTINCT invoice_id::text ORDER BY invoice_id::text) AS invoice_ids
230
+ FROM invoice_device_month
231
+ GROUP BY source, place_id, date_trunc('year', period_start)::date
232
+ ),
233
+
234
+ porsenna_period AS (
235
+ SELECT *
236
+ FROM (
237
+ SELECT
238
+ d.identifier AS place_id,
239
+ c.commodity,
240
+ c."var" AS period_type,
241
+ f_enapo_safe_period(c."var", c."period") AS period_start,
242
+ c.value,
243
+ c.unit,
244
+ c."count" AS days_covered
245
+ FROM enapo_porsenna_consumption c
246
+ INNER JOIN enapo_porsenna_devices d
247
+ ON d.source = c.source AND d.id = c.device_id
248
+ WHERE d.identifier IS NOT NULL
249
+ AND (d."level" IS NULL OR d."level" = 'main')
250
+ AND c.commodity IS NOT NULL
251
+ AND c."var" IN ('yearly', 'monthly')
252
+ ) parsed
253
+ WHERE parsed.period_start IS NOT NULL
254
+ ),
255
+
256
+ eb_month AS (
257
+ -- Energy Broker history, full calendar months only: a period that is longer or does not start on
258
+ -- the 1st needs the pro-rata methodology, which is a separate step. One EAN/EIC can appear under
259
+ -- two idodbmistoom with identical values (a re-registered OM), so one row per month is kept instead
260
+ -- of summing them into a double. The highest idodbmisto_rspotrebyfa stands in for the newest row: the
261
+ -- one-off import has no timestamp, and in production the duplicates carry identical values.
262
+ SELECT DISTINCT ON (eb.ean_eic, eb.komodita, eb.datum_od)
263
+ eb.ean_eic AS place_id,
264
+ CASE eb.komodita WHEN 1 THEN 'electricity' WHEN 2 THEN 'gas' END AS commodity,
265
+ eb.datum_od AS period_start,
266
+ eb.spotreba_mwh * 1000 AS value_kwh,
267
+ eb.datum_do - eb.datum_od + 1 AS days_covered
268
+ FROM enapo_eb_consumption eb
269
+ CROSS JOIN bounds b
270
+ WHERE eb.spotreba_mwh IS NOT NULL
271
+ AND eb.datum_od = date_trunc('month', eb.datum_od)::date
272
+ AND eb.datum_do = (date_trunc('month', eb.datum_od) + interval '1 month - 1 day')::date
273
+ AND eb.datum_od >= b.month_from
274
+ AND eb.datum_od < b.month_to
275
+ ORDER BY eb.ean_eic, eb.komodita, eb.datum_od, eb.idodbmisto_rspotrebyfa DESC
276
+ ),
277
+
278
+ live_monthly AS (
279
+ SELECT
280
+ p.gid,
281
+ p.place_id,
282
+ p.id_type,
283
+ p.commodity,
284
+ 'predi_input'::varchar(50) AS source,
285
+ 'monthly'::varchar(10) AS period_type,
286
+ e.period_start,
287
+ COALESCE(e.total_kwh, e.vt_kwh + e.nt_kwh, e.vt_kwh, e.nt_kwh) AS value,
288
+ 'kWh'::varchar(10) AS unit,
289
+ COALESCE(e.total_kwh, e.vt_kwh + e.nt_kwh, e.vt_kwh, e.nt_kwh) AS value_kwh,
290
+ 'EActi_VT_NT_DaySum'::varchar(50) AS internal_source_var,
291
+ e.days_covered,
292
+ false AS is_estimated,
293
+ '{}'::text[] AS invoice_ids
294
+ FROM point p
295
+ INNER JOIN electricity_metered e ON e.place_id = p.place_id
296
+ WHERE p.commodity = 'electricity'
297
+
298
+ UNION ALL
299
+
300
+ SELECT
301
+ p.gid,
302
+ p.place_id,
303
+ p.id_type,
304
+ p.commodity,
305
+ 'ppas_ave_api',
306
+ 'monthly',
307
+ g.period_start,
308
+ g.m3_sum,
309
+ 'm3',
310
+ -- No conversion inputs for metered gas, but no consumption is 0 kWh whatever the conversion.
311
+ CASE WHEN g.m3_sum = 0 THEN 0 END,
312
+ 'core',
313
+ g.days_covered,
314
+ false,
315
+ '{}'::text[]
316
+ FROM point p
317
+ INNER JOIN gas_metered g ON g.place_id = p.place_id
318
+ WHERE p.commodity = 'gas'
319
+
320
+ UNION ALL
321
+
322
+ SELECT
323
+ p.gid,
324
+ p.place_id,
325
+ p.id_type,
326
+ p.commodity,
327
+ i.source,
328
+ 'monthly',
329
+ i.period_start,
330
+ COALESCE(i.volume, i.nm3::numeric),
331
+ CASE WHEN i.volume IS NOT NULL THEN i.volume_unit ELSE 'Nm3' END,
332
+ i.kwh,
333
+ NULL,
334
+ i.days_covered,
335
+ i.is_estimated,
336
+ i.invoice_ids
337
+ FROM point p
338
+ INNER JOIN invoice_month i ON i.place_id = p.place_id
339
+ WHERE p.commodity = 'gas'
340
+ ),
341
+
342
+ monthly AS (
343
+ SELECT * FROM live_monthly
344
+
345
+ UNION ALL
346
+
347
+ SELECT
348
+ p.gid,
349
+ p.place_id,
350
+ p.id_type,
351
+ p.commodity,
352
+ 'energy_broker',
353
+ 'monthly',
354
+ e.period_start,
355
+ e.value_kwh,
356
+ 'kWh',
357
+ e.value_kwh,
358
+ NULL,
359
+ e.days_covered,
360
+ false,
361
+ '{}'::text[]
362
+ FROM point p
363
+ INNER JOIN eb_month e ON e.place_id = p.place_id AND e.commodity = p.commodity
364
+ -- A live source that read the whole month in kWh wins, so the overlap is not charted twice. A pro-rata
365
+ -- invoice or a partially covered month does not win: the EB figure is billed for the whole month. Nor
366
+ -- does a volume-only row (the PPAS gas meter): EB is the only kWh for that month, which the combined
367
+ -- kWh series needs - unless the meter read no consumption, which is 0 kWh and wins.
368
+ WHERE NOT EXISTS (
369
+ SELECT 1
370
+ FROM live_monthly l
371
+ WHERE l.place_id = e.place_id
372
+ AND l.commodity = e.commodity
373
+ AND l.period_start = e.period_start
374
+ AND l.value_kwh IS NOT NULL
375
+ AND NOT l.is_estimated
376
+ AND l.days_covered >= (e.period_start + interval '1 month')::date - e.period_start
377
+ )
378
+ ),
379
+
380
+ yearly_rollup AS (
381
+ SELECT
382
+ gid,
383
+ place_id,
384
+ id_type,
385
+ commodity,
386
+ source,
387
+ 'yearly'::varchar(10) AS period_type,
388
+ date_trunc('year', period_start)::date AS period_start,
389
+ sum(value) AS value,
390
+ unit,
391
+ -- count(*) = count(value_kwh) is an all-non-null check: only sum value_kwh when every month has one.
392
+ CASE WHEN count(*) = count(value_kwh) THEN sum(value_kwh) END AS value_kwh,
393
+ max(internal_source_var) AS internal_source_var,
394
+ sum(days_covered) AS days_covered,
395
+ bool_or(is_estimated) AS is_estimated
396
+ FROM monthly m
397
+ GROUP BY gid, place_id, id_type, commodity, source, date_trunc('year', period_start)::date, unit
398
+ ),
399
+ yearly AS (
400
+ SELECT
401
+ r.gid,
402
+ r.place_id,
403
+ r.id_type,
404
+ r.commodity,
405
+ r.source,
406
+ r.period_type,
407
+ r.period_start,
408
+ r.value,
409
+ r.unit,
410
+ r.value_kwh,
411
+ r.internal_source_var,
412
+ r.days_covered,
413
+ r.is_estimated,
414
+ COALESCE(iy.invoice_ids, '{}'::text[]) AS invoice_ids
415
+ FROM yearly_rollup r
416
+ LEFT JOIN invoice_year iy
417
+ ON iy.source = r.source AND iy.place_id = r.place_id AND iy.period_start = r.period_start
418
+
419
+ UNION ALL
420
+
421
+ SELECT
422
+ p.gid,
423
+ p.place_id,
424
+ p.id_type,
425
+ p.commodity,
426
+ 'porsenna',
427
+ 'yearly',
428
+ pp.period_start,
429
+ pp.value,
430
+ pp.unit,
431
+ CASE WHEN lower(pp.unit) = 'kwh' THEN pp.value ELSE NULL END,
432
+ NULL,
433
+ pp.days_covered,
434
+ false,
435
+ '{}'::text[]
436
+ FROM point p
437
+ INNER JOIN porsenna_period pp
438
+ ON pp.place_id = p.place_id AND pp.commodity = p.commodity
439
+ WHERE pp.period_type = 'yearly'
440
+ )
441
+
442
+ SELECT
443
+ gid::varchar AS gid,
444
+ place_id::varchar(50) AS place_id,
445
+ max(id_type)::varchar(10) AS id_type,
446
+ commodity::varchar(20) AS commodity,
447
+ source::varchar(50) AS source,
448
+ period_type::varchar(10) AS period_type,
449
+ period_start,
450
+ sum(value)::numeric(30, 6) AS value,
451
+ unit::varchar(10) AS unit,
452
+ (CASE WHEN count(*) = count(value_kwh) THEN sum(value_kwh) END)::numeric(30, 6) AS value_kwh,
453
+ max(internal_source_var)::varchar(50) AS internal_source_var,
454
+ COALESCE(max(invoice_ids), '{}'::text[]) AS invoice_ids,
455
+ max(days_covered)::int AS coverage_count,
456
+ (
457
+ CASE period_type
458
+ WHEN 'yearly' THEN (make_date(extract(year FROM period_start)::int + 1, 1, 1) - period_start)
459
+ ELSE (period_start + interval '1 month')::date - period_start
460
+ END
461
+ )::int AS expected_count,
462
+ bool_or(is_estimated) AS is_estimated
463
+ FROM (
464
+ SELECT * FROM monthly
465
+ UNION ALL
466
+ SELECT * FROM yearly
467
+ ) all_periods
468
+ WHERE value IS NOT NULL
469
+ GROUP BY gid, place_id, commodity, source, unit, period_type, period_start;
470
+
471
+
472
+ COMMENT ON MATERIALIZED VIEW mv_enapo_om_consumption IS
473
+ 'Monthly and yearly consumption per consumption point per source, for the last 48 months, behind the DeD building detail endpoint. One row per (gid, place_id, commodity, source, unit, period_type, period_start). Refreshed by EnapoConsumptionHistoryRefreshTask (integration engine).';
474
+
475
+ COMMENT ON COLUMN mv_enapo_om_consumption.place_id IS
476
+ 'EAN / EIC / OM code, as in enapo_gid_mapping.place_id - NOT enapo_measurements.place_id, which holds a PPAS-internal place id. Measurements are joined through enapo_measurements.id.';
477
+ COMMENT ON COLUMN mv_enapo_om_consumption.value IS
478
+ 'Consumption for the period in `unit`. Electricity is kWh (the upstream T1_Wh_total naming is wrong); metered gas is m3, the difference between the cumulative `core` reading on the first day of the next month and the first day of the current month; invoiced gas follows the invoice unit; Porsenna carries its own unit; Energy Broker (enapo_eb_consumption, full calendar months only, suppressed where a live source read the whole month) is kWh converted from MWh for both electricity and gas.';
479
+ COMMENT ON COLUMN mv_enapo_om_consumption.value_kwh IS
480
+ 'Derived comparable energy: for gas, normometers_nm3 * combustion_heat (verified exact). NULL when the conversion inputs are missing - `value` + `unit` stay authoritative. NULL for metered gas (no normometers_nm3 input). Always 0 when no gas was consumed, whatever the conversion inputs.';
481
+ COMMENT ON COLUMN mv_enapo_om_consumption.internal_source_var IS
482
+ 'Which enapo_measurements.var produced the row; NULL for invoice and Porsenna rows. Provenance for debugging, not part of the public response.';
483
+ COMMENT ON COLUMN mv_enapo_om_consumption.invoice_ids IS
484
+ 'Billing documents the row was allocated from, sorted and deduplicated; empty for metered and Porsenna rows. A period can be allocated from several invoices (a yearly roll-up usually is), which is why it is an array.';
485
+ COMMENT ON COLUMN mv_enapo_om_consumption.coverage_count IS
486
+ 'Days of the period that carry data. Days for every commodity and both period types, so it is comparable with expected_count.';
487
+ COMMENT ON COLUMN mv_enapo_om_consumption.expected_count IS 'Days in the period (28-31, or 365/366).';
488
+ COMMENT ON COLUMN mv_enapo_om_consumption.is_estimated IS
489
+ 'True when the value was allocated pro rata from a billing period spanning more than one month, rather than read for the period itself.';
490
+
491
+ CREATE UNIQUE INDEX mv_enapo_om_consumption_uidx
492
+ ON mv_enapo_om_consumption (gid, place_id, commodity, source, unit, period_type, period_start);
493
+
494
+ CREATE INDEX mv_enapo_om_consumption_gid_period_idx
495
+ ON mv_enapo_om_consumption (gid, period_type, period_start);
@@ -3,13 +3,13 @@ import { IEnapoMeasurementsDto } from "../../../schema-definitions/models/enapo/
3
3
  import { AbstractTransformation } from "@golemio/core/dist/helpers/transformation/AbstractTransformation";
4
4
  import { IPPasPlaces } from "../interfaces/IPPasPlaces";
5
5
  export declare class PPasMeasurementTransformation extends AbstractTransformation<IAveApiDeviceDataDto & IPPasPlaces, {
6
- operatingAmount: IEnapoMeasurementsDto;
6
+ operatingAmount?: IEnapoMeasurementsDto;
7
7
  convertDifference?: IEnapoMeasurementsDto;
8
8
  operatingDifference?: IEnapoMeasurementsDto;
9
9
  }> {
10
10
  name: string;
11
11
  protected transformInternal: (data: IAveApiDeviceDataDto & IPPasPlaces) => {
12
- operatingAmount: IEnapoMeasurementsDto;
12
+ operatingAmount?: IEnapoMeasurementsDto;
13
13
  convertDifference?: IEnapoMeasurementsDto;
14
14
  operatingDifference?: IEnapoMeasurementsDto;
15
15
  };
@@ -29,11 +29,14 @@ let PPasMeasurementTransformation = class PPasMeasurementTransformation extends
29
29
  device_serial_number: String(data.serialNumber),
30
30
  };
31
31
  return {
32
- operatingAmount: {
33
- value: data.OperatingAmount,
34
- var: MeasurementVariableEnum_1.MeasurementVariable.Core,
35
- ...baseData,
36
- },
32
+ // A cumulative meter reading is never zero: AVE sends 0 for an hour it has no reading for.
33
+ ...(data.OperatingAmount !== 0 && {
34
+ operatingAmount: {
35
+ value: data.OperatingAmount,
36
+ var: MeasurementVariableEnum_1.MeasurementVariable.Core,
37
+ ...baseData,
38
+ },
39
+ }),
37
40
  ...(data.ConvertDifference !== undefined && {
38
41
  convertDifference: {
39
42
  value: data.ConvertDifference,
@@ -1 +1 @@
1
- {"version":3,"file":"PPasMeasurementTransformation.js","sourceRoot":"","sources":["../../../../src/integration-engine/enapo/transformations/PPasMeasurementTransformation.ts"],"names":[],"mappings":";;;;;;;;;AAAA,kHAA6G;AAE7G,qHAAuF;AACvF,wGAA0E;AAE1E,6GAA0G;AAC1G,2DAA2D;AAC3D,iEAAgE;AAIzD,IAAM,6BAA6B,GAAnC,MAAM,6BAA8B,SAAQ,+CAOlD;IAPM;;QAQI,SAAI,GAAG,+BAA+B,CAAC;QAEpC,sBAAiB,GAAG,CAC1B,IAAwC,EAK1C,EAAE;YACA,MAAM,QAAQ,GAAG;gBACb,EAAE,EAAE,IAAI,CAAC,GAAG;gBACZ,OAAO,EAAE,KAAK;gBACd,SAAS,EAAE,gBAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,EAAE;oBACxC,IAAI,EAAE,mCAAgB,CAAC,WAAW;iBACrC,CAAC,CAAC,QAAQ,EAAE;gBACb,MAAM,EAAE,+BAAY,CAAC,kBAAkB;gBACvC,QAAQ,EAAE,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC;gBAC9B,oBAAoB,EAAE,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC;aAClD,CAAC;YAEF,OAAO;gBACH,eAAe,EAAE;oBACb,KAAK,EAAE,IAAI,CAAC,eAAe;oBAC3B,GAAG,EAAE,6CAAmB,CAAC,IAAI;oBAC7B,GAAG,QAAQ;iBACd;gBACD,GAAG,CAAC,IAAI,CAAC,iBAAiB,KAAK,SAAS,IAAI;oBACxC,iBAAiB,EAAE;wBACf,KAAK,EAAE,IAAI,CAAC,iBAAiB;wBAC7B,GAAG,EAAE,6CAAmB,CAAC,KAAK;wBAC9B,GAAG,QAAQ;qBACd;iBACJ,CAAC;gBACF,GAAG,CAAC,IAAI,CAAC,mBAAmB,KAAK,SAAS,IAAI;oBAC1C,mBAAmB,EAAE;wBACjB,KAAK,EAAE,IAAI,CAAC,mBAAmB;wBAC/B,GAAG,EAAE,6CAAmB,CAAC,QAAQ;wBACjC,GAAG,QAAQ;qBACd;iBACJ,CAAC;aACL,CAAC;QACN,CAAC,CAAC;IACN,CAAC;CAAA,CAAA;AAlDY,sEAA6B;wCAA7B,6BAA6B;IADzC,IAAA,qBAAU,GAAE;GACA,6BAA6B,CAkDzC"}
1
+ {"version":3,"file":"PPasMeasurementTransformation.js","sourceRoot":"","sources":["../../../../src/integration-engine/enapo/transformations/PPasMeasurementTransformation.ts"],"names":[],"mappings":";;;;;;;;;AAAA,kHAA6G;AAE7G,qHAAuF;AACvF,wGAA0E;AAE1E,6GAA0G;AAC1G,2DAA2D;AAC3D,iEAAgE;AAIzD,IAAM,6BAA6B,GAAnC,MAAM,6BAA8B,SAAQ,+CAOlD;IAPM;;QAQI,SAAI,GAAG,+BAA+B,CAAC;QAEpC,sBAAiB,GAAG,CAC1B,IAAwC,EAK1C,EAAE;YACA,MAAM,QAAQ,GAAG;gBACb,EAAE,EAAE,IAAI,CAAC,GAAG;gBACZ,OAAO,EAAE,KAAK;gBACd,SAAS,EAAE,gBAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,EAAE;oBACxC,IAAI,EAAE,mCAAgB,CAAC,WAAW;iBACrC,CAAC,CAAC,QAAQ,EAAE;gBACb,MAAM,EAAE,+BAAY,CAAC,kBAAkB;gBACvC,QAAQ,EAAE,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC;gBAC9B,oBAAoB,EAAE,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC;aAClD,CAAC;YAEF,OAAO;gBACH,2FAA2F;gBAC3F,GAAG,CAAC,IAAI,CAAC,eAAe,KAAK,CAAC,IAAI;oBAC9B,eAAe,EAAE;wBACb,KAAK,EAAE,IAAI,CAAC,eAAe;wBAC3B,GAAG,EAAE,6CAAmB,CAAC,IAAI;wBAC7B,GAAG,QAAQ;qBACd;iBACJ,CAAC;gBACF,GAAG,CAAC,IAAI,CAAC,iBAAiB,KAAK,SAAS,IAAI;oBACxC,iBAAiB,EAAE;wBACf,KAAK,EAAE,IAAI,CAAC,iBAAiB;wBAC7B,GAAG,EAAE,6CAAmB,CAAC,KAAK;wBAC9B,GAAG,QAAQ;qBACd;iBACJ,CAAC;gBACF,GAAG,CAAC,IAAI,CAAC,mBAAmB,KAAK,SAAS,IAAI;oBAC1C,mBAAmB,EAAE;wBACjB,KAAK,EAAE,IAAI,CAAC,mBAAmB;wBAC/B,GAAG,EAAE,6CAAmB,CAAC,QAAQ;wBACjC,GAAG,QAAQ;qBACd;iBACJ,CAAC;aACL,CAAC;QACN,CAAC,CAAC;IACN,CAAC;CAAA,CAAA;AArDY,sEAA6B;wCAA7B,6BAA6B;IADzC,IAAA,qBAAU,GAAE;GACA,6BAA6B,CAqDzC"}
@@ -1,3 +1,4 @@
1
+ import { ISimpleConfig } from "@golemio/core/dist/helpers/configuration/ISimpleConfig";
1
2
  import { AbstractRouter } from "@golemio/core/dist/helpers/routing/AbstractRouter";
2
3
  export declare class V2EnoBuildingsRouter extends AbstractRouter {
3
4
  private controller;
@@ -6,6 +7,8 @@ export declare class V2EnoBuildingsRouter extends AbstractRouter {
6
7
  private staleWhileRevalidate;
7
8
  private searchMaxAge;
8
9
  private searchStaleWhileRevalidate;
9
- constructor();
10
+ constructor(config: ISimpleConfig);
10
11
  protected initRoutes(): void;
12
+ /** The detail is cached for hours by default, so an invalid value falls back to it rather than to the core's minute. */
13
+ private static parseMaxAge;
11
14
  }
@@ -8,6 +8,10 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
8
8
  var __metadata = (this && this.__metadata) || function (k, v) {
9
9
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
10
  };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ var V2EnoBuildingsRouter_1;
11
15
  Object.defineProperty(exports, "__esModule", { value: true });
12
16
  exports.V2EnoBuildingsRouter = void 0;
13
17
  const ConsumptionHistory_1 = require("../../constants/ConsumptionHistory");
@@ -16,19 +20,21 @@ const Links_1 = require("../../constants/Links");
16
20
  const RouteVersion_1 = require("../../constants/RouteVersion");
17
21
  const Di_1 = require("../../ioc/Di");
18
22
  const ModuleContainerToken_1 = require("../../ioc/ModuleContainerToken");
23
+ const CoreToken_1 = require("@golemio/core/dist/helpers/ioc/CoreToken");
19
24
  const AbstractRouter_1 = require("@golemio/core/dist/helpers/routing/AbstractRouter");
20
25
  const Validation_1 = require("@golemio/core/dist/output-gateway/Validation");
21
26
  const ioc_1 = require("@golemio/core/dist/output-gateway/ioc");
22
27
  const express_validator_1 = require("@golemio/core/dist/shared/express-validator");
23
28
  const tsyringe_1 = require("@golemio/core/dist/shared/tsyringe");
24
- let V2EnoBuildingsRouter = class V2EnoBuildingsRouter extends AbstractRouter_1.AbstractRouter {
25
- constructor() {
29
+ const DEFAULT_MAX_AGE = 6 * 60 * 60; // 6 hours
30
+ let V2EnoBuildingsRouter = V2EnoBuildingsRouter_1 = class V2EnoBuildingsRouter extends AbstractRouter_1.AbstractRouter {
31
+ constructor(config) {
26
32
  super(RouteVersion_1.RouteVersion.v2, Links_1.DED_BUILDING_PATH);
27
- this.maxAge = 6 * 60 * 60; // 6 hours
28
33
  this.staleWhileRevalidate = 60; // 1 minute
29
34
  // Suggestions must follow ingest closely, so the search is cached far more briefly than the detail.
30
35
  this.searchMaxAge = 60; // 1 minute
31
36
  this.searchStaleWhileRevalidate = 30; // 30 seconds
37
+ this.maxAge = V2EnoBuildingsRouter_1.parseMaxAge(config.getValue("env.ENERGETICS_DED_BUILDING_CACHE_MAX_AGE", String(DEFAULT_MAX_AGE)));
32
38
  this.controller = Di_1.OGEnergeticsContainer.resolve(ModuleContainerToken_1.ModuleContainerToken.EnoBuildingsController);
33
39
  this.cacheHeaderMiddleware = Di_1.OGEnergeticsContainer.resolve(ioc_1.ContainerToken.CacheHeaderMiddleware);
34
40
  this.initRoutes();
@@ -37,10 +43,16 @@ let V2EnoBuildingsRouter = class V2EnoBuildingsRouter extends AbstractRouter_1.A
37
43
  this.router.get("/", (0, express_validator_1.query)("q").exists().isString().not().isArray().trim().isLength({ min: 1, max: DedSearchWeights_1.DED_SEARCH_QUERY_MAX_LENGTH }), (0, express_validator_1.query)("limit").optional().isInt({ min: 1, max: DedSearchWeights_1.DED_SEARCH_LIMIT_MAX }).toInt(), Validation_1.checkErrors, this.cacheHeaderMiddleware.getMiddleware(this.searchMaxAge, this.searchStaleWhileRevalidate), this.controller.search);
38
44
  this.router.get("/:gid", (0, express_validator_1.param)("gid").exists().isString().not().isArray().trim(), (0, express_validator_1.query)("months").optional().isInt({ min: 1, max: ConsumptionHistory_1.CONSUMPTION_HISTORY_MONTHS_MAX }).toInt(), Validation_1.checkErrors, this.cacheHeaderMiddleware.getMiddleware(this.maxAge, this.staleWhileRevalidate), this.controller.getOne);
39
45
  }
46
+ /** The detail is cached for hours by default, so an invalid value falls back to it rather than to the core's minute. */
47
+ static parseMaxAge(value) {
48
+ const maxAge = Number(value);
49
+ return Number.isInteger(maxAge) && maxAge > 0 ? maxAge : DEFAULT_MAX_AGE;
50
+ }
40
51
  };
41
52
  exports.V2EnoBuildingsRouter = V2EnoBuildingsRouter;
42
- exports.V2EnoBuildingsRouter = V2EnoBuildingsRouter = __decorate([
53
+ exports.V2EnoBuildingsRouter = V2EnoBuildingsRouter = V2EnoBuildingsRouter_1 = __decorate([
43
54
  (0, tsyringe_1.injectable)(),
44
- __metadata("design:paramtypes", [])
55
+ __param(0, (0, tsyringe_1.inject)(CoreToken_1.CoreToken.SimpleConfig)),
56
+ __metadata("design:paramtypes", [Object])
45
57
  ], V2EnoBuildingsRouter);
46
58
  //# sourceMappingURL=V2EnoBuildingsRouter.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"V2EnoBuildingsRouter.js","sourceRoot":"","sources":["../../../../src/output-gateway/routers/v2/V2EnoBuildingsRouter.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,2EAAkF;AAClF,uEAAmG;AACnG,iDAAwD;AACxD,+DAA0D;AAE1D,qCAAmD;AACnD,yEAAoE;AACpE,sFAAmF;AAEnF,6EAA2E;AAC3E,+DAAuE;AACvE,mFAA2E;AAC3E,iEAAgE;AAGzD,IAAM,oBAAoB,GAA1B,MAAM,oBAAqB,SAAQ,+BAAc;IASpD;QACI,KAAK,CAAC,2BAAY,CAAC,EAAE,EAAE,yBAAiB,CAAC,CAAC;QAPtC,WAAM,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC,UAAU;QAChC,yBAAoB,GAAG,EAAE,CAAC,CAAC,WAAW;QAC9C,oGAAoG;QAC5F,iBAAY,GAAG,EAAE,CAAC,CAAC,WAAW;QAC9B,+BAA0B,GAAG,EAAE,CAAC,CAAC,aAAa;QAIlD,IAAI,CAAC,UAAU,GAAG,0BAAqB,CAAC,OAAO,CAAyB,2CAAoB,CAAC,sBAAsB,CAAC,CAAC;QACrH,IAAI,CAAC,qBAAqB,GAAG,0BAAqB,CAAC,OAAO,CAAwB,oBAAc,CAAC,qBAAqB,CAAC,CAAC;QACxH,IAAI,CAAC,UAAU,EAAE,CAAC;IACtB,CAAC;IAES,UAAU;QAChB,IAAI,CAAC,MAAM,CAAC,GAAG,CACX,GAAG,EACH,IAAA,yBAAK,EAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,CAAC,QAAQ,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,8CAA2B,EAAE,CAAC,EAC5G,IAAA,yBAAK,EAAC,OAAO,CAAC,CAAC,QAAQ,EAAE,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,uCAAoB,EAAE,CAAC,CAAC,KAAK,EAAE,EAC9E,wBAAW,EACX,IAAI,CAAC,qBAAqB,CAAC,aAAa,CAAC,IAAI,CAAC,YAAY,EAAE,IAAI,CAAC,0BAA0B,CAAC,EAC5F,IAAI,CAAC,UAAU,CAAC,MAAM,CACzB,CAAC;QAEF,IAAI,CAAC,MAAM,CAAC,GAAG,CACX,OAAO,EACP,IAAA,yBAAK,EAAC,KAAK,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,EACvD,IAAA,yBAAK,EAAC,QAAQ,CAAC,CAAC,QAAQ,EAAE,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,mDAA8B,EAAE,CAAC,CAAC,KAAK,EAAE,EACzF,wBAAW,EACX,IAAI,CAAC,qBAAqB,CAAC,aAAa,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,oBAAoB,CAAC,EAChF,IAAI,CAAC,UAAU,CAAC,MAAM,CACzB,CAAC;IACN,CAAC;CACJ,CAAA;AAnCY,oDAAoB;+BAApB,oBAAoB;IADhC,IAAA,qBAAU,GAAE;;GACA,oBAAoB,CAmChC"}
1
+ {"version":3,"file":"V2EnoBuildingsRouter.js","sourceRoot":"","sources":["../../../../src/output-gateway/routers/v2/V2EnoBuildingsRouter.ts"],"names":[],"mappings":"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"}