@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,1419 @@
|
|
|
1
|
+
|
|
2
|
+
<style>
|
|
3
|
+
.scada-editor {
|
|
4
|
+
--scada-border: #d7dde5;
|
|
5
|
+
--scada-muted: #667085;
|
|
6
|
+
--scada-surface: #f8fafc;
|
|
7
|
+
--scada-head: #edf4fb;
|
|
8
|
+
--scada-accent: #22577a;
|
|
9
|
+
--scada-danger-bg: #fff4f2;
|
|
10
|
+
--scada-danger-border: #f3c2bd;
|
|
11
|
+
--scada-danger-text: #b54745;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.scada-editor .scada-section {
|
|
15
|
+
margin: 0 0 18px;
|
|
16
|
+
padding: 14px 16px 16px;
|
|
17
|
+
border: 1px solid var(--scada-border);
|
|
18
|
+
border-radius: 10px;
|
|
19
|
+
background: var(--scada-surface);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.scada-editor .scada-section-title {
|
|
23
|
+
display: flex;
|
|
24
|
+
align-items: center;
|
|
25
|
+
gap: 8px;
|
|
26
|
+
margin: 0 0 4px;
|
|
27
|
+
font-size: 15px;
|
|
28
|
+
font-weight: 600;
|
|
29
|
+
color: #243447;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.scada-editor .scada-section-note {
|
|
33
|
+
margin: 0 0 14px;
|
|
34
|
+
color: var(--scada-muted);
|
|
35
|
+
font-size: 12px;
|
|
36
|
+
line-height: 1.45;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.scada-editor .scada-grid {
|
|
40
|
+
display: grid;
|
|
41
|
+
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
|
42
|
+
gap: 12px 16px;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.scada-editor .form-row {
|
|
46
|
+
margin-bottom: 12px;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.scada-editor .form-row:last-child,
|
|
50
|
+
.scada-editor .scada-grid .form-row {
|
|
51
|
+
margin-bottom: 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.scada-editor .form-row > label {
|
|
55
|
+
width: 140px;
|
|
56
|
+
font-weight: 600;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.scada-editor .form-row > input[type="text"],
|
|
60
|
+
.scada-editor .form-row > input[type="number"],
|
|
61
|
+
.scada-editor .form-row > select {
|
|
62
|
+
width: calc(100% - 150px);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.scada-editor .scada-toolbar {
|
|
66
|
+
display: flex;
|
|
67
|
+
flex-wrap: wrap;
|
|
68
|
+
gap: 10px;
|
|
69
|
+
align-items: center;
|
|
70
|
+
margin-bottom: 12px;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.scada-editor .scada-toolbar .button,
|
|
74
|
+
.scada-editor .scada-toolbar button {
|
|
75
|
+
min-width: 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.scada-editor .scada-toolbar .button-group {
|
|
79
|
+
display: flex;
|
|
80
|
+
flex-wrap: wrap;
|
|
81
|
+
gap: 8px;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.scada-editor .scada-toolbar .button-group input[type="file"] {
|
|
85
|
+
display: none;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.scada-editor .scada-toolbar .scada-filter {
|
|
89
|
+
flex: 1 1 220px;
|
|
90
|
+
min-width: 220px;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.scada-editor .scada-summary {
|
|
94
|
+
display: flex;
|
|
95
|
+
flex-wrap: wrap;
|
|
96
|
+
gap: 10px;
|
|
97
|
+
margin-bottom: 12px;
|
|
98
|
+
color: #344054;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.scada-editor .scada-pill {
|
|
102
|
+
display: inline-flex;
|
|
103
|
+
align-items: center;
|
|
104
|
+
gap: 8px;
|
|
105
|
+
padding: 7px 12px;
|
|
106
|
+
border: 1px solid var(--scada-border);
|
|
107
|
+
border-radius: 999px;
|
|
108
|
+
background: #fff;
|
|
109
|
+
font-size: 12px;
|
|
110
|
+
font-weight: 600;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.scada-editor .scada-table-wrap {
|
|
114
|
+
overflow-x: auto;
|
|
115
|
+
border: 1px solid var(--scada-border);
|
|
116
|
+
border-radius: 10px;
|
|
117
|
+
background: #fff;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.scada-editor .scada-table {
|
|
121
|
+
width: 100%;
|
|
122
|
+
min-width: 760px;
|
|
123
|
+
border-collapse: collapse;
|
|
124
|
+
table-layout: fixed;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/* Keep the short values compact and reserve flexible space for message paths. */
|
|
128
|
+
.scada-editor .scada-col-key {
|
|
129
|
+
width: 145px;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.scada-editor .scada-col-address {
|
|
133
|
+
width: 110px;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.scada-editor .scada-col-label {
|
|
137
|
+
width: 150px;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.scada-editor.scada-show-advanced .scada-table {
|
|
141
|
+
min-width: 1380px;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.scada-editor .scada-table th,
|
|
145
|
+
.scada-editor .scada-table td {
|
|
146
|
+
padding: 8px;
|
|
147
|
+
border-bottom: 1px solid #e6ebf2;
|
|
148
|
+
vertical-align: middle;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.scada-editor .scada-table th {
|
|
152
|
+
position: sticky;
|
|
153
|
+
top: 0;
|
|
154
|
+
background: var(--scada-head);
|
|
155
|
+
text-align: left;
|
|
156
|
+
font-size: 12px;
|
|
157
|
+
text-transform: uppercase;
|
|
158
|
+
letter-spacing: 0.03em;
|
|
159
|
+
color: #455468;
|
|
160
|
+
z-index: 1;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.scada-editor .scada-table tbody tr:nth-child(even) {
|
|
164
|
+
background: #fbfdff;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.scada-editor .scada-table tbody tr.scada-hidden {
|
|
168
|
+
display: none;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.scada-editor .scada-table td input,
|
|
172
|
+
.scada-editor .scada-table td select {
|
|
173
|
+
width: 100%;
|
|
174
|
+
margin: 0;
|
|
175
|
+
box-sizing: border-box;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.scada-editor .scada-col-index {
|
|
179
|
+
width: 38px;
|
|
180
|
+
text-align: center;
|
|
181
|
+
color: var(--scada-muted);
|
|
182
|
+
font-weight: 600;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.scada-editor .scada-col-actions {
|
|
186
|
+
width: 92px;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.scada-editor .scada-row-actions {
|
|
190
|
+
display: flex;
|
|
191
|
+
gap: 6px;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.scada-editor .scada-row-actions .button {
|
|
195
|
+
padding-left: 8px;
|
|
196
|
+
padding-right: 8px;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.scada-editor .scada-advanced {
|
|
200
|
+
display: none;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.scada-editor.scada-show-advanced .scada-advanced {
|
|
204
|
+
display: table-cell;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.scada-editor .scada-validation {
|
|
208
|
+
display: none;
|
|
209
|
+
margin-top: 12px;
|
|
210
|
+
padding: 10px 12px;
|
|
211
|
+
border: 1px solid var(--scada-danger-border);
|
|
212
|
+
border-radius: 8px;
|
|
213
|
+
background: var(--scada-danger-bg);
|
|
214
|
+
color: var(--scada-danger-text);
|
|
215
|
+
font-weight: 600;
|
|
216
|
+
white-space: pre-line;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.scada-editor .scada-empty {
|
|
220
|
+
padding: 18px;
|
|
221
|
+
text-align: center;
|
|
222
|
+
color: var(--scada-muted);
|
|
223
|
+
}
|
|
224
|
+
</style>
|
|
225
|
+
|
|
226
|
+
<script type="text/javascript">
|
|
227
|
+
|
|
228
|
+
(function () {
|
|
229
|
+
|
|
230
|
+
const ADVANCED_CLASS = "scada-show-advanced";
|
|
231
|
+
|
|
232
|
+
function defaultMetricOptions(extra) {
|
|
233
|
+
|
|
234
|
+
return Object.assign({
|
|
235
|
+
byteOrder: "big_endian",
|
|
236
|
+
wordOrder: "msw_first"
|
|
237
|
+
}, extra || {});
|
|
238
|
+
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
function defaultMetrics() {
|
|
243
|
+
|
|
244
|
+
return [
|
|
245
|
+
defaultMetricOptions({ key: "p", label: "P", address: 0, type: "int32", scale: 1, path: "payload.active_power_w", unit: "W", signMode: "as_is" }),
|
|
246
|
+
defaultMetricOptions({ key: "q", label: "Q", address: 2, type: "int32", scale: 1, path: "payload.reactive_power_var", unit: "VAr", signMode: "as_is" }),
|
|
247
|
+
defaultMetricOptions({ key: "f", label: "F", address: 4, type: "uint32", scale: 100, path: "payload.frequency_hz", unit: "Hz" }),
|
|
248
|
+
defaultMetricOptions({ key: "u12", label: "U12", address: 6, type: "uint32", scale: 10, path: "payload.voltage_l1_l2_v", unit: "V" }),
|
|
249
|
+
defaultMetricOptions({ key: "u23", label: "U23", address: 8, type: "uint32", scale: 10, path: "payload.voltage_l2_l3_v", unit: "V" }),
|
|
250
|
+
defaultMetricOptions({ key: "u13", label: "U13", address: 10, type: "uint32", scale: 10, path: "payload.voltage_l1_l3_v", unit: "V" }),
|
|
251
|
+
defaultMetricOptions({ key: "cef_pos", label: "CEF", address: 12, type: "uint16", scale: 1, path: "payload.cef_breaker_position", unit: "" }),
|
|
252
|
+
defaultMetricOptions({ key: "bess_soc", label: "SOC", address: 13, type: "uint16", scale: 10, path: "payload.bess_soc_pct", unit: "%" }),
|
|
253
|
+
defaultMetricOptions({ key: "bess_p", label: "BP", address: 14, type: "int32", scale: 1, path: "payload.bess_power_w", unit: "W", signMode: "as_is" }),
|
|
254
|
+
defaultMetricOptions({ key: "bess_ext", label: "BEX", address: 16, type: "uint16", scale: 1, path: "payload.bess_external_control", unit: "" })
|
|
255
|
+
];
|
|
256
|
+
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
const AGGREGATOR_VOLTAGE_PROFILES = {
|
|
261
|
+
compact: ["u_ln_avg", "u_ll_avg"],
|
|
262
|
+
three_phase: ["u_l1n", "u_l2n", "u_l3n", "u_l12", "u_l23", "u_l31"],
|
|
263
|
+
complete: ["u_l1n", "u_l2n", "u_l3n", "u_ln_avg", "u_l12", "u_l23", "u_l31", "u_ll_avg"]
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const AGGREGATOR_SIGNALS = {
|
|
267
|
+
p: ["P", "W"], q: ["Q", "VAr"], f: ["F", "Hz"],
|
|
268
|
+
u_l1n: ["U L1-N", "V"], u_l2n: ["U L2-N", "V"], u_l3n: ["U L3-N", "V"], u_ln_avg: ["U L-N medie", "V"],
|
|
269
|
+
u_l12: ["U L1-L2", "V"], u_l23: ["U L2-L3", "V"], u_l31: ["U L3-L1", "V"], u_ll_avg: ["U L-L medie", "V"]
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
function aggregatorGroupsFromEditor() {
|
|
273
|
+
const groups = [];
|
|
274
|
+
$("#node-scada-aggregator-groups .scada-aggregator-group").each(function (index) {
|
|
275
|
+
const row = $(this);
|
|
276
|
+
const id = String(row.find(".scada-aggregator-group-id").val() || "").trim().toLowerCase().replace(/[^a-z0-9_]+/g, "_").replace(/^_+|_+$/g, "") || ("group_" + (index + 1));
|
|
277
|
+
const label = String(row.find(".scada-aggregator-group-label").val() || id).trim() || id;
|
|
278
|
+
const count = Math.max(0, Math.min(99, Math.round(Number(row.find(".scada-aggregator-group-count").val()) || 0)));
|
|
279
|
+
groups.push({ id: id, label: label, count: count });
|
|
280
|
+
});
|
|
281
|
+
return groups.length ? groups : [{ id: "grid_meter", label: "Grid meter", count: 3 }, { id: "pv_inverter", label: "PV inverter", count: 13 }, { id: "bess", label: "BESS", count: 1 }];
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function buildAggregatorMetricsFromEditor() {
|
|
285
|
+
const prefix = String($("#node-input-aggregatorPrefix").val() || "payload.scada").trim().replace(/\.+$/, "") || "payload.scada";
|
|
286
|
+
const profile = $("#node-input-aggregatorVoltageProfile").val();
|
|
287
|
+
const signals = ["p", "q"].concat(AGGREGATOR_VOLTAGE_PROFILES[profile] || AGGREGATOR_VOLTAGE_PROFILES.complete, ["f"]);
|
|
288
|
+
const groups = aggregatorGroupsFromEditor();
|
|
289
|
+
const sources = [];
|
|
290
|
+
groups.forEach(function (group) { for (let index = 1; index <= group.count; index += 1) sources.push([group.id, group.count === 1 ? group.id : group.id + "_" + String(index).padStart(2, "0"), group.count === 1 ? group.label : group.label + " " + String(index).padStart(2, "0")]); });
|
|
291
|
+
const metrics = [];
|
|
292
|
+
const addBlock = function (group, id, label, address, total) {
|
|
293
|
+
signals.forEach(function (signal, index) {
|
|
294
|
+
const info = AGGREGATOR_SIGNALS[signal];
|
|
295
|
+
metrics.push(defaultMetricOptions({ key: (total ? "total_" + group : id) + "_" + signal, label: (total ? "Total " + group : label) + " " + info[0], address: address + (index * 2), type: "float32", scale: 1, path: prefix + "." + (total ? "totals." + group : group + "." + id) + "." + signal, unit: info[1], signMode: "as_is" }));
|
|
296
|
+
});
|
|
297
|
+
metrics.push(defaultMetricOptions({ key: (total ? "total_" + group : id) + "_" + (total ? "active" : "online"), label: (total ? "Total " + group : label) + " " + (total ? "active" : "online"), address: address + 28, type: "uint16", scale: 1, path: prefix + "." + (total ? "totals." + group : group + "." + id) + "." + (total ? "active" : "online"), unit: "" }));
|
|
298
|
+
if (!total) metrics.push(defaultMetricOptions({ key: id + "_age_s", label: label + " age", address: address + 29, type: "uint16", scale: 1, path: prefix + "." + group + "." + id + ".age_s", unit: "s" }));
|
|
299
|
+
};
|
|
300
|
+
sources.forEach(function (source, index) { addBlock(source[0], source[1], source[2], index * 32, false); });
|
|
301
|
+
const totalsAddress = sources.length * 32;
|
|
302
|
+
groups.forEach(function (group, index) { addBlock(group.id, "", group.label, totalsAddress + (index * 32), true); });
|
|
303
|
+
addBlock("site", "", "Site", totalsAddress + (groups.length * 32), true);
|
|
304
|
+
return metrics;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
function makeFloatMetrics(items) {
|
|
309
|
+
|
|
310
|
+
return items.map(function (item, index) {
|
|
311
|
+
return defaultMetricOptions({
|
|
312
|
+
key: item[0],
|
|
313
|
+
label: item[1],
|
|
314
|
+
address: index * 2,
|
|
315
|
+
type: "float32",
|
|
316
|
+
scale: item[3] || 1,
|
|
317
|
+
path: item[2],
|
|
318
|
+
unit: item[4] || "",
|
|
319
|
+
signMode: "as_is"
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
function froniusInverterMetrics() {
|
|
327
|
+
|
|
328
|
+
return makeFloatMetrics([
|
|
329
|
+
["p_ac", "P AC", "payload.summary.ac.power_w", 1, "W"],
|
|
330
|
+
["p_dc", "P DC", "payload.summary.dc.dc_total_power_w", 1, "W"],
|
|
331
|
+
["f", "F", "payload.summary.ac.frequency_hz", 1, "Hz"],
|
|
332
|
+
["u1", "U1", "payload.summary.ac.voltage_l1_v", 1, "V"],
|
|
333
|
+
["u2", "U2", "payload.summary.ac.voltage_l2_v", 1, "V"],
|
|
334
|
+
["u3", "U3", "payload.summary.ac.voltage_l3_v", 1, "V"],
|
|
335
|
+
["i_total", "I total", "payload.summary.ac.current_total_a", 1, "A"],
|
|
336
|
+
["i1", "I1", "payload.summary.ac.current_l1_a", 1, "A"],
|
|
337
|
+
["i2", "I2", "payload.summary.ac.current_l2_a", 1, "A"],
|
|
338
|
+
["i3", "I3", "payload.summary.ac.current_l3_a", 1, "A"]
|
|
339
|
+
]);
|
|
340
|
+
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
function froniusSmartMeterMetrics() {
|
|
345
|
+
|
|
346
|
+
return makeFloatMetrics([
|
|
347
|
+
["p", "P", "payload.summary.meter.power_w", 1, "W"],
|
|
348
|
+
["p1", "P1", "payload.summary.meter.power_l1_w", 1, "W"],
|
|
349
|
+
["p2", "P2", "payload.summary.meter.power_l2_w", 1, "W"],
|
|
350
|
+
["p3", "P3", "payload.summary.meter.power_l3_w", 1, "W"],
|
|
351
|
+
["q", "Q", "payload.summary.meter.reactive_power_var", 1, "var"],
|
|
352
|
+
["s", "S", "payload.summary.meter.apparent_power_va", 1, "VA"],
|
|
353
|
+
["f", "F", "payload.summary.meter.frequency_hz", 1, "Hz"],
|
|
354
|
+
["u_ln", "U L-N", "payload.summary.meter.voltage_ln_avg_v", 1, "V"],
|
|
355
|
+
["u_ll", "U L-L", "payload.summary.meter.voltage_ll_avg_v", 1, "V"],
|
|
356
|
+
["i_total", "I total", "payload.summary.meter.current_total_a", 1, "A"],
|
|
357
|
+
["pf", "PF", "payload.summary.meter.power_factor", 1, ""],
|
|
358
|
+
["e_imp", "E imp", "payload.summary.meter.energy_import_wh", 1, "Wh"],
|
|
359
|
+
["e_exp", "E exp", "payload.summary.meter.energy_export_wh", 1, "Wh"]
|
|
360
|
+
]);
|
|
361
|
+
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
function janitzaMetrics() {
|
|
366
|
+
|
|
367
|
+
return makeFloatMetrics([
|
|
368
|
+
["p", "P", "payload.summary.power.total_w", 1, "W"],
|
|
369
|
+
["p1", "P1", "payload.summary.quick.power.l1_w", 1, "W"],
|
|
370
|
+
["p2", "P2", "payload.summary.quick.power.l2_w", 1, "W"],
|
|
371
|
+
["p3", "P3", "payload.summary.quick.power.l3_w", 1, "W"],
|
|
372
|
+
["q", "Q", "payload.summary.reactive.total_var", 1, "var"],
|
|
373
|
+
["f", "F", "payload.summary.frequency_hz", 1, "Hz"],
|
|
374
|
+
["u12", "U12", "payload.summary.voltage.l1_l2_v", 1, "V"],
|
|
375
|
+
["u23", "U23", "payload.summary.voltage.l2_l3_v", 1, "V"],
|
|
376
|
+
["u31", "U31", "payload.summary.voltage.l3_l1_v", 1, "V"],
|
|
377
|
+
["i1", "I1", "payload.summary.current.l1_a", 1, "A"],
|
|
378
|
+
["i2", "I2", "payload.summary.current.l2_a", 1, "A"],
|
|
379
|
+
["i3", "I3", "payload.summary.current.l3_a", 1, "A"]
|
|
380
|
+
]);
|
|
381
|
+
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
function carloGavazziMetrics() {
|
|
386
|
+
|
|
387
|
+
return makeFloatMetrics([
|
|
388
|
+
["p", "P", "payload.summary.power.total_w", 1, "W"],
|
|
389
|
+
["p1", "P1", "payload.summary.power.l1_w", 1, "W"],
|
|
390
|
+
["p2", "P2", "payload.summary.power.l2_w", 1, "W"],
|
|
391
|
+
["p3", "P3", "payload.summary.power.l3_w", 1, "W"],
|
|
392
|
+
["q", "Q", "payload.summary.reactive.total_var", 1, "var"],
|
|
393
|
+
["f", "F", "payload.summary.frequency_hz", 1, "Hz"],
|
|
394
|
+
["u12", "U12", "payload.summary.voltage.l1_l2_v", 1, "V"],
|
|
395
|
+
["u23", "U23", "payload.summary.voltage.l2_l3_v", 1, "V"],
|
|
396
|
+
["u31", "U31", "payload.summary.voltage.l3_l1_v", 1, "V"],
|
|
397
|
+
["i1", "I1", "payload.summary.current.l1_a", 1, "A"],
|
|
398
|
+
["i2", "I2", "payload.summary.current.l2_a", 1, "A"],
|
|
399
|
+
["i3", "I3", "payload.summary.current.l3_a", 1, "A"]
|
|
400
|
+
]);
|
|
401
|
+
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
function presetMetrics(name) {
|
|
406
|
+
|
|
407
|
+
if (name === "grid") {
|
|
408
|
+
return defaultMetrics().filter(function (metric) {
|
|
409
|
+
return ["p", "q", "f", "u12", "u23", "u13", "cef_pos"].includes(metric.key);
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (name === "bess") {
|
|
414
|
+
return defaultMetrics().filter(function (metric) {
|
|
415
|
+
return ["bess_soc", "bess_p", "bess_ext"].includes(metric.key);
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (name === "fronius_inverter") {
|
|
420
|
+
return froniusInverterMetrics();
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (name === "fronius_meter") {
|
|
424
|
+
return froniusSmartMeterMetrics();
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (name === "janitza") {
|
|
428
|
+
return janitzaMetrics();
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (name === "carlo_gavazzi") {
|
|
432
|
+
return carloGavazziMetrics();
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return defaultMetrics();
|
|
436
|
+
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
function escapeAttribute(value) {
|
|
441
|
+
|
|
442
|
+
return String(value == null ? "" : value)
|
|
443
|
+
.replace(/&/g, "&")
|
|
444
|
+
.replace(/"/g, """)
|
|
445
|
+
.replace(/</g, "<")
|
|
446
|
+
.replace(/>/g, ">");
|
|
447
|
+
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
function toMetric(metric, index) {
|
|
452
|
+
|
|
453
|
+
const value = metric || {};
|
|
454
|
+
|
|
455
|
+
return {
|
|
456
|
+
key: String(value.key || "").trim(),
|
|
457
|
+
label: String(value.label || value.key || "").trim(),
|
|
458
|
+
address: Math.max(0, Number(value.address || 0)),
|
|
459
|
+
type: String(value.type || "int32").trim(),
|
|
460
|
+
scale: Number.isFinite(Number(value.scale)) ? Number(value.scale) : 1,
|
|
461
|
+
path: String(value.path || "").trim(),
|
|
462
|
+
unit: String(value.unit || "").trim(),
|
|
463
|
+
signMode: String(value.signMode || "as_is").trim(),
|
|
464
|
+
byteOrder: String(value.byteOrder || "big_endian").trim(),
|
|
465
|
+
wordOrder: String(value.wordOrder || "msw_first").trim()
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
function wordLength(type) {
|
|
472
|
+
|
|
473
|
+
return (type === "int16" || type === "uint16") ? 1 : 2;
|
|
474
|
+
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
function readMetricRows() {
|
|
479
|
+
|
|
480
|
+
const metrics = [];
|
|
481
|
+
|
|
482
|
+
$("#node-input-metrics-container .scada-metric-row").each(function (index) {
|
|
483
|
+
|
|
484
|
+
const row = $(this);
|
|
485
|
+
|
|
486
|
+
metrics.push(toMetric({
|
|
487
|
+
key: row.find(".scada-metric-key").val(),
|
|
488
|
+
label: row.find(".scada-metric-label").val(),
|
|
489
|
+
address: row.find(".scada-metric-address").val(),
|
|
490
|
+
type: row.find(".scada-metric-type").val(),
|
|
491
|
+
scale: row.find(".scada-metric-scale").val(),
|
|
492
|
+
path: row.find(".scada-metric-path").val(),
|
|
493
|
+
unit: row.find(".scada-metric-unit").val(),
|
|
494
|
+
signMode: row.find(".scada-metric-sign").val(),
|
|
495
|
+
byteOrder: row.find(".scada-metric-byte-order").val(),
|
|
496
|
+
wordOrder: row.find(".scada-metric-word-order").val()
|
|
497
|
+
}, index));
|
|
498
|
+
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
return metrics;
|
|
502
|
+
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
function validateMetrics(metrics) {
|
|
507
|
+
|
|
508
|
+
const errors = [];
|
|
509
|
+
const warnings = [];
|
|
510
|
+
const seenKeys = new Set();
|
|
511
|
+
const ranges = [];
|
|
512
|
+
const validByteOrders = ["big_endian", "little_endian"];
|
|
513
|
+
const validWordOrders = ["msw_first", "lsw_first"];
|
|
514
|
+
|
|
515
|
+
if (!metrics.length) {
|
|
516
|
+
errors.push("Adauga cel putin un registru.");
|
|
517
|
+
return { errors: errors, warnings: warnings };
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
metrics.forEach(function (metric, index) {
|
|
521
|
+
|
|
522
|
+
const rowLabel = "Randul " + (index + 1);
|
|
523
|
+
|
|
524
|
+
if (!metric.key) {
|
|
525
|
+
errors.push(rowLabel + ": cheia este obligatorie.");
|
|
526
|
+
} else if (seenKeys.has(metric.key)) {
|
|
527
|
+
errors.push(rowLabel + ': cheia "' + metric.key + '" este duplicata.');
|
|
528
|
+
} else {
|
|
529
|
+
seenKeys.add(metric.key);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
if (!metric.path) {
|
|
533
|
+
errors.push(rowLabel + ": calea mesajului este obligatorie.");
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (!Number.isFinite(Number(metric.address)) || Number(metric.address) < 0) {
|
|
537
|
+
errors.push(rowLabel + ": adresa trebuie sa fie >= 0.");
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (!Number.isFinite(Number(metric.scale)) || Number(metric.scale) === 0) {
|
|
542
|
+
errors.push(rowLabel + ": scale trebuie sa fie un numar diferit de 0.");
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
if (!validByteOrders.includes(metric.byteOrder)) {
|
|
546
|
+
errors.push(rowLabel + ": byte order invalid.");
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (!validWordOrders.includes(metric.wordOrder)) {
|
|
550
|
+
errors.push(rowLabel + ": word order invalid.");
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
ranges.push({
|
|
554
|
+
key: metric.key || ("row_" + index),
|
|
555
|
+
start: Number(metric.address),
|
|
556
|
+
end: Number(metric.address) + wordLength(metric.type) - 1
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
if ((Number(metric.address) + wordLength(metric.type) - 1) > 65535) {
|
|
560
|
+
errors.push(rowLabel + ": registrul depaseste limita Modbus 65535.");
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
ranges.sort(function (left, right) {
|
|
566
|
+
return left.start - right.start;
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
for (let index = 1; index < ranges.length; index += 1) {
|
|
570
|
+
const previous = ranges[index - 1];
|
|
571
|
+
const current = ranges[index];
|
|
572
|
+
|
|
573
|
+
if (current.start <= previous.end) {
|
|
574
|
+
errors.push('Conflicte de adrese intre "' + previous.key + '" si "' + current.key + '".');
|
|
575
|
+
} else if (current.start > (previous.end + 1)) {
|
|
576
|
+
warnings.push('Gap intre "' + previous.key + '" si "' + current.key + '" (adrese libere: ' + (previous.end + 1) + ".." + (current.start - 1) + ").");
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
return { errors: errors, warnings: warnings };
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
function updateSummary(metrics) {
|
|
585
|
+
|
|
586
|
+
const registerCount = metrics.reduce(function (sum, metric) {
|
|
587
|
+
return sum + wordLength(metric.type);
|
|
588
|
+
}, 0);
|
|
589
|
+
|
|
590
|
+
$("#node-scada-metric-count").text(String(metrics.length));
|
|
591
|
+
$("#node-scada-register-count").text(String(registerCount));
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
function applyFilter() {
|
|
596
|
+
|
|
597
|
+
const query = String($("#node-input-metrics-filter").val() || "").trim().toLowerCase();
|
|
598
|
+
|
|
599
|
+
$("#node-input-metrics-container .scada-metric-row").each(function () {
|
|
600
|
+
|
|
601
|
+
const row = $(this);
|
|
602
|
+
const text = row.text().toLowerCase() + " " + row.find("input,select").map(function () {
|
|
603
|
+
return $(this).val();
|
|
604
|
+
}).get().join(" ").toLowerCase();
|
|
605
|
+
|
|
606
|
+
row.toggleClass("scada-hidden", Boolean(query) && text.indexOf(query) === -1);
|
|
607
|
+
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
function validateServerSettings() {
|
|
613
|
+
|
|
614
|
+
const errors = [];
|
|
615
|
+
const port = Number($("#node-input-port").val());
|
|
616
|
+
const unitId = Number($("#node-input-unitId").val());
|
|
617
|
+
const maxConnections = Number($("#node-input-maxConnections").val());
|
|
618
|
+
const idleTimeoutMs = Number($("#node-input-socketIdleTimeoutMs").val());
|
|
619
|
+
const rateLimit = Number($("#node-input-rateLimitPerSecond").val());
|
|
620
|
+
const diagIntervalMs = Number($("#node-input-diagIntervalMs").val());
|
|
621
|
+
const host = String($("#node-input-host").val() || "").trim();
|
|
622
|
+
|
|
623
|
+
if (!Number.isFinite(port) || port < 1 || port > 65535) {
|
|
624
|
+
errors.push("Port trebuie sa fie intre 1 si 65535.");
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
if (!host) {
|
|
628
|
+
errors.push("Bind host este obligatoriu.");
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
if (!Number.isFinite(unitId) || unitId < 0 || unitId > 255) {
|
|
632
|
+
errors.push("Unit ID trebuie sa fie intre 0 si 255.");
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
if (!Number.isFinite(maxConnections) || maxConnections < 1 || maxConnections > 2000) {
|
|
636
|
+
errors.push("Max Clients trebuie sa fie intre 1 si 2000.");
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
if (!Number.isFinite(idleTimeoutMs) || idleTimeoutMs < 500) {
|
|
640
|
+
errors.push("Idle timeout trebuie sa fie >= 500 ms.");
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
if (!Number.isFinite(rateLimit) || rateLimit < 0) {
|
|
644
|
+
errors.push("Rate limit trebuie sa fie >= 0.");
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
if (!Number.isFinite(diagIntervalMs) || diagIntervalMs < 500) {
|
|
648
|
+
errors.push("Diag interval trebuie sa fie >= 500 ms.");
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
return errors;
|
|
652
|
+
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
function validateAndRefresh() {
|
|
657
|
+
|
|
658
|
+
const metrics = readMetricRows();
|
|
659
|
+
const validation = validateMetrics(metrics);
|
|
660
|
+
const serverErrors = validateServerSettings();
|
|
661
|
+
const validationBox = $("#node-scada-validation");
|
|
662
|
+
const lines = [];
|
|
663
|
+
|
|
664
|
+
updateSummary(metrics);
|
|
665
|
+
applyFilter();
|
|
666
|
+
|
|
667
|
+
if (serverErrors.length) {
|
|
668
|
+
lines.push("Erori server:");
|
|
669
|
+
serverErrors.forEach(function (line) {
|
|
670
|
+
lines.push(" - " + line);
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
if (validation.errors.length) {
|
|
675
|
+
lines.push("Erori mapare:");
|
|
676
|
+
validation.errors.forEach(function (line) {
|
|
677
|
+
lines.push(" - " + line);
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
if (validation.warnings.length) {
|
|
682
|
+
lines.push("Avertismente:");
|
|
683
|
+
validation.warnings.forEach(function (line) {
|
|
684
|
+
lines.push(" - " + line);
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
validationBox.text(lines.join("\n")).toggle(lines.length > 0);
|
|
689
|
+
return !serverErrors.length && !validation.errors.length;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
function createRow(metric, index) {
|
|
694
|
+
|
|
695
|
+
const row = $(`
|
|
696
|
+
<tr class="scada-metric-row">
|
|
697
|
+
<td class="scada-col-index">${index + 1}</td>
|
|
698
|
+
<td class="scada-col-key"><input type="text" class="scada-metric-key" value="${escapeAttribute(metric.key)}" aria-label="Key"></td>
|
|
699
|
+
<td class="scada-col-address"><input type="number" class="scada-metric-address" value="${escapeAttribute(metric.address)}" aria-label="Address"></td>
|
|
700
|
+
<td><input type="text" class="scada-metric-path" value="${escapeAttribute(metric.path)}"></td>
|
|
701
|
+
<td class="scada-col-label"><input type="text" class="scada-metric-label" value="${escapeAttribute(metric.label)}" aria-label="Label"></td>
|
|
702
|
+
<td class="scada-advanced">
|
|
703
|
+
<select class="scada-metric-type">
|
|
704
|
+
<option value="int16" ${metric.type === "int16" ? "selected" : ""}>int16</option>
|
|
705
|
+
<option value="uint16" ${metric.type === "uint16" ? "selected" : ""}>uint16</option>
|
|
706
|
+
<option value="int32" ${metric.type === "int32" ? "selected" : ""}>int32</option>
|
|
707
|
+
<option value="uint32" ${metric.type === "uint32" ? "selected" : ""}>uint32</option>
|
|
708
|
+
<option value="float32" ${metric.type === "float32" ? "selected" : ""}>float32</option>
|
|
709
|
+
</select>
|
|
710
|
+
</td>
|
|
711
|
+
<td class="scada-advanced"><input type="number" class="scada-metric-scale" step="0.01" value="${escapeAttribute(metric.scale)}"></td>
|
|
712
|
+
<td class="scada-advanced">
|
|
713
|
+
<select class="scada-metric-sign">
|
|
714
|
+
<option value="as_is" ${metric.signMode === "as_is" ? "selected" : ""}>as_is</option>
|
|
715
|
+
<option value="invert" ${metric.signMode === "invert" ? "selected" : ""}>invert</option>
|
|
716
|
+
</select>
|
|
717
|
+
</td>
|
|
718
|
+
<td class="scada-advanced">
|
|
719
|
+
<select class="scada-metric-word-order">
|
|
720
|
+
<option value="msw_first" ${metric.wordOrder === "msw_first" ? "selected" : ""}>MSW first</option>
|
|
721
|
+
<option value="lsw_first" ${metric.wordOrder === "lsw_first" ? "selected" : ""}>LSW first</option>
|
|
722
|
+
</select>
|
|
723
|
+
</td>
|
|
724
|
+
<td class="scada-advanced">
|
|
725
|
+
<select class="scada-metric-byte-order">
|
|
726
|
+
<option value="big_endian" ${metric.byteOrder === "big_endian" ? "selected" : ""}>Big endian</option>
|
|
727
|
+
<option value="little_endian" ${metric.byteOrder === "little_endian" ? "selected" : ""}>Little endian</option>
|
|
728
|
+
</select>
|
|
729
|
+
</td>
|
|
730
|
+
<td class="scada-advanced"><input type="text" class="scada-metric-unit" value="${escapeAttribute(metric.unit)}"></td>
|
|
731
|
+
<td class="scada-col-actions">
|
|
732
|
+
<div class="scada-row-actions">
|
|
733
|
+
<a href="#" class="button scada-duplicate-row"><i class="fa fa-copy"></i></a>
|
|
734
|
+
<a href="#" class="button scada-delete-row"><i class="fa fa-trash"></i></a>
|
|
735
|
+
</div>
|
|
736
|
+
</td>
|
|
737
|
+
</tr>
|
|
738
|
+
`);
|
|
739
|
+
|
|
740
|
+
row.find("input, select").on("keyup change", validateAndRefresh);
|
|
741
|
+
|
|
742
|
+
row.find(".scada-duplicate-row").on("click", function (event) {
|
|
743
|
+
event.preventDefault();
|
|
744
|
+
const metrics = readMetricRows();
|
|
745
|
+
metrics.splice(index + 1, 0, Object.assign({}, metrics[index], {
|
|
746
|
+
key: metrics[index].key ? (metrics[index].key + "_copy") : ""
|
|
747
|
+
}));
|
|
748
|
+
buildMetricRows(metrics);
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
row.find(".scada-delete-row").on("click", function (event) {
|
|
752
|
+
event.preventDefault();
|
|
753
|
+
const metrics = readMetricRows().filter(function (_, itemIndex) {
|
|
754
|
+
return itemIndex !== index;
|
|
755
|
+
});
|
|
756
|
+
buildMetricRows(metrics);
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
return row;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
|
|
763
|
+
function autoPackAddresses() {
|
|
764
|
+
|
|
765
|
+
const metrics = readMetricRows();
|
|
766
|
+
let nextAddress = 0;
|
|
767
|
+
|
|
768
|
+
metrics.forEach(function (metric) {
|
|
769
|
+
metric.address = nextAddress;
|
|
770
|
+
nextAddress += wordLength(metric.type);
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
buildMetricRows(metrics);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
|
|
777
|
+
function csvEscape(value) {
|
|
778
|
+
|
|
779
|
+
const text = String(value == null ? "" : value);
|
|
780
|
+
|
|
781
|
+
if (/[",\r\n]/.test(text)) {
|
|
782
|
+
return '"' + text.replace(/"/g, '""') + '"';
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
return text;
|
|
786
|
+
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
function metricsToCsv(metrics) {
|
|
791
|
+
|
|
792
|
+
const headers = ["key", "label", "address", "type", "scale", "path", "unit", "signMode", "wordOrder", "byteOrder"];
|
|
793
|
+
|
|
794
|
+
return [headers.join(",")].concat(metrics.map(function (metric) {
|
|
795
|
+
return headers.map(function (header) {
|
|
796
|
+
return csvEscape(metric[header]);
|
|
797
|
+
}).join(",");
|
|
798
|
+
})).join("\n");
|
|
799
|
+
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function registerExportToCsv(exportData) {
|
|
803
|
+
const headers = ["key", "label", "address", "type", "scale", "path", "unit", "value", "active", "access", "last_update_ts", "last_publish_ts"];
|
|
804
|
+
return [headers.join(",")].concat((exportData.rows || []).map(function (row) {
|
|
805
|
+
return headers.map(function (header) { return csvEscape(row[header]); }).join(",");
|
|
806
|
+
})).join("\n");
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
|
|
810
|
+
function parseCsvLine(line) {
|
|
811
|
+
|
|
812
|
+
const values = [];
|
|
813
|
+
let current = "";
|
|
814
|
+
let quoted = false;
|
|
815
|
+
|
|
816
|
+
for (let index = 0; index < line.length; index += 1) {
|
|
817
|
+
const ch = line[index];
|
|
818
|
+
|
|
819
|
+
if (quoted && ch === '"' && line[index + 1] === '"') {
|
|
820
|
+
current += '"';
|
|
821
|
+
index += 1;
|
|
822
|
+
continue;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
if (ch === '"') {
|
|
826
|
+
quoted = !quoted;
|
|
827
|
+
continue;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
if (!quoted && ch === ",") {
|
|
831
|
+
values.push(current);
|
|
832
|
+
current = "";
|
|
833
|
+
continue;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
current += ch;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
values.push(current);
|
|
840
|
+
return values;
|
|
841
|
+
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
|
|
845
|
+
function csvToMetrics(text) {
|
|
846
|
+
|
|
847
|
+
const lines = String(text || "").split(/\r?\n/).filter(function (line) {
|
|
848
|
+
return line.trim();
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
if (!lines.length) {
|
|
852
|
+
return [];
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
const headers = parseCsvLine(lines[0]).map(function (header) {
|
|
856
|
+
return String(header || "").trim();
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
return lines.slice(1).map(function (line) {
|
|
860
|
+
const values = parseCsvLine(line);
|
|
861
|
+
const metric = {};
|
|
862
|
+
headers.forEach(function (header, index) {
|
|
863
|
+
metric[header] = values[index];
|
|
864
|
+
});
|
|
865
|
+
return toMetric(metric);
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
|
|
871
|
+
function downloadText(filename, text, type) {
|
|
872
|
+
|
|
873
|
+
const blob = new Blob([text], { type: type || "text/plain" });
|
|
874
|
+
const url = URL.createObjectURL(blob);
|
|
875
|
+
const link = document.createElement("a");
|
|
876
|
+
link.href = url;
|
|
877
|
+
link.download = filename;
|
|
878
|
+
document.body.appendChild(link);
|
|
879
|
+
link.click();
|
|
880
|
+
document.body.removeChild(link);
|
|
881
|
+
URL.revokeObjectURL(url);
|
|
882
|
+
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
function importMetricText(text, filename) {
|
|
887
|
+
|
|
888
|
+
let metrics;
|
|
889
|
+
const name = String(filename || "").toLowerCase();
|
|
890
|
+
const trimmed = String(text || "").trim();
|
|
891
|
+
|
|
892
|
+
if (!trimmed) {
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
if (name.endsWith(".csv") || (!trimmed.startsWith("[") && !trimmed.startsWith("{"))) {
|
|
897
|
+
metrics = csvToMetrics(trimmed);
|
|
898
|
+
} else {
|
|
899
|
+
const parsed = JSON.parse(trimmed);
|
|
900
|
+
metrics = Array.isArray(parsed) ? parsed : parsed.metrics;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
buildMetricRows(metrics);
|
|
904
|
+
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
|
|
908
|
+
function buildMetricRows(metrics) {
|
|
909
|
+
|
|
910
|
+
const list = (Array.isArray(metrics) && metrics.length ? metrics : defaultMetrics()).map(toMetric);
|
|
911
|
+
const container = $("#node-input-metrics-container");
|
|
912
|
+
const table = $(`
|
|
913
|
+
<div class="scada-table-wrap">
|
|
914
|
+
<table class="scada-table">
|
|
915
|
+
<thead>
|
|
916
|
+
<tr>
|
|
917
|
+
<th class="scada-col-index">#</th>
|
|
918
|
+
<th class="scada-col-key">Key</th>
|
|
919
|
+
<th class="scada-col-address">Addr</th>
|
|
920
|
+
<th>Message path</th>
|
|
921
|
+
<th class="scada-col-label">Label</th>
|
|
922
|
+
<th class="scada-advanced">Type</th>
|
|
923
|
+
<th class="scada-advanced">Scale</th>
|
|
924
|
+
<th class="scada-advanced">Sign</th>
|
|
925
|
+
<th class="scada-advanced">Word</th>
|
|
926
|
+
<th class="scada-advanced">Byte</th>
|
|
927
|
+
<th class="scada-advanced">Unit</th>
|
|
928
|
+
<th>Actions</th>
|
|
929
|
+
</tr>
|
|
930
|
+
</thead>
|
|
931
|
+
<tbody></tbody>
|
|
932
|
+
</table>
|
|
933
|
+
</div>
|
|
934
|
+
`);
|
|
935
|
+
|
|
936
|
+
const tbody = table.find("tbody");
|
|
937
|
+
|
|
938
|
+
if (!list.length) {
|
|
939
|
+
container.html('<div class="scada-empty">Nu exista registre configurate.</div>');
|
|
940
|
+
validateAndRefresh();
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
list.forEach(function (metric, index) {
|
|
945
|
+
tbody.append(createRow(metric, index));
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
container.empty().append(table);
|
|
949
|
+
validateAndRefresh();
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
|
|
953
|
+
RED.nodes.registerType("scada-server", {
|
|
954
|
+
|
|
955
|
+
category: "Atelierul de Tehnologii",
|
|
956
|
+
color: "#cfe2f3",
|
|
957
|
+
|
|
958
|
+
defaults: {
|
|
959
|
+
license: { value: "", type: "atelier-license", required: true },
|
|
960
|
+
name: { value: "SCADA Server" },
|
|
961
|
+
host: {
|
|
962
|
+
value: "0.0.0.0",
|
|
963
|
+
validate: function (value) {
|
|
964
|
+
return !!String(value || "").trim();
|
|
965
|
+
}
|
|
966
|
+
},
|
|
967
|
+
port: {
|
|
968
|
+
value: 1502,
|
|
969
|
+
validate: function (value) {
|
|
970
|
+
const n = Number(value);
|
|
971
|
+
return Number.isFinite(n) && n >= 1 && n <= 65535;
|
|
972
|
+
}
|
|
973
|
+
},
|
|
974
|
+
unitId: {
|
|
975
|
+
value: 1,
|
|
976
|
+
validate: function (value) {
|
|
977
|
+
const n = Number(value);
|
|
978
|
+
return Number.isFinite(n) && n >= 0 && n <= 255;
|
|
979
|
+
}
|
|
980
|
+
},
|
|
981
|
+
plantId: { value: "" },
|
|
982
|
+
publishIntervalMs: { value: 500, validate: function (value) { return Number(value) >= 100; } },
|
|
983
|
+
publishOnInput: { value: false },
|
|
984
|
+
maxReadLogEntries: { value: 100, validate: function (value) { return Number(value) >= 10; } },
|
|
985
|
+
maxConnections: { value: 20, validate: function (value) { return Number(value) >= 1; } },
|
|
986
|
+
socketIdleTimeoutMs: { value: 30000, validate: function (value) { return Number(value) >= 500; } },
|
|
987
|
+
rateLimitPerSecond: { value: 0, validate: function (value) { return Number(value) >= 0; } },
|
|
988
|
+
diagIntervalMs: { value: 2000, validate: function (value) { return Number(value) >= 500; } },
|
|
989
|
+
outOfRangeMode: { value: "zeros" },
|
|
990
|
+
addressingMode: { value: "zero_based" },
|
|
991
|
+
mapMode: { value: "direct" },
|
|
992
|
+
aggregatorPrefix: { value: "payload.scada" },
|
|
993
|
+
aggregatorGroups: { value: '[{"id":"grid_meter","label":"Grid meter","count":3},{"id":"pv_inverter","label":"PV inverter","count":13},{"id":"bess","label":"BESS","count":1}]' },
|
|
994
|
+
aggregatorCategories: { value: '[{"id":"grid_meter","label":"Grid meter","base":256,"slots":16},{"id":"pv_inverter","label":"PV inverter","base":768,"slots":64},{"id":"bess","label":"BESS","base":2816,"slots":16}]' },
|
|
995
|
+
aggregatorVoltageProfile: { value: "complete" },
|
|
996
|
+
metrics: { value: defaultMetrics() }
|
|
997
|
+
},
|
|
998
|
+
|
|
999
|
+
inputs: 1,
|
|
1000
|
+
outputs: 2,
|
|
1001
|
+
icon: "font-awesome/fa-server",
|
|
1002
|
+
|
|
1003
|
+
outputLabels: function (index) {
|
|
1004
|
+
return index === 0 ? "state" : "diagnostics";
|
|
1005
|
+
},
|
|
1006
|
+
|
|
1007
|
+
label: function () {
|
|
1008
|
+
return "SCADA Server";
|
|
1009
|
+
},
|
|
1010
|
+
|
|
1011
|
+
oneditprepare: function () {
|
|
1012
|
+
|
|
1013
|
+
const editor = $("#node-dialog-form").closest(".red-ui-editor, .red-ui-tray, body");
|
|
1014
|
+
const root = $(".scada-editor");
|
|
1015
|
+
const nodeId = this.id;
|
|
1016
|
+
$("#node-input-name").val("SCADA Server").prop("readonly", true);
|
|
1017
|
+
const manualMetrics = (Array.isArray(this.metrics) && this.metrics.length ? this.metrics : defaultMetrics());
|
|
1018
|
+
|
|
1019
|
+
function addAggregatorGroup(group) {
|
|
1020
|
+
const value = group || { id: "custom_group", label: "Custom group", count: 1 };
|
|
1021
|
+
const row = $("<div class='scada-aggregator-group scada-toolbar'></div>");
|
|
1022
|
+
row.append("<input type='text' class='scada-aggregator-group-id' placeholder='id' aria-label='ID grup'>").find("input:last").val(value.id);
|
|
1023
|
+
row.append("<input type='text' class='scada-aggregator-group-label' placeholder='Denumire' aria-label='Denumire grup'>").find("input:last").val(value.label);
|
|
1024
|
+
row.append("<input type='number' class='scada-aggregator-group-count' min='0' aria-label='Numar surse'>").find("input:last").val(value.count);
|
|
1025
|
+
row.append("<a href='#' class='button scada-remove-aggregator-group'><i class='fa fa-trash'></i></a>");
|
|
1026
|
+
row.find(".scada-remove-aggregator-group").on("click", function (event) { event.preventDefault(); row.remove(); });
|
|
1027
|
+
$("#node-scada-aggregator-groups").append(row);
|
|
1028
|
+
}
|
|
1029
|
+
let savedAggregatorGroups;
|
|
1030
|
+
try { savedAggregatorGroups = JSON.parse(this.aggregatorGroups); } catch (_) { savedAggregatorGroups = null; }
|
|
1031
|
+
(Array.isArray(savedAggregatorGroups) && savedAggregatorGroups.length ? savedAggregatorGroups : [{ id: "grid_meter", label: "Grid meter", count: 3 }, { id: "pv_inverter", label: "PV inverter", count: 13 }, { id: "bess", label: "BESS", count: 1 }]).forEach(addAggregatorGroup);
|
|
1032
|
+
$("#node-scada-add-aggregator-group").off(".scada").on("click.scada", function (event) { event.preventDefault(); addAggregatorGroup(); });
|
|
1033
|
+
|
|
1034
|
+
function refreshMapMode() {
|
|
1035
|
+
const mode = $("#node-input-mapMode").val();
|
|
1036
|
+
const aggregator = mode === "aggregator";
|
|
1037
|
+
const auto = mode === "aggregator_auto";
|
|
1038
|
+
$("#node-scada-aggregator-config").toggle(aggregator);
|
|
1039
|
+
$("#node-scada-auto-info").toggle(auto);
|
|
1040
|
+
$("#node-scada-direct-tools").toggle(mode === "direct");
|
|
1041
|
+
if (aggregator) buildMetricRows(buildAggregatorMetricsFromEditor());
|
|
1042
|
+
else if (auto) $("#node-input-metrics-container").html('<div class="scada-empty">Harta se populeaza automat dupa deploy, cand scada-aggregator publica surse noi. Adresele alocate raman stabile pana la redeploy.</div>');
|
|
1043
|
+
else buildMetricRows(manualMetrics);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
buildMetricRows(this.mapMode === "aggregator" ? buildAggregatorMetricsFromEditor() : this.metrics);
|
|
1047
|
+
$("#node-input-mapMode").off(".scada").on("change.scada", refreshMapMode);
|
|
1048
|
+
$("#node-scada-generate-aggregator-map").off(".scada").on("click.scada", function (event) { event.preventDefault(); buildMetricRows(buildAggregatorMetricsFromEditor()); });
|
|
1049
|
+
refreshMapMode();
|
|
1050
|
+
|
|
1051
|
+
$("#node-scada-download-active-json, #node-scada-download-active-csv").off(".scada").on("click.scada", function (event) {
|
|
1052
|
+
event.preventDefault();
|
|
1053
|
+
const format = this.id.endsWith("csv") ? "csv" : "json";
|
|
1054
|
+
if (!nodeId) { $("#node-scada-validation").text("Salveaza si da deploy nodului inainte de exportul live.").show(); return; }
|
|
1055
|
+
$.getJSON("scada-server/register-map/" + encodeURIComponent(nodeId) + "?activeOnly=true").done(function (result) {
|
|
1056
|
+
const text = format === "csv" ? registerExportToCsv(result) : JSON.stringify(result, null, 2);
|
|
1057
|
+
downloadText("scada-active-registers." + format, text, format === "csv" ? "text/csv" : "application/json");
|
|
1058
|
+
}).fail(function () { $("#node-scada-validation").text("Nu pot citi harta live. Verifica daca nodul este deployat si a primit date.").show(); });
|
|
1059
|
+
});
|
|
1060
|
+
|
|
1061
|
+
$("#node-input-metrics-filter").off(".scada").on("keyup.scada change.scada", applyFilter);
|
|
1062
|
+
|
|
1063
|
+
$("#node-scada-toggle-advanced").off(".scada").on("click.scada", function (event) {
|
|
1064
|
+
event.preventDefault();
|
|
1065
|
+
root.toggleClass(ADVANCED_CLASS);
|
|
1066
|
+
$(this).text(root.hasClass(ADVANCED_CLASS) ? "Ascunde avansate" : "Arata avansate");
|
|
1067
|
+
});
|
|
1068
|
+
|
|
1069
|
+
$("#node-scada-add-metric").off(".scada").on("click.scada", function (event) {
|
|
1070
|
+
event.preventDefault();
|
|
1071
|
+
const metrics = readMetricRows();
|
|
1072
|
+
const nextAddress = metrics.reduce(function (max, metric) {
|
|
1073
|
+
return Math.max(max, Number(metric.address || 0) + wordLength(metric.type));
|
|
1074
|
+
}, 0);
|
|
1075
|
+
|
|
1076
|
+
metrics.push({
|
|
1077
|
+
key: "",
|
|
1078
|
+
label: "",
|
|
1079
|
+
address: nextAddress,
|
|
1080
|
+
type: "int32",
|
|
1081
|
+
scale: 1,
|
|
1082
|
+
path: "",
|
|
1083
|
+
unit: "",
|
|
1084
|
+
signMode: "as_is",
|
|
1085
|
+
byteOrder: "big_endian",
|
|
1086
|
+
wordOrder: "msw_first"
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
buildMetricRows(metrics);
|
|
1090
|
+
});
|
|
1091
|
+
|
|
1092
|
+
$("#node-scada-sort-metrics").off(".scada").on("click.scada", function (event) {
|
|
1093
|
+
event.preventDefault();
|
|
1094
|
+
const metrics = readMetricRows().sort(function (left, right) {
|
|
1095
|
+
return Number(left.address || 0) - Number(right.address || 0);
|
|
1096
|
+
});
|
|
1097
|
+
buildMetricRows(metrics);
|
|
1098
|
+
});
|
|
1099
|
+
|
|
1100
|
+
$("#node-scada-autopack-metrics").off(".scada").on("click.scada", function (event) {
|
|
1101
|
+
event.preventDefault();
|
|
1102
|
+
autoPackAddresses();
|
|
1103
|
+
});
|
|
1104
|
+
|
|
1105
|
+
$("#node-scada-reset-defaults").off(".scada").on("click.scada", function (event) {
|
|
1106
|
+
event.preventDefault();
|
|
1107
|
+
buildMetricRows(defaultMetrics());
|
|
1108
|
+
$("#node-input-scada-preset").val("default");
|
|
1109
|
+
});
|
|
1110
|
+
|
|
1111
|
+
$("#node-input-scada-preset").off(".scada").on("change.scada", function () {
|
|
1112
|
+
const value = $(this).val();
|
|
1113
|
+
if (value) {
|
|
1114
|
+
buildMetricRows(presetMetrics(value));
|
|
1115
|
+
}
|
|
1116
|
+
});
|
|
1117
|
+
|
|
1118
|
+
$("#node-scada-export-json").off(".scada").on("click.scada", function (event) {
|
|
1119
|
+
event.preventDefault();
|
|
1120
|
+
downloadText("scada-register-map.json", JSON.stringify(readMetricRows(), null, 2), "application/json");
|
|
1121
|
+
});
|
|
1122
|
+
|
|
1123
|
+
$("#node-scada-export-csv").off(".scada").on("click.scada", function (event) {
|
|
1124
|
+
event.preventDefault();
|
|
1125
|
+
downloadText("scada-register-map.csv", metricsToCsv(readMetricRows()), "text/csv");
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1128
|
+
$("#node-scada-import-map").off(".scada").on("click.scada", function (event) {
|
|
1129
|
+
event.preventDefault();
|
|
1130
|
+
$("#node-input-scada-import-file").val("").trigger("click");
|
|
1131
|
+
});
|
|
1132
|
+
|
|
1133
|
+
$("#node-input-scada-import-file").off(".scada").on("change.scada", function () {
|
|
1134
|
+
const file = this.files && this.files[0];
|
|
1135
|
+
if (!file) {
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
const reader = new FileReader();
|
|
1140
|
+
reader.onload = function () {
|
|
1141
|
+
try {
|
|
1142
|
+
importMetricText(reader.result, file.name);
|
|
1143
|
+
} catch (error) {
|
|
1144
|
+
$("#node-scada-validation").text("Import invalid: " + error.message).show();
|
|
1145
|
+
}
|
|
1146
|
+
};
|
|
1147
|
+
reader.readAsText(file);
|
|
1148
|
+
});
|
|
1149
|
+
|
|
1150
|
+
if (editor.length) {
|
|
1151
|
+
editor.off(".scadaValidate").on("keyup.scadaValidate change.scadaValidate", ".scada-editor input, .scada-editor select", validateAndRefresh);
|
|
1152
|
+
}
|
|
1153
|
+
},
|
|
1154
|
+
|
|
1155
|
+
oneditsave: function () {
|
|
1156
|
+
this.aggregatorGroups = JSON.stringify(aggregatorGroupsFromEditor());
|
|
1157
|
+
this.metrics = $("#node-input-mapMode").val() === "aggregator" ? buildAggregatorMetricsFromEditor() : readMetricRows();
|
|
1158
|
+
$("#node-input-name").val("SCADA Server");
|
|
1159
|
+
},
|
|
1160
|
+
|
|
1161
|
+
oneditcancel: function () {
|
|
1162
|
+
$("#node-input-name").prop("readonly", false);
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
});
|
|
1166
|
+
|
|
1167
|
+
})();
|
|
1168
|
+
|
|
1169
|
+
</script>
|
|
1170
|
+
|
|
1171
|
+
|
|
1172
|
+
<script type="text/html" data-template-name="scada-server">
|
|
1173
|
+
<div class="form-row"><label for="node-input-license"><i class="fa fa-key"></i> License</label><input type="text" id="node-input-license"></div>
|
|
1174
|
+
|
|
1175
|
+
<div class="scada-editor">
|
|
1176
|
+
|
|
1177
|
+
<div class="scada-section">
|
|
1178
|
+
<div class="scada-section-title"><i class="fa fa-server"></i> Server</div>
|
|
1179
|
+
<p class="scada-section-note">Setarile de conectivitate sunt separate de harta de registre, ca sa poti lucra rapid chiar si cu configuratii mari.</p>
|
|
1180
|
+
|
|
1181
|
+
<div class="scada-grid">
|
|
1182
|
+
<div class="form-row">
|
|
1183
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
1184
|
+
<input type="text" id="node-input-name" placeholder="scada-server">
|
|
1185
|
+
</div>
|
|
1186
|
+
|
|
1187
|
+
<div class="form-row">
|
|
1188
|
+
<label for="node-input-plantId"><i class="fa fa-industry"></i> Plant ID</label>
|
|
1189
|
+
<input type="text" id="node-input-plantId" placeholder="optional">
|
|
1190
|
+
</div>
|
|
1191
|
+
|
|
1192
|
+
<div class="form-row">
|
|
1193
|
+
<label for="node-input-host"><i class="fa fa-globe"></i> Bind host</label>
|
|
1194
|
+
<input type="text" id="node-input-host" placeholder="0.0.0.0">
|
|
1195
|
+
</div>
|
|
1196
|
+
|
|
1197
|
+
<div class="form-row">
|
|
1198
|
+
<label for="node-input-port"><i class="fa fa-plug"></i> Port</label>
|
|
1199
|
+
<input type="text" id="node-input-port" placeholder="1502">
|
|
1200
|
+
</div>
|
|
1201
|
+
|
|
1202
|
+
<div class="form-row">
|
|
1203
|
+
<label for="node-input-unitId"><i class="fa fa-hashtag"></i> Unit ID</label>
|
|
1204
|
+
<input type="text" id="node-input-unitId" placeholder="1">
|
|
1205
|
+
</div>
|
|
1206
|
+
|
|
1207
|
+
<div class="form-row">
|
|
1208
|
+
<label for="node-input-publishIntervalMs"><i class="fa fa-refresh"></i> Publish (ms)</label>
|
|
1209
|
+
<input type="text" id="node-input-publishIntervalMs" placeholder="500">
|
|
1210
|
+
</div>
|
|
1211
|
+
|
|
1212
|
+
<div class="form-row">
|
|
1213
|
+
<label for="node-input-maxReadLogEntries"><i class="fa fa-book"></i> Read log</label>
|
|
1214
|
+
<input type="text" id="node-input-maxReadLogEntries" placeholder="100">
|
|
1215
|
+
</div>
|
|
1216
|
+
|
|
1217
|
+
<div class="form-row">
|
|
1218
|
+
<label for="node-input-maxConnections"><i class="fa fa-users"></i> Max Clients</label>
|
|
1219
|
+
<input type="text" id="node-input-maxConnections" placeholder="20">
|
|
1220
|
+
</div>
|
|
1221
|
+
|
|
1222
|
+
<div class="form-row">
|
|
1223
|
+
<label for="node-input-socketIdleTimeoutMs"><i class="fa fa-hourglass-half"></i> Idle timeout (ms)</label>
|
|
1224
|
+
<input type="text" id="node-input-socketIdleTimeoutMs" placeholder="30000">
|
|
1225
|
+
</div>
|
|
1226
|
+
|
|
1227
|
+
<div class="form-row">
|
|
1228
|
+
<label for="node-input-rateLimitPerSecond"><i class="fa fa-tachometer"></i> Rate limit (req/s)</label>
|
|
1229
|
+
<input type="text" id="node-input-rateLimitPerSecond" placeholder="0">
|
|
1230
|
+
</div>
|
|
1231
|
+
|
|
1232
|
+
<div class="form-row">
|
|
1233
|
+
<label for="node-input-diagIntervalMs"><i class="fa fa-area-chart"></i> Diag interval (ms)</label>
|
|
1234
|
+
<input type="text" id="node-input-diagIntervalMs" placeholder="2000">
|
|
1235
|
+
</div>
|
|
1236
|
+
|
|
1237
|
+
<div class="form-row">
|
|
1238
|
+
<label for="node-input-outOfRangeMode"><i class="fa fa-exclamation-triangle"></i> Out of range</label>
|
|
1239
|
+
<select id="node-input-outOfRangeMode">
|
|
1240
|
+
<option value="zeros">Return 0</option>
|
|
1241
|
+
<option value="exception">Modbus exception</option>
|
|
1242
|
+
</select>
|
|
1243
|
+
</div>
|
|
1244
|
+
|
|
1245
|
+
<div class="form-row">
|
|
1246
|
+
<label for="node-input-addressingMode"><i class="fa fa-map-signs"></i> Addressing docs</label>
|
|
1247
|
+
<select id="node-input-addressingMode">
|
|
1248
|
+
<option value="zero_based">0-based (0, 1, 2...)</option>
|
|
1249
|
+
<option value="modbus_40001">40001-style docs</option>
|
|
1250
|
+
</select>
|
|
1251
|
+
</div>
|
|
1252
|
+
|
|
1253
|
+
<div class="form-row">
|
|
1254
|
+
<label for="node-input-publishOnInput"><i class="fa fa-bolt"></i> Publish input</label>
|
|
1255
|
+
<input type="checkbox" id="node-input-publishOnInput" style="width:auto;">
|
|
1256
|
+
</div>
|
|
1257
|
+
</div>
|
|
1258
|
+
</div>
|
|
1259
|
+
|
|
1260
|
+
<div class="scada-section">
|
|
1261
|
+
<div class="scada-section-title"><i class="fa fa-table"></i> Register Map</div>
|
|
1262
|
+
<p class="scada-section-note">Modul direct pastreaza harta manuala. Modul Aggregator genereaza harta pentru mesajele normalizate de nodul scada-aggregator.</p>
|
|
1263
|
+
|
|
1264
|
+
<div class="form-row">
|
|
1265
|
+
<label for="node-input-mapMode"><i class="fa fa-sitemap"></i> Mod harta</label>
|
|
1266
|
+
<select id="node-input-mapMode">
|
|
1267
|
+
<option value="direct">Direct (manual)</option>
|
|
1268
|
+
<option value="aggregator">Aggregator (generat)</option>
|
|
1269
|
+
<option value="aggregator_auto">Aggregator (auto-discovery)</option>
|
|
1270
|
+
</select>
|
|
1271
|
+
</div>
|
|
1272
|
+
|
|
1273
|
+
<div id="node-scada-auto-info" class="scada-validation" style="display:none; border-color:#b8d7c0; background:#f3fbf5; color:#26633a;">Conecteaza scada-aggregator. Acesta decide ce valori publica; scada-server aloca automat registre pentru datele primite. Dupa ce apar date, foloseste descarcarea JSON sau CSV pentru harta efectiv descoperita.</div>
|
|
1274
|
+
|
|
1275
|
+
<div class="scada-toolbar"><a href="#" class="button" id="node-scada-download-active-json"><i class="fa fa-download"></i> Registre active JSON</a><a href="#" class="button" id="node-scada-download-active-csv"><i class="fa fa-download"></i> Registre active CSV</a></div>
|
|
1276
|
+
|
|
1277
|
+
<div id="node-scada-aggregator-config" class="scada-section" style="display:none; margin-top:12px;">
|
|
1278
|
+
<div class="scada-section-title"><i class="fa fa-object-group"></i> Aggregator map</div>
|
|
1279
|
+
<p class="scada-section-note">Fiecare sursa ocupa 32 registre. Grupele sunt editabile si se folosesc identic in scada-aggregator; sunt generate si totalurile fiecarei grupe plus site.</p>
|
|
1280
|
+
<div class="scada-grid">
|
|
1281
|
+
<div class="form-row"><label for="node-input-aggregatorPrefix">Prefix mesaj</label><input type="text" id="node-input-aggregatorPrefix" placeholder="payload.scada"></div>
|
|
1282
|
+
<div class="form-row"><label for="node-input-aggregatorVoltageProfile">Tensiuni</label><select id="node-input-aggregatorVoltageProfile"><option value="compact">Compact: U L-N/U L-L medie</option><option value="three_phase">Trifazat: L-N si L-L pe faze</option><option value="complete">Complet: faze si medii</option></select></div>
|
|
1283
|
+
</div>
|
|
1284
|
+
<div id="node-scada-aggregator-groups"></div>
|
|
1285
|
+
<div class="form-row"><label for="node-input-aggregatorCategories">Categorii auto</label><textarea id="node-input-aggregatorCategories" rows="4" style="width:calc(100% - 150px); font-family:monospace;"></textarea></div>
|
|
1286
|
+
<div class="scada-toolbar"><a href="#" class="button" id="node-scada-add-aggregator-group"><i class="fa fa-plus"></i> Adauga grupa</a><a href="#" class="button" id="node-scada-generate-aggregator-map"><i class="fa fa-magic"></i> Genereaza harta</a></div>
|
|
1287
|
+
</div>
|
|
1288
|
+
|
|
1289
|
+
<div id="node-scada-direct-tools" class="scada-toolbar">
|
|
1290
|
+
<select id="node-input-scada-preset">
|
|
1291
|
+
<option value="">Preset rapid</option>
|
|
1292
|
+
<option value="default">Default complet</option>
|
|
1293
|
+
<option value="grid">Doar grid</option>
|
|
1294
|
+
<option value="bess">Doar BESS</option>
|
|
1295
|
+
<option value="fronius_inverter">Fronius inverter</option>
|
|
1296
|
+
<option value="fronius_meter">Fronius Smart Meter</option>
|
|
1297
|
+
<option value="janitza">Janitza</option>
|
|
1298
|
+
<option value="carlo_gavazzi">Carlo Gavazzi</option>
|
|
1299
|
+
</select>
|
|
1300
|
+
|
|
1301
|
+
<input type="text" id="node-input-metrics-filter" class="scada-filter" placeholder="Filtreaza dupa key, path sau label">
|
|
1302
|
+
|
|
1303
|
+
<div class="button-group">
|
|
1304
|
+
<a href="#" class="button" id="node-scada-add-metric"><i class="fa fa-plus"></i> Add row</a>
|
|
1305
|
+
<a href="#" class="button" id="node-scada-sort-metrics"><i class="fa fa-sort-numeric-asc"></i> Sort</a>
|
|
1306
|
+
<a href="#" class="button" id="node-scada-autopack-metrics"><i class="fa fa-magic"></i> Auto-pack</a>
|
|
1307
|
+
<a href="#" class="button" id="node-scada-toggle-advanced">Arata avansate</a>
|
|
1308
|
+
<a href="#" class="button" id="node-scada-import-map"><i class="fa fa-upload"></i> Import</a>
|
|
1309
|
+
<a href="#" class="button" id="node-scada-export-json"><i class="fa fa-download"></i> JSON</a>
|
|
1310
|
+
<a href="#" class="button" id="node-scada-export-csv"><i class="fa fa-download"></i> CSV</a>
|
|
1311
|
+
<a href="#" class="button" id="node-scada-reset-defaults"><i class="fa fa-repeat"></i> Reset</a>
|
|
1312
|
+
<input type="file" id="node-input-scada-import-file" accept=".json,.csv,application/json,text/csv">
|
|
1313
|
+
</div>
|
|
1314
|
+
</div>
|
|
1315
|
+
|
|
1316
|
+
<div class="scada-summary">
|
|
1317
|
+
<div class="scada-pill">Metrici: <span id="node-scada-metric-count">0</span></div>
|
|
1318
|
+
<div class="scada-pill">Registre ocupate: <span id="node-scada-register-count">0</span></div>
|
|
1319
|
+
</div>
|
|
1320
|
+
|
|
1321
|
+
<div id="node-input-metrics-container"></div>
|
|
1322
|
+
<div class="scada-validation" id="node-scada-validation"></div>
|
|
1323
|
+
</div>
|
|
1324
|
+
|
|
1325
|
+
</div>
|
|
1326
|
+
|
|
1327
|
+
</script>
|
|
1328
|
+
|
|
1329
|
+
|
|
1330
|
+
<script type="text/html" data-help-name="scada-server">
|
|
1331
|
+
|
|
1332
|
+
<p>
|
|
1333
|
+
Nodul <b>scada-server</b> colecteaza valori din flow si le expune intr-un server Modbus TCP virtual read-only.
|
|
1334
|
+
</p>
|
|
1335
|
+
|
|
1336
|
+
<h3>Editor</h3>
|
|
1337
|
+
<ul>
|
|
1338
|
+
<li>mod compact pentru configurari rapide</li>
|
|
1339
|
+
<li>coloane pentru <code>type</code>, <code>scale</code>, <code>sign</code> si <code>unit</code></li>
|
|
1340
|
+
<li>validare live pentru chei duplicate, path lipsa, conflicte de adrese si depasirea limitei 65535</li>
|
|
1341
|
+
<li>preseturi rapide pentru <b>default</b>, <b>grid</b>, <b>BESS</b>, <b>Fronius</b>, <b>Janitza</b> si <b>Carlo Gavazzi</b></li>
|
|
1342
|
+
<li>import/export harta registre in format JSON sau CSV</li>
|
|
1343
|
+
<li>mod <b>Aggregator</b> care genereaza automat harta pentru grupe de surse si totaluri</li>
|
|
1344
|
+
</ul>
|
|
1345
|
+
|
|
1346
|
+
<h3>Mod Aggregator</h3>
|
|
1347
|
+
<p>Conecteaza iesirea nodului <b>scada-aggregator</b> la acest nod si alege <b>Mod harta: Aggregator</b>. Prefixul implicit este <code>payload.scada</code>.</p>
|
|
1348
|
+
<ul>
|
|
1349
|
+
<li>fiecare sursa are un bloc stabil de 32 registre: P, Q, tensiuni, frecventa, <code>online</code> si <code>age_s</code></li>
|
|
1350
|
+
<li>profilul tensiunilor poate fi compact (medii L-N/L-L), trifazat (L1-N, L2-N, L3-N, L1-L2, L2-L3, L3-L1) sau complet</li>
|
|
1351
|
+
<li>grupele implicite sunt <code>grid_meter</code>, <code>pv_inverter</code> si <code>bess</code>; le poti redenumi, modifica numarul de surse sau adauga grupe noi</li>
|
|
1352
|
+
<li>totalurile fiecarei grupe si <code>site</code> sunt generate dupa blocurile surselor</li>
|
|
1353
|
+
<li>in modul Aggregator harta este generata la deploy; nu edita manual randurile rezultate</li>
|
|
1354
|
+
</ul>
|
|
1355
|
+
|
|
1356
|
+
<h3>Auto-discovery</h3>
|
|
1357
|
+
<p>Cu <b>Mod hartă: Aggregator (auto-discovery)</b>, nu configurezi numărul de echipamente sau valorile publicate: acestea sunt stabilite în scada-aggregator. Fiecare sursă nouă primește următorul bloc liber de 32 registre. Folosește butoanele <b>Registre active JSON/CSV</b>, după deploy, pentru a descărca harta efectivă.</p>
|
|
1358
|
+
|
|
1359
|
+
<h3>Functii</h3>
|
|
1360
|
+
<ul>
|
|
1361
|
+
<li>colecteaza mesaje multiple si publica snapshot-uri atomice la un interval stabil sau imediat la fiecare input</li>
|
|
1362
|
+
<li>expune Modbus TCP pentru telemetrie read-only și pentru blocul de comandă R/W, pe host și port configurabile</li>
|
|
1363
|
+
<li>output 1 = stare server si snapshot curent, output 2 = diagnostic periodic (uptime, req/s, ultim client, contoare erori)</li>
|
|
1364
|
+
<li>hardening: limita clienti simultani, inchidere socket-uri la redeploy, idle timeout per socket si rate-limit per IP</li>
|
|
1365
|
+
<li>accepta tipuri <code>int16</code>, <code>uint16</code>, <code>int32</code>, <code>uint32</code> si <code>float32</code></li>
|
|
1366
|
+
<li>pentru valori pe 32 biti poti alege <code>word order</code> si <code>byte order</code></li>
|
|
1367
|
+
<li>citirile in afara hartii pot returna zero sau Modbus exception, in functie de setarea <b>Out of range</b></li>
|
|
1368
|
+
<li>o configuratie invalida nu mai porneste serverul, ca sa evite suprapuneri de registre</li>
|
|
1369
|
+
<li>bloc R/W SCADA la inceputul hărții automate, adresele 0–9; în harta directă/manuală, același bloc este la 128–137. Valorile sunt sincronizate bidirecțional cu variabilele globale cu aceleași nume.</li>
|
|
1370
|
+
</ul>
|
|
1371
|
+
|
|
1372
|
+
<h3>Registre de control R/W</h3>
|
|
1373
|
+
<p>La pornire, nodul creează cheile globale care lipsesc cu valoarea <code>0</code>; cheile existente își păstrează valoarea. Aceste registre apar în output-ul <code>scada-server/state</code> în <code>rw_registers</code>, <code>metrics</code> și <code>values_live</code>.</p>
|
|
1374
|
+
<ul>
|
|
1375
|
+
<li><code>0–1</code> / <code>128–129</code> — <code>scada_nominal_active_power</code>, <code>float32</code>, W. Domeniul de lucru este <code>0…P_nominal</code> al instalației.</li>
|
|
1376
|
+
<li><code>2–3</code> / <code>130–131</code> — <code>scada_nominal_reactive_power</code>, <code>float32</code>, VAr semnat. Domeniul de lucru este <code>-Q_nominal…+Q_nominal</code>. Dacă un consumator din aval lucrează cu referință procentuală Q, acesta trebuie limitat la <code>-100…+100 %</code>.</li>
|
|
1377
|
+
<li><code>4</code> / <code>132</code> — <code>scada_ramp_type</code>, <code>uint16</code>: <code>0</code> = fără rampă; cele trei moduri de rampă sunt <code>1</code> = timp fix (s), <code>2</code> = gradient procentual (%/s), <code>3</code> = gradient absolut (W/s pentru P, VAr/s pentru Q).</li>
|
|
1378
|
+
<li><code>6–7</code> / <code>134–135</code> — <code>scada_ramp_up_value</code>, <code>float32</code>; <code>8–9</code> / <code>136–137</code> — <code>scada_ramp_down_value</code>, <code>float32</code>. Valorile sunt mărimi pozitive; direcția este dată de registrul Up/Down. Pentru tipul <code>2</code>, domeniul permis este <code>0…100 %/s</code>. Pentru tipul <code>1</code>, valoarea este durata în secunde (≥ 0); pentru tipul <code>3</code>, este gradientul absolut (≥ 0).</li>
|
|
1379
|
+
</ul>
|
|
1380
|
+
<p>Scrierile Modbus cu FC6 sau FC16 sunt permise exclusiv în acest bloc. O scriere acceptată actualizează variabilele globale și produce mesajul <code>scada-server/commands</code> pe ieșirea 1.</p>
|
|
1381
|
+
|
|
1382
|
+
<h3>Interoperabilitate adrese</h3>
|
|
1383
|
+
<ul>
|
|
1384
|
+
<li><code>0-based</code>: documentatia afiseaza adresele exact ca in request-ul Modbus (ex: 0, 2, 4)</li>
|
|
1385
|
+
<li><code>40001-style docs</code>: documentatia afiseaza 40001+offset (ex: 40001, 40003, 40005), dar request-ul ramane tot 0-based</li>
|
|
1386
|
+
<li>exemplu metrica <code>p</code> la adresa interna 0: in docs 0-based = <code>0</code>, in docs 40001-style = <code>40001</code></li>
|
|
1387
|
+
</ul>
|
|
1388
|
+
|
|
1389
|
+
<h3>Referinta completa parametri</h3>
|
|
1390
|
+
<ul>
|
|
1391
|
+
<li><code>host</code>, <code>port</code>, <code>unitId</code>: interfata si identitatea serverului Modbus TCP virtual.</li>
|
|
1392
|
+
<li><code>plantId</code>: identificatorul instalatiei inclus in diagnostic.</li>
|
|
1393
|
+
<li><code>publishIntervalMs</code>: intervalul de publicare snapshot.</li>
|
|
1394
|
+
<li><code>publishOnInput</code>: publica snapshot imediat la fiecare mesaj de intrare.</li>
|
|
1395
|
+
<li><code>maxReadLogEntries</code>: cate citiri Modbus recente sunt pastrate in diagnostic.</li>
|
|
1396
|
+
<li><code>maxConnections</code>: numarul maxim de clienti simultani.</li>
|
|
1397
|
+
<li><code>socketIdleTimeoutMs</code>: inchide conexiunile inactive dupa acest timp.</li>
|
|
1398
|
+
<li><code>rateLimitPerSecond</code>: limita de request-uri pe secunda per client.</li>
|
|
1399
|
+
<li><code>diagIntervalMs</code>: intervalul iesirii de diagnostic.</li>
|
|
1400
|
+
<li><code>outOfRangeMode</code>: zero sau Modbus exception pentru citiri in afara hartii.</li>
|
|
1401
|
+
<li><code>addressingMode</code>: modul in care se afiseaza adresele in documentatie.</li>
|
|
1402
|
+
<li><code>metrics</code>: harta completa de registre: key, path, address, type, scale, sign, word/byte order si unit.</li>
|
|
1403
|
+
<li><code>scada-preset</code>: selectorul de preset rapid din editor.</li>
|
|
1404
|
+
<li><code>metrics-filter</code>: filtrul text pentru randurile din harta de registre.</li>
|
|
1405
|
+
<li><code>scada-import-file</code>: input-ul de fisier folosit la import JSON/CSV.</li>
|
|
1406
|
+
<li><code>metrics-container</code>: zona editorului in care sunt randate randurile din <code>metrics</code>.</li>
|
|
1407
|
+
</ul>
|
|
1408
|
+
|
|
1409
|
+
<h3>Exemplu metrica</h3>
|
|
1410
|
+
<pre>{
|
|
1411
|
+
"key": "grid_power_w",
|
|
1412
|
+
"path": "payload.power_w",
|
|
1413
|
+
"address": 0,
|
|
1414
|
+
"type": "int32",
|
|
1415
|
+
"scale": 1,
|
|
1416
|
+
"unit": "W"
|
|
1417
|
+
}</pre>
|
|
1418
|
+
|
|
1419
|
+
</script>
|