666 lines
39 KiB
JavaScript
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));
|
|
}
|