@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,185 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { stringToRegisters, float32ToRegisters } = require("./binary-codec");
|
|
4
|
+
|
|
5
|
+
// These are effective addresses carried in the Modbus PDU. Use addressToPdu()
|
|
6
|
+
// if a future profile needs 40001-style documentation addresses translated.
|
|
7
|
+
const ADDRESS = Object.freeze({
|
|
8
|
+
GEN24_PROBE_A: 768,
|
|
9
|
+
GEN24_PROBE_B: 1706,
|
|
10
|
+
SUNSPEC: 40000,
|
|
11
|
+
COMMON_MODEL: 40002,
|
|
12
|
+
COMMON_DATA: 40004,
|
|
13
|
+
METER_MODEL: 40069,
|
|
14
|
+
METER_DATA: 40071,
|
|
15
|
+
ACTIVE_POWER: 40097,
|
|
16
|
+
EXPORT_ENERGY: 40129,
|
|
17
|
+
IMPORT_ENERGY: 40137,
|
|
18
|
+
METER_END: 40195,
|
|
19
|
+
COMPATIBILITY: 50000
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const DEFAULT_MEASUREMENTS = Object.freeze({
|
|
23
|
+
power: 0,
|
|
24
|
+
powerL1: 0,
|
|
25
|
+
powerL2: 0,
|
|
26
|
+
powerL3: 0,
|
|
27
|
+
importEnergy: 0,
|
|
28
|
+
exportEnergy: 0,
|
|
29
|
+
voltageL1: 230,
|
|
30
|
+
voltageL2: 230,
|
|
31
|
+
voltageL3: 230,
|
|
32
|
+
currentL1: 0,
|
|
33
|
+
currentL2: 0,
|
|
34
|
+
currentL3: 0,
|
|
35
|
+
frequency: 50,
|
|
36
|
+
reactivePower: 0,
|
|
37
|
+
reactivePowerL1: 0,
|
|
38
|
+
reactivePowerL2: 0,
|
|
39
|
+
reactivePowerL3: 0,
|
|
40
|
+
apparentPower: 0,
|
|
41
|
+
apparentPowerL1: 0,
|
|
42
|
+
apparentPowerL2: 0,
|
|
43
|
+
apparentPowerL3: 0,
|
|
44
|
+
powerFactor: 1,
|
|
45
|
+
powerFactorL1: 1,
|
|
46
|
+
powerFactorL2: 1,
|
|
47
|
+
powerFactorL3: 1
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const ZERO_FILL_RANGES = Object.freeze([
|
|
51
|
+
// GEN24 probes these legacy/vendor locations before scanning SunSpec.
|
|
52
|
+
Object.freeze([768, 768]),
|
|
53
|
+
Object.freeze([1706, 1706]),
|
|
54
|
+
Object.freeze([40000, 40255]),
|
|
55
|
+
Object.freeze([50000, 50063])
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
function addressToPdu(address, mode) {
|
|
59
|
+
const numeric = Number(address);
|
|
60
|
+
if (!Number.isInteger(numeric)) throw new TypeError("address must be an integer");
|
|
61
|
+
return mode === "modbus-40001" ? numeric - 40001 : numeric;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function putWords(map, startAddress, words) {
|
|
65
|
+
words.forEach(function (word, index) { map.set(startAddress + index, word & 0xffff); });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function putFloat(map, address, value, dynamicAddresses) {
|
|
69
|
+
putWords(map, address, float32ToRegisters(value));
|
|
70
|
+
dynamicAddresses.add(address);
|
|
71
|
+
dynamicAddresses.add(address + 1);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function createFroniusRegisterMap(options) {
|
|
75
|
+
const settings = Object.assign({
|
|
76
|
+
manufacturer: "Fronius",
|
|
77
|
+
model: "Smart Meter 63A",
|
|
78
|
+
version: "1.0",
|
|
79
|
+
serialNumber: "00000001",
|
|
80
|
+
unitId: 1,
|
|
81
|
+
// Confirmed by community captures of a GEN24-compatible meter profile.
|
|
82
|
+
// This SunSpec DA is independent from the MBAP Unit ID selected in the UI.
|
|
83
|
+
deviceAddress: 240,
|
|
84
|
+
powerSignConvention: "import-positive"
|
|
85
|
+
}, options || {});
|
|
86
|
+
|
|
87
|
+
function build(measurements, stale, staleBehaviour) {
|
|
88
|
+
const values = Object.assign({}, DEFAULT_MEASUREMENTS, measurements || {});
|
|
89
|
+
const map = new Map();
|
|
90
|
+
const dynamicAddresses = new Set();
|
|
91
|
+
|
|
92
|
+
ZERO_FILL_RANGES.forEach(function (range) {
|
|
93
|
+
for (let address = range[0]; address <= range[1]; address += 1) map.set(address, 0);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
putWords(map, ADDRESS.SUNSPEC, [0x5375, 0x6e53]);
|
|
97
|
+
putWords(map, ADDRESS.COMMON_MODEL, [1, 65]);
|
|
98
|
+
putWords(map, 40004, stringToRegisters(settings.manufacturer, 16));
|
|
99
|
+
putWords(map, 40020, stringToRegisters(settings.model, 16));
|
|
100
|
+
putWords(map, 40036, stringToRegisters("", 8));
|
|
101
|
+
putWords(map, 40044, stringToRegisters(settings.version, 8));
|
|
102
|
+
putWords(map, 40052, stringToRegisters(settings.serialNumber, 16));
|
|
103
|
+
map.set(40068, settings.deviceAddress & 0xff);
|
|
104
|
+
|
|
105
|
+
// Community compatibility profile: SunSpec meter model 213, float layout.
|
|
106
|
+
map.set(ADDRESS.METER_MODEL, 213);
|
|
107
|
+
map.set(ADDRESS.METER_MODEL + 1, 124);
|
|
108
|
+
const zeroFlow = stale && staleBehaviour === "zero-power";
|
|
109
|
+
const currentL1 = zeroFlow ? 0 : values.currentL1;
|
|
110
|
+
const currentL2 = zeroFlow ? 0 : values.currentL2;
|
|
111
|
+
const currentL3 = zeroFlow ? 0 : values.currentL3;
|
|
112
|
+
const totalCurrent = currentL1 + currentL2 + currentL3;
|
|
113
|
+
putFloat(map, 40071, totalCurrent, dynamicAddresses);
|
|
114
|
+
putFloat(map, 40073, currentL1, dynamicAddresses);
|
|
115
|
+
putFloat(map, 40075, currentL2, dynamicAddresses);
|
|
116
|
+
putFloat(map, 40077, currentL3, dynamicAddresses);
|
|
117
|
+
putFloat(map, 40079, (values.voltageL1 + values.voltageL2 + values.voltageL3) / 3, dynamicAddresses);
|
|
118
|
+
putFloat(map, 40081, values.voltageL1, dynamicAddresses);
|
|
119
|
+
putFloat(map, 40083, values.voltageL2, dynamicAddresses);
|
|
120
|
+
putFloat(map, 40085, values.voltageL3, dynamicAddresses);
|
|
121
|
+
putFloat(map, 40095, values.frequency, dynamicAddresses);
|
|
122
|
+
|
|
123
|
+
const stalePower = zeroFlow ? 0 : values.power;
|
|
124
|
+
const stalePowerL1 = zeroFlow ? 0 : values.powerL1;
|
|
125
|
+
const stalePowerL2 = zeroFlow ? 0 : values.powerL2;
|
|
126
|
+
const stalePowerL3 = zeroFlow ? 0 : values.powerL3;
|
|
127
|
+
const signedPower = settings.powerSignConvention === "export-positive" ? -stalePower : stalePower;
|
|
128
|
+
const signedPowerL1 = settings.powerSignConvention === "export-positive" ? -stalePowerL1 : stalePowerL1;
|
|
129
|
+
const signedPowerL2 = settings.powerSignConvention === "export-positive" ? -stalePowerL2 : stalePowerL2;
|
|
130
|
+
const signedPowerL3 = settings.powerSignConvention === "export-positive" ? -stalePowerL3 : stalePowerL3;
|
|
131
|
+
putFloat(map, ADDRESS.ACTIVE_POWER, signedPower, dynamicAddresses);
|
|
132
|
+
putFloat(map, 40099, signedPowerL1, dynamicAddresses);
|
|
133
|
+
putFloat(map, 40101, signedPowerL2, dynamicAddresses);
|
|
134
|
+
putFloat(map, 40103, signedPowerL3, dynamicAddresses);
|
|
135
|
+
putFloat(map, 40105, zeroFlow ? 0 : values.apparentPower, dynamicAddresses);
|
|
136
|
+
putFloat(map, 40107, zeroFlow ? 0 : values.apparentPowerL1, dynamicAddresses);
|
|
137
|
+
putFloat(map, 40109, zeroFlow ? 0 : values.apparentPowerL2, dynamicAddresses);
|
|
138
|
+
putFloat(map, 40111, zeroFlow ? 0 : values.apparentPowerL3, dynamicAddresses);
|
|
139
|
+
putFloat(map, 40113, zeroFlow ? 0 : values.reactivePower, dynamicAddresses);
|
|
140
|
+
putFloat(map, 40115, zeroFlow ? 0 : values.reactivePowerL1, dynamicAddresses);
|
|
141
|
+
putFloat(map, 40117, zeroFlow ? 0 : values.reactivePowerL2, dynamicAddresses);
|
|
142
|
+
putFloat(map, 40119, zeroFlow ? 0 : values.reactivePowerL3, dynamicAddresses);
|
|
143
|
+
putFloat(map, 40121, values.powerFactor, dynamicAddresses);
|
|
144
|
+
putFloat(map, 40123, values.powerFactorL1, dynamicAddresses);
|
|
145
|
+
putFloat(map, 40125, values.powerFactorL2, dynamicAddresses);
|
|
146
|
+
putFloat(map, 40127, values.powerFactorL3, dynamicAddresses);
|
|
147
|
+
// Cumulative energies are exposed as Wh and never have their sign inverted.
|
|
148
|
+
putFloat(map, ADDRESS.EXPORT_ENERGY, values.exportEnergy, dynamicAddresses);
|
|
149
|
+
putFloat(map, ADDRESS.IMPORT_ENERGY, values.importEnergy, dynamicAddresses);
|
|
150
|
+
map.set(ADDRESS.METER_END, 0xffff);
|
|
151
|
+
map.set(ADDRESS.METER_END + 1, 0);
|
|
152
|
+
|
|
153
|
+
// 50000 must remain zero-filled: GEN24 uses it as a pre-SunSpec probe.
|
|
154
|
+
return { map, dynamicAddresses };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function read(startAddress, quantity, state) {
|
|
158
|
+
if (!Number.isInteger(startAddress) || !Number.isInteger(quantity) || quantity < 1 || quantity > 125 || startAddress < 0 || startAddress + quantity > 65536) {
|
|
159
|
+
return { exceptionCode: 3, issueCode: "invalid_read_range" };
|
|
160
|
+
}
|
|
161
|
+
const current = state || {};
|
|
162
|
+
const image = build(current.measurements, !!current.stale, current.staleBehaviour || "zero-power");
|
|
163
|
+
if (current.stale && current.staleBehaviour === "modbus-exception") {
|
|
164
|
+
for (let address = startAddress; address < startAddress + quantity; address += 1) {
|
|
165
|
+
if (image.dynamicAddresses.has(address)) return { exceptionCode: 4, issueCode: "stale_dynamic_data" };
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const registers = [];
|
|
169
|
+
for (let address = startAddress; address < startAddress + quantity; address += 1) {
|
|
170
|
+
if (!image.map.has(address)) return { exceptionCode: 2, issueCode: "illegal_data_address" };
|
|
171
|
+
registers.push(image.map.get(address));
|
|
172
|
+
}
|
|
173
|
+
return { registers };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return { read, build, addresses: ADDRESS };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
module.exports = {
|
|
180
|
+
ADDRESS,
|
|
181
|
+
DEFAULT_MEASUREMENTS,
|
|
182
|
+
ZERO_FILL_RANGES,
|
|
183
|
+
addressToPdu,
|
|
184
|
+
createFroniusRegisterMap
|
|
185
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
class ModbusParserError extends Error {
|
|
4
|
+
constructor(code, message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "ModbusParserError";
|
|
7
|
+
this.code = code;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Stateful parser for fragmented or coalesced Modbus TCP ADUs. */
|
|
12
|
+
class ModbusTcpParser {
|
|
13
|
+
constructor(options) {
|
|
14
|
+
const settings = options || {};
|
|
15
|
+
this.maxAduLength = Number(settings.maxAduLength || 260);
|
|
16
|
+
this.maxBufferLength = Number(settings.maxBufferLength || 4096);
|
|
17
|
+
this.buffer = Buffer.alloc(0);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
push(chunk) {
|
|
21
|
+
if (!Buffer.isBuffer(chunk)) throw new TypeError("chunk must be a Buffer");
|
|
22
|
+
if (this.buffer.length + chunk.length > this.maxBufferLength) {
|
|
23
|
+
this.reset();
|
|
24
|
+
throw new ModbusParserError("BUFFER_LIMIT", "Modbus receive buffer limit exceeded");
|
|
25
|
+
}
|
|
26
|
+
this.buffer = this.buffer.length ? Buffer.concat([this.buffer, chunk]) : Buffer.from(chunk);
|
|
27
|
+
const frames = [];
|
|
28
|
+
while (this.buffer.length >= 7) {
|
|
29
|
+
const protocolId = this.buffer.readUInt16BE(2);
|
|
30
|
+
const length = this.buffer.readUInt16BE(4);
|
|
31
|
+
const frameLength = 6 + length;
|
|
32
|
+
if (length < 2 || frameLength > this.maxAduLength) {
|
|
33
|
+
this.reset();
|
|
34
|
+
throw new ModbusParserError("INVALID_LENGTH", "Invalid MBAP length: " + length);
|
|
35
|
+
}
|
|
36
|
+
if (protocolId !== 0) {
|
|
37
|
+
this.reset();
|
|
38
|
+
throw new ModbusParserError("INVALID_PROTOCOL", "Invalid Modbus protocol identifier: " + protocolId);
|
|
39
|
+
}
|
|
40
|
+
if (this.buffer.length < frameLength) break;
|
|
41
|
+
const frame = Buffer.from(this.buffer.subarray(0, frameLength));
|
|
42
|
+
this.buffer = Buffer.from(this.buffer.subarray(frameLength));
|
|
43
|
+
frames.push({
|
|
44
|
+
transactionId: frame.readUInt16BE(0),
|
|
45
|
+
protocolId,
|
|
46
|
+
length,
|
|
47
|
+
unitId: frame.readUInt8(6),
|
|
48
|
+
functionCode: frame.readUInt8(7),
|
|
49
|
+
pdu: Buffer.from(frame.subarray(7)),
|
|
50
|
+
adu: frame
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return frames;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
reset() {
|
|
57
|
+
this.buffer = Buffer.alloc(0);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = { ModbusTcpParser, ModbusParserError };
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const net = require("net");
|
|
4
|
+
const { performance } = require("perf_hooks");
|
|
5
|
+
const { ModbusTcpParser } = require("./modbus-parser");
|
|
6
|
+
|
|
7
|
+
function adu(transactionId, unitId, pdu) {
|
|
8
|
+
const response = Buffer.allocUnsafe(7 + pdu.length);
|
|
9
|
+
response.writeUInt16BE(transactionId, 0);
|
|
10
|
+
response.writeUInt16BE(0, 2);
|
|
11
|
+
response.writeUInt16BE(pdu.length + 1, 4);
|
|
12
|
+
response.writeUInt8(unitId, 6);
|
|
13
|
+
pdu.copy(response, 7);
|
|
14
|
+
return response;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function exception(transactionId, unitId, functionCode, code) {
|
|
18
|
+
return adu(transactionId, unitId, Buffer.from([(functionCode | 0x80) & 0xff, code & 0xff]));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function readResponse(frame, registers) {
|
|
22
|
+
const pdu = Buffer.allocUnsafe(2 + registers.length * 2);
|
|
23
|
+
pdu.writeUInt8(frame.functionCode, 0);
|
|
24
|
+
pdu.writeUInt8(registers.length * 2, 1);
|
|
25
|
+
registers.forEach(function (word, index) { pdu.writeUInt16BE(word & 0xffff, 2 + index * 2); });
|
|
26
|
+
return adu(frame.transactionId, frame.unitId, pdu);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function createModbusTcpServer(options) {
|
|
30
|
+
const settings = Object.assign({
|
|
31
|
+
bindAddress: "0.0.0.0",
|
|
32
|
+
port: 1502,
|
|
33
|
+
unitId: 1,
|
|
34
|
+
maxClients: 5,
|
|
35
|
+
clientIdleTimeout: 30000,
|
|
36
|
+
maxAduLength: 260,
|
|
37
|
+
maxBufferLength: 4096,
|
|
38
|
+
allowWrites: false
|
|
39
|
+
}, options || {});
|
|
40
|
+
let server = null;
|
|
41
|
+
let stopping = false;
|
|
42
|
+
const clients = new Set();
|
|
43
|
+
|
|
44
|
+
function notify(name, payload) {
|
|
45
|
+
if (typeof settings[name] === "function") settings[name](payload);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function send(state, buffer) {
|
|
49
|
+
if (state.socket.destroyed) return;
|
|
50
|
+
if (state.blocked) {
|
|
51
|
+
state.pending.push(buffer);
|
|
52
|
+
if (state.pending.length > 32) state.socket.destroy(new Error("response queue limit exceeded"));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
state.blocked = !state.socket.write(buffer);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function drain(state) {
|
|
59
|
+
state.blocked = false;
|
|
60
|
+
while (!state.blocked && state.pending.length && !state.socket.destroyed) {
|
|
61
|
+
state.blocked = !state.socket.write(state.pending.shift());
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function parseRead(frame) {
|
|
66
|
+
if (frame.pdu.length !== 5) return { response: exception(frame.transactionId, frame.unitId, frame.functionCode, 3) };
|
|
67
|
+
const startAddress = frame.pdu.readUInt16BE(1);
|
|
68
|
+
const quantity = frame.pdu.readUInt16BE(3);
|
|
69
|
+
if (quantity < 1 || quantity > 125 || startAddress + quantity > 65536) {
|
|
70
|
+
return { startAddress, quantity, response: exception(frame.transactionId, frame.unitId, frame.functionCode, 3) };
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
const result = settings.readRegisters(startAddress, quantity, frame.functionCode, frame.unitId);
|
|
74
|
+
if (!result || result.exceptionCode) {
|
|
75
|
+
return { startAddress, quantity, response: exception(frame.transactionId, frame.unitId, frame.functionCode, result && result.exceptionCode || 4) };
|
|
76
|
+
}
|
|
77
|
+
if (!Array.isArray(result.registers) || result.registers.length !== quantity) {
|
|
78
|
+
return { startAddress, quantity, response: exception(frame.transactionId, frame.unitId, frame.functionCode, 4) };
|
|
79
|
+
}
|
|
80
|
+
return { startAddress, quantity, response: readResponse(frame, result.registers) };
|
|
81
|
+
} catch (error) {
|
|
82
|
+
notify("onError", error);
|
|
83
|
+
return { startAddress, quantity, response: exception(frame.transactionId, frame.unitId, frame.functionCode, 4) };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function performWrite(frame, broadcast) {
|
|
88
|
+
const fc = frame.functionCode;
|
|
89
|
+
if (!settings.allowWrites) return broadcast ? {} : { response: exception(frame.transactionId, frame.unitId, fc, 2) };
|
|
90
|
+
try {
|
|
91
|
+
if (fc === 6) {
|
|
92
|
+
if (frame.pdu.length !== 5) return broadcast ? {} : { response: exception(frame.transactionId, frame.unitId, fc, 3) };
|
|
93
|
+
const startAddress = frame.pdu.readUInt16BE(1);
|
|
94
|
+
const value = frame.pdu.readUInt16BE(3);
|
|
95
|
+
const result = typeof settings.writeRegisters === "function" ? settings.writeRegisters(startAddress, [value], frame.unitId) : { exceptionCode: 2 };
|
|
96
|
+
if (broadcast) return { startAddress, quantity: 1 };
|
|
97
|
+
if (!result || result.exceptionCode) return { startAddress, quantity: 1, response: exception(frame.transactionId, frame.unitId, fc, result && result.exceptionCode || 2) };
|
|
98
|
+
return { startAddress, quantity: 1, response: adu(frame.transactionId, frame.unitId, Buffer.from(frame.pdu)) };
|
|
99
|
+
}
|
|
100
|
+
if (frame.pdu.length < 6) return broadcast ? {} : { response: exception(frame.transactionId, frame.unitId, fc, 3) };
|
|
101
|
+
const startAddress = frame.pdu.readUInt16BE(1);
|
|
102
|
+
const quantity = frame.pdu.readUInt16BE(3);
|
|
103
|
+
const byteCount = frame.pdu.readUInt8(5);
|
|
104
|
+
if (quantity < 1 || quantity > 123 || startAddress + quantity > 65536 || byteCount !== quantity * 2 || frame.pdu.length !== 6 + byteCount) {
|
|
105
|
+
return broadcast ? { startAddress, quantity } : { startAddress, quantity, response: exception(frame.transactionId, frame.unitId, fc, 3) };
|
|
106
|
+
}
|
|
107
|
+
const values = [];
|
|
108
|
+
for (let index = 0; index < quantity; index += 1) values.push(frame.pdu.readUInt16BE(6 + index * 2));
|
|
109
|
+
const result = typeof settings.writeRegisters === "function" ? settings.writeRegisters(startAddress, values, frame.unitId) : { exceptionCode: 2 };
|
|
110
|
+
if (broadcast) return { startAddress, quantity };
|
|
111
|
+
if (!result || result.exceptionCode) return { startAddress, quantity, response: exception(frame.transactionId, frame.unitId, fc, result && result.exceptionCode || 2) };
|
|
112
|
+
const pdu = Buffer.allocUnsafe(5);
|
|
113
|
+
pdu.writeUInt8(16, 0);
|
|
114
|
+
pdu.writeUInt16BE(startAddress, 1);
|
|
115
|
+
pdu.writeUInt16BE(quantity, 3);
|
|
116
|
+
return { startAddress, quantity, response: adu(frame.transactionId, frame.unitId, pdu) };
|
|
117
|
+
} catch (error) {
|
|
118
|
+
notify("onError", error);
|
|
119
|
+
return broadcast ? {} : { response: exception(frame.transactionId, frame.unitId, fc, 4) };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function handleFrame(state, frame) {
|
|
124
|
+
const started = performance.now();
|
|
125
|
+
const isWrite = frame.functionCode === 6 || frame.functionCode === 16;
|
|
126
|
+
const broadcast = frame.unitId === 0 && isWrite;
|
|
127
|
+
const unitAllowed = typeof settings.isUnitIdAllowed === "function"
|
|
128
|
+
? settings.isUnitIdAllowed(frame.unitId)
|
|
129
|
+
: frame.unitId === settings.unitId;
|
|
130
|
+
if (!unitAllowed && !broadcast) return;
|
|
131
|
+
let outcome;
|
|
132
|
+
if (frame.functionCode === 3 || frame.functionCode === 4) outcome = parseRead(frame);
|
|
133
|
+
else if (isWrite) outcome = performWrite(frame, broadcast);
|
|
134
|
+
else outcome = { response: exception(frame.transactionId, frame.unitId, frame.functionCode, 1) };
|
|
135
|
+
if (outcome.response) send(state, outcome.response);
|
|
136
|
+
notify("onRequest", {
|
|
137
|
+
remoteAddress: state.remoteAddress,
|
|
138
|
+
transactionId: frame.transactionId,
|
|
139
|
+
unitId: frame.unitId,
|
|
140
|
+
functionCode: frame.functionCode,
|
|
141
|
+
startAddress: outcome.startAddress,
|
|
142
|
+
quantity: outcome.quantity,
|
|
143
|
+
responseTimeMs: Number((performance.now() - started).toFixed(3))
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function start() {
|
|
148
|
+
if (server) return Promise.resolve(server.address());
|
|
149
|
+
stopping = false;
|
|
150
|
+
return new Promise(function (resolve, reject) {
|
|
151
|
+
const instance = net.createServer(function (socket) {
|
|
152
|
+
if (clients.size >= settings.maxClients) {
|
|
153
|
+
notify("onClientRejected", { remoteAddress: socket.remoteAddress || null });
|
|
154
|
+
socket.destroy();
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const state = {
|
|
158
|
+
socket,
|
|
159
|
+
parser: new ModbusTcpParser({ maxAduLength: settings.maxAduLength, maxBufferLength: settings.maxBufferLength }),
|
|
160
|
+
remoteAddress: String(socket.remoteAddress || "").replace(/^::ffff:/, ""),
|
|
161
|
+
pending: [],
|
|
162
|
+
blocked: false
|
|
163
|
+
};
|
|
164
|
+
clients.add(state);
|
|
165
|
+
socket.setTimeout(settings.clientIdleTimeout);
|
|
166
|
+
notify("onClientConnected", { remoteAddress: state.remoteAddress, clients: clients.size });
|
|
167
|
+
socket.on("data", function (chunk) {
|
|
168
|
+
try {
|
|
169
|
+
state.parser.push(chunk).forEach(function (frame) { handleFrame(state, frame); });
|
|
170
|
+
} catch (error) {
|
|
171
|
+
notify("onProtocolError", { code: error.code || "PARSER_ERROR", message: error.message, remoteAddress: state.remoteAddress });
|
|
172
|
+
socket.destroy();
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
socket.on("drain", function () { drain(state); });
|
|
176
|
+
socket.on("timeout", function () { socket.destroy(); });
|
|
177
|
+
socket.on("error", function (error) { notify("onSocketError", error); });
|
|
178
|
+
socket.on("close", function () {
|
|
179
|
+
if (!clients.delete(state)) return;
|
|
180
|
+
state.pending.length = 0;
|
|
181
|
+
notify("onClientDisconnected", { remoteAddress: state.remoteAddress, clients: clients.size });
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
server = instance;
|
|
185
|
+
function listenError(error) {
|
|
186
|
+
instance.removeListener("listening", listening);
|
|
187
|
+
server = null;
|
|
188
|
+
reject(error);
|
|
189
|
+
}
|
|
190
|
+
function listening() {
|
|
191
|
+
instance.removeListener("error", listenError);
|
|
192
|
+
instance.on("error", function (error) { notify("onError", error); });
|
|
193
|
+
resolve(instance.address());
|
|
194
|
+
}
|
|
195
|
+
instance.once("error", listenError);
|
|
196
|
+
instance.once("listening", listening);
|
|
197
|
+
instance.listen(settings.port, settings.bindAddress);
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function stop() {
|
|
202
|
+
if (!server || stopping) return Promise.resolve();
|
|
203
|
+
stopping = true;
|
|
204
|
+
const instance = server;
|
|
205
|
+
server = null;
|
|
206
|
+
clients.forEach(function (state) { state.socket.destroy(); });
|
|
207
|
+
return new Promise(function (resolve) {
|
|
208
|
+
instance.close(function () {
|
|
209
|
+
clients.clear();
|
|
210
|
+
stopping = false;
|
|
211
|
+
resolve();
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
start,
|
|
218
|
+
stop,
|
|
219
|
+
address: function () { return server ? server.address() : null; },
|
|
220
|
+
clientCount: function () { return clients.size; }
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
module.exports = { createModbusTcpServer, exception };
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const MEASUREMENT_RULES = Object.freeze({
|
|
4
|
+
power: { min: -1e9, max: 1e9 },
|
|
5
|
+
powerL1: { min: -1e9, max: 1e9 },
|
|
6
|
+
powerL2: { min: -1e9, max: 1e9 },
|
|
7
|
+
powerL3: { min: -1e9, max: 1e9 },
|
|
8
|
+
importEnergy: { min: 0, max: 1e15 },
|
|
9
|
+
exportEnergy: { min: 0, max: 1e15 },
|
|
10
|
+
voltageL1: { min: 0, max: 10000 },
|
|
11
|
+
voltageL2: { min: 0, max: 10000 },
|
|
12
|
+
voltageL3: { min: 0, max: 10000 },
|
|
13
|
+
currentL1: { min: 0, max: 1e6 },
|
|
14
|
+
currentL2: { min: 0, max: 1e6 },
|
|
15
|
+
currentL3: { min: 0, max: 1e6 },
|
|
16
|
+
frequency: { min: 0, max: 1000 },
|
|
17
|
+
reactivePower: { min: -1e9, max: 1e9 },
|
|
18
|
+
reactivePowerL1: { min: -1e9, max: 1e9 },
|
|
19
|
+
reactivePowerL2: { min: -1e9, max: 1e9 },
|
|
20
|
+
reactivePowerL3: { min: -1e9, max: 1e9 },
|
|
21
|
+
apparentPower: { min: 0, max: 1e9 },
|
|
22
|
+
apparentPowerL1: { min: 0, max: 1e9 },
|
|
23
|
+
apparentPowerL2: { min: 0, max: 1e9 },
|
|
24
|
+
apparentPowerL3: { min: 0, max: 1e9 },
|
|
25
|
+
powerFactor: { min: -1, max: 1 },
|
|
26
|
+
powerFactorL1: { min: -1, max: 1 },
|
|
27
|
+
powerFactorL2: { min: -1, max: 1 },
|
|
28
|
+
powerFactorL3: { min: -1, max: 1 }
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const ALIASES = Object.freeze({
|
|
32
|
+
power: ["power", "activePower", "gridPower", "watts", "W", "power_w", "active_power_w"],
|
|
33
|
+
importEnergy: ["importEnergy", "energyImport", "importedEnergy", "importWh"],
|
|
34
|
+
exportEnergy: ["exportEnergy", "energyExport", "exportedEnergy", "exportWh"]
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const FULL_PAYLOAD_FIELDS = Object.freeze([
|
|
38
|
+
Object.freeze({ key: "power", paths: ["summary.power.total_w"] }),
|
|
39
|
+
Object.freeze({ key: "powerL1", paths: ["summary.power.l1_w"] }),
|
|
40
|
+
Object.freeze({ key: "powerL2", paths: ["summary.power.l2_w"] }),
|
|
41
|
+
Object.freeze({ key: "powerL3", paths: ["summary.power.l3_w"] }),
|
|
42
|
+
Object.freeze({ key: "importEnergy", paths: ["summary.energy.import_kwh"], scale: 1000 }),
|
|
43
|
+
Object.freeze({ key: "exportEnergy", paths: ["summary.energy.export_kwh"], scale: 1000 }),
|
|
44
|
+
Object.freeze({ key: "voltageL1", paths: ["summary.voltage.l1_n_v"] }),
|
|
45
|
+
Object.freeze({ key: "voltageL2", paths: ["summary.voltage.l2_n_v"] }),
|
|
46
|
+
Object.freeze({ key: "voltageL3", paths: ["summary.voltage.l3_n_v"] }),
|
|
47
|
+
Object.freeze({ key: "currentL1", paths: ["summary.current.l1_a"] }),
|
|
48
|
+
Object.freeze({ key: "currentL2", paths: ["summary.current.l2_a"] }),
|
|
49
|
+
Object.freeze({ key: "currentL3", paths: ["summary.current.l3_a"] }),
|
|
50
|
+
Object.freeze({ key: "frequency", paths: ["summary.frequency_hz"] }),
|
|
51
|
+
Object.freeze({ key: "reactivePower", paths: ["summary.reactive.total_var"] }),
|
|
52
|
+
Object.freeze({ key: "reactivePowerL1", paths: ["summary.reactive.l1_var"] }),
|
|
53
|
+
Object.freeze({ key: "reactivePowerL2", paths: ["summary.reactive.l2_var"] }),
|
|
54
|
+
Object.freeze({ key: "reactivePowerL3", paths: ["summary.reactive.l3_var"] }),
|
|
55
|
+
Object.freeze({ key: "apparentPower", paths: ["summary.apparent.total_va"] }),
|
|
56
|
+
Object.freeze({ key: "apparentPowerL1", paths: ["summary.apparent.l1_va"] }),
|
|
57
|
+
Object.freeze({ key: "apparentPowerL2", paths: ["summary.apparent.l2_va"] }),
|
|
58
|
+
Object.freeze({ key: "apparentPowerL3", paths: ["summary.apparent.l3_va"] }),
|
|
59
|
+
Object.freeze({ key: "powerFactor", paths: ["summary.power_factor.total", "summary.power_quality.power_factor.avg"] }),
|
|
60
|
+
Object.freeze({ key: "powerFactorL1", paths: ["summary.power_factor.l1"] }),
|
|
61
|
+
Object.freeze({ key: "powerFactorL2", paths: ["summary.power_factor.l2"] }),
|
|
62
|
+
Object.freeze({ key: "powerFactorL3", paths: ["summary.power_factor.l3"] })
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
const COMPACT_PAYLOAD_FIELDS = Object.freeze([
|
|
66
|
+
Object.freeze({ key: "importEnergy", names: ["energy_import_kwh"], scale: 1000 }),
|
|
67
|
+
Object.freeze({ key: "exportEnergy", names: ["energy_export_kwh"], scale: 1000 }),
|
|
68
|
+
Object.freeze({ key: "voltageL1", names: ["voltage_l1_n_v"] }),
|
|
69
|
+
Object.freeze({ key: "voltageL2", names: ["voltage_l2_n_v"] }),
|
|
70
|
+
Object.freeze({ key: "voltageL3", names: ["voltage_l3_n_v"] }),
|
|
71
|
+
Object.freeze({ key: "currentL1", names: ["current_l1_a"] }),
|
|
72
|
+
Object.freeze({ key: "currentL2", names: ["current_l2_a"] }),
|
|
73
|
+
Object.freeze({ key: "currentL3", names: ["current_l3_a"] }),
|
|
74
|
+
Object.freeze({ key: "frequency", names: ["frequency_hz"] }),
|
|
75
|
+
Object.freeze({ key: "reactivePower", names: ["reactive_power_var", "reactive_total_var"] }),
|
|
76
|
+
Object.freeze({ key: "reactivePowerL1", names: ["reactive_power_l1_var", "reactive_l1_var"] }),
|
|
77
|
+
Object.freeze({ key: "reactivePowerL2", names: ["reactive_power_l2_var", "reactive_l2_var"] }),
|
|
78
|
+
Object.freeze({ key: "reactivePowerL3", names: ["reactive_power_l3_var", "reactive_l3_var"] }),
|
|
79
|
+
Object.freeze({ key: "apparentPower", names: ["apparent_power_va", "apparent_total_va"] }),
|
|
80
|
+
Object.freeze({ key: "apparentPowerL1", names: ["apparent_power_l1_va", "apparent_l1_va"] }),
|
|
81
|
+
Object.freeze({ key: "apparentPowerL2", names: ["apparent_power_l2_va", "apparent_l2_va"] }),
|
|
82
|
+
Object.freeze({ key: "apparentPowerL3", names: ["apparent_power_l3_va", "apparent_l3_va"] }),
|
|
83
|
+
Object.freeze({ key: "powerFactor", names: ["power_factor", "power_factor_total"] }),
|
|
84
|
+
Object.freeze({ key: "powerFactorL1", names: ["power_factor_l1"] }),
|
|
85
|
+
Object.freeze({ key: "powerFactorL2", names: ["power_factor_l2"] }),
|
|
86
|
+
Object.freeze({ key: "powerFactorL3", names: ["power_factor_l3"] }),
|
|
87
|
+
Object.freeze({ key: "powerL1", names: ["power_l1_w", "active_power_l1_w"] }),
|
|
88
|
+
Object.freeze({ key: "powerL2", names: ["power_l2_w", "active_power_l2_w"] }),
|
|
89
|
+
Object.freeze({ key: "powerL3", names: ["power_l3_w", "active_power_l3_w"] })
|
|
90
|
+
]);
|
|
91
|
+
|
|
92
|
+
function integerInRange(value, fallback, min, max) {
|
|
93
|
+
const numeric = Number(value);
|
|
94
|
+
return Number.isInteger(numeric) && numeric >= min && numeric <= max ? numeric : fallback;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function cleanText(value, fallback, maximumLength) {
|
|
98
|
+
const text = String(value == null ? "" : value).trim();
|
|
99
|
+
return (text || fallback).slice(0, maximumLength);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function canonicalKey(key) {
|
|
103
|
+
const text = String(key || "");
|
|
104
|
+
const direct = Object.prototype.hasOwnProperty.call(MEASUREMENT_RULES, text) ? text : null;
|
|
105
|
+
if (direct) return direct;
|
|
106
|
+
return Object.keys(ALIASES).find(function (canonical) {
|
|
107
|
+
return ALIASES[canonical].includes(text);
|
|
108
|
+
}) || null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function finiteMeasurement(key, value) {
|
|
112
|
+
const rule = MEASUREMENT_RULES[key];
|
|
113
|
+
const numeric = value === null || value === undefined || value === "" ? NaN : Number(value);
|
|
114
|
+
if (!rule || !Number.isFinite(numeric) || numeric < rule.min || numeric > rule.max) {
|
|
115
|
+
throw new RangeError(key + " must be a finite number between " + (rule ? rule.min : "?") + " and " + (rule ? rule.max : "?"));
|
|
116
|
+
}
|
|
117
|
+
return numeric;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function getByPath(source, path) {
|
|
121
|
+
return String(path).split(".").reduce(function (value, part) {
|
|
122
|
+
return value && typeof value === "object" ? value[part] : undefined;
|
|
123
|
+
}, source);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function firstValue(source, paths) {
|
|
127
|
+
for (const path of paths) {
|
|
128
|
+
const value = getByPath(source, path);
|
|
129
|
+
if (value !== null && value !== undefined && value !== "") return value;
|
|
130
|
+
}
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function setMeasurement(normalized, key, value, scale) {
|
|
135
|
+
if (Object.prototype.hasOwnProperty.call(normalized, key) || value === undefined) return;
|
|
136
|
+
normalized[key] = finiteMeasurement(key, Number(value) * (scale || 1));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function phasePowerFactor(source) {
|
|
140
|
+
const values = ["l1", "l2", "l3"].map(function (phase) {
|
|
141
|
+
const raw = getByPath(source, "summary.power_factor." + phase);
|
|
142
|
+
return raw === null || raw === undefined || raw === "" ? NaN : Number(raw);
|
|
143
|
+
}).filter(Number.isFinite);
|
|
144
|
+
return values.length ? values.reduce(function (sum, value) { return sum + value; }, 0) / values.length : undefined;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Normalize native emulator input and full meter payloads from this palette. */
|
|
148
|
+
function normalizePayload(payload, requirePower, message) {
|
|
149
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
150
|
+
throw new TypeError("msg.payload must be an object");
|
|
151
|
+
}
|
|
152
|
+
const normalized = {};
|
|
153
|
+
Object.keys(MEASUREMENT_RULES).forEach(function (key) {
|
|
154
|
+
const names = ALIASES[key] || [key];
|
|
155
|
+
const found = names.find(function (name) {
|
|
156
|
+
return Object.prototype.hasOwnProperty.call(payload, name);
|
|
157
|
+
});
|
|
158
|
+
if (found) normalized[key] = finiteMeasurement(key, payload[found]);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
COMPACT_PAYLOAD_FIELDS.forEach(function (definition) {
|
|
162
|
+
const found = definition.names.find(function (name) {
|
|
163
|
+
return Object.prototype.hasOwnProperty.call(payload, name);
|
|
164
|
+
});
|
|
165
|
+
if (found) setMeasurement(normalized, definition.key, payload[found], definition.scale);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const envelope = message && typeof message === "object" ? message : {};
|
|
169
|
+
const fullSources = [payload, envelope.carloGavazzi, envelope.phoenixContact, envelope.janitza]
|
|
170
|
+
.filter(function (source, index, sources) {
|
|
171
|
+
return source && typeof source === "object" && !Array.isArray(source) && sources.indexOf(source) === index;
|
|
172
|
+
});
|
|
173
|
+
fullSources.forEach(function (source) {
|
|
174
|
+
FULL_PAYLOAD_FIELDS.forEach(function (definition) {
|
|
175
|
+
setMeasurement(normalized, definition.key, firstValue(source, definition.paths), definition.scale);
|
|
176
|
+
});
|
|
177
|
+
setMeasurement(normalized, "powerFactor", phasePowerFactor(source));
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
if (requirePower && !Object.prototype.hasOwnProperty.call(normalized, "power")) {
|
|
181
|
+
throw new Error("power is required (msg.payload.power or msg.payload.summary.power.total_w)");
|
|
182
|
+
}
|
|
183
|
+
const sourceTimestamp = fullSources.map(function (source) {
|
|
184
|
+
return firstValue(source, ["meta.timestamp", "timestamp"]);
|
|
185
|
+
}).find(function (value) { return value !== undefined; });
|
|
186
|
+
const timestamp = firstValue(payload, ["timestamp", "meta.timestamp"]) || sourceTimestamp || envelope.timestamp;
|
|
187
|
+
if (timestamp !== undefined) {
|
|
188
|
+
const date = new Date(timestamp);
|
|
189
|
+
if (Number.isNaN(date.getTime())) throw new RangeError("timestamp is invalid");
|
|
190
|
+
normalized.timestamp = date.toISOString();
|
|
191
|
+
}
|
|
192
|
+
return normalized;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
module.exports = {
|
|
196
|
+
MEASUREMENT_RULES,
|
|
197
|
+
ALIASES,
|
|
198
|
+
FULL_PAYLOAD_FIELDS,
|
|
199
|
+
COMPACT_PAYLOAD_FIELDS,
|
|
200
|
+
integerInRange,
|
|
201
|
+
cleanText,
|
|
202
|
+
canonicalKey,
|
|
203
|
+
finiteMeasurement,
|
|
204
|
+
getByPath,
|
|
205
|
+
normalizePayload
|
|
206
|
+
};
|