@atelieruldetehnologii/node-red-custome-solutions 0.62.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +56 -0
- package/PROPRIETARY-LICENSE.txt +157 -0
- package/README.md +105 -0
- package/TERMS-AND-CONDITIONS.md +311 -0
- package/advantech-read/advantech-read.html +775 -0
- package/advantech-read/advantech-read.js +449 -0
- package/advantech-write/advantech-write.html +413 -0
- package/advantech-write/advantech-write.js +338 -0
- package/bess-control/bess-control.html +569 -0
- package/bess-control/bess-control.js +541 -0
- package/bess-control/lib/allocation.js +149 -0
- package/bess-control/lib/batteries.js +86 -0
- package/bess-control/lib/helpers.js +157 -0
- package/bess-control/lib/messages.js +131 -0
- package/bess-control/lib/runtime.js +156 -0
- package/carlo-gavazzi/carlo-gavazzi.html +4327 -0
- package/carlo-gavazzi/carlo-gavazzi.js +3081 -0
- package/control-export/control-export.html +3644 -0
- package/control-export/control-export.js +2192 -0
- package/control-reactive/control-reactive.html +3136 -0
- package/control-reactive/control-reactive.js +2124 -0
- package/fronius-emulator/README.md +258 -0
- package/fronius-emulator/fronius-emulator.html +170 -0
- package/fronius-emulator/fronius-emulator.js +520 -0
- package/fronius-emulator/lib/binary-codec.js +65 -0
- package/fronius-emulator/lib/fronius-register-map.js +185 -0
- package/fronius-emulator/lib/modbus-parser.js +61 -0
- package/fronius-emulator/lib/modbus-tcp-server.js +224 -0
- package/fronius-emulator/lib/validation.js +206 -0
- package/fronius-emulator/lib/virtual-meter.js +132 -0
- package/fronius-read/fronius-read.html +3944 -0
- package/fronius-read/fronius-read.js +1466 -0
- package/fronius-read/lib/inverter-profiles.js +711 -0
- package/fronius-read/lib/sunspec-core.js +303 -0
- package/fronius-read/lib/sunspec-model-parsers.js +845 -0
- package/fronius-write/fronius-write.html +1789 -0
- package/fronius-write/fronius-write.js +2531 -0
- package/influxdb/influxdb.html +1077 -0
- package/influxdb/influxdb.js +1487 -0
- package/janitza/janitza.html +2562 -0
- package/janitza/janitza.js +1500 -0
- package/janitza/lib/janitza-profiles.js +371 -0
- package/lib/a3-test-server.js +199 -0
- package/lib/adam-6000-profiles.js +173 -0
- package/lib/licensing/constants.js +103 -0
- package/lib/licensing/device-id.js +251 -0
- package/lib/licensing/errors.js +21 -0
- package/lib/licensing/license-client.js +377 -0
- package/lib/licensing/license-state.js +50 -0
- package/lib/licensing/node-guard.js +150 -0
- package/lib/licensing/package-integrity.js +167 -0
- package/lib/licensing/package-integrity.json +513 -0
- package/lib/licensing/public-key.pem +3 -0
- package/lib/licensing/public-key.pem.example +2 -0
- package/lib/licensing/public-keys.js +18 -0
- package/lib/licensing/storage.js +63 -0
- package/lib/licensing/token-verifier.js +84 -0
- package/lib/numbers.js +26 -0
- package/license/atelier-license.html +88 -0
- package/license/atelier-license.js +170 -0
- package/modbus-tcp-discover/modbus-tcp-discover.html +190 -0
- package/modbus-tcp-discover/modbus-tcp-discover.js +706 -0
- package/multi-battery/multi-battery.html +649 -0
- package/multi-battery/multi-battery.js +668 -0
- package/multi-grid/multi-grid.html +745 -0
- package/multi-grid/multi-grid.js +1392 -0
- package/multi-inverter/lib/engine-core.js +237 -0
- package/multi-inverter/multi-inverter.html +2674 -0
- package/multi-inverter/multi-inverter.js +1393 -0
- package/package.json +109 -0
- package/phoenix-contact/lib/eem-ma370-advanced.js +175 -0
- package/phoenix-contact/lib/eem-ma370.js +138 -0
- package/phoenix-contact/lib/integrations.js +149 -0
- package/phoenix-contact/lib/phoenix-contact-profile.js +35030 -0
- package/phoenix-contact/phoenix-contact.html +840 -0
- package/phoenix-contact/phoenix-contact.js +679 -0
- package/pv-generator/pv-generator.html +284 -0
- package/pv-generator/pv-generator.js +387 -0
- package/pylontech-read/pylontech-read.html +763 -0
- package/pylontech-read/pylontech-read.js +518 -0
- package/pylontech-write/pylontech-write.html +405 -0
- package/pylontech-write/pylontech-write.js +546 -0
- package/scada-aggregator/lib/aggregate.js +72 -0
- package/scada-aggregator/lib/groups.js +39 -0
- package/scada-aggregator/scada-aggregator.html +108 -0
- package/scada-aggregator/scada-aggregator.js +117 -0
- package/scada-server/lib/aggregator-map.js +58 -0
- package/scada-server/lib/encoding.js +235 -0
- package/scada-server/lib/modbus-server.js +492 -0
- package/scada-server/lib/object-path.js +18 -0
- package/scada-server/lib/register-map.js +288 -0
- package/scada-server/scada-server.html +1419 -0
- package/scada-server/scada-server.js +704 -0
- package/scripts/check-syntax.js +66 -0
- package/scripts/package-integrity.js +46 -0
- package/scripts/package-release.js +116 -0
- package/scripts/run-tests.js +2178 -0
- package/scripts/tgz-integrity.js +74 -0
- package/sma-read/lib/sunspec-core.js +83 -0
- package/sma-read/lib/sunspec-model-parsers.js +853 -0
- package/sma-read/sma-read.html +659 -0
- package/sma-read/sma-read.js +458 -0
- package/sma-write/sma-write.html +105 -0
- package/sma-write/sma-write.js +270 -0
- package/victron-read/lib/mqtt-state.js +176 -0
- package/victron-read/victron-read.html +1074 -0
- package/victron-read/victron-read.js +1222 -0
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
module.exports = function (RED) {
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const { runLicensedNode } = require("../lib/licensing/node-guard");
|
|
5
|
+
|
|
6
|
+
const ModbusRTU = require("modbus-serial");
|
|
7
|
+
const {
|
|
8
|
+
profileForModel,
|
|
9
|
+
normalizeModel
|
|
10
|
+
} = require("../lib/adam-6000-profiles");
|
|
11
|
+
|
|
12
|
+
function toInt(value, fallback) {
|
|
13
|
+
const n = Math.round(Number(value));
|
|
14
|
+
return Number.isFinite(n) ? n : fallback;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function toPositiveInt(value, fallback) {
|
|
18
|
+
const n = toInt(value, fallback);
|
|
19
|
+
return n > 0 ? n : fallback;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function toNonNegativeInt(value, fallback) {
|
|
23
|
+
const n = toInt(value, fallback);
|
|
24
|
+
return n >= 0 ? n : fallback;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function toFinite(value, fallback) {
|
|
28
|
+
const n = Number(value);
|
|
29
|
+
return Number.isFinite(n) ? n : fallback;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function manualAddressToOffset(address, area) {
|
|
33
|
+
const n = toInt(address, 0);
|
|
34
|
+
if (area === "holding" && n >= 40001) return n - 40001;
|
|
35
|
+
if (area === "coil" && n >= 1) return n - 1;
|
|
36
|
+
return Math.max(0, n);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function resolveAddress(address, area, addressMode) {
|
|
40
|
+
if (addressMode === "zero") {
|
|
41
|
+
return Math.max(0, toInt(address, 0));
|
|
42
|
+
}
|
|
43
|
+
return manualAddressToOffset(address, area);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function parseBooleanLike(value) {
|
|
47
|
+
if (typeof value === "boolean") return value;
|
|
48
|
+
if (typeof value === "number") {
|
|
49
|
+
if (value === 1) return true;
|
|
50
|
+
if (value === 0) return false;
|
|
51
|
+
}
|
|
52
|
+
if (typeof value === "string") {
|
|
53
|
+
const normalized = value.trim().toLowerCase();
|
|
54
|
+
if (["1", "true", "on", "enable", "enabled", "activ", "active"].includes(normalized)) return true;
|
|
55
|
+
if (["0", "false", "off", "disable", "disabled", "dezactiv", "inactive"].includes(normalized)) return false;
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function toUnsigned16(value) {
|
|
61
|
+
let v = Math.round(Number(value) || 0);
|
|
62
|
+
if (v < 0) v = 0;
|
|
63
|
+
if (v > 65535) v = 65535;
|
|
64
|
+
return v;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function rawAoFromValue(value, scale, offset) {
|
|
68
|
+
const n = Number(value);
|
|
69
|
+
if (!Number.isFinite(n)) {
|
|
70
|
+
throw new Error("AO value trebuie sa fie numeric");
|
|
71
|
+
}
|
|
72
|
+
if (!Number.isFinite(scale) || scale === 0) {
|
|
73
|
+
throw new Error("AO scale trebuie sa fie diferit de 0");
|
|
74
|
+
}
|
|
75
|
+
return toUnsigned16((n - offset) / scale);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function normalizeDoCommands(payload, defaultChannel, allowMask) {
|
|
79
|
+
if (payload === null || payload === undefined) return [];
|
|
80
|
+
|
|
81
|
+
if (Array.isArray(payload)) {
|
|
82
|
+
return payload.map(function (value, index) {
|
|
83
|
+
const boolValue = parseBooleanLike(value);
|
|
84
|
+
if (boolValue === null) return null;
|
|
85
|
+
return { channel: index, value: boolValue };
|
|
86
|
+
}).filter(Boolean);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (typeof payload === "object") {
|
|
90
|
+
if (Array.isArray(payload.do)) {
|
|
91
|
+
return normalizeDoCommands(payload.do, defaultChannel, allowMask);
|
|
92
|
+
}
|
|
93
|
+
if (payload.channel !== undefined || payload.value !== undefined) {
|
|
94
|
+
const boolValue = parseBooleanLike(payload.value);
|
|
95
|
+
if (boolValue === null) {
|
|
96
|
+
throw new Error("DO value invalid");
|
|
97
|
+
}
|
|
98
|
+
return [{ channel: toNonNegativeInt(payload.channel, defaultChannel), value: boolValue }];
|
|
99
|
+
}
|
|
100
|
+
if (payload.do_channel !== undefined || payload.do_value !== undefined) {
|
|
101
|
+
const boolValue = parseBooleanLike(payload.do_value);
|
|
102
|
+
if (boolValue === null) {
|
|
103
|
+
throw new Error("DO value invalid");
|
|
104
|
+
}
|
|
105
|
+
return [{ channel: toNonNegativeInt(payload.do_channel, defaultChannel), value: boolValue }];
|
|
106
|
+
}
|
|
107
|
+
if (allowMask && payload.do_mask !== undefined) {
|
|
108
|
+
return [{ mask: toUnsigned16(payload.do_mask) }];
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const boolValue = parseBooleanLike(payload);
|
|
113
|
+
if (boolValue === null) {
|
|
114
|
+
throw new Error("Payload DO invalid");
|
|
115
|
+
}
|
|
116
|
+
return [{ channel: defaultChannel, value: boolValue }];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function normalizeAoCommands(payload, defaultChannel, scale, offset) {
|
|
120
|
+
if (!payload || typeof payload !== "object") return [];
|
|
121
|
+
|
|
122
|
+
if (Array.isArray(payload.ao)) {
|
|
123
|
+
return payload.ao.map(function (value, index) {
|
|
124
|
+
return { channel: index, raw: rawAoFromValue(value, scale, offset), value: Number(value) };
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (payload.ao_channel !== undefined || payload.ao_value !== undefined) {
|
|
129
|
+
return [{
|
|
130
|
+
channel: toNonNegativeInt(payload.ao_channel, defaultChannel),
|
|
131
|
+
raw: rawAoFromValue(payload.ao_value, scale, offset),
|
|
132
|
+
value: Number(payload.ao_value)
|
|
133
|
+
}];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (payload.ao_raw !== undefined) {
|
|
137
|
+
return [{
|
|
138
|
+
channel: toNonNegativeInt(payload.ao_channel, defaultChannel),
|
|
139
|
+
raw: toUnsigned16(payload.ao_raw),
|
|
140
|
+
value: null
|
|
141
|
+
}];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return [];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function AdvantechWriteNode(config) {
|
|
148
|
+
RED.nodes.createNode(this, config);
|
|
149
|
+
const node = this;
|
|
150
|
+
|
|
151
|
+
runLicensedNode(node, RED, config, function startLicensedRuntime() {
|
|
152
|
+
|
|
153
|
+
node.name = String(config.name || "").trim();
|
|
154
|
+
node.host = String(config.host || "").trim();
|
|
155
|
+
node.port = toPositiveInt(config.port, 502);
|
|
156
|
+
node.unitId = toPositiveInt(config.unitId, 1);
|
|
157
|
+
node.addressMode = config.addressMode === "zero" ? "zero" : "manual";
|
|
158
|
+
node.moduleType = String(config.moduleType || "ADAM-6066").trim();
|
|
159
|
+
node.writeType = ["do", "ao", "both"].includes(config.writeType) ? config.writeType : "do";
|
|
160
|
+
node.doStart = toNonNegativeInt(config.doStart, 17);
|
|
161
|
+
node.aoStart = toNonNegativeInt(config.aoStart, 40011);
|
|
162
|
+
node.defaultChannel = toNonNegativeInt(config.defaultChannel, 0);
|
|
163
|
+
node.aoScale = toFinite(config.aoScale, 1);
|
|
164
|
+
node.aoOffset = toFinite(config.aoOffset, 0);
|
|
165
|
+
node.allowDoMask = !!config.allowDoMask;
|
|
166
|
+
node.doMaskRegister = toNonNegativeInt(config.doMaskRegister, 40303);
|
|
167
|
+
node.verifyWrite = config.verifyWrite !== false;
|
|
168
|
+
|
|
169
|
+
let client = new ModbusRTU();
|
|
170
|
+
let queue = Promise.resolve();
|
|
171
|
+
|
|
172
|
+
async function connect() {
|
|
173
|
+
if (!client.isOpen) {
|
|
174
|
+
await client.connectTCP(node.host, { port: node.port });
|
|
175
|
+
}
|
|
176
|
+
client.setID(node.unitId);
|
|
177
|
+
client.setTimeout(2500);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function closeConnection() {
|
|
181
|
+
try {
|
|
182
|
+
if (client && client.isOpen) {
|
|
183
|
+
await client.close();
|
|
184
|
+
}
|
|
185
|
+
} catch (e) {
|
|
186
|
+
// ignore close errors
|
|
187
|
+
}
|
|
188
|
+
client = new ModbusRTU();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function getProfile() {
|
|
192
|
+
const configured = profileForModel(node.moduleType);
|
|
193
|
+
if (configured) {
|
|
194
|
+
configured.detectedBy = "manual";
|
|
195
|
+
configured.modelCode = normalizeModel(node.moduleType);
|
|
196
|
+
return configured;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
model: node.moduleType && node.moduleType !== "auto" ? node.moduleType : "Manual",
|
|
201
|
+
modelCode: "",
|
|
202
|
+
diCount: 0,
|
|
203
|
+
doCount: 999,
|
|
204
|
+
aiCount: 0,
|
|
205
|
+
aoCount: 999,
|
|
206
|
+
detectedBy: "manual"
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function writeDo(command, profile) {
|
|
211
|
+
const doCount = toNonNegativeInt(profile.doCount, 0);
|
|
212
|
+
if (command.mask === undefined && command.channel >= doCount) {
|
|
213
|
+
throw new Error((profile.model || "ADAM") + " nu are DO channel " + command.channel + " (DO count: " + doCount + ")");
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (command.mask !== undefined && doCount <= 0) {
|
|
217
|
+
throw new Error((profile.model || "ADAM") + " nu are iesiri digitale detectate");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (command.mask !== undefined) {
|
|
221
|
+
const addr = resolveAddress(node.doMaskRegister, "holding", node.addressMode);
|
|
222
|
+
await client.writeRegister(addr, command.mask);
|
|
223
|
+
return { type: "do_mask", register: node.doMaskRegister, raw: command.mask };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const manualAddress = node.doStart + command.channel;
|
|
227
|
+
const addr = resolveAddress(manualAddress, "coil", node.addressMode);
|
|
228
|
+
await client.writeCoil(addr, command.value);
|
|
229
|
+
if (node.verifyWrite) {
|
|
230
|
+
const readback = await client.readCoils(addr, 1);
|
|
231
|
+
const actual = !!(readback.data && readback.data[0]);
|
|
232
|
+
if (actual !== command.value) {
|
|
233
|
+
throw new Error("DO channel " + command.channel + " nu a fost confirmat");
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return { type: "do", channel: command.channel, address: manualAddress, value: command.value };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function writeAo(command, profile) {
|
|
240
|
+
const aoCount = toNonNegativeInt(profile.aoCount, 0);
|
|
241
|
+
if (command.channel >= aoCount) {
|
|
242
|
+
throw new Error((profile.model || "ADAM") + " nu are AO channel " + command.channel + " (AO count: " + aoCount + ")");
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const manualAddress = node.aoStart + command.channel;
|
|
246
|
+
const addr = resolveAddress(manualAddress, "holding", node.addressMode);
|
|
247
|
+
await client.writeRegister(addr, command.raw);
|
|
248
|
+
if (node.verifyWrite) {
|
|
249
|
+
const readback = await client.readHoldingRegisters(addr, 1);
|
|
250
|
+
const actual = readback.data && readback.data[0];
|
|
251
|
+
if (actual !== command.raw) {
|
|
252
|
+
throw new Error("AO channel " + command.channel + " nu a fost confirmat");
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return {
|
|
256
|
+
type: "ao",
|
|
257
|
+
channel: command.channel,
|
|
258
|
+
address: manualAddress,
|
|
259
|
+
value: command.value,
|
|
260
|
+
raw: command.raw
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async function handleInput(msg) {
|
|
265
|
+
const payload = msg ? msg.payload : undefined;
|
|
266
|
+
const operations = [];
|
|
267
|
+
|
|
268
|
+
if (node.writeType === "do" || node.writeType === "both") {
|
|
269
|
+
normalizeDoCommands(payload, node.defaultChannel, node.allowDoMask).forEach(function (command) {
|
|
270
|
+
operations.push({ kind: "do", command: command });
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (node.writeType === "ao" || node.writeType === "both") {
|
|
275
|
+
normalizeAoCommands(payload, node.defaultChannel, node.aoScale, node.aoOffset).forEach(function (command) {
|
|
276
|
+
operations.push({ kind: "ao", command: command });
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (!operations.length) {
|
|
281
|
+
throw new Error("Nu exista comenzi DO/AO valide in payload");
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
await connect();
|
|
285
|
+
const profile = await getProfile();
|
|
286
|
+
|
|
287
|
+
const written = [];
|
|
288
|
+
for (let i = 0; i < operations.length; i += 1) {
|
|
289
|
+
const op = operations[i];
|
|
290
|
+
written.push(op.kind === "do" ? await writeDo(op.command, profile) : await writeAo(op.command, profile));
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
node.status({ fill: "green", shape: "dot", text: (profile.model || "ADAM") + " write ok" });
|
|
294
|
+
node.send({
|
|
295
|
+
topic: "advantech/write",
|
|
296
|
+
payload: {
|
|
297
|
+
status: "ok",
|
|
298
|
+
host: node.host,
|
|
299
|
+
port: node.port,
|
|
300
|
+
unit_id: node.unitId,
|
|
301
|
+
module_model: profile.model || null,
|
|
302
|
+
module_model_code: profile.modelCode || null,
|
|
303
|
+
detected_by: profile.detectedBy || null,
|
|
304
|
+
channel_counts: {
|
|
305
|
+
di: toNonNegativeInt(profile.diCount, 0),
|
|
306
|
+
do: toNonNegativeInt(profile.doCount, 0),
|
|
307
|
+
ai: toNonNegativeInt(profile.aiCount, 0),
|
|
308
|
+
ao: toNonNegativeInt(profile.aoCount, 0)
|
|
309
|
+
},
|
|
310
|
+
written: written,
|
|
311
|
+
ts: new Date().toISOString()
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
node.on("input", function (msg) {
|
|
317
|
+
queue = queue.then(function () {
|
|
318
|
+
return handleInput(msg);
|
|
319
|
+
}).catch(async function (error) {
|
|
320
|
+
node.status({ fill: "red", shape: "ring", text: "error" });
|
|
321
|
+
node.error(error.message || error, msg || {});
|
|
322
|
+
await closeConnection();
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
node.on("close", function (removed, done) {
|
|
327
|
+
closeConnection().then(function () {
|
|
328
|
+
done();
|
|
329
|
+
}).catch(function () {
|
|
330
|
+
done();
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
RED.nodes.registerType("advantech-write", AdvantechWriteNode);
|
|
338
|
+
};
|