node-red-contrib-golc-alice 0.1.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/README.md +36 -0
- package/examples/example-flow.json +82 -0
- package/nodes/alice-device.html +82 -0
- package/nodes/alice-device.js +179 -0
- package/package.json +28 -0
package/README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# node-red-contrib-golc-alice
|
|
2
|
+
|
|
3
|
+
DIY free Node-RED nodes for your personal Yandex Alice integration.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
Inside your Node-RED user directory (usually `~/.node-red`):
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install /opt/GOLC-HOME-lab-alise/nodered/node-red-contrib-golc-alice
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Then restart Node-RED.
|
|
14
|
+
|
|
15
|
+
## Node: `alice-device`
|
|
16
|
+
|
|
17
|
+
Modes:
|
|
18
|
+
|
|
19
|
+
- `register` — register/update virtual device in backend registry.
|
|
20
|
+
- `state` — update state for an existing device.
|
|
21
|
+
|
|
22
|
+
Required backend env:
|
|
23
|
+
|
|
24
|
+
- `INTERNAL_TOKEN` (must match node config `Internal Token`)
|
|
25
|
+
|
|
26
|
+
Backend endpoints used:
|
|
27
|
+
|
|
28
|
+
- `POST /internal/registry/devices`
|
|
29
|
+
- `POST /internal/devices/:id/state`
|
|
30
|
+
|
|
31
|
+
## Message overrides
|
|
32
|
+
|
|
33
|
+
- `msg.mode` — override mode (`register` or `state`)
|
|
34
|
+
- `msg.device` — full device object for register mode
|
|
35
|
+
- `msg.deviceId` — device id for state mode
|
|
36
|
+
- `msg.state` — state object for state mode
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"id": "inject_reg",
|
|
4
|
+
"type": "inject",
|
|
5
|
+
"z": "flow1",
|
|
6
|
+
"name": "Register virtual lamp+temp",
|
|
7
|
+
"props": [],
|
|
8
|
+
"repeat": "",
|
|
9
|
+
"crontab": "",
|
|
10
|
+
"once": false,
|
|
11
|
+
"onceDelay": 0.1,
|
|
12
|
+
"topic": "",
|
|
13
|
+
"x": 200,
|
|
14
|
+
"y": 120,
|
|
15
|
+
"wires": [["alice_register"]]
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"id": "alice_register",
|
|
19
|
+
"type": "alice-device",
|
|
20
|
+
"z": "flow1",
|
|
21
|
+
"name": "Register device",
|
|
22
|
+
"backendUrl": "http://localhost:3000",
|
|
23
|
+
"internalToken": "local-internal-token",
|
|
24
|
+
"mode": "register",
|
|
25
|
+
"deviceId": "virtual_lamp_1",
|
|
26
|
+
"deviceName": "Виртуальная лампа",
|
|
27
|
+
"deviceType": "devices.types.light",
|
|
28
|
+
"withOnOff": true,
|
|
29
|
+
"withTemperature": true,
|
|
30
|
+
"withHumidity": false,
|
|
31
|
+
"withOpenSensor": false,
|
|
32
|
+
"x": 460,
|
|
33
|
+
"y": 120,
|
|
34
|
+
"wires": [["debug_out"]]
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"id": "inject_state",
|
|
38
|
+
"type": "inject",
|
|
39
|
+
"z": "flow1",
|
|
40
|
+
"name": "State update temp=27",
|
|
41
|
+
"props": [
|
|
42
|
+
{ "p": "state", "v": "{\"on\":true,\"temperature\":27}", "vt": "json" }
|
|
43
|
+
],
|
|
44
|
+
"x": 190,
|
|
45
|
+
"y": 180,
|
|
46
|
+
"wires": [["alice_state"]]
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"id": "alice_state",
|
|
50
|
+
"type": "alice-device",
|
|
51
|
+
"z": "flow1",
|
|
52
|
+
"name": "State update",
|
|
53
|
+
"backendUrl": "http://localhost:3000",
|
|
54
|
+
"internalToken": "local-internal-token",
|
|
55
|
+
"mode": "state",
|
|
56
|
+
"deviceId": "virtual_lamp_1",
|
|
57
|
+
"deviceName": "",
|
|
58
|
+
"deviceType": "devices.types.light",
|
|
59
|
+
"withOnOff": true,
|
|
60
|
+
"withTemperature": false,
|
|
61
|
+
"withHumidity": false,
|
|
62
|
+
"withOpenSensor": false,
|
|
63
|
+
"x": 440,
|
|
64
|
+
"y": 180,
|
|
65
|
+
"wires": [["debug_out"]]
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"id": "debug_out",
|
|
69
|
+
"type": "debug",
|
|
70
|
+
"z": "flow1",
|
|
71
|
+
"name": "result",
|
|
72
|
+
"active": true,
|
|
73
|
+
"tosidebar": true,
|
|
74
|
+
"console": false,
|
|
75
|
+
"tostatus": false,
|
|
76
|
+
"complete": "payload",
|
|
77
|
+
"targetType": "msg",
|
|
78
|
+
"x": 680,
|
|
79
|
+
"y": 150,
|
|
80
|
+
"wires": []
|
|
81
|
+
}
|
|
82
|
+
]
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
RED.nodes.registerType('alice-device', {
|
|
3
|
+
category: 'function',
|
|
4
|
+
color: '#6ab7ff',
|
|
5
|
+
defaults: {
|
|
6
|
+
name: { value: '' },
|
|
7
|
+
backendUrl: { value: 'http://localhost:3000', required: true },
|
|
8
|
+
internalToken: { value: 'local-internal-token', required: true },
|
|
9
|
+
mode: { value: 'register', required: true },
|
|
10
|
+
deviceId: { value: '' },
|
|
11
|
+
deviceName: { value: '' },
|
|
12
|
+
deviceType: { value: 'devices.types.light' },
|
|
13
|
+
withOnOff: { value: true },
|
|
14
|
+
withTemperature: { value: false },
|
|
15
|
+
withHumidity: { value: false },
|
|
16
|
+
withOpenSensor: { value: false }
|
|
17
|
+
},
|
|
18
|
+
inputs: 1,
|
|
19
|
+
outputs: 1,
|
|
20
|
+
icon: 'bridge.svg',
|
|
21
|
+
label: function () {
|
|
22
|
+
return this.name || 'alice-device';
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<script type="text/html" data-template-name="alice-device">
|
|
28
|
+
<div class="form-row">
|
|
29
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
30
|
+
<input type="text" id="node-input-name" placeholder="alice-device" />
|
|
31
|
+
</div>
|
|
32
|
+
<div class="form-row">
|
|
33
|
+
<label for="node-input-backendUrl">Backend URL</label>
|
|
34
|
+
<input type="text" id="node-input-backendUrl" placeholder="http://localhost:3000" />
|
|
35
|
+
</div>
|
|
36
|
+
<div class="form-row">
|
|
37
|
+
<label for="node-input-internalToken">Internal Token</label>
|
|
38
|
+
<input type="text" id="node-input-internalToken" placeholder="local-internal-token" />
|
|
39
|
+
</div>
|
|
40
|
+
<div class="form-row">
|
|
41
|
+
<label for="node-input-mode">Mode</label>
|
|
42
|
+
<select id="node-input-mode">
|
|
43
|
+
<option value="register">register device</option>
|
|
44
|
+
<option value="state">update state</option>
|
|
45
|
+
</select>
|
|
46
|
+
</div>
|
|
47
|
+
<hr />
|
|
48
|
+
<div class="form-row">
|
|
49
|
+
<label for="node-input-deviceId">Device ID</label>
|
|
50
|
+
<input type="text" id="node-input-deviceId" placeholder="lamp_virtual_1" />
|
|
51
|
+
</div>
|
|
52
|
+
<div class="form-row">
|
|
53
|
+
<label for="node-input-deviceName">Device Name</label>
|
|
54
|
+
<input type="text" id="node-input-deviceName" placeholder="Лампа кухня" />
|
|
55
|
+
</div>
|
|
56
|
+
<div class="form-row">
|
|
57
|
+
<label for="node-input-deviceType">Device Type</label>
|
|
58
|
+
<input type="text" id="node-input-deviceType" placeholder="devices.types.light" />
|
|
59
|
+
</div>
|
|
60
|
+
<div class="form-row">
|
|
61
|
+
<label for="node-input-withOnOff">on/off</label>
|
|
62
|
+
<input type="checkbox" id="node-input-withOnOff" style="width:auto;" />
|
|
63
|
+
</div>
|
|
64
|
+
<div class="form-row">
|
|
65
|
+
<label for="node-input-withTemperature">temperature</label>
|
|
66
|
+
<input type="checkbox" id="node-input-withTemperature" style="width:auto;" />
|
|
67
|
+
</div>
|
|
68
|
+
<div class="form-row">
|
|
69
|
+
<label for="node-input-withHumidity">humidity</label>
|
|
70
|
+
<input type="checkbox" id="node-input-withHumidity" style="width:auto;" />
|
|
71
|
+
</div>
|
|
72
|
+
<div class="form-row">
|
|
73
|
+
<label for="node-input-withOpenSensor">open sensor</label>
|
|
74
|
+
<input type="checkbox" id="node-input-withOpenSensor" style="width:auto;" />
|
|
75
|
+
</div>
|
|
76
|
+
</script>
|
|
77
|
+
|
|
78
|
+
<script type="text/html" data-help-name="alice-device">
|
|
79
|
+
<p>DIY node for GOLC Alice backend.</p>
|
|
80
|
+
<p><b>register</b>: creates/updates virtual device in backend.</p>
|
|
81
|
+
<p><b>state</b>: updates device state from <code>msg.state</code> or <code>msg.payload</code>.</p>
|
|
82
|
+
</script>
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
const http = require('http');
|
|
2
|
+
const https = require('https');
|
|
3
|
+
|
|
4
|
+
function requestJson(method, targetUrl, headers, body) {
|
|
5
|
+
return new Promise((resolve, reject) => {
|
|
6
|
+
const parsed = new URL(targetUrl);
|
|
7
|
+
const lib = parsed.protocol === 'https:' ? https : http;
|
|
8
|
+
|
|
9
|
+
const payload = body ? JSON.stringify(body) : null;
|
|
10
|
+
|
|
11
|
+
const req = lib.request(
|
|
12
|
+
{
|
|
13
|
+
method,
|
|
14
|
+
hostname: parsed.hostname,
|
|
15
|
+
port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
|
|
16
|
+
path: `${parsed.pathname}${parsed.search}`,
|
|
17
|
+
headers: {
|
|
18
|
+
Accept: 'application/json',
|
|
19
|
+
...(payload ? { 'Content-Type': 'application/json' } : {}),
|
|
20
|
+
...(payload ? { 'Content-Length': Buffer.byteLength(payload) } : {}),
|
|
21
|
+
...(headers || {}),
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
(res) => {
|
|
25
|
+
let raw = '';
|
|
26
|
+
res.setEncoding('utf8');
|
|
27
|
+
res.on('data', (chunk) => {
|
|
28
|
+
raw += chunk;
|
|
29
|
+
});
|
|
30
|
+
res.on('end', () => {
|
|
31
|
+
try {
|
|
32
|
+
const json = raw ? JSON.parse(raw) : {};
|
|
33
|
+
resolve({ statusCode: res.statusCode, body: json });
|
|
34
|
+
} catch (error) {
|
|
35
|
+
reject(error);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
req.on('error', reject);
|
|
42
|
+
if (payload) req.write(payload);
|
|
43
|
+
req.end();
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
module.exports = function (RED) {
|
|
48
|
+
function AliceDeviceNode(config) {
|
|
49
|
+
RED.nodes.createNode(this, config);
|
|
50
|
+
const node = this;
|
|
51
|
+
|
|
52
|
+
node.backendUrl = (config.backendUrl || 'http://localhost:3000').replace(/\/$/, '');
|
|
53
|
+
node.internalToken = config.internalToken || 'local-internal-token';
|
|
54
|
+
node.mode = config.mode || 'register';
|
|
55
|
+
|
|
56
|
+
node.deviceId = config.deviceId || '';
|
|
57
|
+
node.deviceName = config.deviceName || '';
|
|
58
|
+
node.deviceType = config.deviceType || 'devices.types.light';
|
|
59
|
+
|
|
60
|
+
node.withOnOff = !!config.withOnOff;
|
|
61
|
+
node.withTemperature = !!config.withTemperature;
|
|
62
|
+
node.withHumidity = !!config.withHumidity;
|
|
63
|
+
node.withOpenSensor = !!config.withOpenSensor;
|
|
64
|
+
|
|
65
|
+
async function handleRegister(msg) {
|
|
66
|
+
const baseDevice = {
|
|
67
|
+
id: node.deviceId,
|
|
68
|
+
name: node.deviceName || node.deviceId,
|
|
69
|
+
type: node.deviceType,
|
|
70
|
+
status_info: { reportable: true },
|
|
71
|
+
capabilities: [],
|
|
72
|
+
properties: [],
|
|
73
|
+
state: {},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
if (node.withOnOff) {
|
|
77
|
+
baseDevice.capabilities.push({ type: 'devices.capabilities.on_off', retrievable: true });
|
|
78
|
+
baseDevice.state.on = false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (node.withTemperature) {
|
|
82
|
+
baseDevice.properties.push({
|
|
83
|
+
type: 'devices.properties.float',
|
|
84
|
+
retrievable: true,
|
|
85
|
+
reportable: true,
|
|
86
|
+
parameters: { instance: 'temperature', unit: 'unit.temperature.celsius' },
|
|
87
|
+
});
|
|
88
|
+
baseDevice.state.temperature = 22;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (node.withHumidity) {
|
|
92
|
+
baseDevice.properties.push({
|
|
93
|
+
type: 'devices.properties.float',
|
|
94
|
+
retrievable: true,
|
|
95
|
+
reportable: true,
|
|
96
|
+
parameters: { instance: 'humidity', unit: 'unit.percent' },
|
|
97
|
+
});
|
|
98
|
+
baseDevice.state.humidity = 50;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (node.withOpenSensor) {
|
|
102
|
+
baseDevice.properties.push({
|
|
103
|
+
type: 'devices.properties.event',
|
|
104
|
+
retrievable: true,
|
|
105
|
+
reportable: true,
|
|
106
|
+
parameters: { instance: 'open', events: [{ value: 'opened' }, { value: 'closed' }] },
|
|
107
|
+
});
|
|
108
|
+
baseDevice.state.open = 'closed';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const deviceFromMsg = msg.device && typeof msg.device === 'object' ? msg.device : {};
|
|
112
|
+
const merged = {
|
|
113
|
+
...baseDevice,
|
|
114
|
+
...deviceFromMsg,
|
|
115
|
+
state: {
|
|
116
|
+
...baseDevice.state,
|
|
117
|
+
...(deviceFromMsg.state || {}),
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const url = `${node.backendUrl}/internal/registry/devices`;
|
|
122
|
+
const response = await requestJson(
|
|
123
|
+
'POST',
|
|
124
|
+
url,
|
|
125
|
+
{ 'X-Internal-Token': node.internalToken },
|
|
126
|
+
{ devices: [merged] }
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
msg.payload = response.body;
|
|
130
|
+
msg.statusCode = response.statusCode;
|
|
131
|
+
return msg;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function handleStateUpdate(msg) {
|
|
135
|
+
const id = (msg.deviceId || node.deviceId || '').trim();
|
|
136
|
+
if (!id) {
|
|
137
|
+
throw new Error('deviceId is required for state mode');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const state = (msg.state && typeof msg.state === 'object') ? msg.state : (msg.payload && typeof msg.payload === 'object' ? msg.payload : null);
|
|
141
|
+
if (!state) {
|
|
142
|
+
throw new Error('msg.state or msg.payload object is required for state mode');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const url = `${node.backendUrl}/internal/devices/${encodeURIComponent(id)}/state`;
|
|
146
|
+
const response = await requestJson(
|
|
147
|
+
'POST',
|
|
148
|
+
url,
|
|
149
|
+
{ 'X-Internal-Token': node.internalToken },
|
|
150
|
+
{ state }
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
msg.payload = response.body;
|
|
154
|
+
msg.statusCode = response.statusCode;
|
|
155
|
+
return msg;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
node.on('input', async (msg, send, done) => {
|
|
159
|
+
const sender = send || node.send.bind(node);
|
|
160
|
+
try {
|
|
161
|
+
const mode = (msg.mode || node.mode || 'register').toLowerCase();
|
|
162
|
+
node.status({ fill: 'blue', shape: 'dot', text: `alice ${mode}...` });
|
|
163
|
+
|
|
164
|
+
const outMsg = mode === 'state'
|
|
165
|
+
? await handleStateUpdate(msg)
|
|
166
|
+
: await handleRegister(msg);
|
|
167
|
+
|
|
168
|
+
node.status({ fill: 'green', shape: 'dot', text: `ok ${mode}` });
|
|
169
|
+
sender(outMsg);
|
|
170
|
+
done();
|
|
171
|
+
} catch (error) {
|
|
172
|
+
node.status({ fill: 'red', shape: 'ring', text: 'error' });
|
|
173
|
+
done(error);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
RED.nodes.registerType('alice-device', AliceDeviceNode);
|
|
179
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "node-red-contrib-golc-alice",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "DIY Node-RED nodes for GOLC Alice smart home backend",
|
|
5
|
+
"main": "nodes/alice-device.js",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"node-red",
|
|
8
|
+
"alice",
|
|
9
|
+
"yandex",
|
|
10
|
+
"smart-home",
|
|
11
|
+
"golc"
|
|
12
|
+
],
|
|
13
|
+
"author": "GOLC-HOME",
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/detdomovski-prog/GOLC-HOME-lab-alise.git"
|
|
18
|
+
},
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/detdomovski-prog/GOLC-HOME-lab-alise/issues"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://github.com/detdomovski-prog/GOLC-HOME-lab-alise/tree/main/nodered/node-red-contrib-golc-alice",
|
|
23
|
+
"node-red": {
|
|
24
|
+
"nodes": {
|
|
25
|
+
"alice-device": "nodes/alice-device.js"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|