feat(portal): prepare heat pump licensing and header navigation
Tests / test (push) Failing after 49s
Tests / test (push) Failing after 49s
This commit is contained in:
@@ -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 }
|
||||
]);
|
||||
}
|
||||
Reference in New Issue
Block a user