Files
Enelix-EMS/services/license/changes/20261006-heat-pump-header/files/portal-domain.mjs
T
2026-10-06 14:35:17 +00:00

666 lines
39 KiB
JavaScript

import { randomBytes } from "node:crypto";
export const systemDeviceTypes = Object.freeze({
battery: { name: "Batteriespeicher", catalogKey: "battery", moduleId: "{437FB683-517F-4FEC-8CCB-FE6B0A62B69E}" },
buffer_storage: { name: "Pufferspeicher", catalogKey: "buffer_storage", moduleId: "{C92D5EEF-9632-47A5-9659-4B02BF40FBE9}" },
ev_charger: { name: "EV-Ladestation", catalogKey: "ev_charger", moduleId: "{0D94913C-0F31-4C29-A685-6EB4AE58E55D}", gatewayModuleId: "{32F65988-BE55-4FD6-9FFF-CF1C4C8628F8}" },
consumer_single: { name: "Verbraucher einstufig", catalogKey: "consumer_single", moduleId: "{15879A4E-D0C2-4495-83DE-46E1E462591E}" },
boiler_multi: { name: "Boiler mehrstufig", catalogKey: "boiler_multi", moduleId: "{B7C54AF4-AD7D-4FE4-B75D-203693906251}" },
heat_pump: { name: "Wärmepumpe", catalogKey: "heat_pump", moduleId: "{A31C9274-54F7-4BD2-9804-7AF23E86C4D1}" }
});
export const integrationProfiles = Object.freeze({
energy: [
{ id: "manual", name: "Bestehende Symcon-Variablen", manufacturer: "Allgemein", protocol: "Variablenzuordnung", defaultPort: 0, setup: "manual", requiresHost: false },
{ id: "fronius_solar_api_v1", name: "Fronius Solar API v1", manufacturer: "Fronius", protocol: "HTTP Solar API v1", defaultPort: 80, setup: "automatic", requiresHost: true },
{ id: "huawei_sun2000_modbus", name: "SUN2000 / Smart Dongle", manufacturer: "Huawei", protocol: "Modbus TCP", defaultPort: 502, setup: "model_specific", requiresHost: true },
{ id: "sma_sunspec_modbus", name: "Sunny Boy / Tripower SunSpec", manufacturer: "SMA", protocol: "SunSpec Modbus TCP", defaultPort: 502, setup: "sunspec", requiresHost: true },
{ id: "goodwe_et_modbus", name: "ET / BT / EH Serie", manufacturer: "GoodWe", protocol: "Modbus TCP", defaultPort: 502, setup: "model_specific", requiresHost: true },
{ id: "solaredge_sunspec_modbus", name: "SetApp SunSpec", manufacturer: "SolarEdge", protocol: "SunSpec Modbus TCP", defaultPort: 1502, setup: "sunspec", requiresHost: true }
],
storage: [
{ id: "manual", name: "Bestehende Symcon-Variablen", manufacturer: "Allgemein", protocol: "Variablenzuordnung", defaultPort: 0, setup: "manual", requiresHost: false },
{ id: "fronius_hybrid_storage", name: "BYD über Fronius Hybridwechselrichter", manufacturer: "Fronius", protocol: "HTTP Solar API v1", defaultPort: 80, setup: "automatic", requiresHost: true },
{ id: "huawei_luna2000", name: "LUNA2000 über SUN2000", manufacturer: "Huawei", protocol: "Modbus TCP", defaultPort: 502, setup: "model_specific", requiresHost: true },
{ id: "sma_home_storage", name: "SMA Home Storage", manufacturer: "SMA", protocol: "SunSpec Modbus TCP", defaultPort: 502, setup: "sunspec", requiresHost: true },
{ id: "goodwe_lynx", name: "Lynx Home über Hybridwechselrichter", manufacturer: "GoodWe", protocol: "Modbus TCP", defaultPort: 502, setup: "model_specific", requiresHost: true },
{ id: "solaredge_home_battery", name: "SolarEdge Home Battery", manufacturer: "SolarEdge", protocol: "SunSpec Modbus TCP", defaultPort: 1502, setup: "sunspec", requiresHost: true },
{ id: "sigenergy_sigenstor", name: "SigenStor / Energy Controller", manufacturer: "Sigenergy", protocol: "Modbus TCP", defaultPort: 502, setup: "model_specific", requiresHost: true }
],
sensor: [
{ id: "manual", name: "Bestehender Temperaturfühler", manufacturer: "Allgemein", protocol: "Variablenzuordnung", defaultPort: 0, setup: "manual", requiresTopic: false },
{ id: "shelly_addon_temperature", name: "Shelly Plus Add-on Temperaturfühler", manufacturer: "Shelly", protocol: "MQTT/RPC Gen2+", defaultPort: 0, setup: "automatic", requiresTopic: true }
],
actuator: [
{ id: "manual", name: "Bestehender Symcon-Schaltkontakt", manufacturer: "Allgemein", protocol: "Variablenzuordnung", outputs: 1, setup: "manual", requiresTopic: false },
{ id: "shelly_plus_1pm", name: "Shelly Plus 1PM", manufacturer: "Shelly", protocol: "MQTT/RPC Gen2+", outputs: 1, setup: "automatic", requiresTopic: true },
{ id: "shelly_plus_2pm", name: "Shelly Plus 2PM", manufacturer: "Shelly", protocol: "MQTT/RPC Gen2+", outputs: 2, setup: "automatic", requiresTopic: true },
{ id: "shelly_pro_1pm", name: "Shelly Pro 1PM", manufacturer: "Shelly", protocol: "MQTT/RPC Gen2+", outputs: 1, setup: "automatic", requiresTopic: true },
{ id: "shelly_pro_2pm", name: "Shelly Pro 2PM", manufacturer: "Shelly", protocol: "MQTT/RPC Gen2+", outputs: 2, setup: "automatic", requiresTopic: true },
{ id: "shelly_pro_4pm", name: "Shelly Pro 4PM", manufacturer: "Shelly", protocol: "MQTT/RPC Gen2+", outputs: 4, setup: "automatic", requiresTopic: true },
{ id: "shelly_plug_s_gen3", name: "Shelly Plug S Gen3", manufacturer: "Shelly", protocol: "MQTT/RPC Gen2+", outputs: 1, setup: "automatic", requiresTopic: true }
]
});
const monthLimits = ["Januar", "Februar", "Maerz", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"]
.map((Monat, index) => ({ Monat, Grenze_W: 0, MonatIndex: index + 1 }));
const f = (key, label, type, defaultValue, section, extra = {}) => ({ key, label, type, default: defaultValue, section, ...extra });
export const managerFields = Object.freeze([
f("Rolle", "Rolle", "select", 0, "Grundeinstellungen", { options: [{ value: 0, label: "Alleine" }, { value: 1, label: "Hauptmanager" }, { value: 2, label: "Untermanager" }] }),
f("NetzleistungVariableID", "Netzleistung Variable-ID", "integer", 0, "Grundeinstellungen", { min: 0 }),
f("Netzleistungsfaktor", "Netzleistungsfaktor", "number", 1, "Grundeinstellungen"),
f("MesswertMaxAlter", "Maximales Messwertalter (s)", "integer", 60, "Grundeinstellungen", { min: 1 }),
f("VerbraucherZuordnung", "Manuelle Verbraucherzuordnung (JSON)", "json", "[]", "Grundeinstellungen"),
f("AutomatischeVerbraucherZuordnung", "Automatische Verbraucherzuordnung (JSON)", "json", "[]", "Grundeinstellungen"),
f("AutomatischeSuche", "Verbraucher automatisch suchen", "boolean", true, "Grundeinstellungen"),
f("SuchbereichID", "Suchbereich Objekt-ID", "integer", 0, "Grundeinstellungen", { min: 0 }),
f("KeepAlive", "Keep-Alive (s)", "integer", 60, "Grundeinstellungen", { min: 1 }),
f("VerbraucherTimeout", "Verbraucher-Timeout (s)", "integer", 60, "Grundeinstellungen", { min: 1 }),
f("Lastspitzenmodus", "Lastspitzenmodus", "select", 0, "Regelung", { options: [{ value: 0, label: "Aus" }, { value: 1, label: "Konstant" }, { value: 2, label: "Monatlich" }] }),
f("Lastspitzengrenze", "Lastspitzengrenze (W)", "number", 0, "Regelung", { min: 0 }),
f("Monatsgrenzen", "Monatliche Grenzen (JSON)", "json", JSON.stringify(monthLimits), "Regelung"),
f("SollwertSolarladen", "Sollwert Solarladen (W)", "number", 0, "Regelung"),
f("Umschaltdifferenz", "Umschaltdifferenz (W)", "number", 5, "Regelung", { min: 0 }),
f("EinspeisebegrenzungAktiv", "Einspeisebegrenzung aktiv", "boolean", false, "Regelung"),
f("Einspeisegrenze", "Maximale Einspeisung (W)", "number", 0, "Regelung", { min: 0 }),
f("PrognoseAktiv", "Prognose aktiv", "boolean", false, "Prognose und Dienste"),
f("NetzfahrplanAktiv", "Netzfahrplan aktiv", "boolean", false, "Prognose und Dienste"),
f("PrognosePVVariableID", "PV-Prognose Variable-ID", "integer", 0, "Prognose und Dienste", { min: 0 }),
f("PrognoseHausverbrauchVariableID", "Hausverbrauch-Prognose Variable-ID", "integer", 0, "Prognose und Dienste", { min: 0 }),
f("PrognoseSOCVariableID", "SOC-Prognose Variable-ID", "integer", 0, "Prognose und Dienste", { min: 0 }),
f("PrognoseSendeintervall", "Prognose-Sendeintervall (s)", "integer", 60, "Prognose und Dienste", { min: 1 }),
f("PrognoseAnschluss", "Prognose-Anschluss (JSON)", "json", "{}", "Prognose und Dienste"),
f("SDLAnschluss", "SDL-Anschluss (JSON)", "json", "{}", "Prognose und Dienste"),
f("LizenzAnschluss", "Lizenz-Anschluss (JSON)", "json", "{}", "Prognose und Dienste"),
f("StoermeldeAnschluss", "Störmelde-Anschluss (JSON)", "json", "{}", "Prognose und Dienste"),
f("StoerueberwachungAktiv", "Störüberwachung aktiv", "boolean", false, "Prognose und Dienste"),
f("StoerungsSendeintervall", "Störungs-Sendeintervall (s)", "integer", 300, "Prognose und Dienste", { min: 10 }),
f("AnlagenWechselrichter", "Wechselrichter (JSON)", "json", "[]", "Anlagentopologie"),
f("AnlagenPVFlaechen", "PV-Flächen (JSON)", "json", "[]", "Anlagentopologie"),
f("AnlagenBatterien", "Batterien (JSON)", "json", "[]", "Anlagentopologie"),
f("NetzbezugEnergieVariableID", "Netzbezug Energie Variable-ID", "integer", 0, "Anlagentopologie", { min: 0 }),
f("NetzbezugEnergiefaktor", "Netzbezug Energiefaktor", "number", 1, "Anlagentopologie"),
f("NetzeinspeisungEnergieVariableID", "Netzeinspeisung Energie Variable-ID", "integer", 0, "Anlagentopologie", { min: 0 }),
f("NetzeinspeisungEnergiefaktor", "Netzeinspeisung Energiefaktor", "number", 1, "Anlagentopologie"),
f("EnergieaufzeichnungAktiv", "Energieaufzeichnung aktiv", "boolean", true, "Messung und Darstellung"),
f("MessungPVLeistungVariableID", "PV-Leistung Variable-ID", "integer", 0, "Messung und Darstellung", { min: 0 }),
f("MessungHausverbrauchLeistungVariableID", "Hausverbrauch Variable-ID", "integer", 0, "Messung und Darstellung", { min: 0 }),
f("MessungBatterieleistungVariableID", "Batterieleistung Variable-ID", "integer", 0, "Messung und Darstellung", { min: 0 }),
f("MessungPVLeistungsfaktor", "PV-Leistungsfaktor", "number", 1, "Messung und Darstellung"),
f("MessungHausverbrauchLeistungsfaktor", "Hausverbrauch-Leistungsfaktor", "number", 1, "Messung und Darstellung"),
f("MessungBatterieleistungsfaktor", "Batterie-Leistungsfaktor", "number", 1, "Messung und Darstellung"),
f("MesswerteAnzeigen", "Messwerte anzeigen", "boolean", false, "Messung und Darstellung"),
f("LeistungsaufzeichnungMinuten", "Leistungsaufzeichnung (min)", "integer", 1, "Messung und Darstellung", { min: 1 }),
f("LeistungLoeschenMonate", "Leistungsdaten löschen (Monate)", "integer", 12, "Messung und Darstellung", { min: 0 }),
f("EnergieaufzeichnungMinuten", "Energieaufzeichnung (min)", "integer", 5, "Messung und Darstellung", { min: 1 }),
f("EnergieVerdichtenMonate", "Energiedaten verdichten (Monate)", "integer", 12, "Messung und Darstellung", { min: 0 }),
f("EnergieLoeschenMonate", "Energiedaten löschen (Monate)", "integer", 0, "Messung und Darstellung", { min: 0 }),
f("EnergyPieAnzeigen", "Energie-Kreisdiagramm anzeigen", "boolean", true, "Messung und Darstellung"),
f("EnergiediagrammeAnzeigen", "Energiediagramme anzeigen", "boolean", true, "Messung und Darstellung"),
f("FunFactsAnzeigen", "Fun Facts anzeigen", "boolean", true, "Messung und Darstellung"),
f("EnergieflussAnzeigen", "Energiefluss anzeigen", "boolean", true, "Messung und Darstellung"),
f("DiagnosevariablenAnzeigen", "Diagnosevariablen anzeigen", "boolean", false, "Diagnose"),
f("LoggingEin", "Diagnoseprotokoll aktivieren", "boolean", false, "Diagnose")
]);
const managerWizardFieldKeys = new Set([
"Lastspitzengrenze",
"SollwertSolarladen",
"EinspeisebegrenzungAktiv",
"Einspeisegrenze",
"PrognoseAktiv",
"NetzfahrplanAktiv",
"StoerueberwachungAktiv",
"EnergyPieAnzeigen",
"EnergiediagrammeAnzeigen",
"FunFactsAnzeigen",
"EnergieflussAnzeigen"
]);
export const managerWizardFields = Object.freeze(
managerFields
.filter(field => managerWizardFieldKeys.has(field.key))
.map(field => ({
...field,
section: field.section === "Messung und Darstellung" ? "Darstellung" : field.section
}))
);
export const topologyFields = Object.freeze({
pvSurface: [
f("Name", "Bezeichnung", "text", "PV-Fläche", "PV-Fläche"),
f("DCLeistung_kWp", "DC-Leistung (kWp)", "number", 0, "PV-Fläche", { min: 0 }),
f("Neigung_Grad", "Neigung (Grad)", "number", 30, "Ausrichtung", { min: 0, max: 90 }),
f("Azimut_Grad", "Azimut (Grad)", "number", 0, "Ausrichtung", { min: -180, max: 180 }),
f("MPPT", "MPPT / Eingang", "text", "", "Zuordnung"),
f("Modulanzahl", "Anzahl Module", "integer", 0, "Module", { min: 0 }),
f("Modulleistung_Wp", "Modulleistung (Wp)", "number", 0, "Module", { min: 0 })
],
battery: [
f("Nennkapazitaet_kWh", "Nennkapazität (kWh)", "number", 0, "Anlagentopologie", { min: 0 }),
f("Nutzkapazitaet_kWh", "Nutzkapazität (kWh)", "number", 0, "Anlagentopologie", { min: 0 }),
f("MaxLadeleistung_kW", "Maximale Ladeleistung (kW)", "number", 0, "Anlagentopologie", { min: 0 }),
f("MaxEntladeleistung_kW", "Maximale Entladeleistung (kW)", "number", 0, "Anlagentopologie", { min: 0 }),
f("Kopplung", "Kopplung", "choice", "ac", "Anlagentopologie", { options: [
{ value: "ac", label: "AC-gekoppelt" },
{ value: "dc", label: "DC-gekoppelt" },
{ value: "hybrid", label: "Hybrid" }
] })
]
});
const commonConsumerFields = [
f("PrioritaetPV", "Priorität PV", "integer", 0, "Manager und Zeitverhalten", { min: 0 }),
f("PrioritaetPeak", "Priorität Peak", "integer", 0, "Manager und Zeitverhalten", { min: 0 }),
f("Meldeintervall", "Meldeintervall (s)", "integer", 10, "Manager und Zeitverhalten", { min: 1 }),
f("VorgabeTimeout", "Vorgabe-Timeout (s)", "integer", 120, "Manager und Zeitverhalten", { min: 1 }),
f("EinstellungenInVisu", "Einstellungen in Visualisierung", "boolean", true, "Darstellung und Diagnose"),
f("DiagnosevariablenAnzeigen", "Diagnosevariablen anzeigen", "boolean", false, "Darstellung und Diagnose"),
f("LoggingEin", "Diagnoseprotokoll aktivieren", "boolean", false, "Darstellung und Diagnose")
];
export const consumerFields = Object.freeze({
battery: [...commonConsumerFields,
f("Batterietyp", "Batterietyp", "select", 0, "Batterie", { options: [{ value: 0, label: "Unkonfiguriert" }, { value: 1, label: "Herstellerunabhängig" }, { value: 2, label: "GoodWe" }, { value: 3, label: "SolarEdge" }, { value: 4, label: "Sigenergy" }] }),
f("Batteriemanagement", "Steuerung", "select", 1, "Batterie", { options: [{ value: 1, label: "Wechselrichter" }, { value: 2, label: "Enelix Manager" }] }),
...["MaxLadeleistungVariableID", "MaxEntladeleistungVariableID", "LadezustandVariableID", "NetzleistungVariableID", "IstleistungVariableID", "ManagementRegisterVariableID", "ModusRegisterVariableID", "LeistungsRegisterVariableID", "LadeleistungRegisterVariableID", "EntladeleistungRegisterVariableID"].map(key => f(key, key.replace(/VariableID$/, " Variable-ID"), "integer", 0, "Variablenzuordnung", { min: 0 })),
f("ReserveLadezustand", "Reserve für Peak Shaving (%)", "number", 20, "Batterie", { min: 0, max: 100 }),
f("MindestLadezustand", "Minimaler Ladezustand (%)", "number", 10, "Batterie", { min: 0, max: 100 }),
f("LadezustandHysterese", "Reserve-Hysterese (%)", "number", 2, "Batterie", { min: 0, max: 100 }),
f("MesswertMaxAlter", "Maximales Messwertalter (s)", "integer", 60, "Batterie", { min: 1 }),
f("Aenderungssperre", "Sperrzeit nach Leistungsänderung (s)", "integer", 4, "Batterie", { min: 0 })
],
buffer_storage: [...commonConsumerFields,
f("LeistungsStufen", "Leistungsstufen (JSON)", "json", "[]", "Pufferspeicher"),
f("PufferfuehlerVariableID", "Puffertemperatur Variable-ID", "integer", 0, "Pufferspeicher", { min: 0 }),
f("AussentemperaturVariableID", "Außentemperatur Variable-ID", "integer", 0, "Pufferspeicher", { min: 0 }),
f("WaermepumpenSolltemperaturVariableID", "Wärmepumpen-Solltemperatur Variable-ID", "integer", 0, "Heizkurve", { min: 0 }),
f("FusspunktVorlauftemperatur", "Fußpunkt Vorlauftemperatur (°C)", "number", 35, "Heizkurve"),
f("HeizkurvenSteigung", "Heizkurvensteigung", "number", 1, "Heizkurve", { min: 0 }),
f("HeizkurveMinimaltemperatur", "Minimale Solltemperatur (°C)", "number", 20, "Heizkurve"),
f("HeizkurveMaximaltemperatur", "Maximale Solltemperatur (°C)", "number", 80, "Heizkurve"),
f("Hysterese", "Hysterese (K)", "number", 5, "Heizkurve", { min: 0.1 }),
f("MindesttemperaturModus", "Mindesttemperatur", "select", 0, "Heizkurve", { options: [{ value: 0, label: "Aus" }, { value: 1, label: "Statisch" }, { value: 2, label: "Differenz zur Solltemperatur" }] }),
f("Mindesttemperatur", "Statische Mindesttemperatur (°C)", "number", 20, "Heizkurve"),
f("MindesttemperaturDifferenz", "Differenz zur Solltemperatur (K)", "number", 5, "Heizkurve", { min: 0 }),
f("LastwechselSperrzeit", "Lastwechsel-Sperrzeit (s)", "integer", 5, "Pufferspeicher", { min: 0 }),
f("TemperaturMaxAlter", "Maximales Temperaturalter (s)", "integer", 120, "Pufferspeicher", { min: 1 }),
f("PuffertemperaturGlaetten", "Puffertemperatur glätten", "boolean", false, "Pufferspeicher"),
f("ZeitKonstante", "PT1-Zeitkonstante (s)", "integer", 120, "Pufferspeicher", { min: 1 })
],
ev_charger: [...commonConsumerFields,
f("Geraetetyp", "Gerätetyp", "select", 0, "Ladestation", { options: [{ value: 0, label: "Nicht konfiguriert" }, { value: 1, label: "go-e alte API" }, { value: 2, label: "go-e Gemini" }, { value: 3, label: "smart-me Pico" }] }),
f("Geraeteadresse", "IP-Adresse oder Hostname", "text", "", "Ladestation"),
f("GeraeteID", "Geräte-ID", "text", "", "Ladestation"),
f("Seriennummer", "Seriennummer", "text", "", "Ladestation"),
f("Benutzername", "Benutzername", "text", "", "Ladestation"),
f("MaximalerLadestrom", "Maximaler Ladestrom (A)", "integer", 16, "Ladestation", { min: 6, max: 32 }),
f("Abfrageintervall", "Statusabfrage (s)", "integer", 5, "Ladestation", { min: 1 }),
f("Ladefreigabe", "Ladefreigabe beim Start", "boolean", true, "Ladestation"),
f("Solarladen", "Solarladen beim Start", "boolean", true, "Ladestation")
],
consumer_single: [...commonConsumerFields,
f("Nennleistung", "Nennleistung (W)", "integer", 0, "Verbraucher", { min: 0 }),
f("SchaltkontaktVariableID", "Schaltkontakt Variable-ID", "integer", 0, "Verbraucher", { min: 0 }),
f("SchaltkontaktInvertiert", "Schaltkontakt invertieren", "boolean", false, "Verbraucher"),
f("RueckmeldungVariableID", "Rückmeldung Variable-ID", "integer", 0, "Verbraucher", { min: 0 }),
f("Mindestlaufzeit", "Tägliche Mindestlaufzeit (s)", "integer", 0, "Verbraucher", { min: 0, max: 86400 }),
f("PeakSperreBeiMindestlaufzeitAnbieten", "Bei Peak-Sperre während Mindestlaufzeit anbieten", "boolean", true, "Verbraucher"),
f("Mindesteinschaltdauer", "Mindest-Einschaltdauer (s)", "integer", 5, "Verbraucher", { min: 0 }),
f("Mindestausschaltdauer", "Mindest-Ausschaltdauer (s)", "integer", 5, "Verbraucher", { min: 0 })
],
heat_pump: [...commonConsumerFields,
f("Kontaktart", "Kontaktart", "select", 0, "Kontakte", { min: 0, max: 1, options: [{ value: 0, label: "Sperre und Erhöhung" }, { value: 1, label: "SG Ready" }] }),
f("Kontakt1VariableID", "Kontakt 1: Sperre / SG Ready 1 (Variable-ID)", "integer", 0, "Kontakte", { min: 0 }),
f("Kontakt2VariableID", "Kontakt 2: Erhöhung / SG Ready 2 (Variable-ID)", "integer", 0, "Kontakte", { min: 0 }),
f("Kontakt1Invertiert", "Kontakt 1 invertieren", "boolean", false, "Kontakte"),
f("Kontakt2Invertiert", "Kontakt 2 invertieren", "boolean", false, "Kontakte"),
f("Nennleistung", "Elektrische Nennleistung (W)", "integer", 0, "Wärmepumpe", { min: 0 }),
f("Rueckmeldungsart", "Rückmeldungsart", "select", 0, "Betriebsrückmeldung", { min: 0, max: 1, options: [{ value: 0, label: "Gemessene Leistung" }, { value: 1, label: "Boolean-Betriebsstatus" }] }),
f("IstleistungVariableID", "Leistung oder Betriebsstatus (Variable-ID)", "integer", 0, "Betriebsrückmeldung", { min: 0 }),
f("Laufschwelle", "Laufschwelle bei Leistungsmessung (W)", "number", 100, "Betriebsrückmeldung", { min: 0 }),
f("Anlaufwartezeit", "Anlaufwartezeit (s)", "integer", 30, "Schutzzeiten", { min: 0 }),
f("Wiederholsperre", "Wiederholsperre nach erfolglosem Anlauf (s)", "integer", 300, "Schutzzeiten", { min: 0 }),
f("Mindestlaufzeit", "Mindestlaufzeit nach bestätigtem Start (s)", "integer", 1200, "Schutzzeiten", { min: 0 }),
f("Mindestsperrzeit", "Mindest-Sperrzeit (s)", "integer", 1200, "Schutzzeiten", { min: 0 }),
f("MaximaleSperrzeit", "Maximale Sperrzeit am Stück (s; 0 = aus)", "integer", 7200, "Schutzzeiten", { min: 0 }),
f("MaximaleSperrzeit24h", "Maximale Sperrzeit in 24 Stunden (s; 0 = aus)", "integer", 21600, "Schutzzeiten", { min: 0, max: 86400 })
],
boiler_multi: [...commonConsumerFields,
f("LeistungsStufen", "Leistungsstufen (JSON)", "json", "[]", "Warmwassererwärmer"),
f("Boilerfuehler_PT1", "Boilerfühler Variable-ID", "integer", 0, "Warmwassererwärmer", { min: 0 }),
f("LegionellenfunktionAktiv", "Legionellenschaltung aktiv", "boolean", true, "Warmwassererwärmer"),
f("LegionellenMinimalintervallTage", "Frühestens nach Tagen", "integer", 4, "Warmwassererwärmer", { min: 1 }),
f("LegionellenMaximalintervallTage", "Spätestens nach Tagen", "integer", 7, "Warmwassererwärmer", { min: 1 }),
f("Boilervolumen", "Boilervolumen (l)", "integer", 300, "Warmwassererwärmer", { min: 1 }),
f("Hysterese", "Temperaturhysterese (K)", "number", 5, "Warmwassererwärmer", { min: 0.1 }),
f("Zeitplan", "Solltemperatur-Zeitplan (JSON)", "json", "[]", "Warmwassererwärmer"),
f("LastwechselSperrzeit", "Mindestzeit zwischen Lastwechseln (s)", "integer", 5, "Warmwassererwärmer", { min: 1 }),
f("TemperaturMaxAlter", "Maximales Messwertalter (s)", "integer", 30, "Warmwassererwärmer", { min: 1 }),
f("BoilertemperaturGlaetten", "Boilertemperatur glätten", "boolean", false, "Warmwassererwärmer"),
f("ZeitKonstante", "PT1-Zeitkonstante (s)", "integer", 120, "Warmwassererwärmer", { min: 1 })
]
});
export const evGatewayFields = Object.freeze([
...commonConsumerFields,
f("Betriebsmodus", "Betriebsart", "select", 0, "Gateway-Ladestation", { options: [
{ value: 0, label: "Easee" },
{ value: 1, label: "Easee · nur Solarladen" }
] }),
f("Ladestationskennung", "Easee Seriennummer", "text", "", "Gateway-Ladestation"),
f("MaximalerLadestrom", "Maximaler Ladestrom (A)", "integer", 16, "Gateway-Ladestation", { min: 6, max: 32 }),
f("Ladefreigabe", "Ladefreigabe beim Start", "boolean", true, "Gateway-Ladestation"),
f("Solarladen", "Solarladen beim Start", "boolean", true, "Gateway-Ladestation")
]);
const consumerWizardFieldKeys = Object.freeze({
battery: new Set(["Batteriemanagement", "ReserveLadezustand", "MindestLadezustand"]),
buffer_storage: new Set([
"FusspunktVorlauftemperatur", "HeizkurvenSteigung", "HeizkurveMinimaltemperatur",
"HeizkurveMaximaltemperatur", "Hysterese", "MindesttemperaturModus",
"Mindesttemperatur", "MindesttemperaturDifferenz"
]),
ev_charger: new Set([
"Geraetetyp", "Geraeteadresse", "GeraeteID", "Seriennummer", "Benutzername",
"MaximalerLadestrom", "Ladefreigabe", "Solarladen"
]),
consumer_single: new Set([
"Nennleistung", "SchaltkontaktInvertiert", "Mindestlaufzeit",
"Mindesteinschaltdauer", "Mindestausschaltdauer"
]),
heat_pump: new Set([
"Kontaktart", "Kontakt1VariableID", "Kontakt2VariableID", "Kontakt1Invertiert",
"Kontakt2Invertiert", "Nennleistung", "Rueckmeldungsart", "IstleistungVariableID",
"Laufschwelle", "Anlaufwartezeit", "Wiederholsperre", "Mindestlaufzeit",
"Mindestsperrzeit", "MaximaleSperrzeit", "MaximaleSperrzeit24h"
]),
boiler_multi: new Set([
"Boilervolumen", "Hysterese", "LegionellenfunktionAktiv",
"LegionellenMinimalintervallTage", "LegionellenMaximalintervallTage"
])
});
export const consumerWizardFields = Object.freeze(Object.fromEntries(
Object.entries(consumerFields).map(([key, fields]) => [
key,
fields.filter(field => consumerWizardFieldKeys[key].has(field.key))
])
));
export const evGatewayWizardFields = Object.freeze(
evGatewayFields.filter(field => [
"Betriebsmodus", "Ladestationskennung", "MaximalerLadestrom", "Ladefreigabe", "Solarladen"
].includes(field.key))
);
export const deviceListFields = Object.freeze({
powerLevel: [
f("Stufe", "Stufe", "integer", 1, "Leistungsstufe", { min: 1 }),
f("Leistung", "Leistung (W)", "integer", 0, "Leistungsstufe", { min: 1 })
],
schedule: [
f("Uhrzeit", "Uhrzeit", "text", "00:00", "Zeitplan"),
f("Solltemperatur", "Solltemperatur (°C)", "number", 50, "Zeitplan", { min: 0, max: 100 })
]
});
function text(value, maxLength) {
return String(value || "").trim().replace(/\s+/g, " ").slice(0, maxLength);
}
function count(value, maximum = 50) {
const parsed = Number(value || 0);
if (!Number.isInteger(parsed) || parsed < 0 || parsed > maximum) {
throw Object.assign(new Error("invalid_quantity"), { status: 400, publicMessage: "Mengen müssen zwischen 0 und " + maximum + " liegen." });
}
return parsed;
}
function bool(value) {
return value === true || value === 1 || value === "1" || value === "true" || value === "on";
}
function finite(value, fallback, minimum = -Number.MAX_VALUE, maximum = Number.MAX_VALUE, integer = false) {
const parsed = Number(value);
if (!Number.isFinite(parsed)) return fallback;
const bounded = Math.max(minimum, Math.min(maximum, parsed));
return integer ? Math.round(bounded) : bounded;
}
function jsonText(value, fallback) {
if (value === undefined || value === null || value === "") return fallback;
try {
return JSON.stringify(typeof value === "string" ? JSON.parse(value) : value);
} catch {
throw Object.assign(new Error("invalid_configuration_json"), { status: 400, publicMessage: "Ein JSON-Konfigurationsfeld ist ungültig." });
}
}
function normalizeProperties(input, fields) {
const source = input && typeof input === "object" ? input : {};
return Object.fromEntries(fields.map(field => {
const raw = Object.prototype.hasOwnProperty.call(source, field.key) ? source[field.key] : field.default;
if (field.type === "boolean") return [field.key, bool(raw)];
if (field.type === "integer" || field.type === "select") return [field.key, finite(raw, field.default, field.min, field.max, true)];
if (field.type === "number") return [field.key, finite(raw, field.default, field.min, field.max, false)];
if (field.type === "json") return [field.key, jsonText(raw, field.default)];
return [field.key, text(raw, 240)];
}));
}
function parseList(value) {
if (Array.isArray(value)) return value;
if (typeof value !== "string" || value.trim() === "") return [];
try {
const parsed = JSON.parse(value);
return Array.isArray(parsed) ? parsed : [];
} catch {
return [];
}
}
function normalizeTopologyValues(input, fields) {
const source = input && typeof input === "object" ? input : {};
return Object.fromEntries(fields.map(field => {
const raw = Object.prototype.hasOwnProperty.call(source, field.key) ? source[field.key] : field.default;
if (field.type === "integer") return [field.key, finite(raw, field.default, field.min, field.max, true)];
if (field.type === "number") return [field.key, finite(raw, field.default, field.min, field.max, false)];
if (field.type === "choice") {
const allowed = (field.options || []).map(option => option.value);
return [field.key, allowed.includes(raw) ? raw : field.default];
}
return [field.key, text(raw, 120)];
}));
}
function normalizePvSurfaces(input) {
return (Array.isArray(input) ? input : []).slice(0, 50).map((item, index) => ({
ID: text(item?.ID || "pv-" + (index + 1), 60),
...normalizeTopologyValues(item, topologyFields.pvSurface),
WechselrichterID: "wr-1"
}));
}
function normalizeBatteryTopology(input) {
return normalizeTopologyValues(input, topologyFields.battery);
}
function normalizePowerLevels(input) {
return parseList(input).slice(0, 12).map((item, index) => ({
...normalizeTopologyValues({ ...item, Stufe: item?.Stufe ?? index + 1 }, deviceListFields.powerLevel),
integration: normalizeIntegration(item?.integration || { profile: "manual", sourceVariableId: item?.Schaltkontakt_Stufe || 0 }, "actuator")
}));
}
function normalizeSchedule(input) {
return parseList(input).slice(0, 24).map(item => {
const value = normalizeTopologyValues(item, deviceListFields.schedule);
return {
Uhrzeit: /^([01]\d|2[0-3]):[0-5]\d$/.test(value.Uhrzeit) ? value.Uhrzeit : "00:00",
Solltemperatur: value.Solltemperatur
};
});
}
function batteryTypeForProfile(profileId) {
if (profileId === "goodwe_lynx") return 2;
if (profileId === "solaredge_home_battery") return 3;
if (profileId === "sigenergy_sigenstor") return 4;
return profileId === "manual" ? 0 : 1;
}
function profile(kind, value) {
const profiles = integrationProfiles[kind];
return profiles.find(item => item.id === value) || profiles[0];
}
function normalizeIntegration(input, kind) {
const source = input && typeof input === "object" ? input : {};
const selected = profile(kind, text(source.profile, 60));
const result = {
profile: selected.id,
manufacturer: selected.manufacturer,
protocol: selected.protocol,
setup: selected.setup,
host: text(source.host, 120),
port: finite(source.port, selected.defaultPort || 0, 0, 65535, true),
unitId: finite(source.unitId, 1, 0, 247, true),
topicPrefix: text(source.topicPrefix, 180),
channel: finite(source.channel, 0, 0, Math.max(0, (selected.outputs || 1) - 1), true),
sourceVariableId: finite(source.sourceVariableId, 0, 0, 2147483647, true),
feedbackVariableId: finite(source.feedbackVariableId, 0, 0, 2147483647, true),
notes: text(source.notes, 300)
};
if (selected.requiresHost && result.host === "") {
throw Object.assign(new Error("integration_host_missing"), { status: 400, publicMessage: "Bitte für " + selected.name + " eine IP-Adresse oder einen Hostnamen angeben." });
}
if (selected.requiresTopic && result.topicPrefix === "") {
throw Object.assign(new Error("integration_topic_missing"), { status: 400, publicMessage: "Bitte für " + selected.name + " das MQTT-Topic-Präfix angeben." });
}
return result;
}
function legacyItem(key, source, index) {
const integration = {};
if (key === "battery") {
integration.profile = source.profile || "manual";
integration.host = source.address || source.host || "";
} else if (["buffer_storage", "consumer_single", "boiler_multi"].includes(key)) {
integration.profile = source.hardware === "shelly_1pm" ? "shelly_plus_1pm" : "manual";
integration.topicPrefix = source.topicPrefix || "";
}
const properties = {};
if (key === "consumer_single" && source.ratedPower) properties.Nennleistung = source.ratedPower;
if (key === "boiler_multi" && source.volume) properties.Boilervolumen = source.volume;
if (key === "ev_charger") {
const types = { goe_legacy: 1, goe_gemini: 2, smartme_pico: 3 };
properties.Geraetetyp = types[source.model] || 0;
properties.Geraeteadresse = source.address || "";
}
return {
name: source.name || systemDeviceTypes[key].name + " " + (index + 1),
variant: key === "ev_charger" ? "standalone" : undefined,
integration,
properties,
topology: key === "battery" ? normalizeBatteryTopology(source.topology) : undefined,
powerLevels: ["buffer_storage", "boiler_multi"].includes(key) ? [] : undefined,
schedule: key === "boiler_multi" ? [] : undefined,
sensor: ["buffer_storage", "boiler_multi"].includes(key) ? { profile: "manual", sourceVariableId: 0 } : undefined
};
}
function normalizeDevice(key, input) {
const source = input && typeof input === "object" ? input : {};
const requested = count(source.quantity);
const rawItems = Array.isArray(source.items) ? source.items.slice(0, requested) : [];
while (rawItems.length < requested) rawItems.push(legacyItem(key, source, rawItems.length));
const items = rawItems.map((item, index) => {
const variant = key === "ev_charger" && item.variant === "gateway" ? "gateway" : "standalone";
const kind = key === "battery" ? "storage" : (["buffer_storage", "consumer_single", "boiler_multi", "heat_pump"].includes(key) ? "actuator" : null);
const integration = kind ? normalizeIntegration(key === "heat_pump" ? { profile: "manual" } : item.integration, kind) : {
profile: text(item.integration?.profile || "device_api", 60),
host: text(item.integration?.host, 120),
port: finite(item.integration?.port, 0, 0, 65535, true),
unitId: finite(item.integration?.unitId, 1, 0, 247, true),
notes: text(item.integration?.notes, 300)
};
const propertyFields = key === "ev_charger" && variant === "gateway" ? evGatewayFields : consumerFields[key];
const properties = normalizeProperties(item.properties, propertyFields);
if (key === "battery") properties.Batterietyp = batteryTypeForProfile(integration.profile);
const result = {
name: text(item.name || systemDeviceTypes[key].name + " " + (index + 1), 80),
variant,
integration,
properties
};
if (key === "battery") result.topology = normalizeBatteryTopology(item.topology);
if (["buffer_storage", "boiler_multi"].includes(key)) {
result.powerLevels = normalizePowerLevels(item.powerLevels ?? item.properties?.LeistungsStufen);
result.sensor = normalizeIntegration(item.sensor || {
profile: "manual",
sourceVariableId: key === "boiler_multi"
? item.properties?.Boilerfuehler_PT1
: item.properties?.PufferfuehlerVariableID
}, "sensor");
result.properties.LeistungsStufen = JSON.stringify(result.powerLevels.map(level => ({
Stufe: level.Stufe,
Leistung: level.Leistung,
Schaltkontakt_Stufe: level.integration.sourceVariableId || 0
})));
}
if (key === "boiler_multi") {
result.schedule = normalizeSchedule(item.schedule ?? item.properties?.Zeitplan);
result.properties.Zeitplan = JSON.stringify(result.schedule);
}
return result;
});
return { quantity: requested, items };
}
export function normalizeSystemConfiguration(input = {}) {
const inputDevices = input.devices || {};
const hasSeparatedStorage = Object.prototype.hasOwnProperty.call(inputDevices, "buffer_storage");
const devices = {};
for (const key of Object.keys(systemDeviceTypes)) {
let source = inputDevices[key] || {};
if (!hasSeparatedStorage && key === "buffer_storage") source = inputDevices.battery || {};
if (!hasSeparatedStorage && key === "battery") source = {};
devices[key] = normalizeDevice(key, source);
}
const legacyGridProfile = input.gridMeasurement?.manufacturer === "fronius" ? "fronius_solar_api_v1" : "manual";
const legacyPvProfile = input.pvSystem?.manufacturer === "fronius" ? "fronius_solar_api_v1" : "manual";
const legacyInverter = parseList(input.manager?.properties?.AnlagenWechselrichter)[0] || {};
const legacyPvSurfaces = parseList(input.manager?.properties?.AnlagenPVFlaechen);
const pvEnabled = bool(input.pvSystem?.enabled);
const pvSystem = {
enabled: pvEnabled,
id: "wr-1",
name: text(input.pvSystem?.name || legacyInverter.Name || "PV-Wechselrichter", 80),
acPowerKw: finite(input.pvSystem?.acPowerKw ?? legacyInverter.ACNennleistung_kW, 0, 0),
...normalizeIntegration({
...input.pvSystem,
profile: pvEnabled ? (input.pvSystem?.profile || legacyPvProfile) : "manual"
}, "energy")
};
const topology = {
pvSurfaces: normalizePvSurfaces(input.topology?.pvSurfaces ?? legacyPvSurfaces)
};
const result = {
schemaVersion: 4,
name: text(input.name, 100),
location: text(input.location, 120),
siteType: ["Privat", "Gewerbe", "Industrie"].includes(input.siteType) ? input.siteType : "Gewerbe",
peakShaving: bool(input.peakShaving),
manager: { properties: normalizeProperties(input.manager?.properties, managerFields) },
gridMeasurement: normalizeIntegration({ ...input.gridMeasurement, profile: input.gridMeasurement?.profile || legacyGridProfile }, "energy"),
pvSystem,
topology,
costReport: {
enabled: bool(input.costReport?.enabled),
powerMeters: count(input.costReport?.powerMeters, 999),
auxiliaryMeters: count(input.costReport?.auxiliaryMeters, 999)
},
devices
};
result.manager.properties.Lastspitzenmodus = result.peakShaving
? Math.max(1, result.manager.properties.Lastspitzenmodus)
: 0;
if (result.pvSystem.enabled && result.topology.pvSurfaces.length === 0) {
result.topology.pvSurfaces = normalizePvSurfaces([{ Name: "PV-Fläche 1" }]);
}
if (!result.pvSystem.enabled) {
result.pvSystem = { ...result.pvSystem, profile: "manual", host: "", port: 0 };
result.topology.pvSurfaces = [];
}
const inverters = result.pvSystem.enabled ? [{
ID: "wr-1",
Name: result.pvSystem.name,
Typ: "pv",
ACNennleistung_kW: result.pvSystem.acPowerKw,
IstleistungVariableID: result.pvSystem.sourceVariableId,
Istleistungsfaktor: 1,
ErzeugungsenergieVariableID: 0,
Erzeugungsenergiefaktor: 1,
BegrenzungVariableID: 0,
Begrenzungsart: "percent"
}] : [];
const batteries = result.devices.battery.items.map((item, index) => ({
ID: "bat-" + (index + 1),
Name: item.name,
...item.topology,
WechselrichterID: item.topology.Kopplung === "ac" || !result.pvSystem.enabled ? "" : "wr-1",
LeistungVariableID: item.integration.sourceVariableId || item.properties.IstleistungVariableID || 0,
Leistungsfaktor: 1,
SOCVariableID: item.properties.LadezustandVariableID || 0,
LadeenergieVariableID: 0,
EntladeenergieVariableID: 0,
Energiefaktor: 1
}));
result.manager.properties.AnlagenWechselrichter = JSON.stringify(inverters);
result.manager.properties.AnlagenPVFlaechen = JSON.stringify(result.topology.pvSurfaces);
result.manager.properties.AnlagenBatterien = JSON.stringify(batteries);
if (result.name.length < 2 || result.location.length < 2) {
throw Object.assign(new Error("invalid_system_configuration"), { status: 400, publicMessage: "Bitte Systemname und Standort angeben." });
}
return result;
}
export function licenseCoverage(requested = {}, owned = {}) {
const coverage = { ...owned };
const spareConsumers = Math.max(0, Number(owned.consumer_single || 0) - Number(requested.consumer_single || 0));
coverage.heat_pump = Number(owned.heat_pump || 0) + Math.min(spareConsumers,
Math.max(0, Number(requested.heat_pump || 0) - Number(owned.heat_pump || 0)));
return coverage;
}
export function recommendLicenses(configuration) {
const modules = {};
for (const key of Object.keys(systemDeviceTypes)) modules[key] = configuration.devices[key].quantity;
return {
managerVariant: configuration.peakShaving ? "manager_peak" : "manager_standard",
modules,
costReport: configuration.costReport
};
}
export function publicWizardCatalog() {
return {
schemaVersion: 4,
integrations: integrationProfiles,
managerFields: managerWizardFields,
topologyFields,
consumerFields: consumerWizardFields,
evGatewayFields: evGatewayWizardFields,
deviceListFields,
evVariants: [
{ value: "standalone", label: "Direkt / Stand-alone" },
{ value: "gateway", label: "Easee über Gateway" }
],
deviceTypes: Object.fromEntries(Object.entries(systemDeviceTypes).map(([key, item]) => [key, { name: item.name }]))
};
}
const activationAlphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
export function createActivationCode() {
const bytes = randomBytes(16);
let body = "";
for (let index = 0; index < 16; index += 1) body += activationAlphabet[bytes[index] % activationAlphabet.length];
return "ENX-" + body.slice(0, 4) + "-" + body.slice(4, 8) + "-" + body.slice(8, 12) + "-" + body.slice(12, 16);
}
export function normalizeActivationCode(value) {
return String(value || "").toUpperCase().replace(/[^A-Z0-9]/g, "");
}
export function activationCodeValid(value) {
return /^ENX[A-HJ-NP-Z2-9]{16}$/.test(normalizeActivationCode(value));
}