feat(portal): prepare heat pump licensing and header navigation
Tests / test (push) Failing after 49s

This commit is contained in:
dh
2026-10-06 14:35:17 +00:00
parent faa905ab09
commit 5687596799
14 changed files with 9125 additions and 0 deletions
@@ -0,0 +1,485 @@
import { normalizeSystemConfiguration, recommendLicenses, systemDeviceTypes } from "./portal-domain.mjs";
const managerModuleId = "{6F771B18-59D4-4C8A-B951-3B2FE9F6A2C4}";
const shellyModuleId = "{21D82EDB-AC8E-4F3D-9FEE-D20BCAFC1C33}";
const costReportModuleId = "{9DAC29C7-F381-F2A4-7CE2-F391C4123F38}";
function crc32(buffer) {
let crc = 0xffffffff;
for (const byte of buffer) {
crc ^= byte;
for (let bit = 0; bit < 8; bit += 1) crc = (crc >>> 1) ^ ((crc & 1) ? 0xedb88320 : 0);
}
return (crc ^ 0xffffffff) >>> 0;
}
function dosDateTime(date = new Date()) {
const year = Math.max(1980, date.getFullYear());
return {
time: (date.getHours() << 11) | (date.getMinutes() << 5) | Math.floor(date.getSeconds() / 2),
date: ((year - 1980) << 9) | ((date.getMonth() + 1) << 5) | date.getDate()
};
}
function zipStore(entries) {
const localParts = [];
const centralParts = [];
let offset = 0;
const stamp = dosDateTime();
for (const entry of entries) {
const name = Buffer.from(entry.name, "utf8");
const data = Buffer.isBuffer(entry.data) ? entry.data : Buffer.from(entry.data, "utf8");
const checksum = crc32(data);
const local = Buffer.alloc(30);
local.writeUInt32LE(0x04034b50, 0); local.writeUInt16LE(20, 4); local.writeUInt16LE(0x0800, 6);
local.writeUInt16LE(0, 8); local.writeUInt16LE(stamp.time, 10); local.writeUInt16LE(stamp.date, 12);
local.writeUInt32LE(checksum, 14); local.writeUInt32LE(data.length, 18); local.writeUInt32LE(data.length, 22);
local.writeUInt16LE(name.length, 26); local.writeUInt16LE(0, 28);
localParts.push(local, name, data);
const central = Buffer.alloc(46);
central.writeUInt32LE(0x02014b50, 0); central.writeUInt16LE(20, 4); central.writeUInt16LE(20, 6);
central.writeUInt16LE(0x0800, 8); central.writeUInt16LE(0, 10); central.writeUInt16LE(stamp.time, 12);
central.writeUInt16LE(stamp.date, 14); central.writeUInt32LE(checksum, 16); central.writeUInt32LE(data.length, 20);
central.writeUInt32LE(data.length, 24); central.writeUInt16LE(name.length, 28); central.writeUInt16LE(0, 30);
central.writeUInt16LE(0, 32); central.writeUInt16LE(0, 34); central.writeUInt16LE(0, 36);
central.writeUInt32LE(0, 38); central.writeUInt32LE(offset, 42);
centralParts.push(central, name);
offset += local.length + name.length + data.length;
}
const centralDirectory = Buffer.concat(centralParts);
const end = Buffer.alloc(22);
end.writeUInt32LE(0x06054b50, 0); end.writeUInt16LE(0, 4); end.writeUInt16LE(0, 6);
end.writeUInt16LE(entries.length, 8); end.writeUInt16LE(entries.length, 10);
end.writeUInt32LE(centralDirectory.length, 12); end.writeUInt32LE(offset, 16); end.writeUInt16LE(0, 20);
return Buffer.concat([...localParts, centralDirectory, end]);
}
function provisioningScript(configuration) {
const encoded = Buffer.from(JSON.stringify(configuration), "utf8").toString("base64");
const modules = Object.fromEntries(Object.entries(systemDeviceTypes).map(([key, item]) => [key, item.moduleId]));
return `<?php
declare(strict_types=1);
$config = json_decode(base64_decode('${encoded}'), true, 512, JSON_THROW_ON_ERROR);
$moduleIDs = json_decode('${JSON.stringify(modules)}', true, 512, JSON_THROW_ON_ERROR);
$managerModuleID = '${managerModuleId}';
$shellyModuleID = '${shellyModuleId}';
$costReportModuleID = '${costReportModuleId}';
$energyPieModuleID = '{F73B8F8D-5E36-4B23-B379-4632A517A1D6}';
$energyFlowModuleID = '{BA90FDB3-C203-8F26-B4D6-F3DB99905724}';
$evGatewayModuleID = '{32F65988-BE55-4FD6-9FFF-CF1C4C8628F8}';
$easeeGatewayModuleID = '{B7552FC9-87BC-49D0-8CCB-076BBB045BBE}';
if (version_compare(IPS_GetKernelVersion(), '8.0', '<') || version_compare(IPS_GetKernelVersion(), '9.0', '>=')) {
throw new RuntimeException('Dieses Einrichtungspaket benoetigt IP-Symcon 8.x.');
}
function enxFind(int $parent, string $ident): int {
$id = @IPS_GetObjectIDByIdent($ident, $parent);
return $id === false ? 0 : (int) $id;
}
function enxCategory(int $parent, string $ident, string $name): int {
$id = enxFind($parent, $ident);
if ($id === 0) { $id = IPS_CreateCategory(); IPS_SetParent($id, $parent); IPS_SetIdent($id, $ident); }
IPS_SetName($id, $name);
return $id;
}
function enxInstance(int $parent, string $ident, string $name, string $moduleID): int {
$id = enxFind($parent, $ident);
if ($id === 0) { $id = IPS_CreateInstance($moduleID); IPS_SetParent($id, $parent); IPS_SetIdent($id, $ident); }
if (IPS_GetInstance($id)['ModuleInfo']['ModuleID'] !== $moduleID) throw new RuntimeException('Modulkonflikt bei ' . $ident);
IPS_SetName($id, $name);
return $id;
}
function enxVariable(int $parent, string $ident, string $name, string $profile = '~Watt'): int {
$id = enxFind($parent, $ident);
if ($id === 0) { $id = IPS_CreateVariable(2); IPS_SetParent($id, $parent); IPS_SetIdent($id, $ident); }
IPS_SetName($id, $name);
if ($profile !== '' && IPS_VariableProfileExists($profile)) IPS_SetVariableCustomProfile($id, $profile);
return $id;
}
function enxScript(int $parent, string $ident, string $name): int {
$id = enxFind($parent, $ident);
if ($id === 0) { $id = IPS_CreateScript(0); IPS_SetParent($id, $parent); IPS_SetIdent($id, $ident); }
IPS_SetName($id, $name);
return $id;
}
function enxLink(int $parent, string $ident, string $name, int $target): int {
$id = enxFind($parent, $ident);
if ($id === 0) { $id = IPS_CreateLink(); IPS_SetParent($id, $parent); IPS_SetIdent($id, $ident); }
IPS_SetName($id, $name);
IPS_SetLinkTargetID($id, $target);
return $id;
}
function enxShellyBinding(
int $parent,
string $ident,
string $name,
string $topic,
int $channel,
int $target,
string $role,
int $levelIndex,
string $shellyModuleID
): void {
if ($topic === '') throw new RuntimeException('MQTT-Topic fuer ' . $name . ' fehlt.');
$hardware = enxInstance($parent, 'SHELLY_' . $ident, 'Shelly ' . $name, $shellyModuleID);
IPS_SetProperty($hardware, 'UseDeviceTopicFilter', true);
IPS_SetProperty($hardware, 'DeviceTopicPrefix', $topic);
IPS_SetProperty($hardware, 'CreateSwitches', $role !== 'sensor');
IPS_SetProperty($hardware, 'CreatePower', $role !== 'sensor');
IPS_SetProperty($hardware, 'CreateEnergy', $role !== 'sensor');
IPS_SetProperty($hardware, 'CreateTemperature', $role === 'sensor');
IPS_ApplyChanges($hardware);
$binding = base64_encode(json_encode([
'target' => $target,
'hardware' => $hardware,
'topic' => $topic,
'channel' => $channel,
'role' => $role,
'levelIndex' => $levelIndex,
], JSON_THROW_ON_ERROR));
$finalizer = enxScript($parent, 'SHELLY_LINK_' . $ident, 'Shelly-Zuordnung ' . $name);
$script = '<?php' . "\n"
. '$binding = json_decode(base64_decode(' . var_export($binding, true) . '), true, 512, JSON_THROW_ON_ERROR);' . "\n"
. '$key = trim(preg_replace("/[^a-z0-9_]+/", "_", strtolower($binding["topic"])), "_");' . "\n"
. '$key = substr($key === "" ? "device" : $key, 0, 48) . "_" . substr(hash("sha256", $binding["topic"]), 0, 8);' . "\n"
. '$folder = @IPS_GetObjectIDByIdent("folder_" . $key, $binding["hardware"]);' . "\n"
. 'if ($folder === false) return;' . "\n"
. '$variable = false;' . "\n"
. 'if ($binding["role"] === "sensor") {' . "\n"
. ' foreach (IPS_GetChildrenIDs((int) $folder) as $candidate) {' . "\n"
. ' if (!IPS_VariableExists($candidate)) continue;' . "\n"
. ' $meta = IPS_GetVariable($candidate);' . "\n"
. ' if (($meta["VariableCustomProfile"] ?? $meta["VariableProfile"] ?? "") === "~Temperature") { $variable = $candidate; break; }' . "\n"
. ' }' . "\n"
. '} else {' . "\n"
. ' $variable = @IPS_GetObjectIDByIdent($key . "_output_" . $binding["channel"], (int) $folder);' . "\n"
. '}' . "\n"
. 'if ($variable === false) return;' . "\n"
. 'if ($binding["role"] === "consumer") {' . "\n"
. ' IPS_SetProperty($binding["target"], "SchaltkontaktVariableID", (int) $variable);' . "\n"
. ' IPS_SetProperty($binding["target"], "RueckmeldungVariableID", (int) $variable);' . "\n"
. '} elseif ($binding["role"] === "level") {' . "\n"
. ' $levels = json_decode((string) IPS_GetProperty($binding["target"], "LeistungsStufen"), true);' . "\n"
. ' if (!is_array($levels) || !isset($levels[$binding["levelIndex"]])) return;' . "\n"
. ' $levels[$binding["levelIndex"]]["Schaltkontakt_Stufe"] = (int) $variable;' . "\n"
. ' IPS_SetProperty($binding["target"], "LeistungsStufen", json_encode($levels, JSON_THROW_ON_ERROR));' . "\n"
. '} elseif ($binding["role"] === "sensor") {' . "\n"
. ' $property = IPS_GetInstance($binding["target"])["ModuleInfo"]["ModuleID"] === "{B7C54AF4-AD7D-4FE4-B75D-203693906251}" ? "Boilerfuehler_PT1" : "PufferfuehlerVariableID";' . "\n"
. ' IPS_SetProperty($binding["target"], $property, (int) $variable);' . "\n"
. '}' . "\n"
. 'IPS_ApplyChanges($binding["target"]);' . "\n"
. 'IPS_SetScriptTimer($_IPS["SELF"], 0);';
IPS_SetScriptContent($finalizer, $script);
IPS_SetScriptTimer($finalizer, 15);
IPS_RunScript($finalizer);
}
$required = [$managerModuleID];
foreach ($config['devices'] as $key => $device) {
foreach (($device['items'] ?? []) as $item) {
if ($key === 'ev_charger' && ($item['variant'] ?? 'standalone') === 'gateway') {
$required[] = $evGatewayModuleID;
$required[] = $easeeGatewayModuleID;
} else {
$required[] = $moduleIDs[$key];
}
$integrations = [$item['integration'] ?? [], $item['sensor'] ?? []];
foreach (($item['powerLevels'] ?? []) as $level) $integrations[] = $level['integration'] ?? [];
foreach ($integrations as $integration) {
if (str_starts_with((string) ($integration['profile'] ?? ''), 'shelly_')) $required[] = $shellyModuleID;
}
}
}
if (($config['costReport']['enabled'] ?? false) === true) $required[] = $costReportModuleID;
if (($config['manager']['properties']['EnergyPieAnzeigen'] ?? false) === true) $required[] = $energyPieModuleID;
if (($config['manager']['properties']['EnergieflussAnzeigen'] ?? false) === true) $required[] = $energyFlowModuleID;
foreach (array_unique($required) as $moduleID) {
if (!in_array($moduleID, IPS_GetModuleList(), true)) throw new RuntimeException('Erforderliches Modul fehlt: ' . $moduleID);
}
// Validate heat-pump bindings before creating or changing any system objects.
foreach (($config['devices']['heat_pump']['items'] ?? []) as $item) {
if (!in_array($moduleIDs['heat_pump'], IPS_GetModuleList(), true)) {
throw new RuntimeException('Das Enelix-Waermepumpenmodul fehlt.');
}
$properties = $item['properties'];
$contact1 = (int) $properties['Kontakt1VariableID'];
$contact2 = (int) $properties['Kontakt2VariableID'];
if ($contact1 === $contact2 || (int) $properties['Nennleistung'] <= 0) {
throw new RuntimeException('Waermepumpe: zwei unterschiedliche Kontakte und Nennleistung erforderlich.');
}
foreach ([$contact1, $contact2] as $contact) {
if (!IPS_VariableExists($contact)) throw new RuntimeException('Waermepumpe: Kontaktvariable fehlt.');
$variable = IPS_GetVariable($contact);
if ($variable['VariableType'] !== 0 || max($variable['VariableAction'], $variable['VariableCustomAction']) <= 0) {
throw new RuntimeException('Waermepumpe: Kontakte muessen schaltbare Boolean-Variablen sein.');
}
}
$feedback = (int) $properties['IstleistungVariableID'];
if (!IPS_VariableExists($feedback)) throw new RuntimeException('Waermepumpe: Betriebsrueckmeldung fehlt.');
$feedbackType = IPS_GetVariable($feedback)['VariableType'];
$allowedTypes = (int) $properties['Rueckmeldungsart'] === 1 ? [0] : [1, 2];
if (!in_array($feedbackType, $allowedTypes, true)) {
throw new RuntimeException('Waermepumpe: Typ der Betriebsrueckmeldung passt nicht zur Rueckmeldungsart.');
}
}
$root = enxCategory(0, 'ENELIX_System', (string) $config['name']);
$visualisation = enxCategory($root, 'Visualisierung', 'Visualisierung');
$energyView = enxCategory($visualisation, 'Energie', 'Energie und Bilanz');
$consumerView = enxCategory($visualisation, 'Verbraucher', 'Verbraucher und Speicher');
$serviceView = enxCategory($visualisation, 'Service', 'Service und Diagnose');
$manager = enxInstance($root, 'Manager', 'Enelix Manager', $managerModuleID);
enxLink($energyView, 'Manager', 'Enelix Manager', $manager);
foreach (($config['manager']['properties'] ?? []) as $property => $value) {
IPS_SetProperty($manager, (string) $property, $value);
}
if (($config['manager']['properties']['AutomatischeSuche'] ?? true) && (int) ($config['manager']['properties']['SuchbereichID'] ?? 0) === 0) {
IPS_SetProperty($manager, 'SuchbereichID', $root);
}
IPS_ApplyChanges($manager);
$froniusHost = '';
$energyVariables = [];
$froniusProfiles = ['fronius_solar_api_v1', 'fronius_hybrid_storage'];
foreach ([$config['pvSystem'] ?? [], $config['gridMeasurement'] ?? []] as $source) {
if (in_array((string) ($source['profile'] ?? ''), $froniusProfiles, true) && (string) ($source['host'] ?? '') !== '') {
$froniusHost = (string) $source['host'];
break;
}
}
if ($froniusHost === '') {
foreach (($config['devices']['battery']['items'] ?? []) as $item) {
$source = $item['integration'] ?? [];
if (in_array((string) ($source['profile'] ?? ''), $froniusProfiles, true) && (string) ($source['host'] ?? '') !== '') {
$froniusHost = (string) $source['host'];
break;
}
}
}
if ($froniusHost !== '') {
$energy = enxCategory($root, 'FroniusEnergy', 'Fronius Energiefluss');
$energyVariables = [];
$variableDefinitions = [
'GridPower' => ['Netzleistung', '~Watt'],
'PvPower' => ['PV-Leistung', '~Watt'],
'LoadPower' => ['Verbrauch', '~Watt'],
'BatteryPower' => ['Batterieleistung', '~Watt'],
'BatterySoc' => ['Batterie-Ladezustand', '~Intensity.100'],
];
foreach ($variableDefinitions as $ident => [$name, $profile]) {
$energyVariables[$ident] = enxVariable($energy, $ident, $name, $profile);
}
$script = enxScript($energy, 'FroniusPoll', 'Fronius Solar API einlesen');
$scriptContent = '$host = ' . var_export($froniusHost, true) . ";\n" . <<<'ENELIX'
$energy = IPS_GetParent($_IPS['SELF']);
try {
$raw = Sys_GetURLContent('http://' . $host . '/solar_api/v1/GetPowerFlowRealtimeData.fcgi');
$payload = json_decode($raw, true, 512, JSON_THROW_ON_ERROR);
$site = $payload['Body']['Data']['Site'] ?? [];
$inverters = $payload['Body']['Data']['Inverters'] ?? [];
$firstInverter = is_array($inverters) && $inverters !== [] ? reset($inverters) : [];
$values = [
'GridPower' => $site['P_Grid'] ?? null,
'PvPower' => $site['P_PV'] ?? null,
'LoadPower' => isset($site['P_Load']) ? abs((float) $site['P_Load']) : null,
'BatteryPower' => $site['P_Akku'] ?? null,
'BatterySoc' => is_array($firstInverter) ? ($firstInverter['SOC'] ?? null) : null,
];
foreach ($values as $ident => $value) {
$variable = @IPS_GetObjectIDByIdent($ident, $energy);
if ($variable !== false && is_numeric($value)) SetValueFloat((int) $variable, (float) $value);
}
} catch (Throwable $error) {
IPS_LogMessage('Enelix Fronius', $error->getMessage());
}
ENELIX;
IPS_SetScriptContent($script, "<?php\n" . $scriptContent);
IPS_SetScriptTimer($script, 60);
IPS_RunScript($script);
if (in_array((string) ($config['gridMeasurement']['profile'] ?? ''), $froniusProfiles, true)) {
IPS_SetProperty($manager, 'NetzleistungVariableID', $energyVariables['GridPower']);
}
IPS_SetProperty($manager, 'MessungPVLeistungVariableID', $energyVariables['PvPower']);
IPS_SetProperty($manager, 'MessungHausverbrauchLeistungVariableID', $energyVariables['LoadPower']);
IPS_SetProperty($manager, 'MessungBatterieleistungVariableID', $energyVariables['BatteryPower']);
}
if ((int) ($config['gridMeasurement']['sourceVariableId'] ?? 0) > 0) {
IPS_SetProperty($manager, 'NetzleistungVariableID', (int) $config['gridMeasurement']['sourceVariableId']);
}
IPS_ApplyChanges($manager);
foreach ([
'EnergyPie' => 'Energie-Kreisdiagramm',
'Energiefluss' => 'Energiefluss',
'Energiediagramm' => 'Energiediagramme',
'EnergieFunFacts' => 'Energy Facts',
] as $ident => $name) {
$target = enxFind($manager, $ident);
if ($target > 0) enxLink($energyView, $ident, $name, $target);
}
foreach (['Aktiv' => 'Manager aktiv', 'Betriebsart' => 'Betriebsart', 'Netzleistung' => 'Netzleistung'] as $ident => $name) {
$target = enxFind($manager, $ident);
if ($target > 0) enxLink($serviceView, 'Manager_' . $ident, $name, $target);
}
$easeeGateway = 0;
foreach ($config['devices'] as $key => $device) {
foreach (($device['items'] ?? []) as $position => $item) {
$index = $position + 1;
$ident = strtoupper($key) . '_' . $index;
$label = (string) ($item['name'] ?? ($key . ' ' . $index));
$gatewayVariant = $key === 'ev_charger' && ($item['variant'] ?? 'standalone') === 'gateway';
$moduleID = $gatewayVariant ? $evGatewayModuleID : $moduleIDs[$key];
$instance = enxInstance($root, $ident, $label, $moduleID);
if ($gatewayVariant) {
if ($easeeGateway === 0) {
$easeeGateway = enxInstance($root, 'EaseeGateway', 'Easee Gateway', $easeeGatewayModuleID);
IPS_ApplyChanges($easeeGateway);
enxLink($serviceView, 'EaseeGateway', 'Easee Gateway', $easeeGateway);
}
$connection = (int) (IPS_GetInstance($instance)['ConnectionID'] ?? 0);
if ($connection === 0) IPS_ConnectInstance($instance, $easeeGateway);
elseif ($connection !== $easeeGateway) throw new RuntimeException('Unerwartete Easee-Gateway-Verbindung bei ' . $label);
}
foreach (($item['properties'] ?? []) as $property => $value) {
IPS_SetProperty($instance, (string) $property, $value);
}
$integration = $item['integration'] ?? [];
$profile = (string) ($integration['profile'] ?? '');
$sourceVariableID = (int) ($integration['sourceVariableId'] ?? 0);
if ($key === 'consumer_single' && $sourceVariableID > 0) {
IPS_SetProperty($instance, 'SchaltkontaktVariableID', $sourceVariableID);
IPS_SetProperty($instance, 'RueckmeldungVariableID', (int) ($integration['feedbackVariableId'] ?? $sourceVariableID) ?: $sourceVariableID);
}
if ($key === 'battery' && $sourceVariableID > 0) {
IPS_SetProperty($instance, 'IstleistungVariableID', $sourceVariableID);
}
if ($key === 'battery' && $profile === 'fronius_hybrid_storage' && $energyVariables !== []) {
$maxCharge = enxVariable($instance, 'AutoMaxLadeleistung', 'Maximale Ladeleistung');
$maxDischarge = enxVariable($instance, 'AutoMaxEntladeleistung', 'Maximale Entladeleistung');
SetValueFloat($maxCharge, 1000 * (float) ($item['topology']['MaxLadeleistung_kW'] ?? 0));
SetValueFloat($maxDischarge, 1000 * (float) ($item['topology']['MaxEntladeleistung_kW'] ?? 0));
IPS_SetProperty($instance, 'MaxLadeleistungVariableID', $maxCharge);
IPS_SetProperty($instance, 'MaxEntladeleistungVariableID', $maxDischarge);
IPS_SetProperty($instance, 'LadezustandVariableID', $energyVariables['BatterySoc']);
IPS_SetProperty($instance, 'NetzleistungVariableID', $energyVariables['GridPower']);
IPS_SetProperty($instance, 'IstleistungVariableID', $energyVariables['BatteryPower']);
}
$sensor = $item['sensor'] ?? [];
$sensorVariableID = (int) ($sensor['sourceVariableId'] ?? 0);
if ($sensorVariableID > 0 && $key === 'boiler_multi') IPS_SetProperty($instance, 'Boilerfuehler_PT1', $sensorVariableID);
if ($sensorVariableID > 0 && $key === 'buffer_storage') IPS_SetProperty($instance, 'PufferfuehlerVariableID', $sensorVariableID);
IPS_ApplyChanges($instance);
enxLink($consumerView, $ident, $label, $instance);
if ($key === 'consumer_single' && str_starts_with($profile, 'shelly_')) {
enxShellyBinding($instance, 'MAIN', $label, (string) $integration['topicPrefix'], (int) ($integration['channel'] ?? 0), $instance, 'consumer', 0, $shellyModuleID);
}
foreach (($item['powerLevels'] ?? []) as $levelIndex => $level) {
$levelIntegration = $level['integration'] ?? [];
if (str_starts_with((string) ($levelIntegration['profile'] ?? ''), 'shelly_')) {
enxShellyBinding($instance, 'LEVEL_' . ($levelIndex + 1), $label . ' Stufe ' . ($levelIndex + 1), (string) $levelIntegration['topicPrefix'], (int) ($levelIntegration['channel'] ?? 0), $instance, 'level', $levelIndex, $shellyModuleID);
}
}
if (str_starts_with((string) ($sensor['profile'] ?? ''), 'shelly_')) {
enxShellyBinding($instance, 'SENSOR', $label . ' Temperatur', (string) $sensor['topicPrefix'], 0, $instance, 'sensor', 0, $shellyModuleID);
}
}
}
if (($config['costReport']['enabled'] ?? false) === true) {
$report = enxInstance($root, 'Verbrauchskostenreport', 'Verbrauchskostenreport', $costReportModuleID);
IPS_SetProperty($report, 'PropertyText', (string) $config['name']);
IPS_ApplyChanges($report);
enxLink($energyView, 'Verbrauchskostenreport', 'Verbrauchskostenreport', $report);
}
echo 'Enelix-System wurde angelegt. Bitte Lizenzcodes, Variablenzuordnungen, MQTT-Verbindung und Geraetezugriffe kontrollieren.';
`;
}
function integrationChecklist(configuration) {
const lines = [
"Enelix Geräteintegrationen",
"",
"Automatisch bedeutet, dass das Bootstrap-Skript die lokale Auslesung oder Zuordnung vorbereitet.",
"Modellspezifisch bedeutet, dass Modell, Firmware und Registerstand vor der Inbetriebnahme geprüft werden müssen.",
""
];
const sources = [
["Netzmessung", configuration.gridMeasurement],
["PV-Wechselrichter", configuration.pvSystem?.enabled ? configuration.pvSystem : null]
];
for (const [label, source] of sources) {
if (!source) continue;
lines.push(label + ": " + source.manufacturer + " / " + source.protocol + " / " + source.setup);
if (source.host) lines.push(" Adresse: " + source.host + ":" + source.port + ", Geräte-ID " + source.unitId);
}
for (const [key, device] of Object.entries(configuration.devices)) {
for (const [index, item] of (device.items || []).entries()) {
const source = item.integration || {};
lines.push((item.name || key + " " + (index + 1)) + ": " + (source.manufacturer || "Geräte-API") + " / " + (source.protocol || source.profile || "manuell") + " / " + (source.setup || "manuell"));
if (source.host) lines.push(" Adresse: " + source.host + ":" + source.port + ", Geräte-ID " + source.unitId);
if (source.topicPrefix) lines.push(" MQTT: " + source.topicPrefix + ", Ausgang " + source.channel);
for (const level of item.powerLevels || []) {
const actuator = level.integration || {};
lines.push(" Stufe " + level.Stufe + ": " + level.Leistung + " W / " + (actuator.profile || "manuell"));
if (actuator.topicPrefix) lines.push(" MQTT: " + actuator.topicPrefix + ", Ausgang " + actuator.channel);
}
const sensor = item.sensor || {};
if (sensor.profile && sensor.profile !== "manual") lines.push(" Temperaturfühler: " + sensor.profile + " / MQTT " + sensor.topicPrefix);
}
}
return lines.join("\n") + "\n";
}
export function buildSymconPackage(configuration, recommendation) {
const normalizedConfiguration = normalizeSystemConfiguration(configuration);
const normalizedRecommendation = recommendLicenses(normalizedConfiguration);
const readme = `Enelix Symcon-Einrichtungspaket
Dieses Paket ist kein blind einspielbares Vollbackup. Es enthaelt ein idempotentes
Bootstrap-Skript fuer die IP-Symcon-Schnellausfuehrung und die normalisierte
Systemkonfiguration. Das ist sicherer, weil Objekt-IDs, Zugangsdaten und lokale
Schnittstellen erst auf der Zielinstallation zugeordnet werden koennen. Beim Lauf
werden Manager, Verbraucher, Geraeteanbindungen und die Visualisierungsstruktur
mit Energiefluss, Kreisdiagramm, Diagrammen und Verbraucherlinks angelegt.
Voraussetzungen:
- IP-Symcon 8.x
- Enelix EMS Module aus dem develop-Stand installiert
- Vor Ausfuehrung ein Backup der Zielinstallation erstellen
Ausfuehrung:
1. bootstrap.php in der IP-Symcon-Schnellausfuehrung oeffnen.
2. Inhalt ausfuehren.
3. Angelegte Instanzen, Variablenzuordnungen und Geraetezugriffe pruefen.
4. Den nach dem Kauf erzeugten Aktivierungscode im Enelix Manager eingeben.
5. Bei gewähltem Verbrauchskostenreport dessen separaten Lizenzcode in der Report-Instanz eintragen.
6. Bei Shelly Gen2+ den automatisch verbundenen MQTT Server prüfen und dasselbe Topic-Präfix am Gerät konfigurieren.
7. Schaltkontakte, Rückmeldungen, Leistungsstufen und Shelly-Temperaturfühler werden nach dem ersten MQTT-Datenpunkt automatisch zugeordnet.
8. INTEGRATIONEN.txt prüfen. Fronius Solar API und Shelly MQTT werden automatisch vorbereitet.
9. Bei Huawei, GoodWe, Sigenergy und modellabhängigen SunSpec-Geräten Modell, Firmware und Registerstand vor der Freigabe verifizieren.
`;
return zipStore([
{ name: "bootstrap.php", data: provisioningScript(normalizedConfiguration) },
{ name: "configuration.json", data: JSON.stringify({
configuration: normalizedConfiguration,
recommendation: normalizedRecommendation
}, null, 2) },
{ name: "INTEGRATIONEN.txt", data: integrationChecklist(normalizedConfiguration) },
{ name: "README.txt", data: readme }
]);
}