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
@@ -1,14 +1,5 @@
1
1
  <script src="resources/node-red-contrib-s2/s2-common.js"></script>
2
2
  <script type="text/javascript">
3
- var GRID_CONNECTIONS = {
4
- '1x16A': 3680,
5
- '3x16A': 11040,
6
- '3x25A': 17250,
7
- '3x32A': 22080,
8
- '3x40A': 27600,
9
- '3x63A': 43470
10
- }
11
-
12
3
  RED.nodes.registerType('s2-rm-config', {
13
4
  category: 'config',
14
5
  defaults: {
@@ -21,10 +12,8 @@
21
12
  manufacturer: { value: 'Victron Energy' },
22
13
  model: { value: 'Virtual RM' },
23
14
  firmwareVersion: { value: '1.0.0' },
24
- maxBatteryChargePower: { value: 0, validate: RED.validators.number() },
25
- maxBatteryDischargePower: { value: 0, validate: RED.validators.number() },
26
- gridConnection: { value: '' },
27
- customMaxPowerW: { value: '' },
15
+ providesPowerMeasurement: { value: '3_PHASE_SYMMETRIC' },
16
+ providesForecast: { value: false },
28
17
  instructionPollIntervalMs: { value: 2000, validate: RED.validators.number() },
29
18
  skipInstructionStatus: { value: false }
30
19
  },
@@ -56,26 +45,20 @@
56
45
  $(this).prop('checked', savedRoles.indexOf($(this).val()) !== -1)
57
46
  })
58
47
 
59
- function updateBatteryLimitsVisibility () {
60
- var hasStorage = $('.s2-rm-config-role-checkbox[value="ENERGY_STORAGE"]').is(':checked')
61
- $('#s2-rm-config-battery-section').toggle(hasStorage)
62
- }
63
-
64
48
  $('.s2-rm-config-role-checkbox').on('change', function () {
65
49
  if ($('.s2-rm-config-role-checkbox:checked').length === 0) {
66
50
  $(this).prop('checked', true)
67
51
  }
68
- updateBatteryLimitsVisibility()
69
52
  })
70
53
 
