commit
This commit is contained in:
+116
-6
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user