node-red-contrib-power-genius 0.1.0 → 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.
- package/GeniusPGProtocol.js +135 -135
- package/LICENSE +21 -21
- package/README.md +106 -107
- package/examples/power-genius-example.json +238 -238
- package/package.json +61 -59
- package/power-genius-control.html +69 -69
- package/power-genius-control.js +94 -94
- package/power-genius-info.html +105 -105
- package/power-genius-info.js +63 -63
- package/power-genius-server.html +54 -54
- package/power-genius-server.js +216 -216
- package/power-genius-status.html +170 -170
- package/power-genius-status.js +96 -96
package/power-genius-server.js
CHANGED
|
@@ -1,216 +1,216 @@
|
|
|
1
|
-
const Net = require("net");
|
|
2
|
-
const EventEmitter = require("events");
|
|
3
|
-
const { GeniusPGProtocol, parseKeyValues, parseBanner } = require("./GeniusPGProtocol");
|
|
4
|
-
|
|
5
|
-
class UpdatesEventEmitter extends EventEmitter {}
|
|
6
|
-
|
|
7
|
-
// Read-only commands whose replies make up the device info. Replies are stored under the given key, or merged
|
|
8
|
-
// flat into the info object when the key is null. The documentation does not list the reply fields, so
|
|
9
|
-
// whatever the device sends is kept.
|
|
10
|
-
const INFO_COMMANDS = [
|
|
11
|
-
{ cmd: "setup read", key: null },
|
|
12
|
-
{ cmd: "ifconf read", key: "ifconf" },
|
|
13
|
-
{ cmd: "catradio read=A", key: "catradioA" },
|
|
14
|
-
{ cmd: "catradio read=B", key: "catradioB" },
|
|
15
|
-
{ cmd: "flexradio read=A", key: "flexradioA" },
|
|
16
|
-
{ cmd: "flexradio read=B", key: "flexradioB" },
|
|
17
|
-
];
|
|
18
|
-
|
|
19
|
-
module.exports = (RED) => {
|
|
20
|
-
class PowerGeniusServerNode {
|
|
21
|
-
constructor(n) {
|
|
22
|
-
RED.nodes.createNode(this, n);
|
|
23
|
-
this.host = n.host;
|
|
24
|
-
this.port = n.port;
|
|
25
|
-
this.autoConnect = n.autoConnect || false;
|
|
26
|
-
this.pollInterval = Math.max(parseInt(n.pollInterval, 10) || 0, 0);
|
|
27
|
-
this.poller = null;
|
|
28
|
-
|
|
29
|
-
this.status = {};
|
|
30
|
-
this.info = {};
|
|
31
|
-
this.timer = null;
|
|
32
|
-
this._protocol = null;
|
|
33
|
-
this._dataHandler = null;
|
|
34
|
-
|
|
35
|
-
this.updatesEventEmitter = new UpdatesEventEmitter();
|
|
36
|
-
this.updatesEventEmitter.setMaxListeners(0);
|
|
37
|
-
|
|
38
|
-
this.client = new Net.Socket();
|
|
39
|
-
this.connected = false;
|
|
40
|
-
|
|
41
|
-
this.client.on("close", () => {
|
|
42
|
-
this.log("TCP connection disconnected with the server.");
|
|
43
|
-
clearTimeout(this.timer);
|
|
44
|
-
this.connected = false;
|
|
45
|
-
this._detach();
|
|
46
|
-
this._stopPolling();
|
|
47
|
-
|
|
48
|
-
if (this.updatesEventEmitter.listenerCount("closed") == 0) {
|
|
49
|
-
this.log("Stop retrying. No nodes are refering this connection anymore.");
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (this.autoConnect) {
|
|
54
|
-
this.log("Will try to reconnect in 5 seconds");
|
|
55
|
-
this.timer = setTimeout(() => this.connect(), 5000);
|
|
56
|
-
this.updatesEventEmitter.emit("closed");
|
|
57
|
-
}
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
this.client.on("connect", async () => {
|
|
61
|
-
this.log("TCP connection established with the server.");
|
|
62
|
-
this.connected = true;
|
|
63
|
-
try {
|
|
64
|
-
await this._setup();
|
|
65
|
-
} catch (err) {
|
|
66
|
-
this.warn("Connection setup failed: " + err.message);
|
|
67
|
-
}
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
this.client.on("error", (err) => {
|
|
71
|
-
this.warn(err);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
this.on("close", (done) => {
|
|
75
|
-
clearTimeout(this.timer);
|
|
76
|
-
this.autoConnect = false;
|
|
77
|
-
this.connected = false;
|
|
78
|
-
this._detach();
|
|
79
|
-
this._stopPolling();
|
|
80
|
-
this.client.end();
|
|
81
|
-
done();
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
_detach() {
|
|
86
|
-
if (this._dataHandler) {
|
|
87
|
-
this.client.removeListener("data", this._dataHandler);
|
|
88
|
-
this._dataHandler = null;
|
|
89
|
-
}
|
|
90
|
-
this._protocol = null;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
_stopPolling() {
|
|
94
|
-
clearInterval(this.poller);
|
|
95
|
-
this.poller = null;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
_startPolling() {
|
|
99
|
-
this._stopPolling();
|
|
100
|
-
if (!this.pollInterval) return;
|
|
101
|
-
let busy = false;
|
|
102
|
-
this.poller = setInterval(async () => {
|
|
103
|
-
if (busy || !this._protocol) return;
|
|
104
|
-
busy = true;
|
|
105
|
-
try {
|
|
106
|
-
await this.resendStatus();
|
|
107
|
-
} finally {
|
|
108
|
-
busy = false;
|
|
109
|
-
}
|
|
110
|
-
}, this.pollInterval);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
_mergeStatus(msg) {
|
|
114
|
-
const kv = parseKeyValues(msg);
|
|
115
|
-
if (Object.keys(kv).length === 0) return false;
|
|
116
|
-
this.status = { ...this.status, ...kv };
|
|
117
|
-
return true;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
_applyInfo(parts) {
|
|
121
|
-
const info = { ...this.info };
|
|
122
|
-
for (const { key, values } of parts) {
|
|
123
|
-
if (key === null) {
|
|
124
|
-
Object.assign(info, values);
|
|
125
|
-
} else {
|
|
126
|
-
info[key] = values;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
const nickname = info.nickname !== undefined ? info.nickname : info.name;
|
|
130
|
-
info.name = String(nickname || this.info.name || "").replace(/_/g, " ");
|
|
131
|
-
this.info = info;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Reads the info commands one by one. A command the device rejects is skipped, so that one unsupported
|
|
135
|
-
// command does not hide the rest.
|
|
136
|
-
async _readInfo(protocol) {
|
|
137
|
-
const parts = [];
|
|
138
|
-
for (const { cmd, key } of INFO_COMMANDS) {
|
|
139
|
-
try {
|
|
140
|
-
const values = parseKeyValues(await protocol.sendCommand(cmd, 3000));
|
|
141
|
-
parts.push({ key, values });
|
|
142
|
-
} catch (err) {
|
|
143
|
-
this.debug(`'${cmd}' failed: ${err.message}`);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
this._applyInfo(parts);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
async _setup() {
|
|
150
|
-
const protocol = new GeniusPGProtocol(this.client);
|
|
151
|
-
this._protocol = protocol;
|
|
152
|
-
this._dataHandler = (chunk) => protocol.handleData(chunk);
|
|
153
|
-
|
|
154
|
-
// The device sends "V<version>[ AUTH]" immediately after connecting.
|
|
155
|
-
protocol.onBanner((line) => {
|
|
156
|
-
this.info = { ...this.info, ...parseBanner(line) };
|
|
157
|
-
if (this.info.authRequired) {
|
|
158
|
-
this.warn("Power Genius requires authentication, which is not supported yet.");
|
|
159
|
-
}
|
|
160
|
-
});
|
|
161
|
-
// Anything the device sends is traced, to help finding properties the documentation leaves out.
|
|
162
|
-
protocol.onLine((line) => this.trace("rx " + line));
|
|
163
|
-
// Unsolicited status, such as "S|state=REBOOT".
|
|
164
|
-
protocol.onStatus((msg) => {
|
|
165
|
-
if (this._mergeStatus(msg)) {
|
|
166
|
-
this.updatesEventEmitter.emit("status", { ...this.status });
|
|
167
|
-
}
|
|
168
|
-
});
|
|
169
|
-
this.client.on("data", this._dataHandler);
|
|
170
|
-
|
|
171
|
-
await this._readInfo(protocol);
|
|
172
|
-
this._mergeStatus(await protocol.sendCommand("status", 3000));
|
|
173
|
-
|
|
174
|
-
if (this._protocol !== protocol || !this.connected) return;
|
|
175
|
-
|
|
176
|
-
this._startPolling();
|
|
177
|
-
|
|
178
|
-
this.updatesEventEmitter.emit("connected");
|
|
179
|
-
this.updatesEventEmitter.emit("status", { ...this.status });
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// Send a command to the amplifier. Resolves with the reply message.
|
|
183
|
-
command(cmd) {
|
|
184
|
-
if (!this._protocol) {
|
|
185
|
-
return Promise.reject(new Error("not connected"));
|
|
186
|
-
}
|
|
187
|
-
return this._protocol.sendCommand(cmd, 5000);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
connect() {
|
|
191
|
-
if (this.autoConnect && !this.connected && !this.client.connecting) {
|
|
192
|
-
this.client.connect(this.port, this.host);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// Ask the device for the current status and emit it.
|
|
197
|
-
async resendStatus() {
|
|
198
|
-
if (!this._protocol) return;
|
|
199
|
-
try {
|
|
200
|
-
this._mergeStatus(await this._protocol.sendCommand("status", 5000));
|
|
201
|
-
} catch (err) {
|
|
202
|
-
this.warn("Failed to read status: " + err.message);
|
|
203
|
-
return;
|
|
204
|
-
}
|
|
205
|
-
this.updatesEventEmitter.emit("status", { ...this.status });
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// Re-read the device info.
|
|
209
|
-
async refreshInfo() {
|
|
210
|
-
if (!this._protocol) return;
|
|
211
|
-
await this._readInfo(this._protocol);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
RED.nodes.registerType("power-genius-server", PowerGeniusServerNode);
|
|
216
|
-
};
|
|
1
|
+
const Net = require("net");
|
|
2
|
+
const EventEmitter = require("events");
|
|
3
|
+
const { GeniusPGProtocol, parseKeyValues, parseBanner } = require("./GeniusPGProtocol");
|
|
4
|
+
|
|
5
|
+
class UpdatesEventEmitter extends EventEmitter {}
|
|
6
|
+
|
|
7
|
+
// Read-only commands whose replies make up the device info. Replies are stored under the given key, or merged
|
|
8
|
+
// flat into the info object when the key is null. The documentation does not list the reply fields, so
|
|
9
|
+
// whatever the device sends is kept.
|
|
10
|
+
const INFO_COMMANDS = [
|
|
11
|
+
{ cmd: "setup read", key: null },
|
|
12
|
+
{ cmd: "ifconf read", key: "ifconf" },
|
|
13
|
+
{ cmd: "catradio read=A", key: "catradioA" },
|
|
14
|
+
{ cmd: "catradio read=B", key: "catradioB" },
|
|
15
|
+
{ cmd: "flexradio read=A", key: "flexradioA" },
|
|
16
|
+
{ cmd: "flexradio read=B", key: "flexradioB" },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
module.exports = (RED) => {
|
|
20
|
+
class PowerGeniusServerNode {
|
|
21
|
+
constructor(n) {
|
|
22
|
+
RED.nodes.createNode(this, n);
|
|
23
|
+
this.host = n.host;
|
|
24
|
+
this.port = n.port;
|
|
25
|
+
this.autoConnect = n.autoConnect || false;
|
|
26
|
+
this.pollInterval = Math.max(parseInt(n.pollInterval, 10) || 0, 0);
|
|
27
|
+
this.poller = null;
|
|
28
|
+
|
|
29
|
+
this.status = {};
|
|
30
|
+
this.info = {};
|
|
31
|
+
this.timer = null;
|
|
32
|
+
this._protocol = null;
|
|
33
|
+
this._dataHandler = null;
|
|
34
|
+
|
|
35
|
+
this.updatesEventEmitter = new UpdatesEventEmitter();
|
|
36
|
+
this.updatesEventEmitter.setMaxListeners(0);
|
|
37
|
+
|
|
38
|
+
this.client = new Net.Socket();
|
|
39
|
+
this.connected = false;
|
|
40
|
+
|
|
41
|
+
this.client.on("close", () => {
|
|
42
|
+
this.log("TCP connection disconnected with the server.");
|
|
43
|
+
clearTimeout(this.timer);
|
|
44
|
+
this.connected = false;
|
|
45
|
+
this._detach();
|
|
46
|
+
this._stopPolling();
|
|
47
|
+
|
|
48
|
+
if (this.updatesEventEmitter.listenerCount("closed") == 0) {
|
|
49
|
+
this.log("Stop retrying. No nodes are refering this connection anymore.");
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (this.autoConnect) {
|
|
54
|
+
this.log("Will try to reconnect in 5 seconds");
|
|
55
|
+
this.timer = setTimeout(() => this.connect(), 5000);
|
|
56
|
+
this.updatesEventEmitter.emit("closed");
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
this.client.on("connect", async () => {
|
|
61
|
+
this.log("TCP connection established with the server.");
|
|
62
|
+
this.connected = true;
|
|
63
|
+
try {
|
|
64
|
+
await this._setup();
|
|
65
|
+
} catch (err) {
|
|
66
|
+
this.warn("Connection setup failed: " + err.message);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
this.client.on("error", (err) => {
|
|
71
|
+
this.warn(err);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
this.on("close", (done) => {
|
|
75
|
+
clearTimeout(this.timer);
|
|
76
|
+
this.autoConnect = false;
|
|
77
|
+
this.connected = false;
|
|
78
|
+
this._detach();
|
|
79
|
+
this._stopPolling();
|
|
80
|
+
this.client.end();
|
|
81
|
+
done();
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
_detach() {
|
|
86
|
+
if (this._dataHandler) {
|
|
87
|
+
this.client.removeListener("data", this._dataHandler);
|
|
88
|
+
this._dataHandler = null;
|
|
89
|
+
}
|
|
90
|
+
this._protocol = null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
_stopPolling() {
|
|
94
|
+
clearInterval(this.poller);
|
|
95
|
+
this.poller = null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
_startPolling() {
|
|
99
|
+
this._stopPolling();
|
|
100
|
+
if (!this.pollInterval) return;
|
|
101
|
+
let busy = false;
|
|
102
|
+
this.poller = setInterval(async () => {
|
|
103
|
+
if (busy || !this._protocol) return;
|
|
104
|
+
busy = true;
|
|
105
|
+
try {
|
|
106
|
+
await this.resendStatus();
|
|
107
|
+
} finally {
|
|
108
|
+
busy = false;
|
|
109
|
+
}
|
|
110
|
+
}, this.pollInterval);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
_mergeStatus(msg) {
|
|
114
|
+
const kv = parseKeyValues(msg);
|
|
115
|
+
if (Object.keys(kv).length === 0) return false;
|
|
116
|
+
this.status = { ...this.status, ...kv };
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
_applyInfo(parts) {
|
|
121
|
+
const info = { ...this.info };
|
|
122
|
+
for (const { key, values } of parts) {
|
|
123
|
+
if (key === null) {
|
|
124
|
+
Object.assign(info, values);
|
|
125
|
+
} else {
|
|
126
|
+
info[key] = values;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const nickname = info.nickname !== undefined ? info.nickname : info.name;
|
|
130
|
+
info.name = String(nickname || this.info.name || "").replace(/_/g, " ");
|
|
131
|
+
this.info = info;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Reads the info commands one by one. A command the device rejects is skipped, so that one unsupported
|
|
135
|
+
// command does not hide the rest.
|
|
136
|
+
async _readInfo(protocol) {
|
|
137
|
+
const parts = [];
|
|
138
|
+
for (const { cmd, key } of INFO_COMMANDS) {
|
|
139
|
+
try {
|
|
140
|
+
const values = parseKeyValues(await protocol.sendCommand(cmd, 3000));
|
|
141
|
+
parts.push({ key, values });
|
|
142
|
+
} catch (err) {
|
|
143
|
+
this.debug(`'${cmd}' failed: ${err.message}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
this._applyInfo(parts);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async _setup() {
|
|
150
|
+
const protocol = new GeniusPGProtocol(this.client);
|
|
151
|
+
this._protocol = protocol;
|
|
152
|
+
this._dataHandler = (chunk) => protocol.handleData(chunk);
|
|
153
|
+
|
|
154
|
+
// The device sends "V<version>[ AUTH]" immediately after connecting.
|
|
155
|
+
protocol.onBanner((line) => {
|
|
156
|
+
this.info = { ...this.info, ...parseBanner(line) };
|
|
157
|
+
if (this.info.authRequired) {
|
|
158
|
+
this.warn("Power Genius requires authentication, which is not supported yet.");
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
// Anything the device sends is traced, to help finding properties the documentation leaves out.
|
|
162
|
+
protocol.onLine((line) => this.trace("rx " + line));
|
|
163
|
+
// Unsolicited status, such as "S|state=REBOOT".
|
|
164
|
+
protocol.onStatus((msg) => {
|
|
165
|
+
if (this._mergeStatus(msg)) {
|
|
166
|
+
this.updatesEventEmitter.emit("status", { ...this.status });
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
this.client.on("data", this._dataHandler);
|
|
170
|
+
|
|
171
|
+
await this._readInfo(protocol);
|
|
172
|
+
this._mergeStatus(await protocol.sendCommand("status", 3000));
|
|
173
|
+
|
|
174
|
+
if (this._protocol !== protocol || !this.connected) return;
|
|
175
|
+
|
|
176
|
+
this._startPolling();
|
|
177
|
+
|
|
178
|
+
this.updatesEventEmitter.emit("connected");
|
|
179
|
+
this.updatesEventEmitter.emit("status", { ...this.status });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Send a command to the amplifier. Resolves with the reply message.
|
|
183
|
+
command(cmd) {
|
|
184
|
+
if (!this._protocol) {
|
|
185
|
+
return Promise.reject(new Error("not connected"));
|
|
186
|
+
}
|
|
187
|
+
return this._protocol.sendCommand(cmd, 5000);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
connect() {
|
|
191
|
+
if (this.autoConnect && !this.connected && !this.client.connecting) {
|
|
192
|
+
this.client.connect(this.port, this.host);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Ask the device for the current status and emit it.
|
|
197
|
+
async resendStatus() {
|
|
198
|
+
if (!this._protocol) return;
|
|
199
|
+
try {
|
|
200
|
+
this._mergeStatus(await this._protocol.sendCommand("status", 5000));
|
|
201
|
+
} catch (err) {
|
|
202
|
+
this.warn("Failed to read status: " + err.message);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
this.updatesEventEmitter.emit("status", { ...this.status });
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Re-read the device info.
|
|
209
|
+
async refreshInfo() {
|
|
210
|
+
if (!this._protocol) return;
|
|
211
|
+
await this._readInfo(this._protocol);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
RED.nodes.registerType("power-genius-server", PowerGeniusServerNode);
|
|
216
|
+
};
|