beta. OPC fixed + tested

This commit is contained in:
jhartworks
2026-06-29 15:24:26 +02:00
parent a12ef41d1b
commit 2ce3be068e
9 changed files with 1614 additions and 399 deletions
+390 -44
View File
@@ -1,4 +1,4 @@
import { createServer } from "node:http";
import { createServer } from "node:http";
import { mkdirSync, readFileSync, writeFileSync, existsSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
@@ -72,7 +72,7 @@ function replyJson(res, statusCode, body) {
res.writeHead(statusCode, {
"Content-Type": "application/json; charset=utf-8",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,OPTIONS",
"Access-Control-Allow-Methods": "GET,POST,PUT,PATCH,DELETE,OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
});
res.end(JSON.stringify(body));
@@ -130,16 +130,40 @@ function normalizePointKind(value) {
return String(value || "").toLowerCase() === "digital" ? "digital" : "analog";
}
function repairText(value) {
let current = String(value ?? "");
for (let attempt = 0; attempt < 3; attempt += 1) {
const currentScore = (current.match(/[\u00C3\u00C2\uFFFD]/g) || []).length;
if (!currentScore) {
break;
}
const repaired = Buffer.from(current, "latin1").toString("utf8");
const repairedScore = (repaired.match(/[\u00C3\u00C2\uFFFD]/g) || []).length;
if (repairedScore < currentScore) {
current = repaired;
continue;
}
break;
}
return current.replace(/\u00A0/g, " ").trim();
}
function normalizeText(value) {
return repairText(String(value === undefined || value === null ? "" : value).trim());
}
function normalizeUnit(value) {
const raw = String(value ?? "").trim();
if (!raw) {
return "°C";
const raw = normalizeText(value);
const collapsed = raw.replace(/\s+/g, "");
if (!collapsed) {
return "\u00B0C";
}
return raw
.replace(/°/g, "°")
.replace(/^\?C$/i, "°C")
.replace(/^°\s*C$/i, "°C");
if (/^\?C$/i.test(collapsed) || /^\u00B0C$/i.test(collapsed)) {
return "\u00B0C";
}
return raw;
}
function normalizePointIndex(value, fallback) {
@@ -159,11 +183,11 @@ function ensureSource(store, body) {
const source = {
id: body.id || `src-${Date.now()}`,
protocol: normalizeProtocol(body.protocol),
name: body.name || "Neue Quelle",
host: body.host || "",
port: body.port || "",
deviceId: body.deviceId || "",
displayName: body.displayName || body.name || "Neue Quelle",
name: normalizeText(body.name) || "Neue Quelle",
host: normalizeText(body.host),
port: normalizeText(body.port),
deviceId: normalizeText(body.deviceId),
displayName: normalizeText(body.displayName || body.name) || "Neue Quelle",
tableName,
writeMode: normalizeWriteMode(body.writeMode),
pollIntervalSeconds: normalizePollInterval(body.pollIntervalSeconds),
@@ -297,8 +321,8 @@ function createManualDatapoint(store, payload) {
const fallbackIndex = existing.length + 1;
const pointIndex = normalizePointIndex(payload.pointIndex, fallbackIndex);
const protocol = normalizeProtocol(payload.protocol || source.protocol);
const alias = String(payload.alias || payload.name || `Punkt ${pointIndex}`).trim() || `Punkt ${pointIndex}`;
const name = String(payload.name || payload.alias || alias).trim() || alias;
const alias = normalizeText(payload.alias || payload.name || `Punkt ${pointIndex}`) || `Punkt ${pointIndex}`;
const name = normalizeText(payload.name || payload.alias || alias) || alias;
const datapoint = {
id: payload.id || `dp-${protocol}-${Date.now()}-${pointIndex}`,
@@ -311,10 +335,10 @@ function createManualDatapoint(store, payload) {
kind: normalizePointKind(payload.kind),
enabled: payload.enabled !== false,
writeMode: normalizeWriteMode(payload.writeMode || source.writeMode),
condition: String(payload.condition || "").trim(),
dataType: String(payload.dataType || (protocol === "knx-ip" ? "group-address" : "holding-register")).trim(),
address: String(payload.address || payload.register || "").trim(),
nodeId: String(payload.nodeId || "").trim(),
condition: normalizeText(payload.condition),
dataType: normalizeText(payload.dataType || (protocol === "knx-ip" ? "group-address" : "holding-register")),
address: normalizeText(payload.address || payload.register || ""),
nodeId: normalizeText(payload.nodeId || ""),
scale: Number(payload.scale || 1) || 1,
host: source.host || "",
tableName: source.tableName || "",
@@ -333,27 +357,27 @@ function createManualDatapoint(store, payload) {
function createScanDatapoint(source, payload, index) {
const pointIndex = normalizePointIndex(payload.pointIndex, index + 1);
const protocol = normalizeProtocol(payload.protocol || source.protocol);
const alias = String(payload.alias || payload.name || payload.displayName || `Punkt ${pointIndex}`).trim() || `Punkt ${pointIndex}`;
const alias = normalizeText(payload.alias || payload.name || payload.displayName || `Punkt ${pointIndex}`) || `Punkt ${pointIndex}`;
return {
id: payload.id || `dp-${protocol}-${Date.now()}-${Math.round(Math.random() * 100000)}-${pointIndex}`,
sourceId: source.id,
protocol,
pointIndex,
name: String(payload.name || alias).trim() || alias,
name: normalizeText(payload.name || alias) || alias,
alias,
unit: normalizeUnit(payload.unit),
kind: normalizePointKind(payload.kind),
enabled: payload.enabled === true,
writeMode: normalizeWriteMode(payload.writeMode || source.writeMode),
condition: String(payload.condition || "").trim(),
dataType: String(payload.dataType || "").trim(),
address: String(payload.address || "").trim(),
nodeId: String(payload.nodeId || "").trim(),
condition: normalizeText(payload.condition),
dataType: normalizeText(payload.dataType),
address: normalizeText(payload.address),
nodeId: normalizeText(payload.nodeId),
objectType: payload.objectType,
objectInstance: payload.objectInstance,
propertyId: payload.propertyId || 85,
scale: Number(payload.scale || 1) || 1,
host: payload.host || source.host || "",
host: normalizeText(payload.host || source.host),
tableName: source.tableName || "",
currentValue: payload.currentValue ?? null,
lastReadAt: payload.currentValue === undefined ? null : new Date().toISOString(),
@@ -361,6 +385,50 @@ function createScanDatapoint(source, payload, index) {
};
}
function buildScannedDatapoints(store, source, rawPoints, toPayload) {
const existingPoints = store.datapoints.filter((item) => item.sourceId === source.id);
const existingById = new Map(existingPoints.map((item) => [String(item.id), item]));
const seenIds = new Set();
let nextPointIndex = existingPoints.reduce((max, item) => Math.max(max, Number(item.pointIndex) || 0), 0) + 1;
return rawPoints.reduce((items, rawPoint, index) => {
const payload = toPayload(rawPoint, index);
const stableId = String(payload.id || "");
if (stableId) {
if (seenIds.has(stableId)) {
return items;
}
seenIds.add(stableId);
}
const existing = stableId ? existingById.get(stableId) : null;
const pointIndex = existing ? existing.pointIndex : normalizePointIndex(payload.pointIndex, nextPointIndex);
if (!existing) {
nextPointIndex = Math.max(nextPointIndex, pointIndex + 1);
}
const scanned = createScanDatapoint(source, { ...payload, pointIndex }, index);
items.push(!existing ? scanned : {
...existing,
...scanned,
pointIndex: existing.pointIndex,
alias: existing.alias || scanned.alias,
name: existing.name || scanned.name,
unit: normalizeUnit(existing.unit || scanned.unit),
kind: existing.kind || scanned.kind,
enabled: existing.enabled === true,
writeMode: normalizeWriteMode(existing.writeMode || scanned.writeMode),
condition: normalizeText(existing.condition || scanned.condition),
dataType: normalizeText(existing.dataType || scanned.dataType),
address: normalizeText(existing.address || scanned.address),
nodeId: normalizeText(existing.nodeId || scanned.nodeId),
scale: Number(existing.scale || scanned.scale || 1) || 1,
host: normalizeText(existing.host || scanned.host),
createdAt: existing.createdAt || scanned.createdAt,
});
return items;
}, []);
}
function upsertDatapoints(store, datapoints) {
datapoints.forEach((datapoint) => {
store.datapoints = store.datapoints.filter((item) => item.id !== datapoint.id && !(item.sourceId === datapoint.sourceId && item.pointIndex === datapoint.pointIndex));
@@ -634,7 +702,7 @@ async function readOpcuaValue(source, datapoint) {
return value ? 1 : 0;
}
const numeric = Number(value);
return Number.isFinite(numeric) ? numeric * (Number(datapoint.scale || 1) || 1) : String(value ?? "");
return Number.isFinite(numeric) ? numeric * (Number(datapoint.scale || 1) || 1) : String(value || "");
} finally {
await client.disconnect().catch(() => undefined);
}
@@ -797,7 +865,7 @@ async function readBacnetValue(source, datapoint) {
return;
}
const numeric = Number(raw);
resolve(Number.isFinite(numeric) ? numeric * (Number(datapoint.scale || 1) || 1) : String(raw ?? ""));
resolve(Number.isFinite(numeric) ? numeric * (Number(datapoint.scale || 1) || 1) : String(raw || ""));
});
});
}
@@ -823,7 +891,7 @@ async function readKnxValue(source, datapoint) {
reject(error);
} else {
const numeric = Number(value);
resolve(typeof value === "boolean" ? (value ? 1 : 0) : Number.isFinite(numeric) ? numeric : String(value ?? ""));
resolve(typeof value === "boolean" ? (value ? 1 : 0) : Number.isFinite(numeric) ? numeric : String(value || ""));
}
};
@@ -970,12 +1038,251 @@ async function ensureTrendTable(tableName) {
return table;
}
async function ensureSourceRegistryTable() {
await db.query(`
CREATE TABLE IF NOT EXISTS \`source_registry\` (
\`source_id\` VARCHAR(191) NOT NULL,
\`table_name\` VARCHAR(191) NOT NULL,
\`source_name\` VARCHAR(255) NOT NULL,
\`display_name\` VARCHAR(255) NOT NULL,
\`protocol\` VARCHAR(32) NOT NULL,
\`created_at\` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
\`updated_at\` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (\`source_id\`),
KEY \`idx_source_registry_table_name\` (\`table_name\`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
`);
}
async function syncSourceRegistryEntry(source) {
if (!source?.id || !source?.tableName) {
return;
}
await ensureSourceRegistryTable();
await db.query(
`
INSERT INTO \`source_registry\` (
\`source_id\`,
\`table_name\`,
\`source_name\`,
\`display_name\`,
\`protocol\`
) VALUES (?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
\`table_name\` = VALUES(\`table_name\`),
\`source_name\` = VALUES(\`source_name\`),
\`display_name\` = VALUES(\`display_name\`),
\`protocol\` = VALUES(\`protocol\`)
`,
[
String(source.id),
sanitizeTrendTable(source.tableName),
String(source.name || source.displayName || source.id),
String(source.displayName || source.name || source.id),
String(source.protocol || "unknown"),
]
);
}
async function deleteSourceRegistryEntry(sourceId) {
if (!sourceId) {
return;
}
await ensureSourceRegistryTable();
await db.query("DELETE FROM `source_registry` WHERE `source_id` = ?", [String(sourceId)]);
}
async function ensureSourceDatapointRegistryTable() {
await db.query(`
CREATE TABLE IF NOT EXISTS \`source_datapoint_registry\` (
\`datapoint_id\` VARCHAR(255) NOT NULL,
\`source_id\` VARCHAR(191) NOT NULL,
\`table_name\` VARCHAR(191) NOT NULL,
\`point_index\` INT NOT NULL,
\`alias\` VARCHAR(255) NOT NULL,
\`name\` VARCHAR(255) NOT NULL,
\`protocol\` VARCHAR(32) NOT NULL,
\`unit\` VARCHAR(64) NULL,
\`kind\` VARCHAR(32) NULL,
\`created_at\` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
\`updated_at\` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (\`datapoint_id\`),
UNIQUE KEY \`uniq_source_point\` (\`source_id\`, \`point_index\`),
KEY \`idx_source_datapoint_table\` (\`table_name\`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
`);
}
async function syncDatapointRegistryEntries(datapoints) {
if (!Array.isArray(datapoints) || !datapoints.length) {
return;
}
await ensureSourceDatapointRegistryTable();
for (const datapoint of datapoints) {
if (!datapoint?.id || !datapoint?.sourceId || !datapoint?.tableName) {
continue;
}
const pointIndex = normalizePointIndex(datapoint.pointIndex, 1);
await db.query("DELETE FROM `source_datapoint_registry` WHERE `datapoint_id` = ? OR (`source_id` = ? AND `point_index` = ?)", [
String(datapoint.id),
String(datapoint.sourceId),
pointIndex,
]);
if (datapoint.enabled !== true) {
continue;
}
await db.query(
`
INSERT INTO \`source_datapoint_registry\` (
\`datapoint_id\`,
\`source_id\`,
\`table_name\`,
\`point_index\`,
\`alias\`,
\`name\`,
\`protocol\`,
\`unit\`,
\`kind\`
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`,
[
String(datapoint.id),
String(datapoint.sourceId),
sanitizeTrendTable(datapoint.tableName),
pointIndex,
normalizeText(datapoint.alias || datapoint.name || `Wert${datapoint.pointIndex}`),
normalizeText(datapoint.name || datapoint.alias || `Wert${datapoint.pointIndex}`),
String(datapoint.protocol || "unknown"),
normalizeUnit(datapoint.unit || ""),
String(datapoint.kind || "analog"),
]
);
}
}
async function deleteDatapointRegistryForSource(sourceId) {
if (!sourceId) {
return;
}
await ensureSourceDatapointRegistryTable();
await db.query("DELETE FROM `source_datapoint_registry` WHERE `source_id` = ?", [String(sourceId)]);
}
async function rebuildRegistryTablesFromStore() {
const store = readStore();
await ensureSourceRegistryTable();
await ensureSourceDatapointRegistryTable();
await db.query("DELETE FROM `source_datapoint_registry`");
for (const source of store.sources || []) {
await syncSourceRegistryEntry(source);
}
await syncDatapointRegistryEntries((store.datapoints || []).filter((datapoint) => datapoint.enabled === true));
}
async function getTrendTableInfo(tableName) {
const table = sanitizeTrendTable(tableName);
const [columns] = await db.query(
`
SELECT COLUMN_NAME AS columnName
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = ?
ORDER BY ORDINAL_POSITION
`,
[table]
);
const names = columns.map((column) => column.columnName);
const lower = new Set(names.map((name) => String(name).toLowerCase()));
const wideColumnCount = names.filter((name) => /^Wert\d+$/i.test(name)).length;
if (wideColumnCount > 0) {
return { table, mode: "wide", exists: true };
}
if (lower.has("source_id") && lower.has("point_index") && lower.has("value") && lower.has("datum")) {
return { table, mode: "narrow", exists: true };
}
return { table, mode: null, exists: names.length > 0 };
}
async function ensureSourceTrendTable(source) {
const info = await getTrendTableInfo(source.tableName);
if (info.mode === "wide") {
return info;
}
if (info.mode === "narrow") {
await syncSourceRegistryEntry(source);
return info;
}
if (info.exists) {
throw new Error(`Trendtabelle ${info.table} hat ein unbekanntes Format.`);
}
await db.query(`
CREATE TABLE IF NOT EXISTS \`${info.table}\` (
\`id\` BIGINT NOT NULL AUTO_INCREMENT,
\`datum\` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
\`userlevel\` INT(1) NOT NULL DEFAULT 0,
\`source_id\` VARCHAR(191) NOT NULL,
\`point_index\` INT NOT NULL,
\`alias\` VARCHAR(255) NOT NULL,
\`value\` TEXT NULL,
PRIMARY KEY (\`id\`),
KEY \`idx_${info.table}_datum\` (\`datum\`),
KEY \`idx_${info.table}_point_datum\` (\`point_index\`, \`datum\`),
KEY \`idx_${info.table}_source_point\` (\`source_id\`, \`point_index\`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
`);
await syncSourceRegistryEntry(source);
return { ...info, mode: "narrow", exists: true };
}
async function writeTrendRow(source, readings) {
if (!readings.length) {
return false;
}
const table = await ensureTrendTable(source.tableName);
const tableInfo = await ensureSourceTrendTable(source);
const table = tableInfo.table;
if (tableInfo.mode === "narrow") {
const statement = `
INSERT INTO \`${table}\` (
\`userlevel\`,
\`source_id\`,
\`point_index\`,
\`alias\`,
\`value\`
) VALUES (?, ?, ?, ?, ?)
`;
for (const reading of readings) {
const pointIndex = normalizePointIndex(reading.pointIndex, 0);
if (!pointIndex) {
continue;
}
const datapoint = source.__activeDatapoints?.find((item) => item.pointIndex === pointIndex);
await db.query(statement, [
0,
String(source.id),
pointIndex,
String(datapoint?.alias || `Wert${pointIndex}`),
String(reading.value),
]);
}
return true;
}
const columns = ["`userlevel`"];
const placeholders = ["?"];
const values = [0];
@@ -1057,7 +1364,7 @@ async function pollModbusSource(source, datapoints) {
}
async function pollSource(source, datapoints) {
const sourcePoints = datapoints.filter((datapoint) => datapoint.sourceId === source.id && datapoint.enabled !== false);
const sourcePoints = datapoints.filter((datapoint) => datapoint.sourceId === source.id && datapoint.enabled === true);
if (!sourcePoints.length) {
updateSourceState(source.id, {
lastPollAt: new Date().toISOString(),
@@ -1068,6 +1375,7 @@ async function pollSource(source, datapoints) {
}
try {
source.__activeDatapoints = sourcePoints;
if (source.protocol === "modbus-tcp") {
await pollModbusSource(source, sourcePoints);
return;
@@ -1091,7 +1399,7 @@ async function pollSource(source, datapoints) {
updateSourceState(source.id, {
lastPollAt: new Date().toISOString(),
lastPollStatus: "unsupported",
lastPollError: `${source.protocol} wird vom Collector nicht unterstützt.`,
lastPollError: `${source.protocol} wird vom Collector nicht unterst?tzt.`,
});
} catch (error) {
updateSourceState(source.id, {
@@ -1099,6 +1407,8 @@ async function pollSource(source, datapoints) {
lastPollStatus: "error",
lastPollError: error.message || "Polling fehlgeschlagen.",
});
} finally {
delete source.__activeDatapoints;
}
}
@@ -1185,14 +1495,16 @@ const server = createServer(async (req, res) => {
return;
}
if (req.method === "POST" && url.pathname === "/sources") {
if (req.method === "POST" && url.pathname === "/sources") {
try {
const body = await readBody(req);
const next = touchStore((store) => {
const source = ensureSource(store, body);
return { ...store, lastSource: source.id };
});
replyJson(res, 201, next.sources.at(-1));
const createdSource = next.sources.at(-1);
await ensureSourceTrendTable(createdSource);
replyJson(res, 201, createdSource);
} catch {
replyJson(res, 400, { error: "Ungültige JSON-Daten." });
}
@@ -1211,7 +1523,9 @@ const server = createServer(async (req, res) => {
ensureSource(store, { ...existing, ...body, id: sourceId });
return store;
});
replyJson(res, 200, next.sources.find((item) => item.id === sourceId));
const updatedSource = next.sources.find((item) => item.id === sourceId);
await ensureSourceTrendTable(updatedSource);
replyJson(res, 200, updatedSource);
} catch (error) {
replyJson(res, error.message === "not-found" ? 404 : 400, {
error: error.message === "not-found" ? "Quelle nicht gefunden." : "Ungültige JSON-Daten.",
@@ -1227,6 +1541,8 @@ const server = createServer(async (req, res) => {
sources: store.sources.filter((item) => item.id !== sourceId),
datapoints: store.datapoints.filter((item) => item.sourceId !== sourceId),
}));
await deleteSourceRegistryEntry(sourceId);
await deleteDatapointRegistryForSource(sourceId);
replyJson(res, 200, { ok: true, sources: next.sources.length, datapoints: next.datapoints.length });
return;
}
@@ -1241,6 +1557,7 @@ const server = createServer(async (req, res) => {
return store;
});
const created = next.datapoints.find((item) => item.sourceId === body.sourceId && item.pointIndex === normalizePointIndex(body.pointIndex, 1));
await syncDatapointRegistryEntries(created ? [created] : []);
replyJson(res, 201, { ok: true, datapoint: created || null, updatedAt: next.updatedAt });
} catch (error) {
replyJson(res, 400, { error: error.message || "Datenpunkt konnte nicht angelegt werden." });
@@ -1250,7 +1567,7 @@ const server = createServer(async (req, res) => {
if (req.method === "PATCH" && url.pathname.startsWith("/datapoints/")) {
try {
const datapointId = url.pathname.split("/").at(-1);
const datapointId = decodeURIComponent(url.pathname.split("/").at(-1) || "");
const body = await readBody(req);
const next = touchStore((store) => {
const index = store.datapoints.findIndex((item) => item.id === datapointId);
@@ -1270,7 +1587,9 @@ const server = createServer(async (req, res) => {
};
return store;
});
replyJson(res, 200, { datapoint: next.datapoints.find((item) => item.id === datapointId), updatedAt: next.updatedAt });
const updatedDatapoint = next.datapoints.find((item) => item.id === datapointId);
await syncDatapointRegistryEntries(updatedDatapoint ? [updatedDatapoint] : []);
replyJson(res, 200, { datapoint: updatedDatapoint, updatedAt: next.updatedAt });
} catch (error) {
replyJson(res, error.message === "not-found" ? 404 : 400, { error: error.message === "not-found" ? "Datenpunkt nicht gefunden." : error.message });
}
@@ -1279,7 +1598,7 @@ const server = createServer(async (req, res) => {
if (req.method === "POST" && url.pathname.match(/^\/datapoints\/[^/]+\/read$/)) {
try {
const datapointId = url.pathname.split("/").at(-2);
const datapointId = decodeURIComponent(url.pathname.split("/").at(-2) || "");
const store = readStore();
const datapoint = store.datapoints.find((item) => item.id === datapointId);
const source = datapoint ? store.sources.find((item) => item.id === datapoint.sourceId) : null;
@@ -1299,7 +1618,6 @@ const server = createServer(async (req, res) => {
return;
}
if (req.method === "POST" && url.pathname === "/imports/modbus-csv") {
try {
const body = await readBody(req);
@@ -1311,6 +1629,7 @@ const server = createServer(async (req, res) => {
upsertDatapoints(store, imported);
return store;
});
await syncDatapointRegistryEntries(imported);
replyJson(res, 200, { imported: imported.length, datapoints: imported, updatedAt: next.updatedAt });
} catch {
replyJson(res, 400, { error: "CSV-Import fehlgeschlagen." });
@@ -1329,6 +1648,7 @@ const server = createServer(async (req, res) => {
upsertDatapoints(store, imported);
return store;
});
await syncDatapointRegistryEntries(imported);
replyJson(res, 200, { imported: imported.length, datapoints: imported, updatedAt: next.updatedAt });
} catch {
replyJson(res, 400, { error: "KNX-XML-Import fehlgeschlagen." });
@@ -1344,12 +1664,23 @@ const server = createServer(async (req, res) => {
return;
}
const result = await scanOpcuaEndpoint(body);
let scannedCandidates = [];
touchStore((store) => {
if (body.sourceId) {
const source = store.sources.find((item) => item.id === body.sourceId);
if (source) {
const existingCount = store.datapoints.filter((item) => item.sourceId === source.id).length;
const candidates = (result.nodes || []).filter((node) => node.nodeId && node.hasValue).map((node, index) => createScanDatapoint(source, { ...node, protocol: "opc-ua", pointIndex: existingCount + index + 1, alias: node.path || node.displayName, name: node.displayName, address: node.nodeId, nodeId: node.nodeId }, index));
const rawNodes = (result.nodes || []).filter((node) => node.nodeId);
const candidates = buildScannedDatapoints(store, source, rawNodes, (node, index) => ({
...node,
id: `${source.id}::${node.nodeId}`,
protocol: "opc-ua",
pointIndex: index + 1,
alias: node.displayName || node.browseName || node.nodeId,
name: node.displayName || node.browseName || node.nodeId,
address: node.nodeId,
nodeId: node.nodeId,
}));
scannedCandidates = candidates;
upsertDatapoints(store, candidates);
}
}
@@ -1357,6 +1688,7 @@ const server = createServer(async (req, res) => {
store.scans.opcua = store.scans.opcua.slice(0, 20);
return store;
});
await syncDatapointRegistryEntries(scannedCandidates);
replyJson(res, 200, result);
} catch (error) {
replyJson(res, 400, { error: error.message || "OPC-UA-Scan fehlgeschlagen." });
@@ -1368,12 +1700,21 @@ const server = createServer(async (req, res) => {
try {
const body = await readBody(req);
const payload = await scanBacnetNetwork(body);
let scannedCandidates = [];
touchStore((store) => {
if (body.sourceId) {
const source = store.sources.find((item) => item.id === body.sourceId);
if (source) {
const existingCount = store.datapoints.filter((item) => item.sourceId === source.id).length;
const candidates = (payload.points || []).map((point, index) => createScanDatapoint(source, { ...point, protocol: "bacnet-ip", pointIndex: existingCount + index + 1 }, index));
const rawPoints = payload.points || [];
const candidates = buildScannedDatapoints(store, source, rawPoints, (point, index) => ({
...point,
id: point.id || `${source.id}::${point.host || source.host}::${point.dataType || point.objectType || "object"}::${point.objectInstance || index + 1}`,
protocol: "bacnet-ip",
pointIndex: index + 1,
alias: point.alias || point.name || `${point.dataType || "obj"}${point.objectInstance || index + 1}`,
name: point.name || point.alias || `${point.dataType || "obj"}${point.objectInstance || index + 1}`,
}));
scannedCandidates = candidates;
upsertDatapoints(store, candidates);
}
}
@@ -1381,6 +1722,7 @@ const server = createServer(async (req, res) => {
store.scans.bacnet = store.scans.bacnet.slice(0, 20);
return store;
});
await syncDatapointRegistryEntries(scannedCandidates);
replyJson(res, 200, payload);
} catch (error) {
replyJson(res, 400, { error: error.message || "BACnet-Scan fehlgeschlagen." });
@@ -1391,6 +1733,10 @@ const server = createServer(async (req, res) => {
replyJson(res, 404, { error: "Nicht gefunden." });
});
rebuildRegistryTablesFromStore().catch((error) => console.error("Registry sync failed", error));
server.listen(port, () => {
console.log(`SE Local Trenddata collector listening on port ${port}`);
});