71
- updateBatteryLimitsVisibility()
72
-
73
- function updateCustomPowerVisibility () {
74
- var val = $('#node-config-input-gridConnection').val()
75
- $('#s2-rm-config-custom-power-row').toggle(val === 'custom')
54
+ // Migrate legacy boolean to string value
55
+ if (this.providesPowerMeasurement === true) {
56
+ this.providesPowerMeasurement = '3_PHASE_SYMMETRIC'
57
+ $('#node-config-input-providesPowerMeasurement').val('3_PHASE_SYMMETRIC')
58
+ } else if (this.providesPowerMeasurement === false) {
59
+ this.providesPowerMeasurement = ''
60
+ $('#node-config-input-providesPowerMeasurement').val('')
76
61
  }
77
- $('#node-config-input-gridConnection').on('change', updateCustomPowerVisibility)
78
- updateCustomPowerVisibility()
79
62
 
80
63
  $('#node-config-input-skipInstructionStatus').prop('checked', this.skipInstructionStatus === true)
81
64
 
@@ -158,22 +141,20 @@
158
141
  </div>
159
142
 
160
143
  <div class="form-row">
161
- <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 outer PEBC power limits sent to the CEM as [-max, +max] watts."></i></label>
162
- <select id="node-config-input-gridConnection">
163
- <option value="">-- Not set --</option>
164
- <option value="1x16A">1x16A (3.7 kW)</option>
165
- <option value="3x16A">3x16A (11 kW)</option>
166
- <option value="3x25A">3x25A (17.3 kW)</option>
167
- <option value="3x32A">3x32A (22 kW)</option>
168
- <option value="3x40A">3x40A (27.6 kW)</option>
169
- <option value="3x63A">3x63A (43.5 kW)</option>
170
- <option value="custom">Custom</option>
144
+ <label for="node-config-input-providesPowerMeasurement"><i class="fa fa-bolt"></i> Power Meas.<i class="fa fa-info-circle tooltip-icon" data-tooltip="Commodity quantities for power measurement. Advertised to the CEM in ResourceManagerDetails."></i></label>
145
+ <select id="node-config-input-providesPowerMeasurement" style="width: 70%;">
146
+ <option value="">None</option>
147
+ <option value="3_PHASE_SYMMETRIC">3-phase symmetric</option>
148
+ <option value="L1_L2_L3">Per phase (L1, L2, L3)</option>
171
149
  </select>
172
150
  </div>
173
151
 
174
- <div class="form-row" id="s2-rm-config-custom-power-row" style="display:none">
175
- <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>
176
- <input type="number" id="node-config-input-customMaxPowerW" placeholder="e.g. 15000" min="1">
152
+ <div class="form-row">
153
+ <label for="node-config-input-providesForecast">
154
+ <i class="fa fa-line-chart"></i> Forecast
155
+ </label>
156
+ <input type="checkbox" id="node-config-input-providesForecast" style="width: auto; margin-top: 6px;">
157
+ <span style="margin-left: 6px;">Provides power forecast</span>
177
158
  </div>
178
159
 
179
160
  <div class="form-row">
@@ -196,33 +177,6 @@
196
177
  <input type="text" id="node-config-input-firmwareVersion" placeholder="e.g. 1.0.0">
197
178
  </div>
198
179
 
199
- <div id="s2-rm-config-battery-section">
200
- <div class="form-row">
201
- <label class="s2-section-label">
202
- <i class="fa fa-battery-three-quarters"></i> Battery Limits
203
- <i class="fa fa-info-circle tooltip-icon" data-tooltip="Hardware ceiling for battery power. CEM instructions are clamped to these values. Set to 0 for no limit."></i>
204
- </label>
205
- </div>
206
-
207
- <div class="form-row s2-battery-row">
208
- <label for="node-config-input-maxBatteryChargePower">
209
- <i class="fa fa-arrow-circle-down"></i> Charge
210
- <i class="fa fa-info-circle tooltip-icon" data-tooltip="Maximum battery charge power in Watts. Never exceeded when writing DESS limits."></i>
211
- </label>
212
- <input type="number" id="node-config-input-maxBatteryChargePower" placeholder="0 = no limit" min="0" step="1">
213
- <span class="s2-unit-label">W</span>
214
- </div>
215
-
216
- <div class="form-row s2-battery-row">
217
- <label for="node-config-input-maxBatteryDischargePower">
218
- <i class="fa fa-arrow-circle-up"></i> Discharge
219
- <i class="fa fa-info-circle tooltip-icon" data-tooltip="Maximum battery discharge power in Watts. Never exceeded when writing DESS limits."></i>
220
- </label>
221
- <input type="number" id="node-config-input-maxBatteryDischargePower" placeholder="0 = no limit" min="0" step="1">
222
- <span class="s2-unit-label">W</span>
223
- </div>
224
- </div>
225
-
226
180
  <div class="form-row">
227
181
  <a href="#" class="s2-advanced-toggle" id="s2-rm-config-advanced-toggle">
228
182
  <i class="fa fa-cog"></i> Advanced
@@ -251,4 +205,18 @@
251
205
  </div>
252
206
  </script>
253
207
 
208
+ <script type="text/html" data-help-name="s2-rm-config">
209
+ <p>Configuration for <b>s2-rm</b>: the Resource Manager's identity, advertised capabilities, and protocol-level behavior. Sent to the CEM in <code>ResourceManagerDetails</code> during the S2 handshake.</p>
210
+
211
+ <h3>Identity</h3>
212
+ <p><b>Resource ID</b> is a stable UUID identifying this RM to the CEM - it's also used to build the CEM connection URL on <b>s2-cem-config</b>. Auto-generated the first time the node is added; avoid changing it after deployment, since the CEM associates state with this id.</p>
213
+ <p><b>Roles</b> and <b>Control Types</b> are advertised capabilities - at least one of each must be selected. Control types beyond OMBC and PEBC (FRBC, DDBC, PPBC) have no dedicated node yet; their instructions are still forwarded on <b>s2-rm</b>'s instructions output for you to handle yourself.</p>
214
+
215
+ <h3>Power Meas. / Forecast</h3>
216
+ <p>Advertise whether this RM reports <code>PowerMeasurement</code> (as a single 3-phase-symmetric value, or per-phase L1/L2/L3) and/or <code>PowerForecast</code>. These affect what's advertised to the CEM and whether <b>s2-rm</b> emits its <code>PowerMeasurementStart</code> signal on output 1 - they don't gate whether a <code>PowerMeasurement</code>/<code>Forecast</code> command you send is actually forwarded, which happens regardless.</p>
217
+
218
+ <h3>Advanced</h3>
219
+ <p><b>Poll interval</b> controls how often <b>s2-rm</b> checks for pending instructions whose <code>execution_time</code> has now arrived (PEBC instructions bypass this queue entirely and are delivered immediately). <b>Skip instruction status updates</b> suppresses automatic <code>InstructionStatusUpdate(ACCEPTED/STARTED)</code> messages - <code>ReceptionStatus</code> and control-type status messages are still sent - useful if the CEM generates high load from frequent instructions.</p>
220
+ </script>
221
+
254
222
  <link rel="stylesheet" href="resources/node-red-contrib-s2/s2-styles.css">
@@ -10,8 +10,8 @@ module.exports = function (RED) {
10
10
  this.model = config.model;
11
11
  this.firmwareVersion = config.firmwareVersion;
12
12
  this.controlTypes = config.controlTypes;
13
- this.gridConnection = config.gridConnection;
14
- this.customMaxPowerW = config.customMaxPowerW != null ? Number(config.customMaxPowerW) : undefined;
13
+ this.providesPowerMeasurement = config.providesPowerMeasurement;
14
+ this.providesForecast = config.providesForecast === true;
15
15
  this.instructionPollIntervalMs = config.instructionPollIntervalMs != null ? Number(config.instructionPollIntervalMs) : 2000;
16
16
  this.skipInstructionStatus = config.skipInstructionStatus === true;
17
17
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/nodes/s2-rm-config/index.ts"],"names":[],"mappings":";AAGA,iBAAS,UAAU,GAAe;IAChC,SAAS,yBAAyB,CAAwB,MAAkB;QAC1E,GAAG,CAAC,KAAK,CAAC,UAAU,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;QAClC,IAAI,CAAC,UAAU,GAAG,MAAM,CAAC,UAAoB,CAAA;QAC7C,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,MAAgB,CAAA;QACrC,IAAI,CAAC,KAAK,GAAG,MAAM,CAAC,KAAe,CAAA;QACnC,IAAI,CAAC,YAAY,GAAG,MAAM,CAAC,YAAsB,CAAA;QACjD,IAAI,CAAC,YAAY,GAAG,MAAM,CAAC,YAAsB,CAAA;QACjD,IAAI,CAAC,KAAK,GAAG,MAAM,CAAC,KAAe,CAAA;QACnC,IAAI,CAAC,eAAe,GAAG,MAAM,CAAC,eAAyB,CAAA;QACvD,IAAI,CAAC,YAAY,GAAG,MAAM,CAAC,YAAsB,CAAA;QACjD,IAAI,CAAC,cAAc,GAAG,MAAM,CAAC,cAAwB,CAAA;QACrD,IAAI,CAAC,eAAe,GAAG,MAAM,CAAC,eAAe,IAAI,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;QAClG,IAAI,CAAC,yBAAyB,GAAG,MAAM,CAAC,yBAAyB,IAAI,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,yBAAyB,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;QAC3H,IAAI,CAAC,qBAAqB,GAAG,MAAM,CAAC,qBAAqB,KAAK,IAAI,CAAA;IACpE,CAAC;IAED,GAAG,CAAC,KAAK,CAAC,YAAY,CAAC,cAAc,EAAE,yBAAyB,CAAC,CAAA;AACnE,CAAC,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/nodes/s2-rm-config/index.ts"],"names":[],"mappings":";AAGA,iBAAS,UAAU,GAAe;IAChC,SAAS,yBAAyB,CAAwB,MAAkB;QAC1E,GAAG,CAAC,KAAK,CAAC,UAAU,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;QAClC,IAAI,CAAC,UAAU,GAAG,MAAM,CAAC,UAAoB,CAAA;QAC7C,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,MAAgB,CAAA;QACrC,IAAI,CAAC,KAAK,GAAG,MAAM,CAAC,KAAe,CAAA;QACnC,IAAI,CAAC,YAAY,GAAG,MAAM,CAAC,YAAsB,CAAA;QACjD,IAAI,CAAC,YAAY,GAAG,MAAM,CAAC,YAAsB,CAAA;QACjD,IAAI,CAAC,KAAK,GAAG,MAAM,CAAC,KAAe,CAAA;QACnC,IAAI,CAAC,eAAe,GAAG,MAAM,CAAC,eAAyB,CAAA;QACvD,IAAI,CAAC,YAAY,GAAG,MAAM,CAAC,YAAsB,CAAA;QACjD,IAAI,CAAC,wBAAwB,GAAG,MAAM,CAAC,wBAAkC,CAAA;QACzE,IAAI,CAAC,gBAAgB,GAAG,MAAM,CAAC,gBAAgB,KAAK,IAAI,CAAA;QACxD,IAAI,CAAC,yBAAyB,GAAG,MAAM,CAAC,yBAAyB,IAAI,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,yBAAyB,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;QAC3H,IAAI,CAAC,qBAAqB,GAAG,MAAM,CAAC,qBAAqB,KAAK,IAAI,CAAA;IACpE,CAAC;IAED,GAAG,CAAC,KAAK,CAAC,YAAY,CAAC,cAAc,EAAE,yBAAyB,CAAC,CAAA;AACnE,CAAC,CAAA"}
@@ -12,12 +12,10 @@ export interface S2RmConfigNode extends NodeRedNode {
12
12
  model: string;
13
13
  firmwareVersion: string;
14
14
  controlTypes: string;
15
- /** Hardware ceiling for battery charge power in Watts. 0 = no limit configured. */
16
- maxBatteryChargePower: number;
17
- /** Hardware ceiling for battery discharge power in Watts. 0 = no limit configured. */
18
- maxBatteryDischargePower: number;
19
- gridConnection: string;
20
- customMaxPowerW: number | undefined;
15
+ /** Commodity quantity list for power measurement, or '' for none. */
16
+ providesPowerMeasurement: string;
17
+ /** Whether this RM provides power forecasts to the CEM. */
18
+ providesForecast: boolean;
21
19
  /** How often (ms) to poll for due pending instructions. Defaults to 2000 if not set. */
22
20
  instructionPollIntervalMs: number;
23
21
  /** When true, skip sending InstructionStatusUpdate(ACCEPTED/STARTED) automatically.
@@ -25,6 +23,22 @@ export interface S2RmConfigNode extends NodeRedNode {
25
23
  * acknowledgment messages and the extra D-Bus traffic causes CPU load. */
26
24
  skipInstructionStatus: boolean;
27
25
  }
26
+ /**
27
+ * Server-side shape of the s2-ombc-config config node.
28
+ * Use RED.nodes.getNode(id) and cast to this type.
29
+ */
30
+ export interface S2OmbcConfigNode extends NodeRedNode {
31
+ /** JSON-encoded OMBCSystemDescriptionConfig: { operationModes, transitions, timers } */
32
+ systemDescription: string;
33
+ }
34
+ /**
35
+ * Server-side shape of the s2-pebc-config config node.
36
+ * Use RED.nodes.getNode(id) and cast to this type.
37
+ */
38
+ export interface S2PebcConfigNode extends NodeRedNode {
39
+ gridConnection: string;
40
+ customMaxPowerW: number | undefined;
41
+ }
28
42
  /**
29
43
  * Server-side shape of the s2-cem-config config node.
30
44
  * Use RED.nodes.getNode(id) and cast to this type.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-s2",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "Node-RED nodes for S2 energy management protocol (EN 50491-12-2)",
5
5
  "author": "Victron Energy BV",
6
6
  "repository": {
@@ -10,6 +10,7 @@
10
10
  "main": "dist/lib/index.js",
11
11
  "files": [
12
12
  "dist/",
13
+ "resources/",
13
14
  "README.md",
14
15
  "LICENSE",
15
16
  "CHANGELOG.md"
@@ -20,14 +21,19 @@
20
21
  "s2-rm-config": "./dist/nodes/s2-rm-config/index.js",
21
22
  "s2-cem-config": "./dist/nodes/s2-cem-config/index.js",
22
23
  "s2-websocket": "./dist/nodes/s2-websocket/index.js",
23
- "s2-rm": "./dist/nodes/s2-rm/index.js"
24
+ "s2-rm": "./dist/nodes/s2-rm/index.js",
25
+ "s2-ombc-config": "./dist/nodes/s2-ombc-config/index.js",
26
+ "s2-ombc": "./dist/nodes/s2-ombc/index.js",
27
+ "s2-pebc-config": "./dist/nodes/s2-pebc-config/index.js",
28
+ "s2-pebc": "./dist/nodes/s2-pebc/index.js"
24
29
  }
25
30
  },
26
31
  "scripts": {
27
- "prepublishOnly": "npm run build",
32
+ "prepublishOnly": "npm run build && npm run check-package-files",
28
33
  "build": "tsc && npm run copy-html",
29
34
  "clean": "rm -rf dist",
30
- "copy-html": "mkdir -p dist/nodes/s2-rm dist/nodes/s2-websocket dist/nodes/s2-rm-config dist/nodes/s2-cem-config dist/nodes/s2-rm/icons dist/nodes/s2-websocket/icons && cp src/nodes/s2-rm/index.html dist/nodes/s2-rm/ && cp src/nodes/s2-websocket/index.html dist/nodes/s2-websocket/ && cp src/nodes/s2-rm-config/index.html dist/nodes/s2-rm-config/ && cp src/nodes/s2-cem-config/index.html dist/nodes/s2-cem-config/ && cp icons/s2-logo.svg dist/nodes/s2-rm/icons/ && cp icons/s2-logo.svg dist/nodes/s2-websocket/icons/",
35
+ "copy-html": "node scripts/copy-html.js",
36
+ "check-package-files": "node scripts/check-package-files.js",
31
37
  "test": "npm run lint && npm run test:unit",
32
38
  "test:unit": "jest",
33
39
  "test:watch": "jest --watch",
@@ -0,0 +1,37 @@
1
+ /* global $ */
2
+
3
+ window.__s2Common = window.__s2Common || {}
4
+
5
+ window.__s2Common.initializeTooltips = function () {
6
+ $('.s2-tooltip-container').remove()
7
+ $('.s2-form .tooltip-icon').off('mouseenter.s2 mouseleave.s2')
8
+
9
+ $('.s2-form .tooltip-icon').on('mouseenter.s2', function () {
10
+ const $icon = $(this)
11
+ const tooltipText = $icon.attr('data-tooltip')
12
+ if (!tooltipText) return
13
+
14
+ const $tooltip = $('<div class="s2-tooltip-container"></div>').text(tooltipText)
15
+ $('body').append($tooltip)
16
+
17
+ const iconOffset = $icon.offset()
18
+ const tooltipHeight = $tooltip.outerHeight()
19
+ const tooltipWidth = $tooltip.outerWidth()
20
+
21
+ $tooltip.css({
22
+ top: iconOffset.top - tooltipHeight - 8,
23
+ left: iconOffset.left - (tooltipWidth / 2) + ($icon.outerWidth() / 2)
24
+ })
25
+
26
+ $icon.data('s2-tooltip', $tooltip)
27
+ })
28
+
29
+ $('.s2-form .tooltip-icon').on('mouseleave.s2', function () {
30
+ const $icon = $(this)
31
+ const $tooltip = $icon.data('s2-tooltip')
32
+ if ($tooltip) {
33
+ $tooltip.remove()
34
+ $icon.removeData('s2-tooltip')
35
+ }
36
+ })
37
+ }
@@ -0,0 +1,263 @@
1
+ /**
2
+ * S2 Node-RED Nodes - Stylesheet
3
+ *
4
+ * All selectors MUST be scoped under .s2-form to prevent
5
+ * affecting other Node-RED nodes.
6
+ *
7
+ * Label and input sizing intentionally defers to Node-RED defaults so that
8
+ * config node selectors (red-ui-search-container) lay out correctly inline.
9
+ */
10
+
11
+ /* ============================================================================
12
+ FORM LAYOUT
13
+ ============================================================================ */
14
+
15
+ .s2-form {
16
+ overflow-x: hidden;
17
+ }
18
+
19
+ /* Keep icon + text vertically centred inside labels without overriding the
20
+ width - Node-RED's default ~110px label width is used instead. */
21
+ /* Label width slightly wider than NR default (110px) to accommodate the
22
+ field icon + text. The tooltip icon is absolutely positioned inside the
23
+ label so it never pushes the input - all inputs align consistently. */
24
+ .s2-form .form-row label {
25
+ display: inline-flex;
26
+ align-items: center;
27
+ gap: 4px;
28
+ white-space: nowrap;
29
+ width: 120px !important;
30
+ position: relative;
31
+ padding-right: 20px;
32
+ box-sizing: border-box;
33
+ }
34
+
35
+ .s2-form .form-row input[type="checkbox"] {
36
+ width: auto !important;
37
+ }
38
+
39
+ /* Tooltip icon - absolutely positioned at the right edge of the label. */
40
+ .s2-form .tooltip-icon {
41
+ position: absolute;
42
+ right: 2px;
43
+ top: 50%;
44
+ transform: translateY(-50%);
45
+ font-size: 0.8em;
46
+ color: #00BCFF;
47
+ cursor: help;
48
+ }
49
+
50
+ /* Tooltip container - appended to <body> by s2-common.js so it is never
51
+ clipped by a parent overflow or panel boundary. */
52
+ .s2-tooltip-container {
53
+ position: absolute;
54
+ background: #fff;
55
+ color: #444;
56
+ padding: 8px 12px;
57
+ border: 1px solid #ccc;
58
+ border-radius: 3px;
59
+ font-size: 13px;
60
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
61
+ line-height: 1.4;
62
+ max-width: 280px;
63
+ z-index: 10000;
64
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
65
+ pointer-events: none;
66
+ }
67
+
68
+ /* ============================================================================
69
+ CONTROL TYPES / ROLES - Checkbox Grid
70
+ ============================================================================ */
71
+
72
+ /* Section labels (Roles, Control Types) span the full row.
73
+ Selector includes the `label` element type to match (and, combined with
74
+ source order, beat) .s2-form .form-row label's specificity above - a
75
+ class-only selector loses that tie-break despite both using !important,
76
+ leaving the label clipped to 120px with its text and tooltip icon
77
+ overlapping instead of spanning the row. */
78
+ .s2-form label.s2-section-label {
79
+ width: 100% !important;
80
+ display: inline-block;
81
+ }
82
+
83
+ /* Indent checkbox grid to align with inputs (matches Node-RED default label width). */
84
+ .s2-form .s2-full-row {
85
+ display: block;
86
+ margin-left: 110px;
87
+ }
88
+
89
+ .s2-form .s2-control-types-container {
90
+ display: grid;
91
+ grid-template-columns: 116px 116px;
92
+ gap: 6px 8px;
93
+ }
94
+
95
+ .s2-form .s2-checkbox-label {
96
+ display: flex;
97
+ align-items: center;
98
+ gap: 6px;
99
+ width: auto !important;
100
+ font-weight: normal;
101
+ cursor: pointer;
102
+ }
103
+
104
+ .s2-form .s2-checkbox-label input[type="checkbox"] {
105
+ width: auto;
106
+ margin: 0;
107
+ flex-shrink: 0;
108
+ }
109
+
110
+ /* ============================================================================
111
+ UNIT LABELS AND BATTERY LIMIT ROWS
112
+ ============================================================================ */
113
+
114
+ /* Unit suffix (W, ms) displayed after a number input */
115
+ .s2-form .s2-unit-label {
116
+ margin-left: 6px;
117
+ white-space: nowrap;
118
+ color: #555;
119
+ flex-shrink: 0;
120
+ }
121
+
122
+ /* Battery limit rows use flex so the unit label stays on the same line as
123
+ the input, and are indented to visually nest them under the section header. */
124
+ .s2-form .s2-battery-row {
125
+ display: flex;
126
+ align-items: center;
127
+ padding-left: 20px;
128
+ }
129
+
130
+ .s2-form .s2-battery-row label {
131
+ flex-shrink: 0;
132
+ }
133
+
134
+ .s2-form .s2-battery-row input[type="number"] {
135
+ flex: 1;
136
+ min-width: 0;
137
+ width: auto !important;
138
+ }
139
+
140
+ /* Advanced section rows use flex to keep unit labels inline. */
141
+ .s2-form .s2-advanced-row {
142
+ display: flex;
143
+ align-items: center;
144
+ }
145
+
146
+ .s2-form .s2-advanced-row label {
147
+ flex-shrink: 0;
148
+ }
149
+
150
+ .s2-form .s2-advanced-row input[type="number"] {
151
+ flex: 1;
152
+ min-width: 0;
153
+ width: auto !important;
154
+ }
155
+
156
+ /* Advanced toggle link */
157
+ .s2-form .s2-advanced-toggle {
158
+ color: #666;
159
+ font-size: 0.9em;
160
+ text-decoration: none;
161
+ display: inline-flex;
162
+ align-items: center;
163
+ gap: 5px;
164
+ }
165
+
166
+ .s2-form .s2-advanced-toggle:hover {
167
+ color: #333;
168
+ text-decoration: none;
169
+ }
170
+
171
+ /* ============================================================================
172
+ OMBC SYSTEM DESCRIPTION - Friendly/Advanced tabs and operation-mode rows
173
+ ============================================================================ */
174
+
175
+ .s2-form .s2-ombc-tabs {
176
+ display: flex;
177
+ gap: 4px;
178
+ border-bottom: 1px solid #ccc;
179
+ margin-bottom: 8px;
180
+ }
181
+
182
+ .s2-form .s2-ombc-tab {
183
+ padding: 5px 12px;
184
+ color: #666;
185
+ text-decoration: none;
186
+ border: 1px solid transparent;
187
+ border-bottom: none;
188
+ border-radius: 3px 3px 0 0;
189
+ }
190
+
191
+ .s2-form .s2-ombc-tab:hover {
192
+ color: #333;
193
+ text-decoration: none;
194
+ }
195
+
196
+ .s2-form .s2-ombc-tab.s2-ombc-tab-active {
197
+ color: #333;
198
+ font-weight: 600;
199
+ border-color: #ccc;
200
+ background: #fff;
201
+ margin-bottom: -1px;
202
+ }
203
+
204
+ .s2-form .s2-ombc-mode-row {
205
+ display: flex;
206
+ align-items: center;
207
+ gap: 10px;
208
+ flex-wrap: wrap;
209
+ }
210
+
211
+ .s2-form .s2-ombc-mode-row + .s2-ombc-mode-row {
212
+ margin-top: 4px;
213
+ }
214
+
215
+ .s2-form .s2-ombc-mode-row input[type="text"] {
216
+ flex: 1;
217
+ min-width: 100px;
218
+ width: auto !important;
219
+ }
220
+
221
+ .s2-form .s2-ombc-mode-row input[type="number"] {
222
+ width: 70px !important;
223
+ }
224
+
225
+ /* `label` element type included for the same specificity reason as
226
+ label.s2-section-label above. */
227
+ .s2-form label.s2-ombc-mode-checkbox-label {
228
+ display: inline-flex;
229
+ align-items: center;
230
+ gap: 4px;
231
+ width: auto !important;
232
+ white-space: nowrap;
233
+ font-weight: normal;
234
+ cursor: pointer;
235
+ }
236
+
237
+ .s2-form .s2-ombc-mode-checkbox-label input[type="checkbox"] {
238
+ width: auto !important;
239
+ }
240
+
241
+ .s2-form .s2-ombc-mode-value-wrap {
242
+ display: inline-flex;
243
+ align-items: center;
244
+ gap: 4px;
245
+ white-space: nowrap;
246
+ }
247
+
248
+ .s2-form .s2-ombc-mode-protected-note {
249
+ color: #888;
250
+ font-size: 0.85em;
251
+ font-style: italic;
252
+ margin-top: 2px;
253
+ }
254
+
255
+ .s2-form .s2-ombc-advanced-warning {
256
+ color: #a06400;
257
+ background: #fff8e6;
258
+ border: 1px solid #f0d78c;
259
+ border-radius: 3px;
260
+ padding: 6px 10px;
261
+ margin-bottom: 8px;
262
+ font-size: 0.9em;
263
+ }