node-red-contrib-yandex-station-management 0.3.10 → 0.3.12
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/nodes/local-out.html +8 -0
- package/nodes/station.html +1 -1
- package/nodes/yandex-login.js +16 -4
- package/package.json +20 -19
- package/test/discovery.test.js +162 -0
package/nodes/local-out.html
CHANGED
|
@@ -21,6 +21,14 @@
|
|
|
21
21
|
},
|
|
22
22
|
payload: {
|
|
23
23
|
value: 'payload',
|
|
24
|
+
validate: function(value, opt) {
|
|
25
|
+
var command = $('#node-input-input').val() || this.input;
|
|
26
|
+
// Only TTS uses this field; other commands always read msg.payload.
|
|
27
|
+
if (command !== 'tts') {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
return RED.validators.typedInput('payloadType').call(this, value, opt);
|
|
31
|
+
},
|
|
24
32
|
},
|
|
25
33
|
payloadType: {
|
|
26
34
|
value: 'msg',
|
package/nodes/station.html
CHANGED
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
} else if (this.network.mode == "auto") {
|
|
32
32
|
return true
|
|
33
33
|
} else if (this.network.mode == "manual"){
|
|
34
|
-
return
|
|
34
|
+
return /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test(address)
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
|
package/nodes/yandex-login.js
CHANGED
|
@@ -3,6 +3,20 @@ var mDnsSd = require('node-dns-sd');
|
|
|
3
3
|
var WebSocket = require("ws");
|
|
4
4
|
const { parse } = require('node-dns-sd/lib/dns-sd-parser');
|
|
5
5
|
|
|
6
|
+
// node-dns-sd owns one UDP socket. Share a pending search across all login
|
|
7
|
+
// nodes, including overlapping polls and retries, until it has fully settled.
|
|
8
|
+
let discoveryPromise = null;
|
|
9
|
+
function discoverStations() {
|
|
10
|
+
if (!discoveryPromise) {
|
|
11
|
+
discoveryPromise = Promise.resolve().then(() => mDnsSd.discover({
|
|
12
|
+
name: '_yandexio._tcp.local'
|
|
13
|
+
})).finally(() => {
|
|
14
|
+
discoveryPromise = null;
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return discoveryPromise;
|
|
18
|
+
}
|
|
19
|
+
|
|
6
20
|
module.exports = function(RED) {
|
|
7
21
|
|
|
8
22
|
function YandexLoginNode(config) {
|
|
@@ -129,9 +143,7 @@ module.exports = function(RED) {
|
|
|
129
143
|
});
|
|
130
144
|
}
|
|
131
145
|
async function discoverDevices(deviceList) {
|
|
132
|
-
await
|
|
133
|
-
name: '_yandexio._tcp.local'
|
|
134
|
-
}).then((result) => {
|
|
146
|
+
await discoverStations().then((result) => {
|
|
135
147
|
node.emit('refreshHttpDNS', result);
|
|
136
148
|
if (result.length != 0){
|
|
137
149
|
for (const device of deviceList) {
|
|
@@ -855,4 +867,4 @@ module.exports = function(RED) {
|
|
|
855
867
|
}
|
|
856
868
|
});
|
|
857
869
|
|
|
858
|
-
}
|
|
870
|
+
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-yandex-station-management",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.12",
|
|
4
4
|
"description": "Local management of YandexStation using API on websockets",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"test": "
|
|
7
|
+
"test": "node --test test/*.test.js"
|
|
8
8
|
},
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
@@ -13,22 +13,23 @@
|
|
|
13
13
|
"author": "n0name",
|
|
14
14
|
"license": "ISC",
|
|
15
15
|
"homepage": "https://github.com/n0name45/node-red-contrib-yandex-station-management",
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
},
|
|
26
|
-
"keywords": [ "node-red" ],
|
|
27
|
-
"dependencies": {
|
|
28
|
-
"request": "^2.88.2",
|
|
29
|
-
"request-promise": "^4.2.5",
|
|
30
|
-
"request-promise-native": "^1.0.8",
|
|
31
|
-
"node-dns-sd": "^0.4.2",
|
|
32
|
-
"ws": "^7.4.0"
|
|
16
|
+
"node-red": {
|
|
17
|
+
"nodes": {
|
|
18
|
+
"alice-local-in": "nodes/in.js",
|
|
19
|
+
"alice-local-get": "nodes/get.js",
|
|
20
|
+
"alice-local-out": "nodes/local-out.js",
|
|
21
|
+
"yandex-login": "nodes/yandex-login.js",
|
|
22
|
+
"alice-local-station": "nodes/station.js"
|
|
33
23
|
}
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"node-red"
|
|
27
|
+
],
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"request": "^2.88.2",
|
|
30
|
+
"request-promise": "^4.2.5",
|
|
31
|
+
"request-promise-native": "^1.0.8",
|
|
32
|
+
"node-dns-sd": "^1.0.1",
|
|
33
|
+
"ws": "^7.4.0"
|
|
34
|
+
}
|
|
34
35
|
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const assert = require('node:assert/strict');
|
|
3
|
+
const { EventEmitter } = require('node:events');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const test = require('node:test');
|
|
7
|
+
const vm = require('node:vm');
|
|
8
|
+
|
|
9
|
+
function deferred() {
|
|
10
|
+
let resolve, reject;
|
|
11
|
+
const promise = new Promise((yes, no) => { resolve = yes; reject = no; });
|
|
12
|
+
return { promise, resolve, reject };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const tick = () => new Promise(resolve => setImmediate(resolve));
|
|
16
|
+
|
|
17
|
+
// Run the real node registration and polling code without Node-RED, cloud
|
|
18
|
+
// requests, UDP sockets, or real timers. Each fixture has a fresh module scope.
|
|
19
|
+
function fixture() {
|
|
20
|
+
let constructor;
|
|
21
|
+
let discover = () => Promise.resolve([]);
|
|
22
|
+
const calls = [];
|
|
23
|
+
const polls = [];
|
|
24
|
+
const logs = [];
|
|
25
|
+
const module = { exports: {} };
|
|
26
|
+
const source = process.env.YANDEX_SOURCE || path.resolve(__dirname, '../nodes/yandex-login.js');
|
|
27
|
+
const mocks = {
|
|
28
|
+
'request-promise': () => Promise.resolve(JSON.stringify({ devices: [] })),
|
|
29
|
+
'node-dns-sd': { discover: options => { calls.push(options); return discover(); } },
|
|
30
|
+
'node-dns-sd/lib/dns-sd-parser': {},
|
|
31
|
+
ws: class WebSocket {}
|
|
32
|
+
};
|
|
33
|
+
vm.runInNewContext(fs.readFileSync(source, 'utf8'), {
|
|
34
|
+
module,
|
|
35
|
+
require: name => {
|
|
36
|
+
assert.ok(Object.hasOwn(mocks, name), 'unexpected dependency: ' + name);
|
|
37
|
+
return mocks[name];
|
|
38
|
+
},
|
|
39
|
+
setInterval: (fn, delay, ...args) => {
|
|
40
|
+
const poll = () => fn(...args);
|
|
41
|
+
polls.push(poll);
|
|
42
|
+
return poll;
|
|
43
|
+
},
|
|
44
|
+
clearInterval: () => {},
|
|
45
|
+
setTimeout: () => { throw new Error('Unexpected device connection'); },
|
|
46
|
+
clearTimeout: () => {},
|
|
47
|
+
console
|
|
48
|
+
}, { filename: source });
|
|
49
|
+
module.exports({
|
|
50
|
+
nodes: {
|
|
51
|
+
registerType: (name, node) => { constructor = node; },
|
|
52
|
+
createNode: (node, config) => {
|
|
53
|
+
const events = new EventEmitter();
|
|
54
|
+
for (const method of ['on', 'emit', 'removeListener', 'setMaxListeners']) {
|
|
55
|
+
node[method] = events[method].bind(events);
|
|
56
|
+
}
|
|
57
|
+
node.id = config.id;
|
|
58
|
+
node.credentials = { token: 'test-only-token' };
|
|
59
|
+
node.log = message => logs.push(message);
|
|
60
|
+
node.error = message => logs.push(message);
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
httpAdmin: { get: () => {} },
|
|
64
|
+
auth: { needsPermission: () => () => {} }
|
|
65
|
+
});
|
|
66
|
+
return {
|
|
67
|
+
calls, polls, logs,
|
|
68
|
+
create: id => new constructor({ id, debugFlag: true }),
|
|
69
|
+
discover: fn => { discover = fn; }
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
test('two login nodes share one discovery and both receive its result', async () => {
|
|
74
|
+
const f = fixture();
|
|
75
|
+
const pending = deferred();
|
|
76
|
+
f.discover(() => pending.promise);
|
|
77
|
+
const received = [];
|
|
78
|
+
f.create('first').on('refreshHttpDNS', result => received.push(result));
|
|
79
|
+
f.create('second').on('refreshHttpDNS', result => received.push(result));
|
|
80
|
+
await tick();
|
|
81
|
+
const count = f.calls.length;
|
|
82
|
+
const result = [];
|
|
83
|
+
pending.resolve(result);
|
|
84
|
+
await tick();
|
|
85
|
+
assert.equal(count, 1);
|
|
86
|
+
assert.equal(f.calls[0].name, '_yandexio._tcp.local');
|
|
87
|
+
assert.equal(received.length, 2);
|
|
88
|
+
assert.ok(received.every(value => value === result));
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('repeated polling during a slow search shares the pending operation', async () => {
|
|
92
|
+
const f = fixture();
|
|
93
|
+
const pending = deferred();
|
|
94
|
+
f.discover(() => pending.promise);
|
|
95
|
+
f.create('first');
|
|
96
|
+
await tick();
|
|
97
|
+
f.polls[0]();
|
|
98
|
+
f.polls[0]();
|
|
99
|
+
await tick();
|
|
100
|
+
const count = f.calls.length;
|
|
101
|
+
pending.resolve([]);
|
|
102
|
+
await tick();
|
|
103
|
+
assert.equal(count, 1);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('a later poll starts a fresh discovery after success', async () => {
|
|
107
|
+
const f = fixture();
|
|
108
|
+
f.create('first');
|
|
109
|
+
await tick();
|
|
110
|
+
assert.equal(f.calls.length, 1);
|
|
111
|
+
f.polls[0]();
|
|
112
|
+
await tick();
|
|
113
|
+
assert.equal(f.calls.length, 2);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('shared discovery rejection reaches both callers and allows retry', async () => {
|
|
117
|
+
const f = fixture();
|
|
118
|
+
const pending = deferred();
|
|
119
|
+
f.discover(() => pending.promise);
|
|
120
|
+
f.create('first');
|
|
121
|
+
f.create('second');
|
|
122
|
+
await tick();
|
|
123
|
+
const count = f.calls.length;
|
|
124
|
+
const error = new Error('socket bind failed');
|
|
125
|
+
pending.reject(error);
|
|
126
|
+
await tick();
|
|
127
|
+
assert.equal(count, 1);
|
|
128
|
+
assert.equal(f.logs.filter(value => value === error).length, 2);
|
|
129
|
+
f.discover(() => Promise.resolve([]));
|
|
130
|
+
f.polls[0]();
|
|
131
|
+
await tick();
|
|
132
|
+
assert.equal(f.calls.length, 2);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('synchronous discovery errors are handled and do not prevent retry', async () => {
|
|
136
|
+
const f = fixture();
|
|
137
|
+
const error = new Error('socket setup failed');
|
|
138
|
+
f.discover(() => { throw error; });
|
|
139
|
+
f.create('first');
|
|
140
|
+
await tick();
|
|
141
|
+
assert.ok(f.logs.includes(error));
|
|
142
|
+
f.discover(() => Promise.resolve([]));
|
|
143
|
+
f.polls[0]();
|
|
144
|
+
await tick();
|
|
145
|
+
assert.equal(f.calls.length, 2);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('a login node created during discovery joins the existing search', async () => {
|
|
149
|
+
const f = fixture();
|
|
150
|
+
const pending = deferred();
|
|
151
|
+
f.discover(() => pending.promise);
|
|
152
|
+
f.create('first');
|
|
153
|
+
await tick();
|
|
154
|
+
const received = [];
|
|
155
|
+
f.create('second').on('refreshHttpDNS', value => received.push(value));
|
|
156
|
+
await tick();
|
|
157
|
+
const count = f.calls.length;
|
|
158
|
+
pending.resolve([]);
|
|
159
|
+
await tick();
|
|
160
|
+
assert.equal(count, 1);
|
|
161
|
+
assert.equal(received.length, 1);
|
|
162
|
+
});
|