@golemio/energetics 1.13.0 → 1.13.1-dev.2769672204

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 (69) hide show
  1. package/db/example/07_enapo_om_detail.sql +15 -10
  2. package/db/example/09_enapo_consumption_history.sql +196 -0
  3. package/db/migrations/postgresql/20260813090000-enapo-consumption-history.js +53 -0
  4. package/db/migrations/postgresql/sqls/20260813090000-enapo-consumption-history-down.sql +12 -0
  5. package/db/migrations/postgresql/sqls/20260813090000-enapo-consumption-history-up.sql +504 -0
  6. package/dist/integration-engine/enapo/ioc/Di.js +5 -1
  7. package/dist/integration-engine/enapo/ioc/Di.js.map +1 -1
  8. package/dist/integration-engine/enapo/ioc/EnapoWorkerContainerToken.d.ts +2 -0
  9. package/dist/integration-engine/enapo/ioc/EnapoWorkerContainerToken.js +2 -0
  10. package/dist/integration-engine/enapo/ioc/EnapoWorkerContainerToken.js.map +1 -1
  11. package/dist/integration-engine/enapo/repositories/AbstractMaterializedViewIndexRepository.d.ts +8 -0
  12. package/dist/integration-engine/enapo/repositories/AbstractMaterializedViewIndexRepository.js +25 -0
  13. package/dist/integration-engine/enapo/repositories/AbstractMaterializedViewIndexRepository.js.map +1 -0
  14. package/dist/integration-engine/enapo/repositories/DedSearchIndexRepository.d.ts +2 -3
  15. package/dist/integration-engine/enapo/repositories/DedSearchIndexRepository.js +3 -23
  16. package/dist/integration-engine/enapo/repositories/DedSearchIndexRepository.js.map +1 -1
  17. package/dist/integration-engine/enapo/repositories/EnapoConsumptionHistoryIndexRepository.d.ts +6 -0
  18. package/dist/integration-engine/enapo/repositories/EnapoConsumptionHistoryIndexRepository.js +30 -0
  19. package/dist/integration-engine/enapo/repositories/EnapoConsumptionHistoryIndexRepository.js.map +1 -0
  20. package/dist/integration-engine/enapo/repositories/interfaces/IEnapoConsumptionHistoryIndexRepository.d.ts +7 -0
  21. package/dist/integration-engine/enapo/repositories/interfaces/IEnapoConsumptionHistoryIndexRepository.js +3 -0
  22. package/dist/integration-engine/enapo/repositories/interfaces/IEnapoConsumptionHistoryIndexRepository.js.map +1 -0
  23. package/dist/integration-engine/enapo/workers/EnapoWorker.js +1 -0
  24. package/dist/integration-engine/enapo/workers/EnapoWorker.js.map +1 -1
  25. package/dist/integration-engine/enapo/workers/task/EnapoConsumptionHistoryRefreshTask.d.ts +10 -0
  26. package/dist/integration-engine/enapo/workers/task/EnapoConsumptionHistoryRefreshTask.js +47 -0
  27. package/dist/integration-engine/enapo/workers/task/EnapoConsumptionHistoryRefreshTask.js.map +1 -0
  28. package/dist/output-gateway/constants/ConsumptionHistory.d.ts +2 -0
  29. package/dist/output-gateway/constants/ConsumptionHistory.js +6 -0
  30. package/dist/output-gateway/constants/ConsumptionHistory.js.map +1 -0
  31. package/dist/output-gateway/controllers/v2/EnoBuildingsController.js +3 -3
  32. package/dist/output-gateway/controllers/v2/EnoBuildingsController.js.map +1 -1
  33. package/dist/output-gateway/helpers/ConsumptionHistoryWindow.d.ts +12 -0
  34. package/dist/output-gateway/helpers/ConsumptionHistoryWindow.js +30 -0
  35. package/dist/output-gateway/helpers/ConsumptionHistoryWindow.js.map +1 -0
  36. package/dist/output-gateway/models/EnapoOmConsumptionViewModel.d.ts +23 -0
  37. package/dist/output-gateway/models/EnapoOmConsumptionViewModel.js +48 -0
  38. package/dist/output-gateway/models/EnapoOmConsumptionViewModel.js.map +1 -0
  39. package/dist/output-gateway/models/interfaces/IEnapoOmConsumptionView.d.ts +20 -0
  40. package/dist/output-gateway/models/interfaces/IEnapoOmConsumptionView.js +3 -0
  41. package/dist/output-gateway/models/interfaces/IEnapoOmConsumptionView.js.map +1 -0
  42. package/dist/output-gateway/repositories/EnapoOmConsumptionRepository.d.ts +8 -0
  43. package/dist/output-gateway/repositories/EnapoOmConsumptionRepository.js +59 -0
  44. package/dist/output-gateway/repositories/EnapoOmConsumptionRepository.js.map +1 -0
  45. package/dist/output-gateway/repositories/EnoBuildingDetailRepository.d.ts +2 -1
  46. package/dist/output-gateway/repositories/EnoBuildingDetailRepository.js +13 -3
  47. package/dist/output-gateway/repositories/EnoBuildingDetailRepository.js.map +1 -1
  48. package/dist/output-gateway/repositories/EnoConsumptionPointsRepository.d.ts +2 -0
  49. package/dist/output-gateway/repositories/EnoConsumptionPointsRepository.js +34 -0
  50. package/dist/output-gateway/repositories/EnoConsumptionPointsRepository.js.map +1 -1
  51. package/dist/output-gateway/repositories/interfaces/IEnoBuildingDetailAggregate.d.ts +6 -1
  52. package/dist/output-gateway/repositories/interfaces/IEnoSharedPointRow.d.ts +5 -0
  53. package/dist/output-gateway/repositories/interfaces/IEnoSharedPointRow.js +3 -0
  54. package/dist/output-gateway/repositories/interfaces/IEnoSharedPointRow.js.map +1 -0
  55. package/dist/output-gateway/routers/interfaces/IEnoBuildingDetailResponse.d.ts +43 -18
  56. package/dist/output-gateway/routers/v2/V2EnoBuildingsRouter.js +2 -1
  57. package/dist/output-gateway/routers/v2/V2EnoBuildingsRouter.js.map +1 -1
  58. package/dist/output-gateway/transformations/EnoBuildingDetailTransformation.d.ts +6 -0
  59. package/dist/output-gateway/transformations/EnoBuildingDetailTransformation.js +53 -14
  60. package/dist/output-gateway/transformations/EnoBuildingDetailTransformation.js.map +1 -1
  61. package/dist/output-gateway/transformations/helpers/ConsumptionHistoryBuilder.d.ts +16 -0
  62. package/dist/output-gateway/transformations/helpers/ConsumptionHistoryBuilder.js +133 -0
  63. package/dist/output-gateway/transformations/helpers/ConsumptionHistoryBuilder.js.map +1 -0
  64. package/dist/output-gateway/transformations/helpers/PorsennaCoverage.d.ts +5 -0
  65. package/dist/output-gateway/transformations/helpers/PorsennaCoverage.js +40 -0
  66. package/dist/output-gateway/transformations/helpers/PorsennaCoverage.js.map +1 -0
  67. package/docs/implementation_documentation.md +67 -1
  68. package/docs/openapi-output.yaml +663 -43
  69. package/package.json +1 -1
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PorsennaCoverage = void 0;
4
+ const luxon_1 = require("@golemio/core/dist/shared/luxon");
5
+ const PERIOD_FORMAT = {
6
+ yearly: "yyyy",
7
+ monthly: "yyyy-MM",
8
+ daily: "yyyy-MM-dd",
9
+ };
10
+ const PERIOD_LENGTH = {
11
+ yearly: { years: 1 },
12
+ monthly: { months: 1 },
13
+ daily: { days: 1 },
14
+ };
15
+ class PorsennaCoverage {
16
+ static dataTo(rows) {
17
+ const covered = rows
18
+ .map((row) => PorsennaCoverage.lastCoveredDay(row))
19
+ .filter((day) => day !== null)
20
+ .sort();
21
+ return covered.length === 0 ? null : covered[covered.length - 1];
22
+ }
23
+ static lastCoveredDay(row) {
24
+ const format = PERIOD_FORMAT[row.var];
25
+ if (format === undefined) {
26
+ return null;
27
+ }
28
+ const start = luxon_1.DateTime.fromFormat(row.period, format, { zone: "utc" });
29
+ if (!start.isValid) {
30
+ return null;
31
+ }
32
+ const periodEnd = start.plus(PERIOD_LENGTH[row.var]).minus({ days: 1 });
33
+ // count clamped into [start, periodEnd]: a missing/nonsensical count is treated as fully covered.
34
+ const days = row.count === null || row.count < 1 ? Number.MAX_SAFE_INTEGER : row.count;
35
+ const reached = days >= periodEnd.diff(start, "days").days + 1 ? periodEnd : start.plus({ days: days - 1 });
36
+ return reached.toFormat("yyyy-MM-dd");
37
+ }
38
+ }
39
+ exports.PorsennaCoverage = PorsennaCoverage;
40
+ //# sourceMappingURL=PorsennaCoverage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PorsennaCoverage.js","sourceRoot":"","sources":["../../../../src/output-gateway/transformations/helpers/PorsennaCoverage.ts"],"names":[],"mappings":";;;AACA,2DAA2D;AAE3D,MAAM,aAAa,GAA2B;IAC1C,MAAM,EAAE,MAAM;IACd,OAAO,EAAE,SAAS;IAClB,KAAK,EAAE,YAAY;CACtB,CAAC;AAEF,MAAM,aAAa,GAAuE;IACtF,MAAM,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE;IACpB,OAAO,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE;IACtB,KAAK,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE;CACrB,CAAC;AAEF,MAAa,gBAAgB;IAClB,MAAM,CAAC,MAAM,CAAC,IAAoB;QACrC,MAAM,OAAO,GAAG,IAAI;aACf,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,gBAAgB,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC;aAClD,MAAM,CAAC,CAAC,GAAG,EAAiB,EAAE,CAAC,GAAG,KAAK,IAAI,CAAC;aAC5C,IAAI,EAAE,CAAC;QAEZ,OAAO,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IACrE,CAAC;IAEO,MAAM,CAAC,cAAc,CAAC,GAAiB;QAC3C,MAAM,MAAM,GAAG,aAAa,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACtC,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YACvB,OAAO,IAAI,CAAC;QAChB,CAAC;QAED,MAAM,KAAK,GAAG,gBAAQ,CAAC,UAAU,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QACvE,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC;YACjB,OAAO,IAAI,CAAC;QAChB,CAAC;QAED,MAAM,SAAS,GAAG,KAAK,CAAC,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC;QACxE,kGAAkG;QAClG,MAAM,IAAI,GAAG,GAAG,CAAC,KAAK,KAAK,IAAI,IAAI,GAAG,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,gBAAgB,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC;QACvF,MAAM,OAAO,GAAG,IAAI,IAAI,SAAS,CAAC,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,GAAG,CAAC,EAAE,CAAC,CAAC;QAE5G,OAAO,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC;IAC1C,CAAC;CACJ;AA5BD,4CA4BC"}
@@ -675,6 +675,56 @@ Task přegeneruje materializovaný pohled `mv_ded_building_search`, nad kterým
675
675
  - unit testy
