node-red-contrib-s2 0.1.3 → 0.2.0

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.
Files changed (37) hide show
  1. package/README.md +36 -8
  2. package/dist/lib/index.d.ts +1 -1
  3. package/dist/lib/s2/schedule.d.ts +3 -2
  4. package/dist/lib/s2/schedule.js +8 -7
  5. package/dist/lib/s2/schedule.js.map +1 -1
  6. package/dist/lib/s2/session.d.ts +17 -22
  7. package/dist/lib/s2/session.js +29 -59
  8. package/dist/lib/s2/session.js.map +1 -1
  9. package/dist/nodes/s2-cem-config/index.html +10 -0
  10. package/dist/nodes/s2-ombc/icons/s2-logo.svg +14 -0
  11. package/dist/nodes/s2-ombc/index.d.ts +30 -0
  12. package/dist/nodes/s2-ombc/index.html +82 -0
  13. package/dist/nodes/s2-ombc/index.js +153 -0
  14. package/dist/nodes/s2-ombc/index.js.map +1 -0
  15. package/dist/nodes/s2-ombc-config/index.d.ts +3 -0
  16. package/dist/nodes/s2-ombc-config/index.html +428 -0
  17. package/dist/nodes/s2-ombc-config/index.js +9 -0
  18. package/dist/nodes/s2-ombc-config/index.js.map +1 -0
  19. package/dist/nodes/s2-pebc/icons/s2-logo.svg +14 -0
  20. package/dist/nodes/s2-pebc/index.d.ts +44 -0
  21. package/dist/nodes/s2-pebc/index.html +85 -0
  22. package/dist/nodes/s2-pebc/index.js +417 -0
  23. package/dist/nodes/s2-pebc/index.js.map +1 -0
  24. package/dist/nodes/s2-pebc-config/index.d.ts +3 -0
  25. package/dist/nodes/s2-pebc-config/index.html +64 -0
  26. package/dist/nodes/s2-pebc-config/index.js +10 -0
  27. package/dist/nodes/s2-pebc-config/index.js.map +1 -0
  28. package/dist/nodes/s2-rm/index.d.ts +21 -11
  29. package/dist/nodes/s2-rm/index.html +12 -88
  30. package/dist/nodes/s2-rm/index.js +85 -309
  31. package/dist/nodes/s2-rm/index.js.map +1 -1
  32. package/dist/nodes/s2-rm-config/index.html +34 -66
  33. package/dist/nodes/s2-rm-config/index.js +2 -2
  34. package/dist/nodes/s2-rm-config/index.js.map +1 -1
  35. package/dist/types/config-nodes.d.ts +20 -6
  36. package/package.json +6 -2
  37. package/resources/s2-styles.css +101 -2
