node-red-contrib-power-genius 0.0.0-stage → 1.0.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.
@@ -0,0 +1,135 @@
1
+ // Client for the PowerGenius XL line based API (TCP port 9008).
2
+ // banner: V<a.b.c>[ AUTH]\r\n (sent once on connect)
3
+ // request: C<seq>|<command>\n (seq 1-255; the documentation says CR, but a bare LF is what the device works with)
4
+ // reply: R<seq>|<hex result>|<message>
5
+ // status: S<n>|<message> (asynchronous, for example "S|state=REBOOT")
6
+ // The documentation is incomplete, so everything here is tolerant: unknown lines are passed on, never dropped.
7
+ class GeniusPGProtocol {
8
+ constructor(socket) {
9
+ this.socket = socket;
10
+ this.seq = 0;
11
+ this.pending = new Map();
12
+ this.lineBuffer = "";
13
+ this.statusHandler = null;
14
+ this.bannerHandler = null;
15
+ this.lineHandler = null;
16
+ }
17
+
18
+ nextSeq() {
19
+ this.seq = (this.seq % 255) + 1;
20
+ return this.seq;
21
+ }
22
+
23
+ handleData(chunk) {
24
+ this.lineBuffer += chunk.toString("utf8");
25
+ const lines = this.lineBuffer.split(/\r\n|\r|\n/);
26
+ this.lineBuffer = lines.pop();
27
+ for (const line of lines) {
28
+ this._handleLine(line);
29
+ }
30
+ }
31
+
32
+ _handleLine(line) {
33
+ if (!line) return;
34
+ if (this.lineHandler) this.lineHandler(line);
35
+
36
+ if (line.startsWith("V")) {
37
+ if (this.bannerHandler) this.bannerHandler(line);
38
+ return;
39
+ }
40
+
41
+ if (line.startsWith("S")) {
42
+ const pipeIdx = line.indexOf("|");
43
+ const msg = pipeIdx !== -1 ? line.substring(pipeIdx + 1) : "";
44
+ if (this.statusHandler) this.statusHandler(msg);
45
+ return;
46
+ }
47
+
48
+ if (line.startsWith("R")) {
49
+ const pipeIdx1 = line.indexOf("|");
50
+ if (pipeIdx1 === -1) return;
51
+ const pipeIdx2 = line.indexOf("|", pipeIdx1 + 1);
52
+ const seq = parseInt(line.substring(1, pipeIdx1), 10);
53
+ const pending = this.pending.get(seq);
54
+ if (!pending) return;
55
+ this.pending.delete(seq);
56
+
57
+ // The documentation's examples show replies without the hex result: "R0|state=STANDBY ...".
58
+ // A real result field contains only hex digits, so anything else is the message itself.
59
+ const first = line.substring(pipeIdx1 + 1, pipeIdx2 !== -1 ? pipeIdx2 : undefined);
60
+ const hasResult = pipeIdx2 !== -1 && /^[0-9a-fA-F]+$/.test(first);
61
+ const hexResult = hasResult ? first : "0";
62
+ const message = hasResult ? line.substring(pipeIdx2 + 1) : line.substring(pipeIdx1 + 1);
63
+
64
+ if (parseInt(hexResult, 16) !== 0) {
65
+ const error = new Error(`Command error: 0x${hexResult} - ${message}`);
66
+ error.code = hexResult;
67
+ pending.reject(error);
68
+ } else {
69
+ pending.resolve(message);
70
+ }
71
+ }
72
+ }
73
+
74
+ sendCommand(cmd, timeoutMs) {
75
+ const seq = this.nextSeq();
76
+ return new Promise((resolve, reject) => {
77
+ const entry = { resolve, reject };
78
+ if (timeoutMs) {
79
+ const timer = setTimeout(() => {
80
+ this.pending.delete(seq);
81
+ const error = new Error(`Timeout waiting for reply to '${cmd}'`);
82
+ error.code = "ETIMEDOUT";
83
+ reject(error);
84
+ }, timeoutMs);
85
+ entry.resolve = (v) => { clearTimeout(timer); resolve(v); };
86
+ entry.reject = (e) => { clearTimeout(timer); reject(e); };
87
+ }
88
+ this.pending.set(seq, entry);
89
+ this.socket.write(`C${seq}|${cmd}\n`);
90
+ });
91
+ }
92
+
93
+ onStatus(handler) {
94
+ this.statusHandler = handler;
95
+ }
96
+
97
+ onBanner(handler) {
98
+ this.bannerHandler = handler;
99
+ }
100
+
101
+ // Called for every received line, before it is interpreted. Used to learn undocumented traffic.
102
+ onLine(handler) {
103
+ this.lineHandler = handler;
104
+ }
105
+ }
106
+
107
+ const toValue = (value) => {
108
+ if (value === "") return value;
109
+ const n = Number(value);
110
+ return Number.isNaN(n) ? value : n;
111
+ };
112
+
113
+ // Parses "key=value key=value ..." (optionally prefixed by a word such as "status").
114
+ const parseKeyValues = (msg) => {
115
+ const kv = {};
116
+ const re = /([\w-]+)=(\S*)/g;
117
+ let m;
118
+ while ((m = re.exec(msg)) !== null) {
119
+ kv[m[1]] = toValue(m[2]);
120
+ }
121
+ return kv;
122
+ };
123
+
124
+ // "V3.8.8 AUTH"
125
+ const parseBanner = (line) => {
126
+ const tokens = line.trim().split(/\s+/);
127
+ return {
128
+ firmware: tokens[0].replace(/^V/, ""),
129
+ authRequired: tokens.includes("AUTH"),
130
+ };
131
+ };
132
+
133
+ exports.GeniusPGProtocol = GeniusPGProtocol;
134
+ exports.parseKeyValues = parseKeyValues;
135
+ exports.parseBanner = parseBanner;
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jorgen Dahl
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,106 @@
1
- # Temporary Holding Version
1
+ # node-red-contrib-power-genius
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Node-RED nodes for monitoring a FlexRadio / 4O3A PowerGenius XL (PGXL) amplifier over its TCP API (port 9008).
4
+ Framing: `C<seq>|<command>` request, `R<seq>|<hex>|<message>` reply, `S<n>|<message>` asynchronous status.
5
+
6
+ Requires Node.js 18 or newer and Node-RED 3.1.0 or newer.
7
+
8
+ [![Node.js CI](https://github.com/sm6srw/node-red-contrib-power-genius/actions/workflows/node.js.yml/badge.svg)](https://github.com/sm6srw/node-red-contrib-power-genius/actions/workflows/node.js.yml) [![Node.js Package](https://github.com/sm6srw/node-red-contrib-power-genius/actions/workflows/npm-publish.yml/badge.svg)](https://github.com/sm6srw/node-red-contrib-power-genius/actions/workflows/npm-publish.yml) [![CodeQL](https://github.com/sm6srw/node-red-contrib-power-genius/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/sm6srw/node-red-contrib-power-genius/actions/workflows/codeql-analysis.yml) [![njsscan sarif](https://github.com/sm6srw/node-red-contrib-power-genius/actions/workflows/njsscan-analysis.yml/badge.svg)](https://github.com/sm6srw/node-red-contrib-power-genius/actions/workflows/njsscan-analysis.yml)
9
+ > **Work in progress.** The API documentation ([API Docs](API%20Docs/PGXL-Amplifier-to-Radio-API-Documentation.pdf),
10
+ > firmware 3.8.8) only covers a few commands and says little about the replies, so the nodes keep whatever the
11
+ > amplifier sends. The property tables below come from a real amplifier (firmware 3.9.9); units not given in the
12
+ > documentation are inferred.
13
+
14
+ ## Nodes
15
+
16
+ - **power-genius-server** (config): host, port (default 9008), auto connect and status poll interval (default 1000 ms, 0 = off).
17
+ - **power-genius-status**: emits one status property per node, or *all* as an object. The property is picked from a list, or typed (*other*) so that properties missing from the documentation can be used. Emits `msg.payload` (the value), `msg.topic` (the property name) and `msg.previous`. With `*`, `msg.payload` is the whole status object and `msg.topic` is `status`. By default it only emits when the value changes; an optional deadband suppresses small numeric changes. A start delay (default 1000 ms) holds back the first output after deploy so that downstream nodes, such as dashboard widgets, can initialize. Any input emits the last known value and refreshes from the device.
18
+ - **power-genius-info**: emits the device info once after connecting, either as an object or as a single property. Any input re-reads and emits it.
19
+ - **power-genius-control**: switches `operate` (standby / operate), selects the `antenna` (1-3, amplifiers with the antenna switch only), sets the `fanmode` (`STANDARD`, `CONTEST` or `BROADCAST`), or sends any command (`command`, the payload is the command) and outputs the reply as `msg.payload`. The action is fixed in the node, or taken from `msg.topic` when the node is set to `msg.topic`; a fixed action always wins. For `operate` the payload is `true`/`false`, `1`/`0` or `"on"`/`"off"`; anything else is rejected and nothing is sent.
20
+
21
+ ### Status properties
22
+
23
+ The `status` reply is a list of `key=value` pairs; numbers are emitted as numbers, everything else as text.
24
+
25
+ | Property | Meaning | Values / unit |
26
+ |---|---|---|
27
+ | `state` | Amplifier state | `STANDBY`, `IDLE` (operate, not transmitting), `TRANSMIT_A`, `TRANSMIT_B`; probably `FAULT` |
28
+ | `bandA`, `bandB` | Band of slice A / B | metres, e.g. `20` |
29
+ | `bsrcA`, `bsrcB` | Source of the band data | e.g. `FLEX` |
30
+ | `bsrcAutoA`, `bsrcAutoB` | Band source selected automatically | 1 = yes, 0 = no |
31
+ | `flexA`, `flexB` | FlexRadio nickname (FLEX mode) | text |
32
+ | `vac` | Mains voltage | V |
33
+ | `vdd` | Supply voltage | V; 0 when not transmitting |
34
+ | `id`, `peakid` | Drain current, and its peak | A; 0 when not transmitting |
35
+ | `fwd`, `peakfwd` | Forward power, and its peak | dBm; `fwd` is 30 (the meter minimum) when there is no RF, `peakfwd` keeps the last peak |
36
+ | `swr` | Return loss | dB (negative, e.g. -18; the FLEX `RL` meter); -60 when there is no RF |
37
+ | `temp` | PA temperature | °C |
38
+ | `hltemp` | Harmonic load temperature | °C |
39
+ | `cntfreq` | Frequency counted from the RF | kHz, e.g. 14098; 0 when there is no RF |
40
+ | `cat1freq`, `cat2freq` | Frequency from CAT port 1 / 2 (legacy radios with CAT or CI-V) | kHz; 0 in FLEX mode |
41
+ | `biasA`, `biasB` | Bias setting of input A / B, as `SOURCE_MODE` | e.g. `RADIO_AB`; others seen elsewhere: `AUTO_AAB`, `MANUAL_A` |
42
+ | `fanmode` | Fan mode | `STANDARD`, `CONTEST`, `BROADCAST` |
43
+ | `meffa` | MeFFA state | `OFF`, `ACTIVE`; may name a fault such as SWR, TEMP or CURRENT |
44
+ | `unkey` | Protection fault | 1 = active protection fault, 0 = normal operation |
45
+ | `antA` | Antenna selected on the antenna switch | Only on the 3-port antenna switch version; 0 on the SO2R version, which has no switch |
46
+ | `ants` | Antenna switch | 0 on the SO2R version (no antenna switch); values on the antenna switch version not seen yet |
47
+
48
+ Many status properties are only meaningful for some radio setups. With a FlexRadio (FLEX mode) the amplifier gets the
49
+ band and PTT from the radio over the network, and the properties for legacy radios using CAT or CI-V, such as
50
+ `cat1freq` and `cat2freq`, stay at 0 (the `catradioA`/`catradioB` info objects likewise describe the legacy radio setup).
51
+ It is not known which of the remaining properties, for example `bsrcAutoA/B`, belong to which mode, since the test amplifier is
52
+ used with a FlexRadio.
53
+
54
+ Units are taken from the FLEX meter definitions in the API documentation and from observed values. There are two
55
+ hardware versions: SO2R, and one with a 3-port antenna switch. `antA` and `ants` were only seen on the SO2R version.
56
+ Other properties may show up on other
57
+ firmware or setups: use a status node
58
+ with property `*` and a debug node to list them.
59
+
60
+ ### Info properties
61
+
62
+ Collected with `setup read`, `ifconf read`, `catradio read=A|B` and `flexradio read=A|B`; each reply is parsed as `key=value` pairs.
63
+
64
+ | Property | Meaning |
65
+ |---|---|
66
+ | `name` | Device name (the `nickname`, with `_` replaced by a space) |
67
+ | `nickname` | Device name as reported |
68
+ | `firmware` | Firmware version, from the banner the device sends on connect |
69
+ | `authRequired` | `true` when the device asked for authentication (not supported yet) |
70
+ | `ledintens` | LED intensity |
71
+ | `txdelay` | TX delay |
72
+ | `inactivity-timeout` | Inactivity timeout, 0 = off |
73
+ | `authcode` | Authentication code for WAN access. Treat it as a secret: it is part of the whole info object |
74
+ | `ants`, `ant1name`..`ant3name` | Antenna switch setup: `ants` (0 on the SO2R version, which has no switch) and the names of antennas 1-3 (empty when not set) |
75
+ | `ifconf` | Object with the network configuration: `address`, `netmask`, `gateway`, `dhcp`, `dhcpstatus` and the active `address-status`, `netmask-status`, `gateway-status` |
76
+ | `catradioA`, `catradioB` | Objects with the CAT radio configuration of slice A / B, used with legacy radios (CAT or CI-V): `type`, `baud`, `parity`, `stopbits`, `civ`, `active` |
77
+ | `flexradioA`, `flexradioB` | Objects with the FlexRadio configuration of slice A / B: `serial`, `txant`, `ptt`, `active` |
78
+
79
+ ## Commands used
80
+
81
+ Read-only: `status`, `setup read`, `ifconf read`, `catradio read={A|B}`, `flexradio read={A|B}`.
82
+ Through the control node: `operate={0|1}` (0 = standby, 1 = operate), `antenna={n}` (select the antenna, only on the
83
+ antenna switch version), `setup fanmode={STANDARD|CONTEST|BROADCAST}` and anything with the `command` action.
84
+
85
+ `operate` comes from another open-source PGXL client ([Log4YM](https://github.com/brianbruff/Log4YM)) and works; it is not
86
+ in the API documentation. `antenna=1` is known to exist: the SO2R version answers `0x50000013 - No antenna switch on this hardware`.
87
+ The antenna numbers (assumed 1-3) have not been tried on an amplifier with the switch.
88
+
89
+ ## Learning what the amplifier sends
90
+
91
+ - Set the Node-RED log level to `trace` to log every line received from the amplifier.
92
+ - Use a **power-genius-control** node with action `command` to try commands and see the replies. An error with result code `0x50000015` means the amplifier does not know the command.
93
+
94
+ Commands are sent as `C<seq>|<command>` terminated by a bare LF; the documentation says CR, but LF works.
95
+
96
+ Not supported: authentication (`AUTH` in the banner, only outside the local network), UDP discovery, and the FLEX radio-side commands (`amplifier create`, `meter create`, `interlock create`, ...), which the amplifier sends to the radio, not to clients.
97
+
98
+ ## Example
99
+
100
+ An example flow is included. After installing, use **Import → Examples → node-red-contrib-power-genius** in the editor.
101
+ Set the host of the **power genius** server node to your amplifier before deploying. The flow is in
102
+ [examples/power-genius-example.json](examples/power-genius-example.json).
103
+
104
+ ## Install
105
+
106
+ npm install node-red-contrib-power-genius
@@ -0,0 +1,238 @@
1
+ [
2
+ {
3
+ "id": "pg-tab",
4
+ "type": "tab",
5
+ "label": "Power Genius",
6
+ "disabled": false,
7
+ "info": "Example flow for node-red-contrib-power-genius.\n\nSet the host of the **power genius** server node to the address of your PowerGenius XL, deploy, and watch the debug sidebar."
8
+ },
9
+ {
10
+ "id": "pg-server",
11
+ "type": "power-genius-server",
12
+ "host": "192.168.1.100",
13
+ "port": 9008,
14
+ "autoConnect": true,
15
+ "pollInterval": 1000
16
+ },
17
+ {
18
+ "id": "pg-comment-setup",
19
+ "type": "comment",
20
+ "z": "pg-tab",
21
+ "name": "Setup: edit the power genius server node (host), then deploy",
22
+ "info": "Double click any Power Genius node, then the pencil next to Server, and enter the host name or IP address of the amplifier. The API port is 9008.",
23
+ "x": 280,
24
+ "y": 40,
25
+ "wires": []
26
+ },
27
+ {
28
+ "id": "pg-comment-monitor",
29
+ "type": "comment",
30
+ "z": "pg-tab",
31
+ "name": "Monitor: the whole status, and one property",
32
+ "info": "The first status node emits the whole status as an object. Use it to find out which properties your amplifier reports, then add one status node per property you care about.",
33
+ "x": 220,
34
+ "y": 100,
35
+ "wires": []
36
+ },
37
+ {
38
+ "id": "pg-status-all",
39
+ "type": "power-genius-status",
40
+ "z": "pg-tab",
41
+ "name": "",
42
+ "server": "pg-server",
43
+ "property": "*",
44
+ "onlyChange": true,
45
+ "deadband": 0,
46
+ "startDelay": 1000,
47
+ "x": 150,
48
+ "y": 160,
49
+ "wires": [["pg-debug-status"]]
50
+ },
51
+ {
52
+ "id": "pg-debug-status",
53
+ "type": "debug",
54
+ "z": "pg-tab",
55
+ "name": "status",
56
+ "active": true,
57
+ "tosidebar": true,
58
+ "console": false,
59
+ "tostatus": false,
60
+ "complete": "payload",
61
+ "targetType": "msg",
62
+ "x": 400,
63
+ "y": 160,
64
+ "wires": []
65
+ },
66
+ {
67
+ "id": "pg-status-state",
68
+ "type": "power-genius-status",
69
+ "z": "pg-tab",
70
+ "name": "",
71
+ "server": "pg-server",
72
+ "property": "state",
73
+ "onlyChange": true,
74
+ "deadband": 0,
75
+ "startDelay": 1000,
76
+ "x": 150,
77
+ "y": 220,
78
+ "wires": [["pg-debug-state"]]
79
+ },
80
+ {
81
+ "id": "pg-debug-state",
82
+ "type": "debug",
83
+ "z": "pg-tab",
84
+ "name": "state",
85
+ "active": true,
86
+ "tosidebar": true,
87
+ "console": false,
88
+ "tostatus": true,
89
+ "complete": "payload",
90
+ "targetType": "msg",
91
+ "statusVal": "payload",
92
+ "statusType": "auto",
93
+ "x": 400,
94
+ "y": 220,
95
+ "wires": []
96
+ },
97
+ {
98
+ "id": "pg-comment-info",
99
+ "type": "comment",
100
+ "z": "pg-tab",
101
+ "name": "Info: setup and interface configuration",
102
+ "info": "Emits the device info after connecting. Click the inject button to read it again.",
103
+ "x": 210,
104
+ "y": 280,
105
+ "wires": []
106
+ },
107
+ {
108
+ "id": "pg-inject-info",
109
+ "type": "inject",
110
+ "z": "pg-tab",
111
+ "name": "read info",
112
+ "props": [{ "p": "payload" }],
113
+ "repeat": "",
114
+ "crontab": "",
115
+ "once": false,
116
+ "onceDelay": 0.1,
117
+ "topic": "",
118
+ "payload": "",
119
+ "payloadType": "date",
120
+ "x": 120,
121
+ "y": 340,
122
+ "wires": [["pg-info"]]
123
+ },
124
+ {
125
+ "id": "pg-info",
126
+ "type": "power-genius-info",
127
+ "z": "pg-tab",
128
+ "name": "",
129
+ "server": "pg-server",
130
+ "property": "*",
131
+ "startDelay": 1000,
132
+ "x": 280,
133
+ "y": 340,
134
+ "wires": [["pg-debug-info"]]
135
+ },
136
+ {
137
+ "id": "pg-debug-info",
138
+ "type": "debug",
139
+ "z": "pg-tab",
140
+ "name": "info",
141
+ "active": true,
142
+ "tosidebar": true,
143
+ "console": false,
144
+ "tostatus": false,
145
+ "complete": "payload",
146
+ "targetType": "msg",
147
+ "x": 450,
148
+ "y": 340,
149
+ "wires": []
150
+ },
151
+ {
152
+ "id": "pg-comment-control",
153
+ "type": "comment",
154
+ "z": "pg-tab",
155
+ "name": "Control: operate / standby, or any command",
156
+ "info": "The control node takes its action from msg.topic. With topic 'operate' the payload is true/false (or 1/0, on/off) and 'operate=<0|1>' is sent. With topic 'command' the payload is sent as the command, for example 'setup read' or 'ping', and the reply is output. Watch the state property of the status node change between STANDBY and IDLE.",
157
+ "x": 190,
158
+ "y": 400,
159
+ "wires": []
160
+ },
161
+ {
162
+ "id": "pg-inject-operate",
163
+ "type": "inject",
164
+ "z": "pg-tab",
165
+ "name": "operate",
166
+ "props": [{ "p": "payload" }, { "p": "topic", "vt": "str" }],
167
+ "repeat": "",
168
+ "crontab": "",
169
+ "once": false,
170
+ "onceDelay": 0.1,
171
+ "topic": "operate",
172
+ "payload": "true",
173
+ "payloadType": "bool",
174
+ "x": 120,
175
+ "y": 460,
176
+ "wires": [["pg-control"]]
177
+ },
178
+ {
179
+ "id": "pg-inject-standby",
180
+ "type": "inject",
181
+ "z": "pg-tab",
182
+ "name": "standby",
183
+ "props": [{ "p": "payload" }, { "p": "topic", "vt": "str" }],
184
+ "repeat": "",
185
+ "crontab": "",
186
+ "once": false,
187
+ "onceDelay": 0.1,
188
+ "topic": "operate",
189
+ "payload": "false",
190
+ "payloadType": "bool",
191
+ "x": 120,
192
+ "y": 520,
193
+ "wires": [["pg-control"]]
194
+ },
195
+ {
196
+ "id": "pg-inject-command",
197
+ "type": "inject",
198
+ "z": "pg-tab",
199
+ "name": "setup read",
200
+ "props": [{ "p": "payload" }, { "p": "topic", "vt": "str" }],
201
+ "repeat": "",
202
+ "crontab": "",
203
+ "once": false,
204
+ "onceDelay": 0.1,
205
+ "topic": "command",
206
+ "payload": "setup read",
207
+ "payloadType": "str",
208
+ "x": 130,
209
+ "y": 580,
210
+ "wires": [["pg-control"]]
211
+ },
212
+ {
213
+ "id": "pg-control",
214
+ "type": "power-genius-control",
215
+ "z": "pg-tab",
216
+ "name": "",
217
+ "server": "pg-server",
218
+ "action": "",
219
+ "x": 300,
220
+ "y": 520,
221
+ "wires": [["pg-debug-reply"]]
222
+ },
223
+ {
224
+ "id": "pg-debug-reply",
225
+ "type": "debug",
226
+ "z": "pg-tab",
227
+ "name": "reply",
228
+ "active": true,
229
+ "tosidebar": true,
230
+ "console": false,
231
+ "tostatus": false,
232
+ "complete": "payload",
233
+ "targetType": "msg",
234
+ "x": 470,
235
+ "y": 520,
236
+ "wires": []
237
+ }
238
+ ]
package/package.json CHANGED
@@ -1,6 +1,61 @@
1
1
  {
2
2
  "name": "node-red-contrib-power-genius",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0",
4
+ "description": "Power Genius XL Control and Monitoring Nodes",
5
+ "keywords": [
6
+ "node-red",
7
+ "power genius",
8
+ "pgxl",
9
+ "4o3a",
10
+ "flexradio",
11
+ "ham radio",
12
+ "sm6srw",
13
+ "nu1t"
14
+ ],
15
+ "scripts": {
16
+ "test": "jest --coverage",
17
+ "watch": "jest --watch",
18
+ "preversion": "npm test"
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/sm6srw/node-red-contrib-power-genius.git"
23
+ },
24
+ "author": "Jorgen Dahl",
25
+ "license": "MIT",
26
+ "bugs": {
27
+ "url": "https://github.com/sm6srw/node-red-contrib-power-genius/issues"
28
+ },
29
+ "homepage": "https://github.com/sm6srw/node-red-contrib-power-genius#readme",
30
+ "engines": {
31
+ "node": ">=18"
32
+ },
33
+ "files": [
34
+ "*.js",
35
+ "*.html",
36
+ "examples"
37
+ ],
38
+ "node-red": {
39
+ "version": ">=3.1.0",
40
+ "nodes": {
41
+ "power-genius-server": "power-genius-server.js",
42
+ "power-genius-status": "power-genius-status.js",
43
+ "power-genius-info": "power-genius-info.js",
44
+ "power-genius-control": "power-genius-control.js"
45
+ }
46
+ },
47
+ "dependencies": {},
48
+ "devDependencies": {
49
+ "auto-dist-tag": "^2.1.1",
50
+ "jest": "^28.1.3",
51
+ "node-red": "^5.0.0",
52
+ "node-red-node-test-helper": "^0.3.6"
53
+ },
54
+ "overrides": {
55
+ "multer": "^2.5.0",
56
+ "body-parser": "^1.20.9"
57
+ },
58
+ "jest": {
59
+ "verbose": true
60
+ }
61
+ }
@@ -0,0 +1,69 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType("power-genius-control", {
3
+ category: "Power Genius",
4
+ color: "#a6bbcf",
5
+ defaults: {
6
+ name: { value: "" },
7
+ server: { type: "power-genius-server", required: true },
8
+ action: { value: "" },
9
+ },
10
+ inputs: 1,
11
+ outputs: 1,
12
+ icon: "bridge.svg",
13
+ label: function () {
14
+ return this.name || "pg control";
15
+ },
16
+ });
17
+ </script>
18
+
19
+ <script type="text/html" data-template-name="power-genius-control">
20
+ <div class="form-row">
21
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
22
+ <input type="text" id="node-input-name" />
23
+ </div>
24
+ <div class="form-row">
25
+ <label for="node-input-server"><i class="fa fa-server"></i> Server</label>
26
+ <input type="text" id="node-input-server" />
27
+ </div>
28
+ <div class="form-row">
29
+ <label for="node-input-action"><i class="fa fa-cog"></i> Action</label>
30
+ <select id="node-input-action">
31
+ <option value="">msg.topic</option>
32
+ <option value="operate">operate</option>
33
+ <option value="antenna">antenna</option>
34
+ <option value="fanmode">fanmode</option>
35
+ <option value="command">command</option>
36
+ </select>
37
+ </div>
38
+ </script>
39
+
40
+ <script type="text/markdown" data-help-name="power-genius-control">
41
+ Controls the Power Genius XL.
42
+
43
+ ### Input
44
+
45
+ : topic (string) : `operate`, `antenna`, `fanmode` or `command`. Only used when the node's Action is set to `msg.topic`; a fixed action in the node always takes precedence.
46
+
47
+ : payload (boolean|number|string) : Depends on the action:
48
+
49
+ | Action | Payload | Command sent |
50
+ |---|---|---|
51
+ | `operate` | `true`, `1`, `"1"` or `"on"` for operate, `false`, `0`, `"0"` or `"off"` for standby. Anything else is an error and nothing is sent. | `operate=<1\|0>` |
52
+ | `antenna` | The antenna number, 1-3. Only for amplifiers with the antenna switch; the SO2R version answers with an error | `antenna=<n>` |
53
+ | `fanmode` | `STANDARD`, `CONTEST` or `BROADCAST` (any case). Shows up as `fanmode` in the status | `setup fanmode=<mode>` |
54
+ | `command` | The command, on a single line, for example `setup read` or `ping` | The payload as it is |
55
+
56
+ ### Output
57
+
58
+ : topic (string) : The action.
59
+ : payload (string) : The reply message from the amplifier, for example `key=value key=value ...`. Empty when the command has no reply message.
60
+
61
+ A failed command is reported as an error on the node, with the result code from the amplifier. For example
62
+ `0x50000015` means that the amplifier does not know the command, and `0x50000013` that the hardware has no antenna switch.
63
+
64
+ ### Details
65
+
66
+ `command` is for trying out commands that the API documentation does not cover. Commands that change the
67
+ configuration, such as `ifconf`, `catradio` and `flexradio`, take effect after `save`, which reboots the amplifier. Use with care.
68
+ The effect of `operate` shows up in the status `state` (`STANDBY`, or `IDLE` when operating).
69
+ </script>