node-red-contrib-s2 0.1.2 → 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 (38) 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 +10 -4
  37. package/resources/s2-common.js +37 -0
  38. package/resources/s2-styles.css +263 -0
@@ -0,0 +1,44 @@
1
+ import { NodeRedApp } from '../../types/node-red';
2
+ /**
3
+ * s2-pebc node
4
+ *
5
+ * Owns Power Envelope Based Control (PEBC) behavior: accumulates power envelope
6
+ * schedules from PEBC instructions and dispatches the currently active bound as
7
+ * it becomes effective. Also pushes a default PowerConstraints range (from
8
+ * s2-pebc-config) on deploy, used until a runtime override is supplied. That same
9
+ * wattage (or null if unconfigured) is published to flow context as
10
+ * `pebcDefaultMaxPowerW`, so downstream flow logic can derive a released-state
11
+ * limit (e.g. amps) from it instead of hardcoding one.
12
+ *
13
+ * Wiring:
14
+ * [s2-rm output 2 (from CEM)] -> [s2-pebc input] (to observe RevokeObject)
15
+ * [s2-rm output 3 (instructions)] -> [s2-pebc input] (to accumulate PEBC instructions)
16
+ * [Forecast command source] -> [s2-pebc input] (optional - to cap the forecast to the accumulated schedule)
17
+ * [PowerMeasurement command source] -> [s2-pebc input] (optional - to resolve which side of an
18
+ * asymmetric bound currently applies; see output port 1)
19
+ * [s2-pebc output 1] -> downstream flow (active element dispatch / pass-through instructions)
20
+ * [s2-pebc output 2] -> downstream flow (full accumulated schedule dump)
21
+ * [s2-pebc output 3] -> [s2-rm input] (PowerConstraints / InstructionStatus / Forecast commands)
22
+ *
23
+ * Output port 1 - active element / pass-through:
24
+ * Active element: { cemId, payload: { startTime, endTime, duration, lowerBound, upperBound, commodityQuantity, direction, limitW } }
25
+ * Released (no PEBC bound active - last instruction revoked, or its final element ended with
26
+ * nothing queued after it): { cemId, payload: { lowerBound: null, upperBound: null, commodityQuantity, direction, limitW: null } }
27
+ * `direction` ('import' or 'export') and `limitW` (watts) are resolved from the last known
28
+ * PowerMeasurement for that commodity, if any was wired in; `direction` defaults to 'import'
29
+ * (limitW = upperBound) otherwise. When a measurement flips `direction` for the currently
30
+ * active element and its two bounds actually differ, output 1 is re-emitted with the updated
31
+ * values - this does not resend InstructionStatus on output 3, since the instruction itself
32
+ * hasn't changed, only which of its bounds is presently binding.
33
+ * Non-PEBC instructions: passed through unchanged (node status shows a warning).
34
+ *
35
+ * Output port 2 - schedule:
36
+ * { cemId, payload: { commodityQuantity, elements: [{ startTime, endTime, durationSec, lowerBound, upperBound }] } }
37
+ *
38
+ * Output port 3 - commands to s2-rm:
39
+ * { payload: { command: 'PowerConstraints', constraints } }
40
+ * { payload: { command: 'InstructionStatus', cemId, instructionId, status } }
41
+ * { payload: { command: 'Forecast', cemId, forecast } } (forecast capped to the accumulated schedule, if any)
42
+ */
43
+ declare const _default: (RED: NodeRedApp) => void;
44
+ export = _default;
@@ -0,0 +1,85 @@
1
+ <script src="resources/node-red-contrib-s2/s2-common.js"></script>
2
+ <script type="text/javascript">
3
+ RED.nodes.registerType('s2-pebc', {
4
+ category: 's2',
5
+ color: '#5A8EC2',
6
+ defaults: {
7
+ name: { value: '' },
8
+ pebcConfig: { value: '', type: 's2-pebc-config', required: true }
9
+ },
10
+ inputs: 1,
11
+ outputs: 3,
12
+ icon: 's2-logo.svg',
13
+ outputLabels: ['active / instructions', 'schedule', 'to s2-rm'],
14
+ inputLabels: ['from s2-rm'],
15
+ label: function () {
16
+ return this.name || 's2-pebc'
17
+ },
18
+ labelStyle: function () {
19
+ return this.name ? 'node_label_italic' : ''
20
+ },
21
+ oneditprepare: function () {
22
+ window.__s2Common.initializeTooltips()
23
+ }
24
+ })
25
+ </script>
26
+
27
+ <script type="text/html" data-template-name="s2-pebc">
28
+ <div class="s2-form">
29
+
30
+ <div class="form-row">
31
+ <label for="node-input-name">
32
+ <i class="fa fa-tag"></i> Name
33
+ </label>
34
+ <input type="text" id="node-input-name" placeholder="Name (optional)">
35
+ </div>
36
+
37
+ <div class="form-row">
38
+ <label for="node-input-pebcConfig"><i class="fa fa-id-card"></i> PEBC Config<i class="fa fa-info-circle tooltip-icon" data-tooltip="Default power constraints (grid connection / custom max power)."></i></label>
39
+ <input type="text" id="node-input-pebcConfig">
40
+ </div>
41
+
42
+ </div>
43
+ </script>
44
+
45
+ <link rel="stylesheet" href="resources/node-red-contrib-s2/s2-styles.css">
46
+
47
+ <script type="text/html" data-help-name="s2-pebc">
48
+ <p>Owns Power Envelope Based Control (PEBC) behavior: accumulates power envelope schedules from PEBC instructions and dispatches the currently active bound to your flow as it becomes effective. Pushes a default PowerConstraints range on deploy, used until a runtime override is supplied. That same wattage is published to flow context as <code>pebcDefaultMaxPowerW</code> (<code>null</code> if unconfigured), so your own flow logic can derive a released-state limit from it instead of hardcoding one.</p>
49
+ <p>Wire this node's input to both <b>s2-rm</b>'s "from CEM" output (to observe <code>RevokeObject</code>) and its "instructions" output (to accumulate PEBC instructions). Optionally also wire a <code>Forecast</code> command into this input instead of directly into s2-rm - it will be capped to the accumulated schedule before being forwarded. Optionally also wire your <code>PowerMeasurement</code> command into this input (in addition to wherever else it already goes) so the node can resolve which side of an asymmetric bound currently applies - see output 1. Wire this node's third output back to <b>s2-rm</b>'s input.</p>
50
+ <h3>Input</h3>
51
+ <dl class="message-properties">
52
+ <dt>payload <span class="property-type">object</span></dt>
53
+ <dd>S2 "from CEM" messages and enriched instructions forwarded from <b>s2-rm</b>, as-is. Also accepts <code>{ command: 'Forecast', cemId, forecast }</code> and <code>{ command: 'PowerMeasurement', cemId, values }</code>.</dd>
54
+ </dl>
55
+ <h3>Outputs</h3>
56
+ <ol class="node-ports">
57
+ <li>Active / instructions
58
+ <dl class="message-properties">
59
+ <dt>payload.startTime / endTime / duration / lowerBound / upperBound / commodityQuantity</dt>
60
+ <dd>The schedule's currently active element, emitted when it becomes effective.</dd>
61
+ <dt>payload.direction <span class="property-type">'import' | 'export'</span></dt>
62
+ <dd>Which side of the bound currently applies, from the sign of the last <code>PowerMeasurement</code> received for this commodity (positive/none seen yet = <code>'import'</code>, negative = <code>'export'</code>).</dd>
63
+ <dt>payload.limitW <span class="property-type">number | null</span></dt>
64
+ <dd>Magnitude, in watts, of whichever bound matches <code>direction</code> (<code>upperBound</code> for import, <code>|lowerBound|</code> for export) - useful when your hardware only supports a single settable limit and you need to know which value to apply. <code>null</code> when that bound is unbounded.</dd>
65
+ </dl>
66
+ When no PEBC bound is active any more - the last instruction is revoked, or its final element ends with nothing queued after it - a released element is emitted instead, with <code>lowerBound</code> and <code>upperBound</code> both <code>null</code>. Treat that explicitly as "no PEBC-imposed limit right now", not as "no data yet".
67
+ <br>If you've wired in <code>PowerMeasurement</code>, this output is also re-emitted (with updated <code>direction</code>/<code>limitW</code>) whenever a measurement flips which side of an asymmetric bound applies mid-slot - without resending <code>InstructionStatus</code> on output 3, since the instruction itself hasn't changed.
68
+ <br>Non-PEBC instructions are passed through unchanged (node status shows a warning), so this node can be wired in parallel with s2-ombc downstream of the same s2-rm.
69
+ </li>
70
+ <li>Schedule
71
+ <dl class="message-properties">
72
+ <dt>payload.commodityQuantity <span class="property-type">string</span></dt>
73
+ <dd>Commodity quantity for all elements (e.g. <code>ELECTRIC.POWER.3_PHASE_SYMMETRIC</code>).</dd>
74
+ <dt>payload.elements <span class="property-type">array</span></dt>
75
+ <dd>Full accumulated PEBC schedule as an array of <code>{ startTime, endTime, durationSec, lowerBound, upperBound }</code>. Emitted each time the schedule is updated. Use this to program DESS schedule slots or similar downstream consumers.</dd>
76
+ </dl>
77
+ </li>
78
+ <li>To s2-rm
79
+ <dl class="message-properties">
80
+ <dt>payload <span class="property-type">object</span></dt>
81
+ <dd><code>{ command: 'PowerConstraints', constraints }</code>, <code>{ command: 'InstructionStatus', cemId, instructionId, status }</code>, or a <code>Forecast</code> command forwarded from the input (capped to the accumulated schedule, if any). Wire to s2-rm's input.</dd>
82
+ </dl>
83
+ </li>
84
+ </ol>
85
+ </script>
@@ -0,0 +1,417 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ const fs = __importStar(require("fs"));
36
+ const os = __importStar(require("os"));
37
+ const path = __importStar(require("path"));
38
+ const messages_1 = require("../../lib/s2/messages");
39
+ const schedule_1 = require("../../lib/s2/schedule");
40
+ const SCHEDULE_CONTEXT_KEY = 's2PebcSchedule';
41
+ module.exports = function (RED) {
42
+ function S2PebcNode(config) {
43
+ var _a;
44
+ RED.nodes.createNode(this, config);
45
+ const node = this;
46
+ const pebcConfigNode = RED.nodes.getNode(config.pebcConfig);
47
+ if (!pebcConfigNode) {
48
+ node.error('s2-pebc-config node is missing - please configure PEBC power constraints defaults');
49
+ node.status({ fill: 'red', shape: 'dot', text: 'config missing' });
50
+ return;
51
+ }
52
+ // Persist/restore schedule across restarts
53
+ const scheduleDir = path.join(((_a = RED.settings) === null || _a === void 0 ? void 0 : _a.userDir) || path.join(os.homedir(), '.node-red'), '.s2');
54
+ const scheduleFile = path.join(scheduleDir, `${node.id}-schedule.json`);
55
+ function saveSchedule(schedule) {
56
+ try {
57
+ fs.mkdirSync(scheduleDir, { recursive: true });
58
+ fs.writeFileSync(scheduleFile, JSON.stringify(schedule, null, 2));
59
+ }
60
+ catch (e) {
61
+ node.warn('Failed to persist S2 schedule: ' + e.message);
62
+ }
63
+ }
64
+ function loadPersistedSchedule() {
65
+ try {
66
+ const raw = fs.readFileSync(scheduleFile, 'utf8');
67
+ const schedule = JSON.parse(raw);
68
+ const now = Date.now();
69
+ const validElements = schedule.elements.filter(el => el.endMs > now);
70
+ if (validElements.length === 0)
71
+ return;
72
+ applySchedule({ ...schedule, elements: validElements });
73
+ node.log(`Restored S2 schedule for CEM ${schedule.cemId} with ${validElements.length} future element(s)`);
74
+ }
75
+ catch (e) {
76
+ if (e.code !== 'ENOENT') {
77
+ node.warn('Failed to load persisted S2 schedule: ' + e.message);
78
+ }
79
+ }
80
+ }
81
+ let scheduleTimer = null;
82
+ // Accumulated PEBC slots: keyed by slot start time (ms).
83
+ // Cleared when power_constraints_id changes (new planning period).
84
+ let pebcConstraintsId = null;
85
+ const pebcSlots = new Map();
86
+ // Deduplication: track last emitted active element to suppress identical re-emits from the CEM.
87
+ let lastEmittedActive = null;
88
+ let duplicateActiveCount = 0;
89
+ // Latest signed PowerMeasurement value per commodity quantity (positive = import, negative = export).
90
+ const lastMeasurement = new Map();
91
+ // Direction last announced to the flow, tracked independently of lastEmittedActive so a
92
+ // measurement-driven re-announcement never interacts with the InstructionStatus dedup above.
93
+ let lastAnnouncedDirection = null;
94
+ function resolveDirection(commodityQuantity) {
95
+ const measured = lastMeasurement.get(commodityQuantity);
96
+ return measured !== undefined && measured < 0 ? 'export' : 'import';
97
+ }
98
+ function resolveLimitW(direction, lowerBound, upperBound) {
99
+ if (direction === 'export')
100
+ return lowerBound === null ? null : Math.abs(lowerBound);
101
+ return upperBound;
102
+ }
103
+ function formatTime(ms) {
104
+ return new Date(ms).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
105
+ }
106
+ function formatBound(bound) {
107
+ return bound === null ? '∞' : `${bound}W`;
108
+ }
109
+ function updateNodeStatus() {
110
+ const count = pebcSlots.size;
111
+ if (count === 0) {
112
+ node.status({ fill: 'grey', shape: 'ring', text: 'no schedule' });
113
+ return;
114
+ }
115
+ const countLabel = `${count} slot${count > 1 ? 's' : ''}`;
116
+ const sorted = [...pebcSlots.values()].sort((a, b) => a.element.startMs - b.element.startMs);
117
+ const now = Date.now();
118
+ const active = sorted.find(s => now >= s.element.startMs && now < s.element.endMs);
119
+ if (active) {
120
+ node.status({
121
+ fill: 'green',
122
+ shape: 'dot',
123
+ text: `${countLabel} · active ${formatBound(active.element.lowerBound)}..${formatBound(active.element.upperBound)} until ${formatTime(active.element.endMs)}`
124
+ });
125
+ return;
126
+ }
127
+ const next = sorted.find(s => s.element.startMs > now);
128
+ if (next) {
129
+ node.status({ fill: 'blue', shape: 'dot', text: `${countLabel} · next @ ${formatTime(next.element.startMs)}` });
130
+ return;
131
+ }
132
+ node.status({ fill: 'yellow', shape: 'ring', text: `${countLabel} · all expired` });
133
+ }
134
+ function emitActiveElement(schedule) {
135
+ const el = (0, schedule_1.getActiveElement)(schedule, Date.now());
136
+ if (!el)
137
+ return;
138
+ const isDuplicate = lastEmittedActive !== null &&
139
+ lastEmittedActive.startMs === el.startMs &&
140
+ lastEmittedActive.lowerBound === el.lowerBound &&
141
+ lastEmittedActive.upperBound === el.upperBound &&
142
+ lastEmittedActive.commodityQuantity === schedule.commodityQuantity;
143
+ if (isDuplicate) {
144
+ duplicateActiveCount++;
145
+ node.debug(`duplicate active element from CEM (${duplicateActiveCount} total) - suppressing emit`);
146
+ node.status({ fill: 'yellow', shape: 'ring', text: `${pebcSlots.size} slot(s) · ${duplicateActiveCount} dup.` });
147
+ return;
148
+ }
149
+ lastEmittedActive = { startMs: el.startMs, lowerBound: el.lowerBound, upperBound: el.upperBound, commodityQuantity: schedule.commodityQuantity };
150
+ duplicateActiveCount = 0;
151
+ updateNodeStatus();
152
+ const slot = pebcSlots.get(el.startMs);
153
+ if (slot) {
154
+ node.send([null, null, { payload: { command: 'InstructionStatus', cemId: slot.cemId, instructionId: slot.instructionId, status: messages_1.InstructionStatus.STARTED } }]);
155
+ }
156
+ const direction = resolveDirection(schedule.commodityQuantity);
157
+ lastAnnouncedDirection = direction;
158
+ node.send([{
159
+ cemId: schedule.cemId,
160
+ payload: {
161
+ startTime: new Date(el.startMs).toISOString(),
162
+ endTime: new Date(el.endMs).toISOString(),
163
+ duration: el.duration,
164
+ lowerBound: el.lowerBound,
165
+ upperBound: el.upperBound,
166
+ commodityQuantity: schedule.commodityQuantity,
167
+ direction,
168
+ limitW: resolveLimitW(direction, el.lowerBound, el.upperBound)
169
+ }
170
+ }, null, null]);
171
+ }
172
+ // Signals that no PEBC-imposed bound is active right now - after the last known
173
+ // instruction is revoked, or its final element ends with nothing queued after it.
174
+ // Reuses the S2 meaning of a null bound ("unbounded on this side") rather than a
175
+ // separate message shape, so downstream consumers can treat it like any other
176
+ // active-element update.
177
+ function emitReleased(cemId, commodityQuantity) {
178
+ lastEmittedActive = null;
179
+ const direction = resolveDirection(commodityQuantity);
180
+ lastAnnouncedDirection = direction;
181
+ node.send([{ cemId, payload: { lowerBound: null, upperBound: null, commodityQuantity, direction, limitW: null } }, null, null]);
182
+ updateNodeStatus();
183
+ }
184
+ function scheduleNextDispatch(schedule) {
185
+ if (scheduleTimer) {
186
+ clearTimeout(scheduleTimer);
187
+ scheduleTimer = null;
188
+ }
189
+ const now = Date.now();
190
+ const nextStart = (0, schedule_1.getNextElementStart)(schedule, now);
191
+ if (nextStart !== null) {
192
+ const delay = Math.max(0, nextStart - now);
193
+ scheduleTimer = setTimeout(() => {
194
+ scheduleTimer = null;
195
+ emitActiveElement(schedule);
196
+ scheduleNextDispatch(schedule);
197
+ }, delay);
198
+ return;
199
+ }
200
+ // No next element queued - if one is currently active, it's the last one:
201
+ // arm a release once it ends instead of leaving output 1 silent forever.
202
+ const active = (0, schedule_1.getActiveElement)(schedule, now);
203
+ if (!active)
204
+ return;
205
+ const delay = Math.max(0, active.endMs - now);
206
+ scheduleTimer = setTimeout(() => {
207
+ scheduleTimer = null;
208
+ emitReleased(schedule.cemId, schedule.commodityQuantity);
209
+ }, delay);
210
+ }
211
+ function applySchedule(schedule) {
212
+ node.context().set(SCHEDULE_CONTEXT_KEY, schedule);
213
+ saveSchedule(schedule);
214
+ emitActiveElement(schedule);
215
+ scheduleNextDispatch(schedule);
216
+ node.send([null, {
217
+ cemId: schedule.cemId,
218
+ payload: {
219
+ commodityQuantity: schedule.commodityQuantity,
220
+ elements: schedule.elements.map(el => ({
221
+ startTime: new Date(el.startMs).toISOString(),
222
+ endTime: new Date(el.endMs).toISOString(),
223
+ durationSec: Math.round(el.duration / 1000),
224
+ lowerBound: el.lowerBound,
225
+ upperBound: el.upperBound
226
+ }))
227
+ }
228
+ }, null]);
229
+ updateNodeStatus();
230
+ }
231
+ function handleInstruction(msg) {
232
+ if (msg.controlType !== messages_1.ControlType.PEBC) {
233
+ node.status({ fill: 'yellow', shape: 'ring', text: `ignoring ${String(msg.controlType)} instruction` });
234
+ node.send([msg, null, null]);
235
+ return;
236
+ }
237
+ const cemId = msg.cemId;
238
+ const rawMsg = (msg.pebc || msg.payload);
239
+ const parsed = (0, schedule_1.parsePebcInstruction)(rawMsg, Date.now(), cemId);
240
+ if (!parsed || parsed.elements.length === 0)
241
+ return;
242
+ const constraintsId = rawMsg.power_constraints_id;
243
+ if (constraintsId && constraintsId !== pebcConstraintsId) {
244
+ pebcSlots.clear();
245
+ pebcConstraintsId = constraintsId;
246
+ }
247
+ for (const el of parsed.elements) {
248
+ pebcSlots.set(el.startMs, { element: el, commodityQuantity: parsed.commodityQuantity, cemId: cemId || '', instructionId: parsed.instructionId });
249
+ }
250
+ const sorted = [...pebcSlots.values()].sort((a, b) => a.element.startMs - b.element.startMs);
251
+ const combined = {
252
+ receivedAt: Date.now(),
253
+ cemId: sorted[0].cemId,
254
+ instructionId: parsed.instructionId,
255
+ commodityQuantity: sorted[0].commodityQuantity,
256
+ elements: sorted.map(s => s.element)
257
+ };
258
+ applySchedule(combined);
259
+ }
260
+ function handleRevoke(msg) {
261
+ const payload = msg.payload;
262
+ const revokedId = payload.object_id;
263
+ const revokedObjectType = payload.object_type;
264
+ if (!revokedId || !revokedObjectType || !revokedObjectType.endsWith('.Instruction'))
265
+ return;
266
+ let found = false;
267
+ let commodityQuantity = '';
268
+ for (const [key, slot] of pebcSlots.entries()) {
269
+ if (slot.instructionId === revokedId) {
270
+ commodityQuantity = slot.commodityQuantity;
271
+ pebcSlots.delete(key);
272
+ found = true;
273
+ }
274
+ }
275
+ if (!found)
276
+ return;
277
+ node.send([null, null, { payload: { command: 'InstructionStatus', cemId: msg.cemId, instructionId: revokedId, status: messages_1.InstructionStatus.REVOKED } }]);
278
+ if (pebcSlots.size === 0) {
279
+ if (scheduleTimer) {
280
+ clearTimeout(scheduleTimer);
281
+ scheduleTimer = null;
282
+ }
283
+ node.context().set(SCHEDULE_CONTEXT_KEY, null);
284
+ emitReleased(msg.cemId || '', commodityQuantity);
285
+ }
286
+ else {
287
+ const sorted = [...pebcSlots.values()].sort((a, b) => a.element.startMs - b.element.startMs);
288
+ const rebuilt = {
289
+ receivedAt: Date.now(),
290
+ cemId: sorted[0].cemId,
291
+ instructionId: sorted[0].instructionId,
292
+ commodityQuantity: sorted[0].commodityQuantity,
293
+ elements: sorted.map(s => s.element)
294
+ };
295
+ applySchedule(rebuilt);
296
+ }
297
+ }
298
+ function buildCurrentSchedule() {
299
+ if (pebcSlots.size === 0)
300
+ return null;
301
+ const sorted = [...pebcSlots.values()].sort((a, b) => a.element.startMs - b.element.startMs);
302
+ return {
303
+ receivedAt: Date.now(),
304
+ cemId: sorted[0].cemId,
305
+ instructionId: sorted[0].instructionId,
306
+ commodityQuantity: sorted[0].commodityQuantity,
307
+ elements: sorted.map(s => s.element)
308
+ };
309
+ }
310
+ function handleForecast(msg) {
311
+ const payload = msg.payload;
312
+ const forecast = payload.forecast;
313
+ const schedule = forecast ? buildCurrentSchedule() : null;
314
+ if (!forecast || !schedule) {
315
+ node.send([null, null, msg]);
316
+ return;
317
+ }
318
+ node.send([null, null, { ...msg, payload: { ...payload, forecast: (0, schedule_1.capForecastToSchedule)(forecast, schedule) } }]);
319
+ }
320
+ function handlePowerMeasurement(msg) {
321
+ const payload = msg.payload;
322
+ const values = payload.values;
323
+ if (!Array.isArray(values))
324
+ return;
325
+ for (const v of values) {
326
+ if (typeof v.value === 'number' && typeof v.commodity_quantity === 'string') {
327
+ lastMeasurement.set(v.commodity_quantity, v.value);
328
+ }
329
+ }
330
+ reannounceIfDirectionChanged();
331
+ }
332
+ // Re-emits the active element on output 1 only (no InstructionStatus on output 3) when a
333
+ // measurement flips which side of an asymmetric bound currently applies. Deliberately
334
+ // independent of emitActiveElement's dedup/InstructionStatus logic - the instruction itself
335
+ // hasn't changed, only which of its two bounds is presently binding.
336
+ function reannounceIfDirectionChanged() {
337
+ const schedule = buildCurrentSchedule();
338
+ if (!schedule)
339
+ return;
340
+ const el = (0, schedule_1.getActiveElement)(schedule, Date.now());
341
+ if (!el)
342
+ return;
343
+ const direction = resolveDirection(schedule.commodityQuantity);
344
+ if (direction === lastAnnouncedDirection)
345
+ return;
346
+ const importLimit = resolveLimitW('import', el.lowerBound, el.upperBound);
347
+ const exportLimit = resolveLimitW('export', el.lowerBound, el.upperBound);
348
+ lastAnnouncedDirection = direction;
349
+ if (importLimit === exportLimit)
350
+ return;
351
+ node.send([{
352
+ cemId: schedule.cemId,
353
+ payload: {
354
+ startTime: new Date(el.startMs).toISOString(),
355
+ endTime: new Date(el.endMs).toISOString(),
356
+ duration: el.duration,
357
+ lowerBound: el.lowerBound,
358
+ upperBound: el.upperBound,
359
+ commodityQuantity: schedule.commodityQuantity,
360
+ direction,
361
+ limitW: resolveLimitW(direction, el.lowerBound, el.upperBound)
362
+ }
363
+ }, null, null]);
364
+ }
365
+ updateNodeStatus();
366
+ node.on('input', (msg, _send, done) => {
367
+ if ('controlType' in msg) {
368
+ handleInstruction(msg);
369
+ done();
370
+ return;
371
+ }
372
+ const payload = msg.payload;
373
+ if (payload && typeof payload === 'object' && payload.message_type === messages_1.MessageType.REVOKE_OBJECT) {
374
+ handleRevoke(msg);
375
+ done();
376
+ return;
377
+ }
378
+ if (payload && typeof payload === 'object' && payload.command === 'Forecast') {
379
+ handleForecast(msg);
380
+ done();
381
+ return;
382
+ }
383
+ if (payload && typeof payload === 'object' && payload.command === 'PowerMeasurement') {
384
+ handlePowerMeasurement(msg);
385
+ done();
386
+ return;
387
+ }
388
+ done();
389
+ });
390
+ node.on('close', (done) => {
391
+ if (scheduleTimer) {
392
+ clearTimeout(scheduleTimer);
393
+ scheduleTimer = null;
394
+ }
395
+ done();
396
+ });
397
+ loadPersistedSchedule();
398
+ // Push the configured default power constraints on deploy, used until a runtime
399
+ // override is supplied. Deferred so s2-rm's input listener is guaranteed attached first.
400
+ const defaultMaxPowerW = (0, messages_1.gridConnectionToWatts)(pebcConfigNode.gridConnection, pebcConfigNode.customMaxPowerW);
401
+ // Published so downstream flow logic can derive a released-state limit (e.g. amps)
402
+ // the same way it derives any other numeric bound, instead of hardcoding one.
403
+ node.context().flow.set('pebcDefaultMaxPowerW', defaultMaxPowerW);
404
+ if (defaultMaxPowerW != null) {
405
+ const constraints = {
406
+ commodityQuantity: 'ELECTRIC.POWER.3_PHASE_SYMMETRIC',
407
+ minPower: -defaultMaxPowerW,
408
+ maxPower: defaultMaxPowerW
409
+ };
410
+ setTimeout(() => {
411
+ node.send([null, null, { payload: { command: 'PowerConstraints', constraints } }]);
412
+ }, 100);
413
+ }
414
+ }
415
+ RED.nodes.registerType('s2-pebc', S2PebcNode);
416
+ };
417
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/nodes/s2-pebc/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,64 @@
1
+ <script src="resources/node-red-contrib-s2/s2-common.js"></script>
2
+ <script type="text/javascript">
3
+ RED.nodes.registerType('s2-pebc-config', {
4
+ category: 'config',
5
+ defaults: {
6
+ name: { value: '' },
7
+ gridConnection: { value: '' },
8
+ customMaxPowerW: { value: '' }
9
+ },
10
+ label: function () {
11
+ return this.name || 's2-pebc-config'
12
+ },
13
+ oneditprepare: function () {
14
+ function updateCustomPowerVisibility () {
15
+ var val = $('#node-config-input-gridConnection').val()
16
+ $('#s2-pebc-config-custom-power-row').toggle(val === 'custom')
17
+ }
18
+ $('#node-config-input-gridConnection').on('change', updateCustomPowerVisibility)
19
+ updateCustomPowerVisibility()
20
+
21
+ window.__s2Common.initializeTooltips()
22
+ }
23
+ })
24
+ </script>
25
+
26
+ <script type="text/html" data-template-name="s2-pebc-config">
27
+ <div class="s2-form">
28
+
29
+ <div class="form-row">
30
+ <label for="node-config-input-name"><i class="fa fa-tag"></i> Name</label>
31
+ <input type="text" id="node-config-input-name" placeholder="Name (optional)">
32
+ </div>
33
+
34
+ <div class="form-row">
35
+ <label for="node-config-input-gridConnection"><i class="fa fa-plug"></i> Grid Fuse<i class="fa fa-info-circle tooltip-icon" data-tooltip="Fuse/breaker capacity at the grid connection point. Sets the default outer PEBC power limits sent to the CEM as [-max, +max] watts, until a runtime override is supplied."></i></label>
36
+ <select id="node-config-input-gridConnection">
37
+ <option value="">-- Not set --</option>
38
+ <option value="1x16A">1x16A (3.7 kW)</option>
39
+ <option value="3x16A">3x16A (11 kW)</option>
40
+ <option value="3x25A">3x25A (17.3 kW)</option>
41
+ <option value="3x32A">3x32A (22 kW)</option>
42
+ <option value="3x40A">3x40A (27.6 kW)</option>
43
+ <option value="3x63A">3x63A (43.5 kW)</option>
44
+ <option value="custom">Custom</option>
45
+ </select>
46
+ </div>
47
+
48
+ <div class="form-row" id="s2-pebc-config-custom-power-row" style="display:none">
49
+ <label for="node-config-input-customMaxPowerW"><i class="fa fa-bolt"></i> Max Power (W)<i class="fa fa-info-circle tooltip-icon" data-tooltip="Maximum power in watts for a custom grid connection. Used as both import and export limit."></i></label>
50
+ <input type="number" id="node-config-input-customMaxPowerW" placeholder="e.g. 15000" min="1">
51
+ </div>
52
+
53
+ </div>
54
+ </script>
55
+
56
+ <script type="text/html" data-help-name="s2-pebc-config">
57
+ <p>Configuration for <b>s2-pebc</b>: the default <code>PEBC.PowerConstraints</code> range it pushes on deploy, used until a runtime override is supplied or a CEM sends its own.</p>
58
+
59
+ <p><b>Grid Fuse</b> picks a standard fuse/breaker size; <b>s2-pebc</b> converts it to a symmetric <code>[-max, +max]</code> watt range (the same limit for import and export). Choose <b>Custom</b> to set an exact wattage instead - useful when the site's actual limit doesn't match a standard fuse size, or is asymmetric in practice.</p>
60
+
61
+ <p>Leaving this unset is valid: <b>s2-pebc</b> simply won't push a default, and relies entirely on whatever the CEM sends. The configured value is also published to flow context as <code>pebcDefaultMaxPowerW</code>, so your own flow logic can fall back to it - for example when a PEBC instruction's bound is released (<code>null</code>) and you need to know what limit to apply instead of leaving your hardware silently unconstrained.</p>
62
+ </script>
63
+
64
+ <link rel="stylesheet" href="resources/node-red-contrib-s2/s2-styles.css">