node-red-contrib-alice 2.2.5 → 2.3.1

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.

Potentially problematic release.


This version of node-red-contrib-alice might be problematic. Click here for more details.

@@ -0,0 +1,11 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(git remote -v && git config --list --local 2>/dev/null)",
5
+ "Bash(git config:*)",
6
+ "Bash(ssh -T git@github.com)",
7
+ "Bash(git remote:*)",
8
+ "Bash(git push:*)"
9
+ ]
10
+ }
11
+ }
package/CLAUDE.md ADDED
@@ -0,0 +1,54 @@
1
+ # CLAUDE.md
2
+
3
+ Этот файл содержит руководство для Claude Code (claude.ai/code) при работе с кодом в данном репозитории.
4
+
5
+ ## Обзор проекта
6
+
7
+ Плагин для Node-RED (`node-red-contrib-alice`), интегрирующий IoT-устройства, управляемые через Node-RED, с голосовым помощником Яндекс Алиса для управления умным домом. Опубликован в npm как `node-red-contrib-alice`.
8
+
9
+ ## Команды сборки и разработки
10
+
11
+ ```bash
12
+ npm run build # Компиляция TypeScript (tsc) + копирование HTML из src/ в nodes/
13
+ npm start # Сборка и запуск Node-RED
14
+ npm run copy-html # Копирование src/*.html в nodes/
15
+ ```
16
+
17
+ Все исходники (`.ts` + `.html`) находятся в `src/`. Папка `nodes/` — только скомпилированный вывод (JS + скопированные HTML). При `npm run build` TypeScript компилируется в JS, а HTML копируется в `nodes/`.
18
+
19
+ Общие типы и интерфейсы вынесены в `src/types.ts`.
20
+
21
+ ## Архитектура
22
+
23
+ ### Паттерн нод Node-RED
24
+
25
+ Каждый тип ноды состоит из пары файлов: `.ts` (логика) и `.html` (UI конфигурации):
26
+
27
+ - **Сервисная нода** (`alice.ts`): Центральный узел — управляет OAuth Яндекса, MQTT-подключением к `mqtts://mqtt.cloud.yandex.net:8883`, HTTP-эндпоинтами администрирования, валидацией подписки. Выступает как источник событий для всех нод устройств.
28
+ - **Нода устройства** (`alice-device.ts`): Группирует умения/сенсоры, управляет регистрацией устройства на шлюзе (`api.nodered-home.ru`), дебаунсит обновления состояния (1000мс).
29
+ - **Ноды умений** (`alice-onoff`, `alice-range`, `alice-color`, `alice-mode`, `alice-togle`, `alice-video`): Двунаправленные — принимают голосовые команды от Алисы и отправляют обновления состояния на шлюз.
30
+ - **Нода сенсора** (`alice-sensor`): Передача свойств только для чтения (температура, влажность и т.д.).
31
+ - **Нода событий** (`alice-event`): Обнаруживает изменения состояния и запускает потоки.
32
+
33
+ ### Коммуникация
34
+
35
+ - **MQTT**: Команды устройствам в реальном времени через Yandex IoT Cloud
36
+ - **HTTP REST**: Конфигурация устройств и синхронизация состояния со шлюзом `api.nodered-home.ru`
37
+ - **OAuth 2.0**: Аутентификация через Яндекс, обмен токенами через `nodered-home.ru/api/v1/getyatoken`
38
+
39
+ ### Структура HTML UI
40
+
41
+ Каждый `.html` файл содержит два блока скриптов:
42
+ 1. `<script>` — клиентская логика (регистрация в редакторе Node-RED, REST-вызовы)
43
+ 2. `<script type="text/x-red" data-template-name="...">` — шаблон формы
44
+
45
+ ### Основные зависимости
46
+
47
+ - `mqtt` — MQTT-клиент для Yandex IoT Cloud
48
+ - `axios` — HTTP-клиент для вызовов API шлюза
49
+
50
+ ## Заметки
51
+
52
+ - Опечатка `alice-togle` (вместо `toggle`) — намеренная, это зарегистрированное имя типа ноды
53
+ - В проекте отсутствуют тесты
54
+ - Комментарии в коде и строки UI написаны преимущественно на русском языке
@@ -1,235 +1,212 @@
1
- module.exports = function(RED) {
2
-
3
- // ************** Color *******************
4
- function AliceColor(config){
5
- RED.nodes.createNode(this,config);
6
- this.device = RED.nodes.getNode(config.device);
7
- this.device.setMaxListeners(this.device.getMaxListeners() + 1); // увеличиваем лимит для event
8
- this.name = config.name;
9
- this.ctype = 'devices.capabilities.color_setting';
10
- this.instance = 'color_model';
11
- this.color_support = config.color_support;
12
- this.scheme = config.scheme;
13
- this.temperature_k = config.temperature_k;
14
- this.temperature_min = parseInt(config.temperature_min);
15
- this.temperature_max = parseInt(config.temperature_max);
16
- this.color_scene = config.color_scene || [];
17
- this.needConvert = false;
18
- this.response = config.response;
19
- this.initState = false;
20
- this.value;
21
-
22
- if (this.scheme == "rgb_normal"){
23
- this.scheme = "rgb";
24
- this.needConvert = true;
25
- };
26
- if (config.response === undefined){
27
- this.response = true;
28
- };
29
- if (config.color_support === undefined){
30
- this.color_support = true
31
- };
32
-
33
- this.init = ()=>{
34
- var value = 0;
35
- if (this.scheme=="hsv"){
36
- value = {
37
- h:0,
38
- s:0,
39
- v:0
40
- };
41
- };
42
- let capab = {
43
- type: this.ctype,
44
- retrievable: true,
45
- reportable: true,
46
- parameters: {
47
- // instance: this.scheme,//this.instance,
48
- // color_model: this.scheme
1
+ "use strict";
2
+ module.exports = (RED) => {
3
+ function AliceColor(config) {
4
+ RED.nodes.createNode(this, config);
5
+ const device = RED.nodes.getNode(config.device);
6
+ device.setMaxListeners(device.getMaxListeners() + 1);
7
+ const ctype = 'devices.capabilities.color_setting';
8
+ let scheme = config.scheme;
9
+ const temperature_k = config.temperature_k;
10
+ const temperature_min = parseInt(config.temperature_min);
11
+ const temperature_max = parseInt(config.temperature_max);
12
+ const color_scene = config.color_scene || [];
13
+ let needConvert = false;
14
+ let response = config.response;
15
+ let color_support = config.color_support;
16
+ let lastValue;
17
+ if (scheme == "rgb_normal") {
18
+ scheme = "rgb";
19
+ needConvert = true;
49
20
  }
50
- };
51
- if (!this.color_support && !this.temperature_k && this.color_scene.length<1){
52
- this.error("Error on create capability: " + "At least one parameter must be enabled");
53
- this.status({fill:"red",shape:"dot",text:"error"});
54
- return;
55
- };
56
- if (this.color_scene.length>0){
57
- let scenes = [];
58
- this.color_scene.forEach(s=>{
59
- scenes.push({id:s});
60
- });
61
- capab.parameters.color_scene = {
62
- scenes:scenes
63
- };
64
- // capab.state.instance = 'scene';
65
- // capab.state.value = this.color_scene[0];
66
- };
67
- if (this.color_support){
68
- capab.parameters.color_model = this.scheme;
69
- // capab.state.instance = this.scheme;
70
- // if (this.scheme=="hsv"){
71
- // capab.state.value = {h:0,s:0,v:0};
72
- // }else{
73
- // capab.state.value = 0;
74
- // }
75
- };
76
- if (this.temperature_k){
77
- capab.parameters.temperature_k = {
78
- min: this.temperature_min,
79
- max: this.temperature_max
21
+ if (config.response === undefined) {
22
+ response = true;
23
+ }
24
+ if (config.color_support === undefined) {
25
+ color_support = true;
26
+ }
27
+ const init = () => {
28
+ const parameters = {};
29
+ const capab = {
30
+ type: ctype,
31
+ retrievable: true,
32
+ reportable: true,
33
+ parameters: parameters
34
+ };
35
+ if (!color_support && !temperature_k && color_scene.length < 1) {
36
+ this.error("Error on create capability: At least one parameter must be enabled");
37
+ this.status({ fill: "red", shape: "dot", text: "error" });
38
+ return;
39
+ }
40
+ if (color_scene.length > 0) {
41
+ const scenes = color_scene.map(s => ({ id: s }));
42
+ parameters.color_scene = { scenes: scenes };
43
+ }
44
+ if (color_support) {
45
+ parameters.color_model = scheme;
46
+ }
47
+ if (temperature_k) {
48
+ parameters.temperature_k = {
49
+ min: temperature_min,
50
+ max: temperature_max
51
+ };
52
+ }
53
+ device.setCapability(this.id, capab)
54
+ .then(() => {
55
+ this.status({ fill: "green", shape: "dot", text: "online" });
56
+ })
57
+ .catch(err => {
58
+ this.error("Error on create capability: " + err.message);
59
+ this.status({ fill: "red", shape: "dot", text: "error" });
60
+ });
80
61
  };
81
- // capab.state.instance = 'temperature_k';
82
- // capab.state.value = this.temperature_min;
83
- };
84
-
85
- this.device.setCapability(this.id,capab)
86
- .then(res=>{
87
- this.initState = true;
88
- // this.value = JSON.stringify(capab.state.value);
89
- this.status({fill:"green",shape:"dot",text:"online"});
90
- })
91
- .catch(err=>{
92
- this.error("Error on create capability: " + err.message);
93
- this.status({fill:"red",shape:"dot",text:"error"});
62
+ if (device.initState)
63
+ init();
64
+ device.on("online", () => {
65
+ init();
94
66
  });
95
- };
96
-
97
- // Проверяем сам девайс уже инициирован
98
- if (this.device.initState) this.init();
99
-
100
- this.device.on("online",()=>{
101
- this.init();
102
- });
103
-
104
- this.device.on("offline",()=>{
105
- this.status({fill:"red",shape:"dot",text:"offline"});
106
- });
107
-
108
- this.device.on(this.id,(val,newstate)=>{
109
- // отправляем данные на выход
110
- let outmsgs=[null,null,null];
111
- switch (newstate.instance) {
112
- case 'rgb':
113
- let value = val;
114
- value = {
115
- r: val >> 16,
116
- g: val >> 8 & 0xFF,
117
- b: val & 0xFF
118
- };
119
- outmsgs[0]={ payload: value };
120
- break;
121
- case 'hsv':
122
- outmsgs[0]={ payload: val };
123
- break;
124
- case 'temperature_k':
125
- outmsgs[1]={ payload: val };
126
- break;
127
- case 'scene':
128
- outmsgs[2]={ payload: val };
129
- break;
130
- }
131
- this.send(outmsgs);
132
- // возвращаем подтверждение в базу
133
- let state= {
134
- type:this.ctype,
135
- state:{
136
- instance: newstate.instance,
137
- value: val
138
- }
139
- };
140
- if (this.response){
141
- this.device.updateCapabState(this.id,state)
142
- .then (res=>{
143
- this.value = JSON.stringify(val);
144
- this.status({fill:"green",shape:"dot",text:"online"});
145
- })
146
- .catch(err=>{
147
- this.error("Error on update capability state: " + err.message);
148
- this.status({fill:"red",shape:"dot",text:"Error"});
149
- })
150
- };
151
- })
152
-
153
- this.on('input', (msg, send, done)=>{
154
- let value = msg.payload;
155
- let state = {};
156
- switch (typeof value) {
157
- case 'object':
158
- if ((value.r>-1 && value.g>-1 && value.b>-1) || (value.h>-1 && value.s>-1 && value.v>-1)){
159
- if (this.scheme == 'rgb'){
160
- value = value.r << 16 | value.g << 8 | value.b;
67
+ device.on("offline", () => {
68
+ this.status({ fill: "red", shape: "dot", text: "offline" });
69
+ });
70
+ device.on(this.id, (val, newstate) => {
71
+ const outmsgs = [null, null, null];
72
+ switch (newstate.instance) {
73
+ case 'rgb': {
74
+ const value = {
75
+ r: val >> 16,
76
+ g: val >> 8 & 0xFF,
77
+ b: val & 0xFF
78
+ };
79
+ outmsgs[0] = { payload: value };
80
+ break;
81
+ }
82
+ case 'hsv':
83
+ outmsgs[0] = { payload: val };
84
+ break;
85
+ case 'temperature_k':
86
+ outmsgs[1] = { payload: val };
87
+ break;
88
+ case 'scene':
89
+ outmsgs[2] = { payload: val };
90
+ break;
91
+ }
92
+ this.send(outmsgs);
93
+ const state = {
94
+ type: ctype,
95
+ state: {
96
+ instance: newstate.instance,
97
+ value: val
98
+ }
99
+ };
100
+ if (response) {
101
+ device.updateCapabState(this.id, state)
102
+ .then(() => {
103
+ lastValue = JSON.stringify(val);
104
+ this.status({ fill: "green", shape: "dot", text: "online" });
105
+ })
106
+ .catch(err => {
107
+ this.error("Error on update capability state: " + err.message);
108
+ this.status({ fill: "red", shape: "dot", text: "Error" });
109
+ });
110
+ }
111
+ });
112
+ this.on('input', (msg, _send, done) => {
113
+ let value = msg.payload;
114
+ const state = {};
115
+ switch (typeof value) {
116
+ case 'object': {
117
+ const obj = value;
118
+ if (('r' in obj && 'g' in obj && 'b' in obj) || ('h' in obj && 's' in obj && 'v' in obj)) {
119
+ if (scheme == 'rgb' && 'r' in obj) {
120
+ value = obj.r << 16 | obj.g << 8 | obj.b;
121
+ }
122
+ state.value = value;
123
+ state.instance = scheme;
124
+ }
125
+ else {
126
+ this.error("Wrong type! For Color, msg.payload must be RGB or HSV Object.");
127
+ if (done) {
128
+ done();
129
+ }
130
+ return;
131
+ }
132
+ break;
133
+ }
134
+ case 'number': {
135
+ value = Math.round(value);
136
+ if (value >= temperature_min && value <= temperature_max) {
137
+ state.value = value;
138
+ state.instance = 'temperature_k';
139
+ }
140
+ else {
141
+ this.error("Wrong type! For Temperature_k, msg.payload must be >=MIN and <=MAX.");
142
+ if (done) {
143
+ done();
144
+ }
145
+ return;
146
+ }
147
+ break;
148
+ }
149
+ case 'string':
150
+ if (color_scene.includes(value)) {
151
+ state.value = value;
152
+ state.instance = 'scene';
153
+ }
154
+ else {
155
+ this.error("Wrong type! For the Scene, the msg.payload must be set in the settings");
156
+ if (done) {
157
+ done();
158
+ }
159
+ return;
160
+ }
161
+ break;
162
+ default:
163
+ this.error("Wrong type! Unsupported msg.payload type");
164
+ if (done) {
165
+ done();
166
+ }
167
+ return;
168
+ }
169
+ if (JSON.stringify(value) === lastValue) {
170
+ this.debug("Value not changed. Cancel update");
171
+ if (done) {
172
+ done();
173
+ }
174
+ return;
175
+ }
176
+ const upState = {
177
+ type: ctype,
178
+ state: state
161
179
  };
162
- state.value = value;
163
- state.instance = this.scheme
164
- }else{
165
- this.error("Wrong type! For Color, msg.payload must be RGB or HSV Object.");
166
- if (done) {done();}
167
- return;
168
- }
169
- break;
170
- case 'number':
171
- value = Math.round(value);
172
- if (value>=this.temperature_min && value<=this.temperature_max){
173
- state.value = value;
174
- state.instance = 'temperature_k';
175
- }else{
176
- this.error("Wrong type! For Temperature_k, msg.payload must be >=MIN and <=MAX.");
177
- if (done) {done();}
178
- return;
179
- }
180
- break;
181
- case 'string':
182
- if (this.color_scene.includes(value)){
183
- state.value = value;
184
- state.instance = 'scene';
185
- }else{
186
- this.error("Wrong type! For the Scene, the msg.payload must be set in the settings");
187
- if (done) {done();}
188
- return;
189
- }
190
- break;
191
- default:
192
- this.error("Wrong type! Unsupported msg.payload type");
193
- if (done) {done();}
194
- return;
195
- }
196
-
197
- if (JSON.stringify(value) === this.value){
198
- this.debug("Value not changed. Cancel update");
199
- if (done) {done();}
200
- return;
201
- };
202
- let upState= {
203
- type:this.ctype,
204
- state:state
205
- };
206
- this.device.updateCapabState(this.id,upState)
207
- .then(ref=>{
208
- this.value = JSON.stringify(value);
209
- this.status({fill:"green",shape:"dot",text:JSON.stringify(msg.payload)});
210
- if (done) {done();}
211
- })
212
- .catch(err=>{
213
- this.error("Error on update capability state: " + err.message);
214
- this.status({fill:"red",shape:"dot",text:"Error"});
215
- if (done) {done();}
216
- })
217
- });
218
-
219
- this.on('close', function(removed, done) {
220
- if (removed) {
221
- this.device.delCapability(this.id)
222
- .then(res=>{
223
- done()
224
- })
225
- .catch(err=>{
226
- this.error("Error on delete capability: " + err.message);
227
- done();
228
- })
229
- }else{
230
- done();
231
- }
232
- });
233
- }
234
- RED.nodes.registerType("Color",AliceColor);
235
- };
180
+ device.updateCapabState(this.id, upState)
181
+ .then(() => {
182
+ lastValue = JSON.stringify(value);
183
+ this.status({ fill: "green", shape: "dot", text: JSON.stringify(msg.payload) });
184
+ if (done) {
185
+ done();
186
+ }
187
+ })
188
+ .catch(err => {
189
+ this.error("Error on update capability state: " + err.message);
190
+ this.status({ fill: "red", shape: "dot", text: "Error" });
191
+ if (done) {
192
+ done();
193
+ }
194
+ });
195
+ });
196
+ this.on('close', (removed, done) => {
197
+ if (removed) {
198
+ device.delCapability(this.id)
199
+ .then(() => { done(); })
200
+ .catch(err => {
201
+ this.error("Error on delete capability: " + err.message);
202
+ done();
203
+ });
204
+ }
205
+ else {
206
+ done();
207
+ }
208
+ });
209
+ }
210
+ RED.nodes.registerType("Color", AliceColor);
211
+ };
212
+ //# sourceMappingURL=alice-color.js.map