This commit is contained in:
jhartworks
2026-06-23 14:48:36 +02:00
parent a445fef521
commit f0cb94ce4e
8 changed files with 796 additions and 81 deletions
+116 -6
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";
@@ -102,6 +102,27 @@ function normalizePollInterval(value) {
return Number.isFinite(numeric) && numeric > 0 ? Math.round(numeric) : 60;
}
function normalizePointKind(value) {
return String(value || "").toLowerCase() === "digital" ? "digital" : "analog";
}
function normalizeUnit(value) {
const raw = String(value ?? "").trim();
if (!raw) {
return "°C";
}
return raw
.replace(/°/g, "°")
.replace(/^\?C$/i, "°C")
.replace(/^°\s*C$/i, "°C");
}
function normalizePointIndex(value, fallback) {
const numeric = Number(value);
return Number.isInteger(numeric) && numeric >= 1 && numeric <= 200 ? numeric : fallback;
}
function ensureSource(store, body) {
const now = new Date().toISOString();
const source = {
@@ -120,6 +141,9 @@ function ensureSource(store, body) {
writeMode: normalizeWriteMode(body.writeMode),
pollIntervalSeconds: normalizePollInterval(body.pollIntervalSeconds),
},
lastPollAt: body.lastPollAt || null,
lastPollStatus: body.lastPollStatus || "idle",
lastPollError: body.lastPollError || "",
createdAt: body.createdAt || now,
updatedAt: now,
};
@@ -155,16 +179,19 @@ function parseModbusCsv(payload) {
return columnIndex >= 0 ? row[columnIndex] || fallback : fallback;
};
const address = get("address", get("register", String(index + 1)));
const pointIndex = normalizePointIndex(get("pointindex", index + 1), index + 1);
return {
id: `dp-modbus-${Date.now()}-${index}`,
sourceId: payload.sourceId || "",
protocol: "modbus-tcp",
address: get("address", get("register", String(index + 1))),
pointIndex: Number(get("pointindex", index + 1)) || index + 1,
address,
pointIndex,
name: get("name", `Register ${index + 1}`),
alias: get("alias", get("name", `Register ${index + 1}`)),
unit: get("unit"),
unit: normalizeUnit(get("unit")),
dataType: get("datatype", "holding-register"),
kind: normalizePointKind(get("kind", get("type", "analog"))),
scale: Number(get("scale", 1)) || 1,
host: payload.host || "",
tableName: payload.tableName || "",
@@ -209,8 +236,9 @@ function parseKnxXml(payload) {
pointIndex: index + 1,
name,
alias: name,
unit: "",
unit: normalizeUnit(""),
dataType: dpt || "group-address",
kind: dpt.startsWith("1.") ? "digital" : "analog",
host: payload.host || "",
tableName: payload.tableName || "",
createdAt: new Date().toISOString(),
@@ -222,6 +250,44 @@ function parseKnxXml(payload) {
return datapoints;
}
function createManualDatapoint(store, payload) {
const source = store.sources.find((item) => item.id === payload.sourceId);
if (!source) {
throw new Error("Quelle nicht gefunden.");
}
const existing = store.datapoints.filter((item) => item.sourceId === source.id);
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 datapoint = {
id: payload.id || `dp-${protocol}-${Date.now()}-${pointIndex}`,
sourceId: source.id,
protocol,
pointIndex,
name,
alias,
unit: normalizeUnit(payload.unit),
kind: normalizePointKind(payload.kind),
dataType: String(payload.dataType || (protocol === "knx-ip" ? "group-address" : "holding-register")).trim(),
address: String(payload.address || payload.register || "").trim(),
nodeId: String(payload.nodeId || "").trim(),
scale: Number(payload.scale || 1) || 1,
host: source.host || "",
tableName: source.tableName || "",
createdAt: new Date().toISOString(),
};
if (!datapoint.address && protocol !== "opc-ua") {
datapoint.address = String(pointIndex);
}
return datapoint;
}
function tryTcpConnect(host, portNumber, timeout = 1200) {
return new Promise((resolve) => {
if (!host || !portNumber) {
@@ -265,7 +331,8 @@ const server = createServer(async (req, res) => {
protocols: ["modbus-tcp", "opc-ua", "bacnet-ip", "knx-ip"],
imports: ["modbus-csv", "knx-xml"],
scans: ["opc-ua", "bacnet-ip"],
note: "Importe und erste Scan-Stubs sind aktiv. Poll-Intervall und Schreibmodus werden bereits pro Quelle gespeichert; die echte Live-Kommunikation folgt als nächste Ausbaustufe.",
manualDatapoints: ["modbus-tcp", "knx-ip"],
note: "Importe, manuelle Datenpunkte und Diagnose sind aktiv. Live-Polling und echte Feldbus-Kommunikation sind noch im nächsten Ausbauschritt.",
});
return;
}
@@ -283,6 +350,28 @@ const server = createServer(async (req, res) => {
return;
}
if (req.method === "GET" && url.pathname === "/debug") {
const store = readStore();
replyJson(res, 200, {
updatedAt: store.updatedAt,
sourceCount: store.sources.length,
datapointCount: store.datapoints.length,
sources: store.sources.map((source) => ({
...source,
datapointCount: store.datapoints.filter((item) => item.sourceId === source.id).length,
})),
latestScans: {
opcua: store.scans.opcua[0] || null,
bacnet: store.scans.bacnet[0] || null,
},
driverState: {
pollingImplemented: false,
note: "Der Collector speichert Quellen, Importe und Scan-Ergebnisse bereits. Echtes zyklisches Polling auf Bus-Ebene ist aktuell noch nicht implementiert.",
},
});
return;
}
if (req.method === "POST" && url.pathname === "/sources") {
try {
const body = await readBody(req);
@@ -329,6 +418,23 @@ const server = createServer(async (req, res) => {
return;
}
if (req.method === "POST" && url.pathname === "/datapoints/manual") {
try {
const body = await readBody(req);
const next = touchStore((store) => {
const datapoint = createManualDatapoint(store, body);
store.datapoints = store.datapoints.filter((item) => !(item.sourceId === datapoint.sourceId && item.pointIndex === datapoint.pointIndex));
store.datapoints.push(datapoint);
return store;
});
const created = next.datapoints.find((item) => item.sourceId === body.sourceId && item.pointIndex === normalizePointIndex(body.pointIndex, 1));
replyJson(res, 201, { ok: true, datapoint: created || null, updatedAt: next.updatedAt });
} catch (error) {
replyJson(res, 400, { error: error.message || "Datenpunkt konnte nicht angelegt werden." });
}
return;
}
if (req.method === "POST" && url.pathname === "/imports/modbus-csv") {
try {
const body = await readBody(req);
@@ -369,6 +475,10 @@ const server = createServer(async (req, res) => {
try {
const body = await readBody(req);
const host = body.host || "";
if (!host) {
replyJson(res, 400, { error: "Für den OPC-UA-Scan fehlt Host oder IP." });
return;
}
const portNumber = Number(body.port || 4840);
const reachable = await tryTcpConnect(host, portNumber);
const result = {