676
676
  - [DedSearchIndexRefreshTask.test.ts](../test/integration-engine/enapo/task/DedSearchIndexRefreshTask.test.ts)
677
677
 
678
+ #### _task: EnapoConsumptionHistoryRefreshTask_
679
+
680
+ Task přegeneruje materializovaný pohled `mv_enapo_om_consumption`, ze kterého se plní blok `consumption_history` v detailu budovy `/v2/energetics/ded/buildings/{gid}` (měsíční a roční spotřeba za odběrné místo a zdroj).
681
+
682
+ - vstupní rabbitmq fronta
683
+ - název: dataplatform.enapoenergetics.refreshConsumptionHistory
684
+ - TTL: 30 minut (starší zpráva už je překonaná novější, nemá smysl ji zpracovávat)
685
+ - parametry: žádné — refresh vždy přestaví celý pohled, takže není co validovat a `schema` je záměrně `undefined`
686
+ - cron definice
687
+ - cron.dataplatform.enapoenergetics.refreshConsumptionHistory — noční běh po okně stahování odečtů
688
+ - rabin `0 45 3 * * *`
689
+ - prod `0 45 3 * * *`
690
+ - samotná definice cronu je vedena mimo tento repozitář (deployment konfigurace)
691
+ - spouštění na základě událostí
692
+ - žádné, a to záměrně: [EnapoPpasTask](#task-enapoppastask) stahuje po pětidenních intervalech a spouštěl by desítky refreshů za jeden běh, přičemž endpoint stejně garantuje svěžest jen na 6 hodin (cache hlavičky)
693
+ - data modely
694
+ - [EnapoConsumptionHistoryIndexRepository](../src/integration-engine/enapo/repositories/EnapoConsumptionHistoryIndexRepository.ts) — `REFRESH MATERIALIZED VIEW CONCURRENTLY energetics.mv_enapo_om_consumption`, kontrakt [IEnapoConsumptionHistoryIndexRepository](../src/integration-engine/enapo/repositories/interfaces/IEnapoConsumptionHistoryIndexRepository.ts)
695
+ - funkce
696
+ - **CONCURRENTLY**: refresh neblokuje čtenáře; vyžaduje unikátní index nad `(gid, place_id, commodity, source, unit, period_type, period_start)` (vytvořen migrací) a **nesmí běžet v transakci**
697
+ - pohled je omezen na posledních 48 měsíců, aby refresh nemusel procházet celou historii; endpoint standardně vrací 36 (`?months=`, max 48)
698
+ - sloupec `invoice_ids` nese doklady, ze kterých bylo období rozpočteno (prázdné pole u měřených a Porsenna řad). Roční řádky ho neskládají z měsíčních polí — `array_agg` nad poli různé délky spadne a sjednocovací agregát nad polem v PG není — ale rollupem přímo z fakturačních řádků (CTE `invoice_device_month` → `invoice_month` / `invoice_year`)
699
+ - `enapo_measurements` má ~45 mil. řádků a jediný index byl primární klíč, jehož vedoucí sloupec je pro tento průchod nepoužitelný — migrace proto přidává `enapo_measurements_source_var_ts_idx (source, var, timestamp)`; bez něj trval elektroměrový agregát 5 min 45 s a plynový 1 min 24 s
700
+ - unit testy
701
+ - [EnapoConsumptionHistoryRefreshTask.test.ts](../test/integration-engine/enapo/task/EnapoConsumptionHistoryRefreshTask.test.ts)
702
+
703
+ #### _Sémantika `var` v `enapo_measurements` (ověřeno proti produkci 2026-08-13)_
704
+
705
+ Sloupec `value` míchá **kumulativní stavy měřidla** a **rozdíly za interval**, rozlišené pouze podle `(source, var)`. Agregace bez tohoto rozlišení dává nesmysly.
706
+
707
+ | `source` | komodita | `var` | granularita | význam | jednotka | agregace |
708
+ | -------------- | ----------- | ---------------------------------- | ----------- | -------------------------------------------------- | -------- | --------------------------------------------------- |
709
+ | `ppas_ave_api` | plyn | `corediff` | hodinová | **rozdíl** (`OperatingDifference`) | m³ | `sum` ← **primární** |
710
+ | `ppas_ave_api` | plyn | `core2` | hodinová | **rozdíl**, přepočtený (`ConvertDifference`) | Nm³ | `sum`, ale v 81 % řádků nula → jen fallback |
711
+ | `ppas_ave_api` | plyn | `core` | hodinová | **kumulativní** stav měřidla (`OperatingAmount`) | m³ | **nepoužívat** (viz níže) |
712
+ | `predi_input` | elektřina | `EActi_VT_NT_DaySum` | denní | **odvozený součet** VT+NT | **kWh** | `sum` ← **primární** |
713
+ | `predi_input` | elektřina | `EActi_VT_DaySum` / `_NT_DaySum` | denní | denní energie; NT je dnes všude nulové | **kWh** | `sum` (jen fallback) |
714
+ | `predi_input` | elektřina | `EFwActi_VT` / `_NT` | 15 min | průměrný **výkon**, tedy 4× energie | kW | **nepoužívat** (`sum / 4` by byly kWh) |
715
+
716
+ Tři pasti, které je potřeba mít zakódované v SQL, ne v hlavě reviewera:
717
+
718
+ 1. **Nikdy nesčítat `EActi_VT_NT_DaySum` společně s `EActi_VT_DaySum` + `EActi_NT_DaySum`** — první je součtem druhých dvou (ověřeno: podíl je 1.0000 na p05, mediánu i p95).
719
+ 2. **Nikdy nemíchat `EFwActi_*` s `EActi_*_DaySum`** — tatáž energie dvakrát, a navíc bez dělení čtyřmi (ověřeno: podíl 4.0000).
720
+ 3. **Jednotka je kWh, ne Wh**, přes to, že se zdrojové pole jmenuje `T1_Wh_total`. Doloženo přes `enapo_pre_metadata_history.circuit_breaker`: odběrná místa s vyplněným jističem vycházejí na 63–130 kW průměrného výkonu proti 173–277 kW kapacity přípojky, což jako Wh nedává smysl.
721
+
722
+ Další ověřené vlastnosti dat, na které agregace spoléhá:
723
+
724
+ - `id_type` **není spolehlivý** — v produkci je aspoň jedno elektroměrové EAN uložené s `id_type = 'eic'`, takže join na měření jde výhradně přes `enapo_measurements.id` a `id_type` se nefiltruje
725
+ - `enapo_gid_mapping.place_id` je **kód EAN/EIC**, zatímco `enapo_measurements.place_id` je interní PPAS place id — join `place_id = place_id` vrací nula řádků a vypadá jako „nejsou data“
726
+ - jedno EIC může mít několik `place_id`. Pokud jejich počet přesahuje počet různých `device_serial_number`, jde o dvě živé registrace jednoho fyzického měřidla a sečtení by spotřebu zdvojilo — agregace proto pracuje na granularitě `(id, device_serial_number)`. Ze stejného důvodu je `max(core) - min(core)` nepoužitelné: rozpětí mezi dvěma nesouvisejícími počítadly.
727
+
678
728
  #### _PPAS Distribution API_
679
729
 
680
730
  - zdroj dat
@@ -686,6 +736,15 @@ Task přegeneruje materializovaný pohled `mv_ded_building_search`, nad kterým
686
736
  - accessToken header: module.energetics.enapo.ppas.distribution.accessToken
687
737
  - API dokumentace: interní dokumentace PPAS
688
738
  - Kdyz jsou hodnoty v consumptionKwh negativni tak se jedna o storno označeno jako CORRECTION pokud ne označeno jako STANDARD, pokud prijdou dve nulove hodnoty za sebou, nazváno zero rows, tak je první brána jako STANDARD a druhá CORRECTION
739
+ - `kind` je sada SAP kódů, které vyjadřují **tentýž plyn v různých dimenzích**, takže součet přes všechny `kind` nadhodnocuje 2–3×. Ověřeno proti produkci 2026-08-13:
740
+ - `ZIZWC` (M3, 434 EIC) — hlavní objemová řada, **používá se**
741
+ - `ZIABN3` (KWH, 22 EIC) — denní kWh velkých odběratelů, **používá se**; těchto 22 EIC je průkazně disjunktní se 434 výše (jejich Nm³ převyšuje celkový součet `ZIZWC`)
742
+ - `ZSATN3` (NM3, 22 EIC) — **tentýž plyn jako `ZIABN3`** (Nm³ součty jsou shodné na cifru), vyloučeno
743
+ - `ZSCBM3` (M3, 49 EIC) — nese 92 % objemu `ZIZWC` z devítiny jeho EIC; vyloučeno jako bezpečnější strana otevřené otázky (chybějící řada se vrací jako `null`, zdvojené číslo by se tvářilo jako platné)
744
+ - sloupec `consumption` je denominovaný podle `unit` (M3 / NM3 / KWH), **není to vždy m³**
745
+ - `combustion_heat` a `volume_coefficient` s hodnotou 0 znamenají „nedodáno“, ne nulu — bez `NULLIF` by se z odvozených kWh stala nula, která se na grafu kreslí jako skutečný odečet
746
+ - existují **duplicitní faktury bez vazby přes `preceding_id`** nesoucí bajtově shodné řádky pro totéž EIC a období; deduplikace proto musí jít podle obsahu řádku `(eic, place_id, device_serial_number, date_from, date_to, kind, reading_type, unit)`, nikoli podle návaznosti faktur
747
+ - `mv_enapo_om_consumption` zatím bere jen `reading_type = 'STANDARD'`. Protože CORRECTION je storno (viz výše, negativní consumptionKwh), znamená to, že se storna neodečítají a spotřeba je **nadhodnocená o ~0,68 %**. Vědomé zjednodušení první verze: znaménko nese pouze `gas_consumption_kwh`, `consumption` zůstává pozitivní, takže naivní součet by v jednom sloupci odečítal a ve druhém přičítal
689
748
  - formát dat
690
749
  - protokol: https (REST API)
691
750
  - datový typ: json
@@ -927,7 +986,7 @@ Přehled endpointů a parametru `accessLimit`:
927
986
  - `enapo_ppas_distribution_invoice(_installation)` — poslední nestornovaná distribuční faktura (jen EIC / plyn, join přes `eic` v tabulce instalací)
928
987
  - `enapo_ppas_commercial_invoice(_installations)` — poslední nestornovaná obchodní faktura (jen EIC / plyn, join přes `eic`)
929
988
  - `UNION ALL` větev pro OM známá pouze z Porsenny (e-manažer): hlavní měřidla (`level = main`) z `enapo_porsenna_devices`, jejichž `identifier` není namapován na daný GID v `enapo_gid_mapping`; `commodity` z typu měřidla (v reálných datech existují pouze `electricity_meter` / `gasometer` / `heat_meter` / `water_meter`), `id_type` odvozen přímo z komodity (elektřina → ean, plyn → eic, teplo a voda → `om` — standardní identifikátor nemají), `link_source = 'porsenna'`, `is_active` z `disabled_at`
930
- - `v_enapo_building_porsenna_devices` — měřidla Porsenny s budovou (`enapo_porsenna_devices` × `enapo_porsenna_buildings` přes `(source, building_id)`); slouží pro obohacení OM o blok `porsenna` (měřidlo + podružná měřidla + roční spotřeby z `enapo_porsenna_consumption`) a pro blok `energy_management` na úrovni budovy
989
+ - `v_enapo_building_porsenna_devices` — měřidla Porsenny s budovou (`enapo_porsenna_devices` × `enapo_porsenna_buildings` přes `(source, building_id)`); slouží pro obohacení OM o blok `porsenna` (měřidlo + podružná měřidla) a pro blok `energy_management` na úrovni budovy; spotřeby Porsenny blok nenese — jdou přes `mv_enapo_om_consumption` do `consumption_history` daného OM, kde mají navíc pokrytí a počty přispěvatelů
931
990
  - `enapo_ppas_distribution_invoice_device` / `_price`, `enapo_ppas_commercial_invoice_device` / `_price` — dotahované podle id faktury a filtrované podle `place_id` instalace (jedna faktura může účtovat více OM, bez filtru by se řádky mísily)
932
991
  - mapovací tabulka `enapo_gid_mapping`
933
992
  - vlastní kontrakt modulu; aktuálně plněná mock daty (`link_source = 'mock'`)
@@ -937,4 +996,11 @@ Přehled endpointů a parametru `accessLimit`:
937
996
  - komoditně specifická data jsou vnořená pod klíči `electricity` / `gas` u každého OM; v odpovědi je pouze klíč odpovídající poli `commodity`, ostatní jsou zcela vynechány
938
997
  - detail existuje, pokud pro GID máme jakákoli data — zná ho alespoň jeden zdroj (`eno_budova`, `eno_adresa`, `enapo_gid_mapping`, nebo Porsenna): pokud GID není v `eno_budova`, ale má adresy, namapovaná OM či měřidla, vrací se 200 s `building: null` a chybějící části jako `null` / prázdná pole (cca 29 % GIDů z mapování nemá záznam v ENO); 404 pouze pokud GID nezná žádný zdroj
939
998
  - OM známé z obou zdrojů (mapování i Porsenna) je v odpovědi jednou — data Porsenny jsou vnořená jako blok `porsenna` v příslušném komoditním klíči; duplicitní záznamy nevznikají
999
+ - `monthly` řada zdroje, který reportuje jen roční agregáty (Porsenna), je prázdné pole, ne 36 `null` položek — spina samých `null` by tvrdila, že zdroj měsíce zná a nemá je, což pro zdroj bez měsíční granularity neplatí; roční data jsou v `yearly`
940
1000
  - budoucí komodity (teplo, voda) se přidají jako nové klíče bez breaking change
1001
+ - `name` / `address` na nejvyšší úrovni: `building` je pro cca 29 % GIDů `null`, ale kartu je pořád potřeba nadepsat — název se proto řeší stejným fallbackem jako ve vyhledávání (`eno_budova.nazev` → název budovy z Porsenny → `null`), adresa jako první popsaná `eno_adresa` (řazené hlavní adresou napřed) → adresa z Porsenny → `null`. Kdyby to zůstalo na klientech, každý si řetězec poskládá jinak.
1002
+ - provenience agregátů (aby klient nemusel sečtené číslo vydávat za přesné)
1003
+ - `points_total` / `points_with_data` u každého období: kolik OM do něj přispělo a kolik z nich mělo hodnotu. Rozdíl proti `points` celé řady je to, co odliší „budova spotřebovala míň“ od „chybí jedno OM“; u prázdného období spiny jsou obě nuly, nikdy `null`
1004
+ - `invoice_ids` u každého období: doklady, ze kterých bylo období rozpočteno; prázdné pole u měřených a Porsenna řad. Na úrovni budovy může být období rozpočteno z několika faktur (jedna na OM), roční agregát skoro vždy — proto pole, ne jedno id
1005
+ - `shared_with` u OM: ostatní budovy, na které je totéž OM namapované (z `enapo_gid_mapping`, název stejným fallbackem jako `name`). Součty počítají takové OM v plné výši — to je to, co měřidla naměřila — takže tohle je jediný způsob, jak se klient dozví, s kým je číslo sdílené. Platí pro obě strany sdíleného OM, tedy i pro tu s `is_first_gid = true`; `has_shared_points` na úrovni budovy se odvozuje odsud, ne z `is_first_gid`
1006
+ - `energy_management.data_to`: kam sahají data e-manažera. Porsenna neposílá čas posledního odečtu, jen počet pokrytých dní za období, takže se datum odvozuje jako začátek období + pokryté dny, oříznuté koncem období ([PorsennaCoverage](../src/output-gateway/transformations/helpers/PorsennaCoverage.ts))