@@ -0,0 +1,153 @@
1
+ "use strict";
2
+ const messages_1 = require("../../lib/s2/messages");
3
+ const PERSISTED_STATUS_KEY_PREFIX = 's2OmbcStatus:';
4
+ module.exports = function (RED) {
5
+ function S2OmbcNode(config) {
6
+ RED.nodes.createNode(this, config);
7
+ const node = this;
8
+ const ombcConfigNode = RED.nodes.getNode(config.ombcConfig);
9
+ if (!ombcConfigNode) {
10
+ node.error('s2-ombc-config node is missing - please configure an OMBC System Description');
11
+ node.status({ fill: 'red', shape: 'dot', text: 'config missing' });
12
+ return;
13
+ }
14
+ let systemDescription = { operationModes: [], transitions: [], timers: [] };
15
+ if (ombcConfigNode.systemDescription) {
16
+ try {
17
+ systemDescription = JSON.parse(ombcConfigNode.systemDescription);
18
+ }
19
+ catch (e) {
20
+ node.error('Invalid OMBC System Description JSON: ' + e.message);
21
+ }
22
+ }
23
+ const cemStates = new Map();
24
+ function getOrCreateState(cemId) {
25
+ let state = cemStates.get(cemId);
26
+ if (!state) {
27
+ state = { selectedControlType: null };
28
+ cemStates.set(cemId, state);
29
+ }
30
+ return state;
31
+ }
32
+ function getPersistedStatus(cemId) {
33
+ return node.context().get(PERSISTED_STATUS_KEY_PREFIX + cemId) || null;
34
+ }
35
+ function setPersistedStatus(cemId, status) {
36
+ node.context().set(PERSISTED_STATUS_KEY_PREFIX + cemId, status);
37
+ }
38
+ function resolveMode(rawInstr) {
39
+ var _a;
40
+ const modeId = (rawInstr.operation_mode_id || rawInstr.operation_mode);
41
+ if (!modeId)
42
+ return null;
43
+ const factor = typeof rawInstr.operation_mode_factor === 'number' ? rawInstr.operation_mode_factor : 1;
44
+ const modes = systemDescription.operationModes;
45
+ const modeIndex = (_a = modes === null || modes === void 0 ? void 0 : modes.findIndex(m => m.id === modeId)) !== null && _a !== void 0 ? _a : -1;
46
+ const mode = modeIndex >= 0 ? modes[modeIndex] : undefined;
47
+ return {
48
+ id: modeId,
49
+ index: modeIndex,
50
+ label: (mode === null || mode === void 0 ? void 0 : mode.diagnostic_label) || modeId,
51
+ factor,
52
+ powerRanges: (mode === null || mode === void 0 ? void 0 : mode.power_ranges) || []
53
+ };
54
+ }
55
+ function handleInstruction(msg) {
56
+ if (msg.controlType !== messages_1.ControlType.OMBC) {
57
+ node.status({ fill: 'yellow', shape: 'ring', text: `ignoring ${String(msg.controlType)} instruction` });
58
+ node.send([msg, null]);
59
+ return;
60
+ }
61
+ const rawInstr = (msg.ombc || msg.payload);
62
+ const resolved = resolveMode(rawInstr);
63
+ const outMsg = { ...msg };
64
+ if (resolved) {
65
+ outMsg.topic = resolved.label;
66
+ outMsg.ombc = { ...(msg.ombc || {}), operationMode: resolved };
67
+ node.status({ fill: 'green', shape: 'dot', text: resolved.label });
68
+ }
69
+ node.send([outMsg, null]);
70
+ }
71
+ function handleSelectControlType(msg) {
72
+ const cemId = msg.cemId;
73
+ if (!cemId)
74
+ return;
75
+ const payload = msg.payload;
76
+ const controlType = payload.control_type;
77
+ getOrCreateState(cemId).selectedControlType = controlType;
78
+ if (controlType !== messages_1.ControlType.OMBC)
79
+ return;
80
+ node.send([null, { payload: { command: 'SystemDescription', cemId, controlType: messages_1.ControlType.OMBC, ombc: systemDescription } }]);
81
+ const persisted = getPersistedStatus(cemId);
82
+ if (persisted) {
83
+ node.send([null, { payload: { command: 'UpdateStatus', cemId, controlType: messages_1.ControlType.OMBC, ombc: persisted } }]);
84
+ }
85
+ }
86
+ function handleConfirm(msg, done) {
87
+ const cemId = msg.cemId;
88
+ const payload = msg.payload;
89
+ const modeId = payload.confirmedOperationModeId;
90
+ if (!cemId || !modeId) {
91
+ done(new Error('Confirm message requires cemId and payload.confirmedOperationModeId'));
92
+ return;
93
+ }
94
+ const state = getOrCreateState(cemId);
95
+ if (state.selectedControlType !== messages_1.ControlType.OMBC) {
96
+ node.status({ fill: 'yellow', shape: 'ring', text: 'OMBC not selected - confirm ignored' });
97
+ done();
98
+ return;
99
+ }
100
+ const factor = payload.operationModeFactor;
101
+ const previous = getPersistedStatus(cemId);
102
+ let status = {
103
+ activeOperationModeId: modeId,
104
+ operationModeFactor: typeof factor === 'number' ? factor : 1
105
+ };
106
+ if (previous && previous.activeOperationModeId !== modeId) {
107
+ status = {
108
+ ...status,
109
+ previousOperationModeId: previous.activeOperationModeId,
110
+ transitionTimestamp: new Date().toISOString()
111
+ };
112
+ }
113
+ setPersistedStatus(cemId, status);
114
+ node.status({ fill: 'green', shape: 'dot', text: `confirmed: ${modeId}` });
115
+ node.send([null, { payload: { command: 'UpdateStatus', cemId, controlType: messages_1.ControlType.OMBC, ombc: status } }]);
116
+ done();
117
+ }
118
+ node.status({ fill: 'grey', shape: 'ring', text: 'idle' });
119
+ node.on('input', (msg, _send, done) => {
120
+ const cemId = msg.cemId;
121
+ const topic = msg.topic;
122
+ if (topic === 'Disconnected' && cemId) {
123
+ const state = cemStates.get(cemId);
124
+ if (state)
125
+ state.selectedControlType = null;
126
+ done();
127
+ return;
128
+ }
129
+ const payload = msg.payload;
130
+ if (!payload || typeof payload !== 'object') {
131
+ done();
132
+ return;
133
+ }
134
+ if ('confirmedOperationModeId' in payload) {
135
+ handleConfirm(msg, done);
136
+ return;
137
+ }
138
+ if ('controlType' in msg) {
139
+ handleInstruction(msg);
140
+ done();
141
+ return;
142
+ }
143
+ if (payload.message_type === messages_1.MessageType.SELECT_CONTROL_TYPE) {
144
+ handleSelectControlType(msg);
145
+ done();
146
+ return;
147
+ }
148
+ done();
149
+ });
150
+ }
151
+ RED.nodes.registerType('s2-ombc', S2OmbcNode);
152
+ };
153
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/nodes/s2-ombc/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import { NodeRedApp } from '../../types/node-red';
2
+ declare const _default: (RED: NodeRedApp) => void;
3
+ export = _default;
@@ -0,0 +1,428 @@
1
+ <script src="resources/node-red-contrib-s2/s2-common.js"></script>
2
+ <script type="text/javascript">
3
+ (function () {
4
+ function generateUuid () {
5
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
6
+ var r = Math.random() * 16 | 0
7
+ return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16)
8
+ })
9
+ }
10
+
11
+ // Every configured mode able to transition to every other configured mode,
12
+ // with no timers - the fully-connected graph friendly mode always assumes.
13
+ function buildTransitions (modeIds) {
14
+ var transitions = []
15
+ for (var i = 0; i < modeIds.length; i++) {
16
+ for (var j = 0; j < modeIds.length; j++) {
17
+ if (i === j) continue
18
+ transitions.push({
19
+ id: generateUuid(),
20
+ from: modeIds[i],
21
+ to: modeIds[j],
22
+ start_timers: [],
23
+ blocking_timers: [],
24
+ abnormal_condition_only: false
25
+ })
26
+ }
27
+ }
28
+ return transitions
29
+ }
30
+
31
+ function friendlyStateToSystemDescription (modes) {
32
+ var operationModes = modes.map(function (m) {
33
+ var powerRanges
34
+ if (m.symmetric) {
35
+ powerRanges = [{
36
+ commodity_quantity: 'ELECTRIC.POWER.3_PHASE_SYMMETRIC',
37
+ start_of_range: m.valueSymmetric,
38
+ end_of_range: m.valueSymmetric
39
+ }]
40
+ } else {
41
+ powerRanges = [
42
+ { commodity_quantity: 'ELECTRIC.POWER.L1', start_of_range: m.valueL1, end_of_range: m.valueL1 },
43
+ { commodity_quantity: 'ELECTRIC.POWER.L2', start_of_range: m.valueL2, end_of_range: m.valueL2 },
44
+ { commodity_quantity: 'ELECTRIC.POWER.L3', start_of_range: m.valueL3, end_of_range: m.valueL3 }
45
+ ]
46
+ }
47
+ return {
48
+ id: m.id,
49
+ diagnostic_label: m.label,
50
+ power_ranges: powerRanges,
51
+ abnormal_condition_only: !!m.abnormalConditionOnly
52
+ }
53
+ })
54
+ return {
55
+ operationModes: operationModes,
56
+ transitions: buildTransitions(operationModes.map(function (m) { return m.id })),
57
+ timers: []
58
+ }
59
+ }
60
+
61
+ function isFriendlyPowerRanges (ranges) {
62
+ if (!Array.isArray(ranges)) return false
63
+ if (ranges.length === 1) {
64
+ var r = ranges[0]
65
+ return !!r && r.commodity_quantity === 'ELECTRIC.POWER.3_PHASE_SYMMETRIC' &&
66
+ typeof r.start_of_range === 'number' && r.start_of_range === r.end_of_range
67
+ }
68
+ if (ranges.length === 3) {
69
+ var byCq = {}
70
+ for (var i = 0; i < ranges.length; i++) {
71
+ var rr = ranges[i]
72
+ if (!rr || typeof rr.start_of_range !== 'number' || rr.start_of_range !== rr.end_of_range) return false
73
+ byCq[rr.commodity_quantity] = rr.start_of_range
74
+ }
75
+ return ('ELECTRIC.POWER.L1' in byCq) && ('ELECTRIC.POWER.L2' in byCq) && ('ELECTRIC.POWER.L3' in byCq)
76
+ }
77
+ return false
78
+ }
79
+
80
+ // Strict structural check: true only when systemDescription is exactly what
81
+ // friendly mode itself would produce, so switching modes never silently
82
+ // narrows something Advanced mode expresses that friendly mode can't.
83
+ function isFriendlyRepresentable (sysDesc) {
84
+ if (!sysDesc || typeof sysDesc !== 'object') return false
85
+ var modes = sysDesc.operationModes
86
+ if (!Array.isArray(modes) || modes.length === 0) return false
87
+
88
+ var modeIds = []
89
+ for (var i = 0; i < modes.length; i++) {
90
+ var m = modes[i]
91
+ if (!m || typeof m.id !== 'string' || !m.id || modeIds.indexOf(m.id) !== -1) return false
92
+ if (!isFriendlyPowerRanges(m.power_ranges)) return false
93
+ modeIds.push(m.id)
94
+ }
95
+
96
+ var transitions = sysDesc.transitions
97
+ if (!Array.isArray(transitions) || transitions.length !== modeIds.length * (modeIds.length - 1)) return false
98
+
99
+ var seenPairs = {}
100
+ for (var j = 0; j < transitions.length; j++) {
101
+ var t = transitions[j]
102
+ if (!t || modeIds.indexOf(t.from) === -1 || modeIds.indexOf(t.to) === -1 || t.from === t.to) return false
103
+ if (!Array.isArray(t.start_timers) || t.start_timers.length !== 0) return false
104
+ if (!Array.isArray(t.blocking_timers) || t.blocking_timers.length !== 0) return false
105
+ if (t.abnormal_condition_only) return false
106
+ var key = t.from + '>' + t.to
107
+ if (seenPairs[key]) return false
108
+ seenPairs[key] = true
109
+ }
110
+ for (var a = 0; a < modeIds.length; a++) {
111
+ for (var b = 0; b < modeIds.length; b++) {
112
+ if (a === b) continue
113
+ if (!seenPairs[modeIds[a] + '>' + modeIds[b]]) return false
114
+ }
115
+ }
116
+
117
+ var timers = sysDesc.timers
118
+ return Array.isArray(timers) && timers.length === 0
119
+ }
120
+
121
+ // Only called after isFriendlyRepresentable has passed.
122
+ function systemDescriptionToFriendlyState (sysDesc) {
123
+ return sysDesc.operationModes.map(function (m) {
124
+ var ranges = m.power_ranges
125
+ if (ranges.length === 1) {
126
+ return {
127
+ id: m.id,
128
+ protected: false,
129
+ label: m.diagnostic_label || '',
130
+ abnormalConditionOnly: !!m.abnormal_condition_only,
131
+ symmetric: true,
132
+ valueSymmetric: ranges[0].start_of_range,
133
+ valueL1: 0,
134
+ valueL2: 0,
135
+ valueL3: 0
136
+ }
137
+ }
138
+ var byCq = {}
139
+ ranges.forEach(function (r) { byCq[r.commodity_quantity] = r.start_of_range })
140
+ return {
141
+ id: m.id,
142
+ protected: false,
143
+ label: m.diagnostic_label || '',
144
+ abnormalConditionOnly: !!m.abnormal_condition_only,
145
+ symmetric: false,
146
+ valueSymmetric: 0,
147
+ valueL1: byCq['ELECTRIC.POWER.L1'],
148
+ valueL2: byCq['ELECTRIC.POWER.L2'],
149
+ valueL3: byCq['ELECTRIC.POWER.L3']
150
+ }
151
+ })
152
+ }
153
+
154
+ function defaultStandbyMode () {
155
+ return {
156
+ id: generateUuid(),
157
+ protected: true,
158
+ label: 'Standby/off',
159
+ abnormalConditionOnly: false,
160
+ symmetric: true,
161
+ valueSymmetric: 0,
162
+ valueL1: 0,
163
+ valueL2: 0,
164
+ valueL3: 0
165
+ }
166
+ }
167
+
168
+ RED.nodes.registerType('s2-ombc-config', {
169
+ category: 'config',
170
+ defaults: {
171
+ name: { value: '' },
172
+ systemDescription: { value: '' }
173
+ },
174
+ label: function () {
175
+ return this.name || 's2-ombc-config'
176
+ },
177
+ oneditprepare: function () {
178
+ var listSel = '#s2-ombc-mode-list'
179
+ var currentMode = 'friendly'
180
+
181
+ $('#node-config-input-systemDescription').typedInput({ types: ['json'] })
182
+
183
+ function addModeItem (container, index, opt) {
184
+ opt = opt || {}
185
+ if (!opt.id) opt.id = generateUuid()
186
+ // Node-RED's default editableList item content is `display:grid;
187
+ // grid-template-columns:50% 50%`, which grid-places our two rows into
188
+ // separate 50%-wide columns instead of stacking them - override back
189
+ // to a normal block flow (core nodes like `switch` do the same thing
190
+ // to get their own single-row flex layout instead of the grid default).
191
+ container.css({ overflow: 'visible', display: 'block' })
192
+
193
+ var row1 = $('<div/>', { class: 's2-ombc-mode-row' }).appendTo(container)
194
+ $('<input/>', { type: 'text', class: 's2-ombc-mode-label', placeholder: 'Mode label' })
195
+ .val(opt.label || '')
196
+ .appendTo(row1)
197
+
198
+ var row1b = $('<div/>', { class: 's2-ombc-mode-row' }).appendTo(container)
199
+ var abnormalLabel = $('<label/>', { class: 's2-ombc-mode-checkbox-label' }).appendTo(row1b)
200
+ $('<input/>', { type: 'checkbox', class: 's2-ombc-mode-abnormal' })
201
+ .prop('checked', !!opt.abnormalConditionOnly)
202
+ .appendTo(abnormalLabel)
203
+ abnormalLabel.append(' Abnormal only')
204
+
205
+ var row2 = $('<div/>', { class: 's2-ombc-mode-row' }).appendTo(container)
206
+ var symLabel = $('<label/>', { class: 's2-ombc-mode-checkbox-label' }).appendTo(row2)
207
+ var symCheckbox = $('<input/>', { type: 'checkbox', class: 's2-ombc-mode-symmetric' })
208
+ .prop('checked', opt.symmetric !== false)
209
+ .appendTo(symLabel)
210
+ symLabel.append(' Same value on all phases')
211
+
212
+ var row3 = $('<div/>', { class: 's2-ombc-mode-row' }).appendTo(container)
213
+ var symWrap = $('<span/>', { class: 's2-ombc-mode-value-wrap' }).appendTo(row3)
214
+ var symValue = $('<input/>', { type: 'number', class: 's2-ombc-mode-value-sym', step: '1' })
215
+ .val(opt.valueSymmetric != null ? opt.valueSymmetric : 0)
216
+ .appendTo(symWrap)
217
+ symWrap.append($('<span/>', { class: 's2-unit-label' }).text('W'))
218
+
219
+ var phaseWrap = $('<span/>', { class: 's2-ombc-mode-value-wrap' }).appendTo(row3)
220
+ phaseWrap.append('L1 ')
221
+ var l1 = $('<input/>', { type: 'number', class: 's2-ombc-mode-value-l1', step: '1' }).val(opt.valueL1 || 0).appendTo(phaseWrap)
222
+ phaseWrap.append(' L2 ')
223
+ var l2 = $('<input/>', { type: 'number', class: 's2-ombc-mode-value-l2', step: '1' }).val(opt.valueL2 || 0).appendTo(phaseWrap)
224
+ phaseWrap.append(' L3 ')
225
+ var l3 = $('<input/>', { type: 'number', class: 's2-ombc-mode-value-l3', step: '1' }).val(opt.valueL3 || 0).appendTo(phaseWrap)
226
+ phaseWrap.append($('<span/>', { class: 's2-unit-label' }).text('W'))
227
+
228
+ function updateSymVisibility () {
229
+ if (symCheckbox.prop('checked')) {
230
+ symWrap.show()
231
+ phaseWrap.hide()
232
+ } else {
233
+ symWrap.hide()
234
+ phaseWrap.show()
235
+ }
236
+ }
237
+ symCheckbox.on('change', updateSymVisibility)
238
+ updateSymVisibility()
239
+
240
+ if (opt.protected) {
241
+ $('<div/>', { class: 's2-ombc-mode-protected-note' }).text('Always present - remove via Advanced mode if not needed.').appendTo(container)
242
+ }
243
+ }
244
+
245
+ $(listSel).css('min-height', '320px').css('min-width', '480px').editableList({
246
+ addItem: addModeItem,
247
+ removeItem: function (opt) {
248
+ // Protected rows can't be removed through the list's own control -
249
+ // restore it. (Still removable by hand-editing Advanced/JSON mode.)
250
+ if (opt && opt.protected) {
251
+ $(listSel).editableList('addItem', opt)
252
+ }
253
+ },
254
+ removable: true,
255
+ sortable: true
256
+ })
257
+
258
+ function getFriendlyModes () {
259
+ var modes = []
260
+ $(listSel).editableList('items').each(function () {
261
+ var $item = $(this)
262
+ var data = $item.data('data') || {}
263
+ modes.push({
264
+ id: data.id,
265
+ protected: !!data.protected,
266
+ label: $item.find('.s2-ombc-mode-label').val(),
267
+ abnormalConditionOnly: $item.find('.s2-ombc-mode-abnormal').prop('checked'),
268
+ symmetric: $item.find('.s2-ombc-mode-symmetric').prop('checked'),
269
+ valueSymmetric: Number($item.find('.s2-ombc-mode-value-sym').val()) || 0,
270
+ valueL1: Number($item.find('.s2-ombc-mode-value-l1').val()) || 0,
271
+ valueL2: Number($item.find('.s2-ombc-mode-value-l2').val()) || 0,
272
+ valueL3: Number($item.find('.s2-ombc-mode-value-l3').val()) || 0
273
+ })
274
+ })
275
+ return modes
276
+ }
277
+
278
+ function showFriendlySection () {
279
+ currentMode = 'friendly'
280
+ $('#s2-ombc-friendly-section').show()
281
+ $('#s2-ombc-advanced-section').hide()
282
+ $('#s2-ombc-advanced-warning').hide()
283
+ $('#s2-ombc-tab-friendly').addClass('s2-ombc-tab-active')
284
+ $('#s2-ombc-tab-advanced').removeClass('s2-ombc-tab-active')
285
+ }
286
+
287
+ function showAdvancedSection (warningText) {
288
+ currentMode = 'advanced'
289
+ $('#s2-ombc-friendly-section').hide()
290
+ $('#s2-ombc-advanced-section').show()
291
+ if (warningText) {
292
+ $('#s2-ombc-advanced-warning').text(warningText).show()
293
+ } else {
294
+ $('#s2-ombc-advanced-warning').hide()
295
+ }
296
+ $('#s2-ombc-tab-advanced').addClass('s2-ombc-tab-active')
297
+ $('#s2-ombc-tab-friendly').removeClass('s2-ombc-tab-active')
298
+ }
299
+
300
+ $('#s2-ombc-tab-advanced').on('click', function (e) {
301
+ e.preventDefault()
302
+ if (currentMode === 'advanced') return
303
+ var sysDesc = friendlyStateToSystemDescription(getFriendlyModes())
304
+ $('#node-config-input-systemDescription').typedInput('value', JSON.stringify(sysDesc, null, 2))
305
+ showAdvancedSection()
306
+ })
307
+
308
+ $('#s2-ombc-tab-friendly').on('click', function (e) {
309
+ e.preventDefault()
310
+ if (currentMode === 'friendly') return
311
+ var raw = $('#node-config-input-systemDescription').typedInput('value')
312
+ var parsed
313
+ try {
314
+ parsed = JSON.parse(raw)
315
+ } catch (err) {
316
+ showAdvancedSection('Invalid JSON - fix it before switching to the friendly editor.')
317
+ return
318
+ }
319
+ if (!isFriendlyRepresentable(parsed)) {
320
+ showAdvancedSection('This system description uses something only Advanced mode supports (a genuine power range, a non-standard commodity, a blocked transition, or a timer) - continue editing it here.')
321
+ return
322
+ }
323
+ $(listSel).editableList('empty')
324
+ systemDescriptionToFriendlyState(parsed).forEach(function (m) {
325
+ $(listSel).editableList('addItem', m)
326
+ })
327
+ showFriendlySection()
328
+ })
329
+
330
+ var raw = this.systemDescription
331
+ var parsed = null
332
+ if (raw) {
333
+ try { parsed = JSON.parse(raw) } catch (e) { parsed = null }
334
+ }
335
+
336
+ if (!raw) {
337
+ $(listSel).editableList('addItem', defaultStandbyMode())
338
+ showFriendlySection()
339
+ } else if (parsed && isFriendlyRepresentable(parsed)) {
340
+ systemDescriptionToFriendlyState(parsed).forEach(function (m) {
341
+ $(listSel).editableList('addItem', m)
342
+ })
343
+ showFriendlySection()
344
+ } else {
345
+ $('#node-config-input-systemDescription').typedInput('value', raw)
346
+ showAdvancedSection()
347
+ }
348
+
349
+ window.__s2Common.initializeTooltips()
350
+ },
351
+ oneditsave: function () {
352
+ // Only friendly mode needs an explicit save step - Advanced mode's JSON
353
+ // typedInput already holds the value that gets read automatically.
354
+ var $list = $('#s2-ombc-mode-list')
355
+ if ($('#s2-ombc-tab-friendly').hasClass('s2-ombc-tab-active') && $list.length) {
356
+ var modes = []
357
+ $list.editableList('items').each(function () {
358
+ var $item = $(this)
359
+ var data = $item.data('data') || {}
360
+ modes.push({
361
+ id: data.id,
362
+ label: $item.find('.s2-ombc-mode-label').val(),
363
+ abnormalConditionOnly: $item.find('.s2-ombc-mode-abnormal').prop('checked'),
364
+ symmetric: $item.find('.s2-ombc-mode-symmetric').prop('checked'),
365
+ valueSymmetric: Number($item.find('.s2-ombc-mode-value-sym').val()) || 0,
366
+ valueL1: Number($item.find('.s2-ombc-mode-value-l1').val()) || 0,
367
+ valueL2: Number($item.find('.s2-ombc-mode-value-l2').val()) || 0,
368
+ valueL3: Number($item.find('.s2-ombc-mode-value-l3').val()) || 0
369
+ })
370
+ })
371
+ var sysDesc = friendlyStateToSystemDescription(modes)
372
+ $('#node-config-input-systemDescription').typedInput('value', JSON.stringify(sysDesc, null, 2))
373
+ }
374
+ }
375
+ })
376
+ })()
377
+ </script>
378
+
379
+ <script type="text/html" data-template-name="s2-ombc-config">
380
+ <div class="s2-form">
381
+
382
+ <div class="form-row">
383
+ <label for="node-config-input-name"><i class="fa fa-tag"></i> Name</label>
384
+ <input type="text" id="node-config-input-name" placeholder="Name (optional)">
385
+ </div>
386
+
387
+ <div class="form-row">
388
+ <label class="s2-section-label"><i class="fa fa-list"></i> System Description<i class="fa fa-info-circle tooltip-icon" data-tooltip="OMBC.SystemDescription: the operation modes this device can be in, and how it can transition between them. Sent to the CEM by s2-ombc once OMBC is the selected control type. Friendly mode covers the common case (fixed-power modes, every mode able to transition to every other); switch to Advanced for anything else - a genuine power range, a non-standard commodity, a blocked transition, or a timer."></i></label>
389
+ </div>
390
+
391
+ <div class="form-row s2-full-row">
392
+ <div class="s2-ombc-tabs">
393
+ <a href="#" id="s2-ombc-tab-friendly" class="s2-ombc-tab s2-ombc-tab-active">Friendly</a>
394
+ <a href="#" id="s2-ombc-tab-advanced" class="s2-ombc-tab">Advanced (JSON)</a>
395
+ </div>
396
+ </div>
397
+
398
+ <div id="s2-ombc-friendly-section" class="form-row s2-full-row">
399
+ <ol id="s2-ombc-mode-list"></ol>
400
+ </div>
401
+
402
+ <div id="s2-ombc-advanced-section" class="form-row s2-full-row" style="display:none;">
403
+ <div id="s2-ombc-advanced-warning" class="s2-ombc-advanced-warning" style="display:none;"></div>
404
+ <input type="text" id="node-config-input-systemDescription">
405
+ </div>
406
+
407
+ </div>
408
+ </script>
409
+
410
+ <script type="text/html" data-help-name="s2-ombc-config">
411
+ <p>Configuration for <b>s2-ombc</b>: the OMBC system description (operation modes and how the device can transition between them). Sent to the CEM by <b>s2-ombc</b> once the CEM has selected Operation Mode Based Control.</p>
412
+
413
+ <h3>Friendly mode</h3>
414
+ <p>Covers the common case: a list of operation modes, each with a diagnostic label, an optional "abnormal condition only" flag, and a fixed power level (the same value on all three phases, or a separate value per phase). Every mode is assumed able to transition to every other mode - there is no separate step to define transitions.</p>
415
+ <p>A "Standby/off" mode is always present and can't be removed from the list itself (use Advanced mode if you genuinely don't want one). Add further modes with the "add" button below the list.</p>
416
+
417
+ <h3>Advanced (JSON) mode</h3>
418
+ <p>The raw <code>OMBC.SystemDescription</code> JSON (<code>operationModes</code>, <code>transitions</code>, <code>timers</code>), for anything friendly mode doesn't cover:</p>
419
+ <ul>
420
+ <li>a genuine power range (a mode whose lower and upper bound differ, rather than one fixed value)</li>
421
+ <li>a commodity quantity other than 3-phase-symmetric or per-phase L1/L2/L3</li>
422
+ <li>blocking a specific transition, rather than every mode being able to reach every other mode</li>
423
+ <li>a timer (<code>start_timers</code>/<code>blocking_timers</code> on a transition)</li>
424
+ </ul>
425
+ <p>Switching from Friendly to Advanced converts the current modes to JSON. Switching back only works if the JSON still matches what friendly mode can represent (fixed-power modes, every mode transitioning to every other, no timers) - otherwise the editor stays in Advanced mode and explains why, rather than discarding anything.</p>
426
+ </script>
427
+
428
+ <link rel="stylesheet" href="resources/node-red-contrib-s2/s2-styles.css">
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ module.exports = function (RED) {
3
+ function S2OmbcConfigNodeConstructor(config) {
4
+ RED.nodes.createNode(this, config);
5
+ this.systemDescription = config.systemDescription;
6
+ }
7
+ RED.nodes.registerType('s2-ombc-config', S2OmbcConfigNodeConstructor);
8
+ };
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/nodes/s2-ombc-config/index.ts"],"names":[],"mappings":";AAGA,iBAAS,UAAU,GAAe;IAChC,SAAS,2BAA2B,CAA0B,MAAkB;QAC9E,GAAG,CAAC,KAAK,CAAC,UAAU,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;QAClC,IAAI,CAAC,iBAAiB,GAAG,MAAM,CAAC,iBAA2B,CAAA;IAC7D,CAAC;IAED,GAAG,CAAC,KAAK,CAAC,YAAY,CAAC,gBAAgB,EAAE,2BAA2B,CAAC,CAAA;AACvE,CAAC,CAAA"}
@@ -0,0 +1,14 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 1042 1042" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
2
+ <g transform="matrix(1,0,0,1,-312,-142)">
3
+ <g transform="matrix(1038.75,-1038.75,-1038.75,-1038.75,314.531,1181.19)">
4
+ <path d="M0.75,0.251C0.739,0.262 0.727,0.272 0.715,0.281C0.708,0.286 0.701,0.291 0.694,0.296C0.65,0.325 0.6,0.344 0.549,0.351C0.516,0.356 0.482,0.356 0.449,0.351C0.398,0.344 0.348,0.325 0.303,0.296C0.284,0.283 0.265,0.268 0.248,0.251C0.22,0.223 0.22,0.178 0.248,0.15C0.276,0.123 0.321,0.123 0.348,0.15C0.366,0.168 0.386,0.182 0.407,0.192C0.465,0.22 0.533,0.22 0.591,0.192C0.612,0.182 0.632,0.168 0.649,0.15L0.758,0.042C0.773,0.027 0.789,0.015 0.806,0.006C0.821,-0.003 0.837,-0.009 0.853,-0.013C0.902,-0.026 0.954,-0.022 1,-0L0.75,0.251ZM0.525,0.115C0.499,0.112 0.482,0.097 0.474,0.088C0.465,0.078 0.466,0.062 0.477,0.052C0.487,0.043 0.503,0.044 0.513,0.054C0.515,0.057 0.521,0.063 0.531,0.064C0.541,0.065 0.551,0.061 0.557,0.052C0.564,0.042 0.564,0.026 0.556,0.016C0.549,0.008 0.538,0.006 0.535,0.005L0.33,-0.014L0.447,-0.132C0.457,-0.142 0.474,-0.142 0.483,-0.132C0.493,-0.122 0.493,-0.106 0.483,-0.096L0.443,-0.055L0.541,-0.045C0.551,-0.044 0.577,-0.039 0.596,-0.015C0.618,0.013 0.619,0.052 0.599,0.081C0.596,0.085 0.593,0.089 0.59,0.092C0.573,0.109 0.549,0.118 0.525,0.115ZM0.649,-0.15C0.632,-0.168 0.612,-0.182 0.591,-0.192C0.533,-0.22 0.465,-0.22 0.407,-0.192C0.386,-0.182 0.366,-0.168 0.348,-0.15L0.24,-0.042C0.225,-0.027 0.209,-0.015 0.192,-0.006C0.177,0.003 0.161,0.009 0.145,0.013C0.096,0.026 0.044,0.022 -0.003,0L0.248,-0.251C0.259,-0.262 0.271,-0.272 0.282,-0.281C0.289,-0.286 0.296,-0.291 0.303,-0.296C0.348,-0.325 0.398,-0.344 0.449,-0.351C0.482,-0.356 0.516,-0.356 0.549,-0.351C0.6,-0.344 0.65,-0.325 0.694,-0.296C0.714,-0.283 0.732,-0.268 0.75,-0.251C0.777,-0.223 0.777,-0.178 0.75,-0.15C0.722,-0.123 0.677,-0.123 0.649,-0.15Z" style="fill:url(#s2logo-gradient);fill-rule:nonzero;"/>
5
+ </g>
6
+ </g>
7
+ <defs>
8
+ <linearGradient id="s2logo-gradient" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1,0,0,-1,0,3.73045e-07)">
9
+ <stop offset="0" style="stop-color:rgb(45,19,75);stop-opacity:1"/>
10
+ <stop offset="0.37" style="stop-color:rgb(25,38,101);stop-opacity:1"/>
11
+ <stop offset="1" style="stop-color:rgb(17,88,163);stop-opacity:1"/>
12
+ </linearGradient>
13
+ </defs>
14
+ </svg>