Compare commits
2 Commits
2ce68cb098
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| f4b5926e1f | |||
| 1047968946 |
@@ -0,0 +1,189 @@
|
|||||||
|
# UVI – Anforderungen (Pilot)
|
||||||
|
|
||||||
|
> Anforderungsdokument. Source of Truth = diese Notion-Seite. Tickets in Linear referenzieren die hier vergebenen IDs.
|
||||||
|
>
|
||||||
|
|
||||||
|
> **Methodische Grundlage:** UBA-Leitfaden „Verständliche monatliche Heizinformation als Schlüssel zur Verbrauchsreduktion" (CLIMATE CHANGE 69/2021), Umweltbundesamt 2021.
|
||||||
|
>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 1. Goal State
|
||||||
|
|
||||||
|
Ein Mieter kann sich jederzeit in das Dashboard seiner Wohneinheit einloggen und seine monatliche Heizinformation einsehen, gestaltet **vollständig nach dem UBA-Leitfaden**. Die Heizinformation umfasst sechs Module:
|
||||||
|
|
||||||
|
1. **Verbrauchsentwicklung** für Heizung und Warmwasser mit 13-Monats-Verlauf, Tendenz-Indikator gegenüber Vorjahresmonat und Witterungs-Kontext (Heizgradtage)
|
||||||
|
2. **In-House-Vergleich** des spezifischen Verbrauchs (kWh/m² Gebäudenutzfläche) mit anderen Wohneinheiten desselben Hauses
|
||||||
|
3. **Bandtacho-Einordnung** des Hauses in die Gebäudeeffizienzklassen A⁺ bis H
|
||||||
|
4. **Spartipp des Monats** für Heizen (in der Heizperiode) bzw. Warmwasser (außerhalb)
|
||||||
|
5. **Kostenschätzung** für den aktuellen Monat und Jahr-zu-Datum
|
||||||
|
6. **CO2-Emissionen** des Haushalts mit alltagsnahem Vergleichswert
|
||||||
|
|
||||||
|
Die Heizinformation erfüllt § 6a Abs. 2 HeizkostenV. Der gesetzlich geforderte Durchschnittsnutzer-Vergleich (§ 6a Abs. 2 Nr. 3) wird durch die Kombination aus Modul 2 und Modul 3 abgedeckt — wie vom UBA-Leitfaden empfohlen.
|
||||||
|
|
||||||
|
## In Scope
|
||||||
|
|
||||||
|
- Alle sechs Module gemäß UBA-Leitfaden
|
||||||
|
- Mieter-Dashboard mit Magic-Link-Login pro Wohneinheit
|
||||||
|
- Datenempfang von Gateways
|
||||||
|
- Read-Only Admin-Dashboard, gruppiert nach Gebäudeadresse
|
||||||
|
- Unterscheidung Heizperiode / außerhalb Heizperiode
|
||||||
|
- Integration externer Referenzdaten: DWD Heizgradtage, GEG-Anlagen 9 und 10
|
||||||
|
|
||||||
|
## Out of Scope (für UVI)
|
||||||
|
|
||||||
|
- Jährliche Heizkostenabrechnung (eigenes Epic)
|
||||||
|
- Vermieter-/Hausverwaltungs-Sicht
|
||||||
|
- Witterungsbereinigung der Verbräuche (gehört zur Jahresabrechnung)
|
||||||
|
- Self-Service-UI für Geräte-zu-Wohneinheit-Zuordnung (post-Pilot)
|
||||||
|
- API-Keys pro Wohneinheit (Future)
|
||||||
|
- Optionale UBA-Leitfaden-Erweiterungen: Lage der Wohnung, energetischer Zustand, Pro-Kopf-Vergleichswerte, tägliche Verbrauchsinformationen, Gamification
|
||||||
|
- „Energieanalyse aus dem Verbrauch" (DIN TS 12831-1, DIN V 18599)
|
||||||
|
|
||||||
|
## Phasenmodell
|
||||||
|
|
||||||
|
| Phase | Geräte-Zuordnung | Admin-Dashboard | Mieter-Dashboard |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| **Pilot** | Manuell direkt in der Datenbank | Read-Only Übersicht aller Wohneinheiten, gruppiert nach Gebäudeadresse | Vollständig (alle 6 Module) |
|
||||||
|
| **Post-Pilot** | UI/Mechanismus *(TBD)* | Erweitert um Zuordnungs-Funktionen | Vollständig + ggf. optionale Erweiterungen |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 2. Anforderungen
|
||||||
|
|
||||||
|
## 2.1 Compliance — § 6a HeizkostenV
|
||||||
|
|
||||||
|
| ID | Anforderung | Quelle | Erfüllung |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| UVI-COMP-01 | Verbrauch des Mieters im letzten Monat in kWh wird bereitgestellt | § 6a Abs. 2 Nr. 1 | Modul 1 |
|
||||||
|
| UVI-COMP-02 | Vergleich mit dem Verbrauch des Vormonats desselben Mieters | § 6a Abs. 2 Nr. 2 | Modul 1 (visuell im 13-Monats-Verlauf) |
|
||||||
|
| UVI-COMP-03 | Vergleich mit dem entsprechenden Monat des Vorjahres desselben Mieters | § 6a Abs. 2 Nr. 2 | Modul 1 (Tendenz-Indikator + Witterungskontext) |
|
||||||
|
| UVI-COMP-04 | Vergleich mit Durchschnittsnutzer derselben Nutzerkategorie | § 6a Abs. 2 Nr. 3 | Modul 2 (In-House) + Modul 3 (Bandtacho) |
|
||||||
|
| UVI-COMP-05 | Bereitstellung mindestens monatlich | § 6a Abs. 1 Nr. 2 | — |
|
||||||
|
|
||||||
|
## 2.2 Funktional
|
||||||
|
|
||||||
|
Spalte „Modul" referenziert die Module des UBA-Leitfadens (1–6).
|
||||||
|
|
||||||
|
| ID | Anforderung | Modul |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| UVI-FUN-01 | Empfang von Verbrauchsdaten von Gateways | — |
|
||||||
|
| UVI-FUN-02 | *(Platzhalter)* Geräte-zu-Wohneinheit-Zuordnung — Pilot manuell in DB; Mechanismus post-Pilot | — |
|
||||||
|
| UVI-FUN-03 | Mieter-Login pro Wohneinheit (Magic Link) | — |
|
||||||
|
| UVI-FUN-04 | Anzeige des aktuellen Monatsverbrauchs in kWh, getrennt für Heizung und Warmwasser. **Anteil ist nicht nach verbrauchs-/flächenabhängigem Schlüssel gewichtet** (Unterschied zur Jahresabrechnung). | 1 |
|
||||||
|
| UVI-FUN-05 | Verlaufsdiagramm der monatlichen Verbräuche der letzten 13 Monate, getrennt für Heizung und Warmwasser; Vorjahresmonat zusätzlich beziffert | 1 |
|
||||||
|
| UVI-FUN-06 | Tendenz-Indikator zum Vorjahresmonat: gesunken (grüner Pfeil), gestiegen (roter Pfeil), gleich (grauer Pfeil). „Gleich" wenn auf Zehnerstelle gerundete kWh identisch sind. | 1 |
|
||||||
|
| UVI-FUN-07 | Witterungs-Kontext als Fußnote: Heizgradtage-Differenz Vorjahresmonat ↔ aktueller Monat in % | 1 |
|
||||||
|
| UVI-FUN-08 | In-House-Vergleich: Sparpotenzial-Pfeil zeigt Spektrum der spezifischen Gesamtverbräuche (Heizung+Warmwasser) aller Wohneinheiten des Hauses in kWh/m² Gebäudenutzfläche | 2 |
|
||||||
|
| UVI-FUN-09 | Bei kleinen Häusern (≤ 4 Wohneinheiten) wird zur Wahrung der Anonymität statt des Einzelwerts der Mittelwert der drei sparsamsten Haushalte ausgewiesen | 2 |
|
||||||
|
| UVI-FUN-10 | Bandtacho-Einordnung des Hauses in Effizienzklassen A⁺ bis H, basierend auf monatlichen Schwellenwerten (Tabelle 1 UBA-Leitfaden, abgeleitet aus Anlage 10 GEG) | 3 |
|
||||||
|
| UVI-FUN-11 | Anzeige eines Spartipps des Monats (Heizen/Lüften in der Heizperiode, Warmwasser außerhalb), inkl. Link zu weiterführenden Beratungsangeboten | 4 |
|
||||||
|
| UVI-FUN-12 | Kostenschätzung für aktuellen Monat und Jahr-zu-Datum, berechnet aus Endenergieverbrauch × aktueller Energiepreis pro Brennstoffart | 5 |
|
||||||
|
| UVI-FUN-13 | Anzeige der monatlichen CO2-Emissionen in kg, berechnet mit Emissionsfaktor aus Anlage 9 GEG; ergänzt um alltagsnahen Vergleichswert | 6 |
|
||||||
|
| UVI-FUN-14 | Außerhalb der Heizperiode: Bandtacho entfällt; Sparpotenzial-Pfeil zeigt nur Warmwasser-Spektrum (rot→blau); Heizungs-Verlaufsdiagramm bleibt sichtbar mit aktuellem Wert 0 kWh | — |
|
||||||
|
| UVI-FUN-15 | Fehlende Daten werden im UI klar als „nicht verfügbar" gekennzeichnet (keine stille 0) | — |
|
||||||
|
| UVI-FUN-16 | Verbrauchsdaten der letzten 13 Monate sind im Dashboard einsehbar | — |
|
||||||
|
| UVI-FUN-17 | Admin-Dashboard zeigt UVI-Daten aller Wohneinheiten als Read-Only-Übersicht, gruppiert nach Gebäudeadresse | — |
|
||||||
|
| UVI-FUN-18 | Admin-Login getrennt vom Mieter-Login (Magic Link) | — |
|
||||||
|
|
||||||
|
## 2.3 Nicht-funktional
|
||||||
|
|
||||||
|
| ID | Anforderung |
|
||||||
|
| --- | --- |
|
||||||
|
| UVI-NFR-01 | Datenresolution: mindestens monatlich |
|
||||||
|
| UVI-NFR-02 | DSGVO-konforme Speicherung und Verarbeitung |
|
||||||
|
| UVI-NFR-03 | Strikte Mandantentrennung: kein wohneinheitsübergreifender Datenzugriff für Mieter |
|
||||||
|
| UVI-NFR-04 | Datenübertragung von Gateway zum System verschlüsselt (TLS) |
|
||||||
|
| UVI-NFR-05 | Daten der UVI sind spätestens am 5. Werktag des Folgemonats im Dashboard sichtbar |
|
||||||
|
| UVI-NFR-06 | Verfügbarkeit Dashboard: *(zu definieren – Vorschlag: 99 % im Monatsmittel)* |
|
||||||
|
| UVI-NFR-07 | Admin-Authentifizierung getrennt vom Mieter-Login; Admin-Zugriff protokolliert |
|
||||||
|
| UVI-NFR-08 | Authentifizierung erfolgt für Mieter und Admin **ausschließlich via Magic Link** (passwortlos) |
|
||||||
|
| UVI-NFR-09 | Anonymitätsschutz im In-House-Vergleich (siehe UVI-FUN-09) |
|
||||||
|
|
||||||
|
## 2.4 Daten / Domäne
|
||||||
|
|
||||||
|
| ID | Anforderung |
|
||||||
|
| --- | --- |
|
||||||
|
| UVI-DAT-01 | Entität *Wohneinheit* mit eindeutiger ID und **Wohnfläche in m²** |
|
||||||
|
| UVI-DAT-02 | *(Platzhalter)* Entität *Gateway/Sensor* und Verknüpfung zu Wohneinheit — Granularität noch offen |
|
||||||
|
| UVI-DAT-03 | Entität *Messwert* mit Zeitstempel, Wert in kWh, Typ (Heizung/Warmwasser), Geräte-Referenz |
|
||||||
|
| UVI-DAT-04 | Entität *Mieter* mit Zuordnung zu Wohneinheit; Nutzungs-Zeiträume zur Erkennung von Mieterwechseln |
|
||||||
|
| UVI-DAT-05 | Entität *Gebäude* mit Adresse, **Brennstoffart**, optional Faktor Wohnfläche → Gebäudenutzfläche (Default 1,2) |
|
||||||
|
| UVI-DAT-06 | Aggregate werden **on-the-fly aus Roh-Messwerten berechnet**; keine Persistierung von Aggregaten |
|
||||||
|
| UVI-DAT-07 | Referenzdaten: **Bandtacho-Schwellenwerte** der Gebäudeeffizienzklassen aus Anlage 10 GEG, monatlich aufgeteilt gemäß Tabelle 1 UBA-Leitfaden |
|
||||||
|
| UVI-DAT-08 | Referenzdaten: **CO2-Emissionsfaktoren** pro Brennstoffart aus Anlage 9 GEG |
|
||||||
|
| UVI-DAT-09 | Referenzdaten: **Energiepreise** pro Brennstoffart, regelmäßig aktualisierbar |
|
||||||
|
| UVI-DAT-10 | Referenzdaten: **Heizgradtage** pro Monat aus DWD Open Data nach VDI 3807 |
|
||||||
|
| UVI-DAT-11 | **Spartipps-Bibliothek**: kategorisiert nach Heizen / Warmwasser, mit optionalen Links |
|
||||||
|
|
||||||
|
## 2.5 Methodische Festlegungen
|
||||||
|
|
||||||
|
| ID | Anforderung |
|
||||||
|
| --- | --- |
|
||||||
|
| UVI-MET-01 | Die UVI folgt dem **UBA-Leitfaden** (CLIMATE CHANGE 69/2021) als kanonische Vorlage. Abweichungen werden explizit begründet. |
|
||||||
|
| UVI-MET-02 | Der Durchschnittsnutzer-Vergleich gemäß § 6a Abs. 2 Nr. 3 wird durch **Kombination aus In-House-Vergleich (Modul 2) und Bandtacho-Einordnung (Modul 3)** erfüllt. Eine externe Vergleichswertquelle (z.B. BMWK-Bekanntmachung) wird **nicht** zusätzlich verwendet. |
|
||||||
|
| UVI-MET-03 | Spezifische Verbräuche werden auf die **Gebäudenutzfläche** bezogen (= Wohnfläche × 1,2, falls keine direkte Angabe vorliegt). |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 3. Mapping Notion ↔ Tickets
|
||||||
|
|
||||||
|
- Anforderungen (UVI-COMP-*, UVI-FUN-*, UVI-NFR-*, UVI-DAT-*, UVI-MET-*) bleiben Single Source of Truth in Notion**.
|
||||||
|
- User Stories werden in Linear angelegt; Stories enthalten in der Beschreibung die `Erfüllt:`-Zeile als Backlink in Notion.
|
||||||
|
- Definition of Done eines Tickets = die Akzeptanzkriterien der Story.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 4. Offene Punkte (zu klären)
|
||||||
|
|
||||||
|
- **Geräte-zu-Wohneinheit-Zuordnung (post-Pilot):** Granularität, Pflege, UI vs. Bulk-Import, Validierung. Pilot manuell in DB.
|
||||||
|
- **Energiepreis-Pflege (Modul 5):** Wer pflegt die Preise, in welcher Frequenz, aus welcher Quelle?
|
||||||
|
- **Mieter-Mail-Erfassung:** Wer hinterlegt die E-Mail-Adresse des Mieters?
|
||||||
|
- **Spartipps-Quelle:** Eigene Kuratierung vs. Verlinken auf [verbraucherzentrale.de](http://verbraucherzentrale.de) / co2online?
|
||||||
|
- **Datenaufbewahrung:** Wie lange werden Roh-Messwerte gespeichert?
|
||||||
|
- **Wetterstationszuordnung pro Gebäude:** Automatisch nach Postleitzahl oder manuell?
|
||||||
|
- **Verfügbarkeits-SLA:** Konkreter Wert für UVI-NFR-06.
|
||||||
|
- **Sprache/Mehrsprachigkeit:** Nur DE oder auch EN?
|
||||||
|
- **Brennstoff-Mix bei Fernwärme:** § 6a Abs. 3 — gehört zur Jahresabrechnung, könnte aber Datenmodell beeinflussen.
|
||||||
|
|
||||||
|
## Geklärt seit Beginn
|
||||||
|
|
||||||
|
- ✅ Authentifizierung: Magic Link (passwortlos) für Mieter und Admin
|
||||||
|
- ✅ Aggregation: on-the-fly, keine Persistierung von Aggregaten
|
||||||
|
- ✅ Methodik: Vollständig nach UBA-Leitfaden — alle 6 Module im Pilot
|
||||||
|
- ✅ Durchschnittsnutzer-Vergleich: über In-House + Bandtacho (nicht über externe Vergleichswert-Datenbank)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Anhang A — Methodische Grundlage
|
||||||
|
|
||||||
|
## A.1 Warum UBA-Leitfaden statt BMWK-Bekanntmachung?
|
||||||
|
|
||||||
|
Eine frühere Version dieses Dokuments hatte die BMWK-Bekanntmachung „Regeln für Energieverbrauchswerte im Wohngebäudebestand" (BAnz AT 16.04.2021 B1) als Quelle vorgesehen. Diese Entscheidung wurde nach Auswertung des UBA-Leitfadens revidiert.
|
||||||
|
|
||||||
|
Begründung:
|
||||||
|
|
||||||
|
1. **Zweck der Quellen unterscheidet sich:** Die BMWK-Bekanntmachung ist für **Energieausweise nach GEG** konzipiert. Der UBA-Leitfaden adressiert exakt unseren Use Case: **monatliche Heizinformation** nach § 6a HeizkostenV.
|
||||||
|
2. **Praktische Argumentation des UBA-Leitfadens (S. 23):** Ein gebäudeübergreifender Vergleich anonymisierter Verbräuche setzt voraus, dass die Gebäude einen ähnlichen energetischen Standard haben. Diese Information liegt Messdienstleistern typischerweise nicht vor.
|
||||||
|
3. **Der Leitfaden liefert konkrete Vergleichswerte** (Tabelle 1, S. 26) und ein vollständiges Gestaltungsmuster.
|
||||||
|
4. **Methodischer Ansatz:** § 6a Abs. 2 Nr. 3 wird erfüllt durch In-House-Vergleich (Modul 2) + Bandtacho (Modul 3).
|
||||||
|
|
||||||
|
## A.2 Edge Case: Mieterwechsel
|
||||||
|
|
||||||
|
Bei einem Mieterwechsel ist der Vergleich zum Vormonat oder Vorjahresmonat nach § 6a Abs. 2 Nr. 2 nicht möglich. In diesem Fall bleibt nur Modul 2 als verpflichtende Information. Datenmodell: Nutzungs-Zeiträume pro Wohneinheit müssen erfasst sein (UVI-DAT-04).
|
||||||
|
|
||||||
|
## A.3 Sanktion bei Nicht-Erfüllung
|
||||||
|
|
||||||
|
§ 7 HeizkostenV: Wird die UVI nicht oder unvollständig mitgeteilt, hat der Mieter ein Kürzungsrecht von **3 %** auf den Kostenanteil.
|
||||||
|
|
||||||
|
## A.4 Quellen
|
||||||
|
|
||||||
|
- **UBA-Leitfaden:** „Verständliche monatliche Heizinformation als Schlüssel zur Verbrauchsreduktion" (CLIMATE CHANGE 69/2021), Umweltbundesamt 2021, Brischke et al. (ifeu)
|
||||||
|
- **HeizkostenV § 6a** — Inhalte der UVI
|
||||||
|
- **HeizkostenV § 7** — Kürzungsrecht
|
||||||
|
- **EU-Richtlinie 2012/27/EU** Anhang VIIa, in der Fassung der RL 2018/2002 (EED)
|
||||||
|
- **GEG Anlage 9** — CO2-Emissionsfaktoren
|
||||||
|
- **GEG Anlage 10** — Schwellenwerte der Gebäudeeffizienzklassen
|
||||||
|
- **VDI 3807** — Heizgradtage und Verbrauchskennwerte (Backup-Referenz)
|
||||||
|
- **DWD Open Data** — [opendata.dwd.de/.../hdd_3807/recent/](http://opendata.dwd.de/.../hdd_3807/recent/)
|
||||||
|
- **UBA-CO2-Rechner** — [uba.co2-rechner.de](http://uba.co2-rechner.de)
|
||||||
@@ -7,7 +7,7 @@ import { createClient } from "@supabase/supabase-js";
|
|||||||
// and VITE_SUPABASE_ANON_KEY set at `nix build .#frontend` time.
|
// and VITE_SUPABASE_ANON_KEY set at `nix build .#frontend` time.
|
||||||
const DEV_SUPABASE_URL = "http://127.0.0.1:8000";
|
const DEV_SUPABASE_URL = "http://127.0.0.1:8000";
|
||||||
const DEV_SUPABASE_ANON_KEY =
|
const DEV_SUPABASE_ANON_KEY =
|
||||||
"dev-anon-key-placeholder-regenerate-with-supabase-self-host-script";
|
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlLWRldiIsImlhdCI6MTc1MTMyODAwMCwiZXhwIjoyMDgyNzU4NDAwfQ.M6U-nZq9dySoUJEfWHsiB0qabhATWobGaTM1LF86HHU";
|
||||||
|
|
||||||
const SUPABASE_URL =
|
const SUPABASE_URL =
|
||||||
import.meta.env.VITE_SUPABASE_URL ??
|
import.meta.env.VITE_SUPABASE_URL ??
|
||||||
|
|||||||
+189
-9
@@ -15,17 +15,25 @@ let
|
|||||||
pgPassword = "dev";
|
pgPassword = "dev";
|
||||||
pgHost = "127.0.0.1";
|
pgHost = "127.0.0.1";
|
||||||
pgPort = 5432;
|
pgPort = 5432;
|
||||||
|
# Unix socket dir, bind-mounted into the compose network's `db` proxy
|
||||||
|
# (see composeDevOverride below). Relative to the process-compose cwd,
|
||||||
|
# like services-flake's ./data/db data dir.
|
||||||
|
pgSocketDir = "./data/db-socket";
|
||||||
|
|
||||||
mqttHost = "127.0.0.1";
|
mqttHost = "127.0.0.1";
|
||||||
mqttPort = 1883;
|
mqttPort = 1883;
|
||||||
|
|
||||||
# Pre-generated dev JWT pair, signed with `jwt_secret = "dev-jwt-secret-32chars-minimum-xxxxx"`.
|
# Pre-generated dev JWT pair, signed HS256 with jwtSecret. Regenerate
|
||||||
# Anyone with this token can read/write the local dev stack only — not prod.
|
# with any JWT tool if jwtSecret ever changes; payload is
|
||||||
|
# {"role":"<anon|service_role>","iss":"supabase-dev","iat":1751328000,"exp":2082758400}.
|
||||||
|
# Anyone with these tokens can read/write the local dev stack only — not prod.
|
||||||
jwtSecret = "dev-jwt-secret-32chars-minimum-xxxxx";
|
jwtSecret = "dev-jwt-secret-32chars-minimum-xxxxx";
|
||||||
anonKey = "dev-anon-key-placeholder-regenerate-with-supabase-self-host-script";
|
anonKey = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlLWRldiIsImlhdCI6MTc1MTMyODAwMCwiZXhwIjoyMDgyNzU4NDAwfQ.M6U-nZq9dySoUJEfWHsiB0qabhATWobGaTM1LF86HHU";
|
||||||
serviceRoleKey = "dev-service-role-key-placeholder-regenerate-with-supabase-self-host-script";
|
serviceRoleKey = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoic2VydmljZV9yb2xlIiwiaXNzIjoic3VwYWJhc2UtZGV2IiwiaWF0IjoxNzUxMzI4MDAwLCJleHAiOjIwODI3NTg0MDB9.dNeWNshQ9e1pIjxTDYseURlbSPPArVIKa9OuTRZyIH8";
|
||||||
|
|
||||||
supabaseUrl = "http://127.0.0.1:8000";
|
supabaseUrl = "http://127.0.0.1:8000";
|
||||||
|
|
||||||
|
frontendPort = 5173; # vite default
|
||||||
};
|
};
|
||||||
|
|
||||||
ingesterEnv = {
|
ingesterEnv = {
|
||||||
@@ -45,6 +53,133 @@ let
|
|||||||
listener ${toString dev.mqttPort} ${dev.mqttHost}
|
listener ${toString dev.mqttPort} ${dev.mqttHost}
|
||||||
allow_anonymous true
|
allow_anonymous true
|
||||||
'';
|
'';
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Supabase — the vendored prod compose bundle (nix/supabase/), unchanged,
|
||||||
|
# plus this dev overlay.
|
||||||
|
#
|
||||||
|
# The overlay's job is wiring the containers to the services-flake postgres.
|
||||||
|
# Bridge→host TCP is a dead end on a typical NixOS dev machine (the host
|
||||||
|
# firewall's INPUT chain rejects it), so instead a socat `db` service
|
||||||
|
# forwards TCP 5432 inside the compose network onto the host postgres unix
|
||||||
|
# socket, bind-mounted from pgSocketDir. Conveniently that restores the
|
||||||
|
# upstream compose convention of a service literally named `db`, so the
|
||||||
|
# vendored file works with just POSTGRES_HOST=db. Socket connections match
|
||||||
|
# pg_hba's `local … trust`, so the dev password is never actually checked.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
composeDevOverride = pkgs.writeText "docker-compose.dev-override.yml" ''
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
container_name: supabase-dev-db
|
||||||
|
# Unpinned minor: dev-only TCP→unix-socket shim, nothing depends on
|
||||||
|
# socat internals.
|
||||||
|
image: alpine/socat:1.8.0.3
|
||||||
|
restart: unless-stopped
|
||||||
|
command: TCP-LISTEN:${toString dev.pgPort},fork,reuseaddr UNIX-CONNECT:/host-postgres/.s.PGSQL.${toString dev.pgPort}
|
||||||
|
volumes:
|
||||||
|
- ''${PG_SOCKET_DIR:?set by gebos-dev-supabase-compose}:/host-postgres
|
||||||
|
|
||||||
|
studio:
|
||||||
|
environment:
|
||||||
|
SUPABASE_PUBLIC_URL: ${dev.supabaseUrl}
|
||||||
|
|
||||||
|
auth:
|
||||||
|
environment:
|
||||||
|
API_EXTERNAL_URL: ${dev.supabaseUrl}
|
||||||
|
GOTRUE_SITE_URL: http://localhost:${toString dev.frontendPort}
|
||||||
|
GOTRUE_JWT_ISSUER: ${dev.supabaseUrl}
|
||||||
|
# No SMTP locally, so magic-link invites can't send. Allow plain
|
||||||
|
# self-signup with auto-confirm instead; prod keeps signup disabled.
|
||||||
|
GOTRUE_DISABLE_SIGNUP: "false"
|
||||||
|
GOTRUE_MAILER_AUTOCONFIRM: "true"
|
||||||
|
'';
|
||||||
|
|
||||||
|
# One wrapper for `up`/`down` so both resolve the socket-dir bind mount the
|
||||||
|
# same way (compose needs an absolute path).
|
||||||
|
supabaseCompose = pkgs.writeShellApplication {
|
||||||
|
name = "gebos-dev-supabase-compose";
|
||||||
|
runtimeInputs = [ pkgs.docker pkgs.coreutils ];
|
||||||
|
text = ''
|
||||||
|
PG_SOCKET_DIR="$(readlink -f ${dev.pgSocketDir})"
|
||||||
|
export PG_SOCKET_DIR
|
||||||
|
exec docker compose \
|
||||||
|
--project-name gebos-supabase-dev \
|
||||||
|
-f nix/supabase/docker-compose.yml \
|
||||||
|
-f ${composeDevOverride} \
|
||||||
|
"$@"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
supabaseEnv = {
|
||||||
|
POSTGRES_HOST = "db"; # the socat proxy above
|
||||||
|
POSTGRES_PORT = toString dev.pgPort;
|
||||||
|
POSTGRES_DB = dev.pgDB;
|
||||||
|
POSTGRES_PASSWORD = dev.pgPassword; # unchecked — see overlay comment
|
||||||
|
JWT_SECRET = dev.jwtSecret;
|
||||||
|
ANON_KEY = dev.anonKey;
|
||||||
|
SERVICE_ROLE_KEY = dev.serviceRoleKey;
|
||||||
|
STUDIO_BIND = "127.0.0.1";
|
||||||
|
};
|
||||||
|
|
||||||
|
# Everything prod splits across nix/supabase/init.sql (first initdb),
|
||||||
|
# the gebos-postgres-passwords oneshot, and a manual `supabase db push`,
|
||||||
|
# collapsed into one idempotent script that runs on every stack start.
|
||||||
|
dbMigrate = pkgs.writeShellApplication {
|
||||||
|
name = "gebos-dev-db-migrate";
|
||||||
|
runtimeInputs = [ pkgs.supabase-cli pkgs.postgresql_17 ];
|
||||||
|
text = ''
|
||||||
|
export PGHOST=${dev.pgHost} PGPORT=${toString dev.pgPort} PGDATABASE=${dev.pgDB} PGSSLMODE=disable
|
||||||
|
|
||||||
|
# 1. Cluster bootstrap: Supabase roles/schemas the compose services
|
||||||
|
# expect (mirrors nix/supabase/init.sql, but idempotent because the
|
||||||
|
# dev data dir may predate this script). Password 'dev' everywhere.
|
||||||
|
psql -v ON_ERROR_STOP=1 --quiet <<'SQL'
|
||||||
|
DO $$ BEGIN CREATE ROLE anon NOLOGIN NOINHERIT; EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN CREATE ROLE authenticated NOLOGIN NOINHERIT; EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN CREATE ROLE service_role NOLOGIN NOINHERIT BYPASSRLS; EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN CREATE ROLE authenticator LOGIN NOINHERIT; EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
GRANT anon, authenticated, service_role TO authenticator;
|
||||||
|
DO $$ BEGIN CREATE ROLE supabase_admin LOGIN CREATEROLE CREATEDB BYPASSRLS; EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
DO $$ BEGIN CREATE ROLE supabase_auth_admin LOGIN CREATEROLE; EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
-- On db-host the NixOS postgresql module creates the `postgres`
|
||||||
|
-- superuser; the services-flake cluster's superuser is the OS user
|
||||||
|
-- instead, and GoTrue's own migrations GRANT to `postgres` by name.
|
||||||
|
DO $$ BEGIN CREATE ROLE postgres LOGIN SUPERUSER; EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
|
|
||||||
|
ALTER ROLE postgres PASSWORD 'dev';
|
||||||
|
ALTER ROLE supabase_admin SUPERUSER PASSWORD 'dev';
|
||||||
|
ALTER ROLE supabase_auth_admin PASSWORD 'dev';
|
||||||
|
ALTER ROLE authenticator PASSWORD 'dev';
|
||||||
|
|
||||||
|
-- GoTrue owns `auth` and runs its own migrations there on startup. Its
|
||||||
|
-- migrator creates tables in the role's default schema, so without the
|
||||||
|
-- search_path it lands in `public` and dies on PG15+'s revoked CREATE
|
||||||
|
-- (upstream's supabase/postgres image sets the same role search_path).
|
||||||
|
CREATE SCHEMA IF NOT EXISTS auth;
|
||||||
|
ALTER SCHEMA auth OWNER TO supabase_auth_admin;
|
||||||
|
ALTER ROLE supabase_auth_admin SET search_path TO auth, extensions;
|
||||||
|
|
||||||
|
-- The upstream supabase/postgres image keeps shared extensions in an
|
||||||
|
-- `extensions` schema on the search path; the services assume that.
|
||||||
|
CREATE SCHEMA IF NOT EXISTS extensions;
|
||||||
|
CREATE EXTENSION IF NOT EXISTS pgcrypto WITH SCHEMA extensions;
|
||||||
|
CREATE EXTENSION IF NOT EXISTS "uuid-ossp" WITH SCHEMA extensions;
|
||||||
|
GRANT USAGE ON SCHEMA extensions TO anon, authenticated, service_role, authenticator, supabase_auth_admin;
|
||||||
|
ALTER DATABASE gebos SET search_path TO "$user", public, extensions;
|
||||||
|
SQL
|
||||||
|
|
||||||
|
# 2. Schema migrations — same mechanism as prod (supabase/README.md),
|
||||||
|
# non-interactive, as the local superuser ($USER).
|
||||||
|
supabase db push --yes --db-url "postgres://$USER@${dev.pgHost}:${toString dev.pgPort}/${dev.pgDB}"
|
||||||
|
|
||||||
|
# 3. The schema-v1 migration created gebos_ingest; in prod its password
|
||||||
|
# comes from the gebos-postgres-passwords oneshot.
|
||||||
|
psql -v ON_ERROR_STOP=1 --quiet -c "ALTER ROLE ${dev.pgUser} PASSWORD '${dev.pgPassword}'"
|
||||||
|
|
||||||
|
# 4. Fixture data — idempotent, dev/tests only (supabase/README.md).
|
||||||
|
psql -v ON_ERROR_STOP=1 --quiet -f supabase/seed.sql
|
||||||
|
'';
|
||||||
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
imports = [ services-flake.processComposeModules.default ];
|
imports = [ services-flake.processComposeModules.default ];
|
||||||
@@ -53,7 +188,17 @@ in
|
|||||||
postgres."db" = {
|
postgres."db" = {
|
||||||
enable = true;
|
enable = true;
|
||||||
port = dev.pgPort;
|
port = dev.pgPort;
|
||||||
|
# TCP stays loopback-only; the compose containers come in through the
|
||||||
|
# unix socket instead (see composeDevOverride).
|
||||||
|
socketDir = dev.pgSocketDir;
|
||||||
initialDatabases = [{ name = dev.pgDB; }];
|
initialDatabases = [{ name = dev.pgDB; }];
|
||||||
|
# The schema-v1 migration calls create_hypertable; without the
|
||||||
|
# extension `supabase db push` rolls back entirely on the dev db.
|
||||||
|
# The apache variant lacks only TSL features (compression, CAggs
|
||||||
|
# policies) — hypertables are enough here, and it keeps dev free
|
||||||
|
# of unfree packages.
|
||||||
|
extensions = extensions: [ extensions.timescaledb-apache ];
|
||||||
|
settings.shared_preload_libraries = "timescaledb";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -67,20 +212,55 @@ in
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# Oneshot: bootstrap roles/schemas, apply migrations, seed. Idempotent,
|
||||||
|
# so it simply re-runs on every stack start.
|
||||||
|
db-migrate = {
|
||||||
|
command = "${dbMigrate}/bin/gebos-dev-db-migrate";
|
||||||
|
depends_on."db".condition = "process_healthy";
|
||||||
|
};
|
||||||
|
|
||||||
|
# Auth (GoTrue) + REST (PostgREST) + Studio + postgres-meta behind Kong
|
||||||
|
# on ${dev.supabaseUrl} — the vendored prod bundle, see composeDevOverride.
|
||||||
|
# Needs a running docker daemon on the dev machine.
|
||||||
|
supabase = {
|
||||||
|
command = "${supabaseCompose}/bin/gebos-dev-supabase-compose up --remove-orphans";
|
||||||
|
environment = supabaseEnv;
|
||||||
|
# `restart: unless-stopped` containers outlive a hard-killed
|
||||||
|
# process-compose; an explicit `down` covers the clean path.
|
||||||
|
shutdown.command = "${supabaseCompose}/bin/gebos-dev-supabase-compose down --remove-orphans";
|
||||||
|
depends_on = {
|
||||||
|
# GoTrue's own migrations need the auth schema + roles from db-migrate.
|
||||||
|
"db".condition = "process_healthy";
|
||||||
|
"db-migrate".condition = "process_completed_successfully";
|
||||||
|
};
|
||||||
|
readiness_probe = {
|
||||||
|
# Kong → key-auth → GoTrue → Postgres: healthy = the whole API path works.
|
||||||
|
exec.command = "${pkgs.curl}/bin/curl -fsS -H 'apikey: ${dev.anonKey}' ${dev.supabaseUrl}/auth/v1/health";
|
||||||
|
initial_delay_seconds = 5;
|
||||||
|
period_seconds = 5;
|
||||||
|
failure_threshold = 60; # first start pulls the images
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
ingester = {
|
ingester = {
|
||||||
command = "${pkgs.go}/bin/go run ./ingester/cmd/gebos-ingester";
|
# -C: the go module root is ingester/, not the repo root.
|
||||||
|
command = "${pkgs.go}/bin/go run -C ingester ./cmd/gebos-ingester";
|
||||||
environment = ingesterEnv;
|
environment = ingesterEnv;
|
||||||
depends_on = {
|
depends_on = {
|
||||||
"db".condition = "process_healthy";
|
# db-migrate (implies db healthy) creates the gebos_ingest role and
|
||||||
|
# the telemetry tables.
|
||||||
|
"db-migrate".condition = "process_completed_successfully";
|
||||||
"broker".condition = "process_started";
|
"broker".condition = "process_started";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
frontend = {
|
frontend = {
|
||||||
command = "${pkgs.nodejs_24}/bin/npm --prefix frontend install && ${pkgs.nodejs_24}/bin/npm --prefix frontend run dev";
|
command = "${pkgs.nodejs_24}/bin/npm --prefix frontend install && ${pkgs.nodejs_24}/bin/npm --prefix frontend run dev";
|
||||||
environment = frontendEnv;
|
environment = frontendEnv;
|
||||||
};
|
};
|
||||||
# TODO: supabase compose (point POSTGRES_HOST at the services-flake db)
|
|
||||||
# TODO: kong (use the vendored kong.yml from nix/supabase/)
|
# No caddy locally: prod's Caddy config is generated by the gebos-caddy
|
||||||
# TODO: caddy (mirror prod routes locally, no TLS)
|
# NixOS module, so a hand-mirrored dev caddyfile wouldn't test it — the
|
||||||
|
# frontend talks to Kong directly (dev.supabaseUrl).
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
v2.109.1
|
||||||
+34
-5
@@ -44,12 +44,41 @@ Write plain SQL. Conventions:
|
|||||||
|
|
||||||
## Local dev
|
## Local dev
|
||||||
|
|
||||||
`nix run .#dev` starts a plain Postgres via process-compose; apply migrations
|
`nix run .#dev` applies everything automatically: its `db-migrate` process
|
||||||
to it the same way:
|
bootstraps the Supabase roles/schemas, runs `supabase db push`, and loads
|
||||||
|
`seed.sql` on every start (all idempotent — see
|
||||||
|
`nix/dev/process-compose.nix`). To re-apply by hand against the running dev
|
||||||
|
Postgres, do it as the local superuser (your OS user — `gebos_ingest` is only
|
||||||
|
*created* by the migration and has no DDL rights). The CLI ignores `sslmode`
|
||||||
|
in the URL, so TLS must be disabled via the environment:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
supabase db push --db-url "postgres://gebos_ingest:dev@127.0.0.1:5432/gebos"
|
PGSSLMODE=disable supabase db push --db-url "postgres://$USER@127.0.0.1:5432/gebos"
|
||||||
```
|
```
|
||||||
|
|
||||||
(Note: the dev database has no TimescaleDB unless the process-compose postgres
|
(The dev postgres ships TimescaleDB — the Apache edition, which covers
|
||||||
gains the extension — `create_hypertable` calls will fail there until then.)
|
hypertables; TSL-only features like compression are absent locally.)
|
||||||
|
|
||||||
|
## Seed / fixture data (dev and tests only)
|
||||||
|
|
||||||
|
`seed.sql` fills the local database with a self-consistent fixture world:
|
||||||
|
2 buildings (gas / district heating), 8 apartments, tenants incl. a
|
||||||
|
Mieterwechsel and a vacancy, gateways, sensors (incl. one unassigned), ~13
|
||||||
|
months of daily cumulative measurements with realistic seasonality, and
|
||||||
|
approximate reference data (thresholds, CO2 factors, prices, HDD). The header
|
||||||
|
comment in the file documents every scenario and the fixed-UUID namespaces.
|
||||||
|
|
||||||
|
Apply it after the migrations, with a superuser (the seed writes tables the
|
||||||
|
`gebos_ingest` role may not):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
psql -h 127.0.0.1 -p 5432 -d gebos -f supabase/seed.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
It is idempotent (fixed UUIDs + `ON CONFLICT DO NOTHING`, deterministic
|
||||||
|
measurement generation) — re-running on a later day only appends the new
|
||||||
|
days of measurements.
|
||||||
|
|
||||||
|
**Never apply it to prod**: the tenants and devices are fake, and the
|
||||||
|
regulatory numbers are development approximations, not verified values from
|
||||||
|
the legal sources. Verified reference data belongs in its own migration.
|
||||||
|
|||||||
@@ -0,0 +1,412 @@
|
|||||||
|
-- ============================================================================
|
||||||
|
-- GebOS – Dev/test fixture data (supabase/seed.sql)
|
||||||
|
--
|
||||||
|
-- DEV ONLY. Never apply to prod: it contains fake tenants and fake devices,
|
||||||
|
-- and the regulatory reference numbers (efficiency class thresholds, CO2
|
||||||
|
-- factors, prices, HDD) are plausible APPROXIMATIONS for development, not
|
||||||
|
-- verified values from the legal sources. Real reference data ships as its
|
||||||
|
-- own migration when the numbers have been verified.
|
||||||
|
--
|
||||||
|
-- Apply with psql against the local process-compose stack (see
|
||||||
|
-- supabase/README.md), AFTER the schema migration:
|
||||||
|
--
|
||||||
|
-- psql -h 127.0.0.1 -p 5432 -d gebos -f supabase/seed.sql
|
||||||
|
--
|
||||||
|
-- Idempotent by construction: fixture rows carry fixed UUIDs (so tests can
|
||||||
|
-- reference them) and every INSERT ends in ON CONFLICT DO NOTHING. The
|
||||||
|
-- measurement series is anchored at a fixed start date (2025-06-01) and
|
||||||
|
-- generated deterministically (md5-based jitter, no random()), so re-running
|
||||||
|
-- on a later day recomputes identical values for existing days (skipped via
|
||||||
|
-- the composite PK) and appends only the new days — cumulative meter totals
|
||||||
|
-- stay monotonic across re-runs.
|
||||||
|
--
|
||||||
|
-- UUID namespaces (first group encodes the table, last digits the row):
|
||||||
|
-- dd... weather_station b0... building a0... apartment
|
||||||
|
-- e0... app_user 0c... occupancy 9e... gateway
|
||||||
|
-- d1... device_type f0... value_parser 5e... sensor
|
||||||
|
-- 7a... received_payload 5a... saving_tip
|
||||||
|
--
|
||||||
|
-- Scenario coverage (what each fixture exists to exercise):
|
||||||
|
-- * Building 1 (Berlin, gas, 5 apartments) — normal in-house spectrum.
|
||||||
|
-- * Building 2 (Potsdam, district heating, 3 apartments) — small-building
|
||||||
|
-- anonymity rule (UVI-FUN-09), second fuel type, and NO weather station
|
||||||
|
-- (weather footnote must render "not available", UVI-FUN-15).
|
||||||
|
-- * Apartment 3 — Mieterwechsel on 2026-03-01: comparisons across that
|
||||||
|
-- date must be suppressed (both months must fall in ONE occupancy row).
|
||||||
|
-- * Apartment 5 — vacant since 2026-05-01 (occupancy ended, none current).
|
||||||
|
-- * Apartment 4 — real heat meter (energy + flow/return temperatures +
|
||||||
|
-- error flags), not a heat cost allocator.
|
||||||
|
-- * Apartment 1 — hot water metered as ENERGY; all others as VOLUME
|
||||||
|
-- (both measurement kinds must work, see measurement_kind comment).
|
||||||
|
-- * Sensor 5e..17 — provisioned but unassigned (apartment_id NULL) with
|
||||||
|
-- measurements: data stored, tenant-invisible until assigned.
|
||||||
|
-- * received_payload — one row per parse_status, incl. an unknown sender.
|
||||||
|
-- * Consumption profiles span thrifty (~45 kWh/m²a) to heavy (~190),
|
||||||
|
-- so the Bandtacho classes and the in-house comparison spread out.
|
||||||
|
--
|
||||||
|
-- app_user note: in prod, app_user.id must equal auth.users.id (GoTrue).
|
||||||
|
-- The dev stack has no GoTrue yet, so these users have no auth identity —
|
||||||
|
-- when it lands, create auth users with these fixed UUIDs to log in as them.
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Reference: weather station (assigned to building 1 only — building 2
|
||||||
|
-- deliberately has none)
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
INSERT INTO weather_station (id, dwd_id, name, lat, lng) VALUES
|
||||||
|
('dd000000-0000-0000-0000-000000000001', '00433', 'Berlin-Tempelhof', 52.467500, 13.402100)
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Buildings & apartments
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
INSERT INTO building (id, street, house_number, postal_code, city, lat, lng,
|
||||||
|
fuel_type, weather_station_id, note) VALUES
|
||||||
|
('b0000000-0000-0000-0000-000000000001',
|
||||||
|
'Fichtestraße', '12', '10967', 'Berlin', 52.489200, 13.417100,
|
||||||
|
'natural_gas', 'dd000000-0000-0000-0000-000000000001',
|
||||||
|
'Schlüsselkasten am Hoftor, Code 4711. Heizungskeller: Zugang über Hof.'),
|
||||||
|
('b0000000-0000-0000-0000-000000000002',
|
||||||
|
'Gartenweg', '3', '14482', 'Potsdam', 52.390800, 13.115600,
|
||||||
|
'district_heating', NULL,
|
||||||
|
'Kleine Anlage, 3 WE. Übergabestation im Keller rechts.')
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
INSERT INTO apartment (id, building_id, label, living_area_m2) VALUES
|
||||||
|
('a0000000-0000-0000-0000-000000000001', 'b0000000-0000-0000-0000-000000000001', 'EG links', 54.00),
|
||||||
|
('a0000000-0000-0000-0000-000000000002', 'b0000000-0000-0000-0000-000000000001', 'EG rechts', 72.00),
|
||||||
|
('a0000000-0000-0000-0000-000000000003', 'b0000000-0000-0000-0000-000000000001', '1. OG links', 85.00),
|
||||||
|
('a0000000-0000-0000-0000-000000000004', 'b0000000-0000-0000-0000-000000000001', '1. OG rechts', 96.00),
|
||||||
|
('a0000000-0000-0000-0000-000000000005', 'b0000000-0000-0000-0000-000000000001', '2. OG', 110.00),
|
||||||
|
('a0000000-0000-0000-0000-000000000006', 'b0000000-0000-0000-0000-000000000002', 'WE 01', 60.00),
|
||||||
|
('a0000000-0000-0000-0000-000000000007', 'b0000000-0000-0000-0000-000000000002', 'WE 02', 75.00),
|
||||||
|
('a0000000-0000-0000-0000-000000000008', 'b0000000-0000-0000-0000-000000000002', 'WE 03', 88.00)
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Users & occupancies
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
INSERT INTO app_user (id, role, display_name) VALUES
|
||||||
|
('e0000000-0000-0000-0000-000000000001', 'admin', 'Anna Admin'),
|
||||||
|
('e0000000-0000-0000-0000-000000000002', 'tenant', 'Max Mustermann'),
|
||||||
|
('e0000000-0000-0000-0000-000000000003', 'tenant', 'Erika Beispiel'),
|
||||||
|
('e0000000-0000-0000-0000-000000000004', 'tenant', 'Otto Vormieter'),
|
||||||
|
('e0000000-0000-0000-0000-000000000005', 'tenant', 'Nina Neumieterin'),
|
||||||
|
('e0000000-0000-0000-0000-000000000006', 'tenant', 'Karl Konstant'),
|
||||||
|
('e0000000-0000-0000-0000-000000000007', 'tenant', 'Ferdinand Fortgezogen'),
|
||||||
|
('e0000000-0000-0000-0000-000000000008', 'tenant', 'Gerda Gartenweg'),
|
||||||
|
('e0000000-0000-0000-0000-000000000009', 'tenant', 'Hans Hofmann'),
|
||||||
|
('e0000000-0000-0000-0000-00000000000a', 'tenant', 'Ines Iser')
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
INSERT INTO occupancy (id, apartment_id, user_id, valid_from, valid_to) VALUES
|
||||||
|
-- building 1
|
||||||
|
('0c000000-0000-0000-0000-000000000001', 'a0000000-0000-0000-0000-000000000001', 'e0000000-0000-0000-0000-000000000002', '2023-09-01', NULL),
|
||||||
|
('0c000000-0000-0000-0000-000000000002', 'a0000000-0000-0000-0000-000000000002', 'e0000000-0000-0000-0000-000000000003', '2024-01-01', NULL),
|
||||||
|
-- Mieterwechsel in apartment 3: comparisons across 2026-03-01 are forbidden
|
||||||
|
('0c000000-0000-0000-0000-000000000003', 'a0000000-0000-0000-0000-000000000003', 'e0000000-0000-0000-0000-000000000004', '2022-05-01', '2026-03-01'),
|
||||||
|
('0c000000-0000-0000-0000-000000000004', 'a0000000-0000-0000-0000-000000000003', 'e0000000-0000-0000-0000-000000000005', '2026-03-01', NULL),
|
||||||
|
('0c000000-0000-0000-0000-000000000005', 'a0000000-0000-0000-0000-000000000004', 'e0000000-0000-0000-0000-000000000006', '2021-11-01', NULL),
|
||||||
|
-- apartment 5 vacant since 2026-05-01
|
||||||
|
('0c000000-0000-0000-0000-000000000006', 'a0000000-0000-0000-0000-000000000005', 'e0000000-0000-0000-0000-000000000007', '2023-02-01', '2026-05-01'),
|
||||||
|
-- building 2
|
||||||
|
('0c000000-0000-0000-0000-000000000007', 'a0000000-0000-0000-0000-000000000006', 'e0000000-0000-0000-0000-000000000008', '2020-01-01', NULL),
|
||||||
|
('0c000000-0000-0000-0000-000000000008', 'a0000000-0000-0000-0000-000000000007', 'e0000000-0000-0000-0000-000000000009', '2024-07-01', NULL),
|
||||||
|
('0c000000-0000-0000-0000-000000000009', 'a0000000-0000-0000-0000-000000000008', 'e0000000-0000-0000-0000-00000000000a', '2023-03-15', NULL)
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Gateways (two in building 1: broadcast radio means overlapping reception)
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
INSERT INTO gateway (id, imei, imsi, building_id, label, installation_note) VALUES
|
||||||
|
('9e000000-0000-0000-0000-000000000001', '861234050000011', '901405000000011',
|
||||||
|
'b0000000-0000-0000-0000-000000000001', 'GW Keller',
|
||||||
|
'Im Heizungskeller, über der Tür zum Anschlussraum.'),
|
||||||
|
('9e000000-0000-0000-0000-000000000002', '861234050000012', '901405000000012',
|
||||||
|
'b0000000-0000-0000-0000-000000000001', 'GW Dach',
|
||||||
|
'Trockenboden, am Kaminschacht montiert.'),
|
||||||
|
('9e000000-0000-0000-0000-000000000003', '861234050000013', '901405000000013',
|
||||||
|
'b0000000-0000-0000-0000-000000000002', 'GW Keller',
|
||||||
|
'Neben der Fernwärme-Übergabestation.')
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Device types & value parsers
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
INSERT INTO device_type (id, manufacturer, model, medium) VALUES
|
||||||
|
('d1000000-0000-0000-0000-000000000001', 'QDS', 'Q caloric 5.5', '08'), -- heat cost allocator
|
||||||
|
('d1000000-0000-0000-0000-000000000002', 'EFE', 'SensoStar U', '04'), -- heat meter
|
||||||
|
('d1000000-0000-0000-0000-000000000003', 'EFE', 'SensoStar E', '06'), -- warm-water heat meter
|
||||||
|
('d1000000-0000-0000-0000-000000000004', 'ZRI', 'Minomess', '06') -- warm-water volume meter
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
INSERT INTO value_parser (id, device_type_id, json_path, parser_instructions, kind, unit) VALUES
|
||||||
|
('f0000000-0000-0000-0000-000000000001', 'd1000000-0000-0000-0000-000000000001',
|
||||||
|
'$.records[0].value', '{"scale": 1}', 'heating_energy', 'kWh'),
|
||||||
|
('f0000000-0000-0000-0000-000000000002', 'd1000000-0000-0000-0000-000000000002',
|
||||||
|
'$.records[?(@.type=="energy")].value', '{"scale": 1}', 'heating_energy', 'kWh'),
|
||||||
|
('f0000000-0000-0000-0000-000000000003', 'd1000000-0000-0000-0000-000000000002',
|
||||||
|
'$.records[?(@.type=="flow_temp")].value', '{"scale": 1}', 'flow_temperature', '°C'),
|
||||||
|
('f0000000-0000-0000-0000-000000000004', 'd1000000-0000-0000-0000-000000000002',
|
||||||
|
'$.records[?(@.type=="return_temp")].value', '{"scale": 1}', 'return_temperature', '°C'),
|
||||||
|
('f0000000-0000-0000-0000-000000000005', 'd1000000-0000-0000-0000-000000000002',
|
||||||
|
'$.status', NULL, 'error_flags', 'flags'),
|
||||||
|
('f0000000-0000-0000-0000-000000000006', 'd1000000-0000-0000-0000-000000000003',
|
||||||
|
'$.records[?(@.type=="energy")].value', '{"scale": 1}', 'hot_water_energy', 'kWh'),
|
||||||
|
('f0000000-0000-0000-0000-000000000007', 'd1000000-0000-0000-0000-000000000004',
|
||||||
|
'$.records[0].value', '{"scale": 0.001}', 'hot_water_volume', 'm3')
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Sensors
|
||||||
|
-- 01–08: heating, apartments 1–8 (apartment 4 has the real heat meter)
|
||||||
|
-- 09–16: hot water, apartments 1–8 (apartment 1 energy, the rest volume)
|
||||||
|
-- 17: provisioned but unassigned
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
INSERT INTO sensor (id, oms_id, device_type_id, apartment_id, decrypt_key, installed_at, installation_note) VALUES
|
||||||
|
('5e000000-0000-0000-0000-000000000001', '71000001', 'd1000000-0000-0000-0000-000000000001', 'a0000000-0000-0000-0000-000000000001', '000102030405060708090a0b0c0d0e01', '2025-04-14', 'HKV Wohnzimmer, einziger Heizkörper.'),
|
||||||
|
('5e000000-0000-0000-0000-000000000002', '71000002', 'd1000000-0000-0000-0000-000000000001', 'a0000000-0000-0000-0000-000000000002', '000102030405060708090a0b0c0d0e02', '2025-04-14', 'HKV Wohnzimmer.'),
|
||||||
|
('5e000000-0000-0000-0000-000000000003', '71000003', 'd1000000-0000-0000-0000-000000000001', 'a0000000-0000-0000-0000-000000000003', '000102030405060708090a0b0c0d0e03', '2025-04-14', 'HKV Schlafzimmer.'),
|
||||||
|
('5e000000-0000-0000-0000-000000000004', '71000004', 'd1000000-0000-0000-0000-000000000002', 'a0000000-0000-0000-0000-000000000004', '000102030405060708090a0b0c0d0e04', '2025-04-15', 'Wärmemengenzähler im Flurschacht.'),
|
||||||
|
('5e000000-0000-0000-0000-000000000005', '71000005', 'd1000000-0000-0000-0000-000000000001', 'a0000000-0000-0000-0000-000000000005', '000102030405060708090a0b0c0d0e05', '2025-04-15', 'HKV Küche.'),
|
||||||
|
('5e000000-0000-0000-0000-000000000006', '71000006', 'd1000000-0000-0000-0000-000000000001', 'a0000000-0000-0000-0000-000000000006', '000102030405060708090a0b0c0d0e06', '2025-05-06', 'HKV Wohnzimmer.'),
|
||||||
|
('5e000000-0000-0000-0000-000000000007', '71000007', 'd1000000-0000-0000-0000-000000000001', 'a0000000-0000-0000-0000-000000000007', '000102030405060708090a0b0c0d0e07', '2025-05-06', 'HKV Wohnzimmer.'),
|
||||||
|
('5e000000-0000-0000-0000-000000000008', '71000008', 'd1000000-0000-0000-0000-000000000001', 'a0000000-0000-0000-0000-000000000008', '000102030405060708090a0b0c0d0e08', '2025-05-06', 'HKV Bad.'),
|
||||||
|
('5e000000-0000-0000-0000-000000000009', '71000009', 'd1000000-0000-0000-0000-000000000003', 'a0000000-0000-0000-0000-000000000001', '000102030405060708090a0b0c0d0e09', '2025-04-14', 'WW-Wärmezähler unter der Spüle.'),
|
||||||
|
('5e000000-0000-0000-0000-000000000010', '71000010', 'd1000000-0000-0000-0000-000000000004', 'a0000000-0000-0000-0000-000000000002', '000102030405060708090a0b0c0d0e10', '2025-04-14', 'WW-Zähler Bad, Vorwandinstallation.'),
|
||||||
|
('5e000000-0000-0000-0000-000000000011', '71000011', 'd1000000-0000-0000-0000-000000000004', 'a0000000-0000-0000-0000-000000000003', '000102030405060708090a0b0c0d0e11', '2025-04-14', 'WW-Zähler Bad.'),
|
||||||
|
('5e000000-0000-0000-0000-000000000012', '71000012', 'd1000000-0000-0000-0000-000000000004', 'a0000000-0000-0000-0000-000000000004', '000102030405060708090a0b0c0d0e12', '2025-04-15', 'WW-Zähler Küche.'),
|
||||||
|
('5e000000-0000-0000-0000-000000000013', '71000013', 'd1000000-0000-0000-0000-000000000004', 'a0000000-0000-0000-0000-000000000005', '000102030405060708090a0b0c0d0e13', '2025-04-15', 'WW-Zähler Bad.'),
|
||||||
|
('5e000000-0000-0000-0000-000000000014', '71000014', 'd1000000-0000-0000-0000-000000000004', 'a0000000-0000-0000-0000-000000000006', '000102030405060708090a0b0c0d0e14', '2025-05-06', 'WW-Zähler Bad.'),
|
||||||
|
('5e000000-0000-0000-0000-000000000015', '71000015', 'd1000000-0000-0000-0000-000000000004', 'a0000000-0000-0000-0000-000000000007', '000102030405060708090a0b0c0d0e15', '2025-05-06', 'WW-Zähler Bad.'),
|
||||||
|
('5e000000-0000-0000-0000-000000000016', '71000016', 'd1000000-0000-0000-0000-000000000004', 'a0000000-0000-0000-0000-000000000008', '000102030405060708090a0b0c0d0e16', '2025-05-06', 'WW-Zähler Bad.'),
|
||||||
|
-- provisioned but not yet assigned to an apartment (normal pilot state)
|
||||||
|
('5e000000-0000-0000-0000-000000000017', '71000017', 'd1000000-0000-0000-0000-000000000001', NULL, '000102030405060708090a0b0c0d0e17', '2025-04-15', 'Lager: noch nicht montiert / zugeordnet.')
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Raw frame log: one row per parse_status, including a frame from a meter
|
||||||
|
-- that is not ours (unknown_sensor — normal on a broadcast band).
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
INSERT INTO received_payload (id, gateway_id, sensor_id, timestamp_sent, timestamp_received, raw_payload, parse_status) VALUES
|
||||||
|
('7a000000-0000-0000-0000-000000000001', '9e000000-0000-0000-0000-000000000001', '5e000000-0000-0000-0000-000000000001',
|
||||||
|
now() - interval '2 hours', now() - interval '2 hours',
|
||||||
|
'{"telegram": "2e44685071000001080c7a...", "decoded": {"manufacturer": "QDS", "serial": "71000001", "records": [{"type": "energy", "unit": "kWh", "value": 2841.7}]}}',
|
||||||
|
'parsed'),
|
||||||
|
('7a000000-0000-0000-0000-000000000002', '9e000000-0000-0000-0000-000000000002', '5e000000-0000-0000-0000-000000000002',
|
||||||
|
now() - interval '30 minutes', now() - interval '29 minutes',
|
||||||
|
'{"telegram": "2e44685071000002080c7a...", "decoded": {"manufacturer": "QDS", "serial": "71000002", "records": [{"type": "energy", "unit": "kWh", "value": 7404.2}]}}',
|
||||||
|
'pending'),
|
||||||
|
('7a000000-0000-0000-0000-000000000003', '9e000000-0000-0000-0000-000000000001', NULL,
|
||||||
|
now() - interval '1 hour', now() - interval '59 minutes',
|
||||||
|
'{"telegram": "2e44a51199887766060c7a...", "decoded": {"manufacturer": "LSE", "serial": "99887766", "records": [{"type": "volume", "unit": "m3", "value": 481.2}]}}',
|
||||||
|
'unknown_sensor'),
|
||||||
|
('7a000000-0000-0000-0000-000000000004', '9e000000-0000-0000-0000-000000000003', '5e000000-0000-0000-0000-000000000014',
|
||||||
|
now() - interval '3 hours', now() - interval '1 hour', -- buffered upload: sent ≪ received
|
||||||
|
'{"telegram": "1e44e5c871000014060c7a...", "decoded": null, "error": "decryption failed: wrong key length"}',
|
||||||
|
'error')
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Measurements: daily cumulative meter readings from 2025-06-01 to today.
|
||||||
|
--
|
||||||
|
-- Values are cumulative totals (meters broadcast counters, consumption is
|
||||||
|
-- computed by differencing at query time). Daily increment = annual total
|
||||||
|
-- × month weight ÷ days-in-month, ± 10% deterministic jitter (md5 of
|
||||||
|
-- sensor/kind/day — NOT random(), so re-runs are byte-identical). Heating
|
||||||
|
-- follows a seasonal curve; hot water is flat across the year.
|
||||||
|
--
|
||||||
|
-- Annual heating totals encode the per-m² profiles the Bandtacho needs:
|
||||||
|
-- apt1 45 kWh/m²a (thrifty) … apt4 140 … apt5 190 kWh/m²a (heavy).
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
WITH weights (m, wt) AS (
|
||||||
|
VALUES (1, 0.170), (2, 0.140), (3, 0.120), (4, 0.080), (5, 0.040), (6, 0.020),
|
||||||
|
(7, 0.015), (8, 0.015), (9, 0.040), (10, 0.080), (11, 0.120), (12, 0.160)
|
||||||
|
),
|
||||||
|
profiles (sensor_id, kind, unit, annual, start_value, seasonal) AS (
|
||||||
|
VALUES
|
||||||
|
-- heating (kWh/yr = profile kWh/m²a × living area)
|
||||||
|
('5e000000-0000-0000-0000-000000000001'::uuid, 'heating_energy'::measurement_kind, 'kWh', 2430.0, 3210.0, true),
|
||||||
|
('5e000000-0000-0000-0000-000000000002'::uuid, 'heating_energy'::measurement_kind, 'kWh', 6120.0, 5480.0, true),
|
||||||
|
('5e000000-0000-0000-0000-000000000003'::uuid, 'heating_energy'::measurement_kind, 'kWh', 8500.0, 9120.0, true),
|
||||||
|
('5e000000-0000-0000-0000-000000000004'::uuid, 'heating_energy'::measurement_kind, 'kWh', 13440.0, 21930.0, true),
|
||||||
|
('5e000000-0000-0000-0000-000000000005'::uuid, 'heating_energy'::measurement_kind, 'kWh', 20900.0, 15040.0, true),
|
||||||
|
('5e000000-0000-0000-0000-000000000006'::uuid, 'heating_energy'::measurement_kind, 'kWh', 4200.0, 2660.0, true),
|
||||||
|
('5e000000-0000-0000-0000-000000000007'::uuid, 'heating_energy'::measurement_kind, 'kWh', 7125.0, 4310.0, true),
|
||||||
|
('5e000000-0000-0000-0000-000000000008'::uuid, 'heating_energy'::measurement_kind, 'kWh', 10560.0, 7750.0, true),
|
||||||
|
-- hot water: apartment 1 as energy, the rest as volume
|
||||||
|
('5e000000-0000-0000-0000-000000000009'::uuid, 'hot_water_energy'::measurement_kind, 'kWh', 1620.0, 1890.0, false),
|
||||||
|
('5e000000-0000-0000-0000-000000000010'::uuid, 'hot_water_volume'::measurement_kind, 'm3', 28.8, 214.3, false),
|
||||||
|
('5e000000-0000-0000-0000-000000000011'::uuid, 'hot_water_volume'::measurement_kind, 'm3', 34.0, 131.9, false),
|
||||||
|
('5e000000-0000-0000-0000-000000000012'::uuid, 'hot_water_volume'::measurement_kind, 'm3', 38.4, 356.0, false),
|
||||||
|
('5e000000-0000-0000-0000-000000000013'::uuid, 'hot_water_volume'::measurement_kind, 'm3', 44.0, 102.4, false),
|
||||||
|
('5e000000-0000-0000-0000-000000000014'::uuid, 'hot_water_volume'::measurement_kind, 'm3', 24.0, 188.7, false),
|
||||||
|
('5e000000-0000-0000-0000-000000000015'::uuid, 'hot_water_volume'::measurement_kind, 'm3', 30.0, 77.5, false),
|
||||||
|
('5e000000-0000-0000-0000-000000000016'::uuid, 'hot_water_volume'::measurement_kind, 'm3', 35.2, 240.1, false),
|
||||||
|
-- unassigned sensor: measured and stored regardless
|
||||||
|
('5e000000-0000-0000-0000-000000000017'::uuid, 'heating_energy'::measurement_kind, 'kWh', 5000.0, 0.0, true)
|
||||||
|
),
|
||||||
|
days AS (
|
||||||
|
SELECT d::date AS d
|
||||||
|
FROM generate_series(date '2025-06-01', current_date, interval '1 day') AS d
|
||||||
|
),
|
||||||
|
daily AS (
|
||||||
|
SELECT p.sensor_id, p.kind, p.unit, dy.d, p.start_value,
|
||||||
|
p.annual
|
||||||
|
* CASE WHEN p.seasonal THEN w.wt ELSE 1.0 / 12 END
|
||||||
|
/ extract(day FROM date_trunc('month', dy.d) + interval '1 month - 1 day')
|
||||||
|
* (0.9 + 0.2 * ((('x' || substr(md5(p.sensor_id::text || p.kind::text || dy.d::text), 1, 8))::bit(32)::int & 1023) / 1023.0))
|
||||||
|
AS increment
|
||||||
|
FROM profiles p
|
||||||
|
CROSS JOIN days dy
|
||||||
|
JOIN weights w ON w.m = extract(month FROM dy.d)::int
|
||||||
|
)
|
||||||
|
INSERT INTO measurement (sensor_id, measured_at, kind, value, unit)
|
||||||
|
SELECT sensor_id,
|
||||||
|
-- per-sensor stable time-of-day, so series don't all tick in lockstep
|
||||||
|
d::timestamp + interval '6 hours'
|
||||||
|
+ make_interval(mins => (('x' || substr(md5(sensor_id::text), 1, 4))::bit(16)::int % 50)),
|
||||||
|
kind,
|
||||||
|
round((start_value + sum(increment) OVER (PARTITION BY sensor_id, kind ORDER BY d))::numeric, 3),
|
||||||
|
unit
|
||||||
|
FROM daily
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
-- Diagnostics from the apartment-4 heat meter: flow/return temperatures
|
||||||
|
-- (seasonal: ~69/50 °C in January, ~44/32 °C in July) and an error register
|
||||||
|
-- that is 0 except for one flagged day.
|
||||||
|
|
||||||
|
WITH weights (m, wt) AS (
|
||||||
|
VALUES (1, 0.170), (2, 0.140), (3, 0.120), (4, 0.080), (5, 0.040), (6, 0.020),
|
||||||
|
(7, 0.015), (8, 0.015), (9, 0.040), (10, 0.080), (11, 0.120), (12, 0.160)
|
||||||
|
),
|
||||||
|
days AS (
|
||||||
|
SELECT d::date AS d
|
||||||
|
FROM generate_series(date '2025-06-01', current_date, interval '1 day') AS d
|
||||||
|
)
|
||||||
|
INSERT INTO measurement (sensor_id, measured_at, kind, value, unit)
|
||||||
|
SELECT '5e000000-0000-0000-0000-000000000004'::uuid,
|
||||||
|
dy.d::timestamp + interval '6 hours 15 minutes',
|
||||||
|
k.kind,
|
||||||
|
round((k.base + k.span * w.wt
|
||||||
|
+ 2 * ((('x' || substr(md5(k.kind::text || dy.d::text), 1, 8))::bit(32)::int & 255) / 255.0))::numeric, 1),
|
||||||
|
'°C'
|
||||||
|
FROM days dy
|
||||||
|
JOIN weights w ON w.m = extract(month FROM dy.d)::int
|
||||||
|
CROSS JOIN (VALUES ('flow_temperature'::measurement_kind, 42.0, 160.0),
|
||||||
|
('return_temperature'::measurement_kind, 30.0, 120.0)) AS k (kind, base, span)
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
INSERT INTO measurement (sensor_id, measured_at, kind, value, unit) VALUES
|
||||||
|
('5e000000-0000-0000-0000-000000000004', date '2026-02-10' + interval '6 hours 15 minutes', 'error_flags', 0, 'flags'),
|
||||||
|
('5e000000-0000-0000-0000-000000000004', date '2026-02-11' + interval '6 hours 15 minutes', 'error_flags', 16, 'flags'), -- e.g. air in flow sensor
|
||||||
|
('5e000000-0000-0000-0000-000000000004', date '2026-02-12' + interval '6 hours 15 minutes', 'error_flags', 0, 'flags')
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Reference data — DEV APPROXIMATIONS, not verified legal values.
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- Efficiency class thresholds: GEG Anlage 10 annual bounds split into monthly
|
||||||
|
-- bounds using the same seasonal weights as the heating fixtures. The real
|
||||||
|
-- UBA CC 69/2021 Tabelle 1 values differ — replace via a verified migration.
|
||||||
|
WITH classes (class, annual_max) AS (
|
||||||
|
VALUES ('A+', 30.0), ('A', 50.0), ('B', 75.0), ('C', 100.0),
|
||||||
|
('D', 130.0), ('E', 160.0), ('F', 200.0), ('G', 250.0)
|
||||||
|
),
|
||||||
|
weights (m, wt) AS (
|
||||||
|
VALUES (1, 0.170), (2, 0.140), (3, 0.120), (4, 0.080), (5, 0.040), (6, 0.020),
|
||||||
|
(7, 0.015), (8, 0.015), (9, 0.040), (10, 0.080), (11, 0.120), (12, 0.160)
|
||||||
|
)
|
||||||
|
INSERT INTO efficiency_class_threshold (class, month, max_kwh_per_m2, source)
|
||||||
|
SELECT c.class, w.m, round((c.annual_max * w.wt)::numeric, 3),
|
||||||
|
'DEV FIXTURE — approximation of UBA CC 69/2021 Tab. 1 / GEG Anl. 10'
|
||||||
|
FROM classes c CROSS JOIN weights w
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'H', w.m, 999.999, -- open-ended class: sentinel, not a real bound
|
||||||
|
'DEV FIXTURE — approximation of UBA CC 69/2021 Tab. 1 / GEG Anl. 10'
|
||||||
|
FROM weights w
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
-- CO2 factors: one superseded natural-gas row to exercise versioning.
|
||||||
|
INSERT INTO co2_emission_factor (fuel_type, g_co2_per_kwh, valid_from, valid_to, source) VALUES
|
||||||
|
('natural_gas', 202.00, '2021-01-01', '2023-12-31', 'DEV FIXTURE — approximates GEG Anlage 9'),
|
||||||
|
('natural_gas', 240.00, '2024-01-01', NULL, 'DEV FIXTURE — approximates GEG Anlage 9'),
|
||||||
|
('heating_oil', 310.00, '2024-01-01', NULL, 'DEV FIXTURE — approximates GEG Anlage 9'),
|
||||||
|
('district_heating', 180.00, '2024-01-01', NULL, 'DEV FIXTURE — approximates GEG Anlage 9'),
|
||||||
|
('heat_pump_electricity', 560.00, '2024-01-01', NULL, 'DEV FIXTURE — approximates GEG Anlage 9'),
|
||||||
|
('wood_pellets', 20.00, '2024-01-01', NULL, 'DEV FIXTURE — approximates GEG Anlage 9'),
|
||||||
|
('other', 300.00, '2024-01-01', NULL, 'DEV FIXTURE — placeholder')
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
-- Energy prices: one superseded natural-gas row to exercise versioning.
|
||||||
|
INSERT INTO energy_price (fuel_type, ct_per_kwh, valid_from, valid_to, source, created_by) VALUES
|
||||||
|
('natural_gas', 11.200, '2024-01-01', '2025-12-31', 'DEV FIXTURE', 'e0000000-0000-0000-0000-000000000001'),
|
||||||
|
('natural_gas', 12.400, '2026-01-01', NULL, 'DEV FIXTURE', 'e0000000-0000-0000-0000-000000000001'),
|
||||||
|
('heating_oil', 10.900, '2026-01-01', NULL, 'DEV FIXTURE', 'e0000000-0000-0000-0000-000000000001'),
|
||||||
|
('district_heating', 15.800, '2026-01-01', NULL, 'DEV FIXTURE', 'e0000000-0000-0000-0000-000000000001'),
|
||||||
|
('heat_pump_electricity', 27.500, '2026-01-01', NULL, 'DEV FIXTURE', 'e0000000-0000-0000-0000-000000000001'),
|
||||||
|
('wood_pellets', 8.200, '2026-01-01', NULL, 'DEV FIXTURE', 'e0000000-0000-0000-0000-000000000001')
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
-- Heating degree days: the 15 complete months before the current month.
|
||||||
|
-- The CURRENT month is deliberately absent — the freshest footnote renders
|
||||||
|
-- "not available" until the (future) monthly DWD import fills it, exactly
|
||||||
|
-- like prod. Deterministic per (year, month), so re-runs insert nothing new
|
||||||
|
-- for existing months and exactly one row when a month completes.
|
||||||
|
WITH base (m, hdd0) AS (
|
||||||
|
VALUES (1, 550), (2, 480), (3, 400), (4, 280), (5, 140), (6, 40),
|
||||||
|
(7, 15), (8, 20), (9, 110), (10, 280), (11, 420), (12, 520)
|
||||||
|
),
|
||||||
|
months AS (
|
||||||
|
SELECT (date_trunc('month', current_date) - make_interval(months => n))::date AS mon
|
||||||
|
FROM generate_series(1, 15) AS n
|
||||||
|
)
|
||||||
|
INSERT INTO heating_degree_days (station_id, year, month, hdd)
|
||||||
|
SELECT 'dd000000-0000-0000-0000-000000000001',
|
||||||
|
extract(year FROM mon)::smallint,
|
||||||
|
extract(month FROM mon)::smallint,
|
||||||
|
round((b.hdd0 * (0.90 + 0.2 * ((('x' || substr(md5(mon::text), 1, 8))::bit(32)::int & 255) / 255.0)))::numeric, 1)
|
||||||
|
FROM months
|
||||||
|
JOIN base b ON b.m = extract(month FROM mon)::int
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- Savings tips (Module 4) — one retired tip to exercise active = false.
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
INSERT INTO saving_tip (id, category, title, body, link_url, active) VALUES
|
||||||
|
('5a000000-0000-0000-0000-000000000001', 'heating', 'Stoßlüften statt Kipplüften',
|
||||||
|
'Mehrmals täglich 5–10 Minuten mit weit geöffnetem Fenster lüften, statt das Fenster dauerhaft zu kippen. So bleibt die Wärme in Wänden und Möbeln erhalten.',
|
||||||
|
'https://www.verbraucherzentrale.de/wissen/energie/heizen-und-warmwasser', true),
|
||||||
|
('5a000000-0000-0000-0000-000000000002', 'heating', 'Raumtemperatur um 1 °C senken',
|
||||||
|
'Ein Grad weniger spart rund 6 % Heizenergie. 20 °C im Wohnzimmer und 17–18 °C im Schlafzimmer reichen in der Regel aus.',
|
||||||
|
'https://www.co2online.de/energie-sparen/heizenergie-sparen/', true),
|
||||||
|
('5a000000-0000-0000-0000-000000000003', 'heating', 'Heizkörper entlüften',
|
||||||
|
'Gluckert der Heizkörper oder wird er nur teilweise warm, ist Luft im System. Entlüften stellt die volle Heizleistung wieder her.',
|
||||||
|
NULL, true),
|
||||||
|
('5a000000-0000-0000-0000-000000000004', 'heating', 'Heizkörper nicht zustellen',
|
||||||
|
'Möbel und Vorhänge vor dem Heizkörper stauen die Wärme und erhöhen den Verbrauch.',
|
||||||
|
NULL, false), -- retired tip: must remain resolvable for past months
|
||||||
|
('5a000000-0000-0000-0000-000000000005', 'hot_water', 'Duschen statt Baden',
|
||||||
|
'Ein Vollbad benötigt etwa dreimal so viel warmes Wasser wie eine kurze Dusche.',
|
||||||
|
'https://www.verbraucherzentrale.de/wissen/energie/heizen-und-warmwasser', true),
|
||||||
|
('5a000000-0000-0000-0000-000000000006', 'hot_water', 'Sparduschkopf einbauen',
|
||||||
|
'Ein Sparduschkopf halbiert den Warmwasserverbrauch beim Duschen — bei gleichem Komfort.',
|
||||||
|
'https://www.co2online.de/energie-sparen/strom-sparen/', true),
|
||||||
|
('5a000000-0000-0000-0000-000000000007', 'hot_water', 'Warmwasser nicht laufen lassen',
|
||||||
|
'Beim Einseifen, Zähneputzen und Spülen das warme Wasser abstellen.',
|
||||||
|
NULL, true)
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
Reference in New Issue
Block a user