├── icons
└── alice.png
├── img
└── siple_dev.PNG
├── nodes
├── icons
│ └── alice.png
├── alice-get.js
├── alice-get.html
├── alice-video.html
├── alice-sensor.js
├── alice-event.js
├── alice-onoff.html
├── alice-togle.html
├── alice-togle.js
├── alice-onoff.js
├── alice-video.js
├── alice-mode.js
├── alice-range.js
├── alice.js
├── alice-device.html
├── alice-event.html
├── alice-color.js
├── alice-device.js
├── alice.html
├── alice-color.html
├── alice-sensor.html
├── alice-range.html
└── alice-mode.html
├── .github
└── dependabot.yml
├── .eslintrc.json
├── src
├── alice-get.ts
├── alice-get.html
├── alice.ts
└── alice.html
├── LICENSE
├── .gitignore
├── package.json
├── README.md
└── tsconfig.json
/icons/alice.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/efa2000/node-red-contrib-alice/HEAD/icons/alice.png
--------------------------------------------------------------------------------
/img/siple_dev.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/efa2000/node-red-contrib-alice/HEAD/img/siple_dev.PNG
--------------------------------------------------------------------------------
/nodes/icons/alice.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/efa2000/node-red-contrib-alice/HEAD/nodes/icons/alice.png
--------------------------------------------------------------------------------
/nodes/alice-get.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | ;
3 | module.exports = (RED) => {
4 | function AliceGet(config) {
5 | RED.nodes.createNode(this, config);
6 | const service = RED.nodes.getNode(config.service);
7 | }
8 | ;
9 | RED.nodes.registerType("Alice-Get", AliceGet);
10 | };
11 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: npm
4 | directory: "/"
5 | schedule:
6 | interval: daily
7 | open-pull-requests-limit: 10
8 | ignore:
9 | - dependency-name: firebase
10 | versions:
11 | - 8.2.10
12 | - 8.2.8
13 | - 8.2.9
14 | - 8.3.0
15 | - 8.3.1
16 | - 8.3.2
17 | - 8.3.3
18 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es2021": true
5 | },
6 | "extends": [
7 | "eslint:recommended",
8 | "plugin:@typescript-eslint/recommended"
9 | ],
10 | "parser": "@typescript-eslint/parser",
11 | "parserOptions": {
12 | "ecmaVersion": "latest",
13 | "sourceType": "module"
14 | },
15 | "plugins": [
16 | "@typescript-eslint"
17 | ],
18 | "rules": {
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/alice-get.ts:
--------------------------------------------------------------------------------
1 | import {NodeAPI, Node, NodeDef } from "node-red";
2 | import axios from "axios";
3 |
4 |
5 | interface NodeAliceGetConfig
6 | extends NodeDef {
7 | service: string;
8 | name:string;
9 | };
10 |
11 | export = (RED: NodeAPI):void =>{
12 | function AliceGet(this:Node, config:NodeAliceGetConfig):void {
13 | RED.nodes.createNode(this,config);
14 | const service = RED.nodes.getNode(config.service);
15 | };
16 |
17 | RED.nodes.registerType("Alice-Get",AliceGet);
18 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Efa200
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.
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | #package-lock
2 | #package-lock.json
3 |
4 | # Logs
5 | logs
6 | *.log
7 | npm-debug.log*
8 | yarn-debug.log*
9 | yarn-error.log*
10 |
11 | # Runtime data
12 | pids
13 | *.pid
14 | *.seed
15 | *.pid.lock
16 |
17 | # Directory for instrumented libs generated by jscoverage/JSCover
18 | lib-cov
19 |
20 | # Coverage directory used by tools like istanbul
21 | coverage
22 |
23 | # nyc test coverage
24 | .nyc_output
25 |
26 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
27 | .grunt
28 |
29 | # Bower dependency directory (https://bower.io/)
30 | bower_components
31 |
32 | # node-waf configuration
33 | .lock-wscript
34 |
35 | # Compiled binary addons (https://nodejs.org/api/addons.html)
36 | build/Release
37 |
38 | # Dependency directories
39 | node_modules/
40 | jspm_packages/
41 |
42 | # TypeScript v1 declaration files
43 | typings/
44 |
45 | # Optional npm cache directory
46 | .npm
47 |
48 | # Optional eslint cache
49 | .eslintcache
50 |
51 | # Optional REPL history
52 | .node_repl_history
53 |
54 | # Output of 'npm pack'
55 | *.tgz
56 |
57 | # Yarn Integrity file
58 | .yarn-integrity
59 |
60 | # dotenv environment variables file
61 | .env
62 |
63 | # next.js build output
64 | .next
65 |
66 | # vscode
67 | .vscode
68 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "node-red-contrib-alice",
3 | "version": "2.2.5",
4 | "description": "",
5 | "scripts": {
6 | "start": "npm run build && node-red",
7 | "build": "tsc && npm run copy-html",
8 | "copy-html": "cp ./src/*.html ./nodes/"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "git+https://github.com/efa2000/node-red-contrib-alice.git"
13 | },
14 | "engines": {
15 | "node": ">=14.0.0"
16 | },
17 | "keywords": [
18 | "node-red",
19 | "yandex",
20 | "alice",
21 | "яндекс",
22 | "алиса"
23 | ],
24 | "node-red": {
25 | "version": ">=3.0.0",
26 | "nodes": {
27 | "alice-service": "./nodes/alice.js",
28 | "alice-device": "./nodes/alice-device.js",
29 | "alice-onoff": "./nodes/alice-onoff.js",
30 | "alice-togle": "./nodes/alice-togle.js",
31 | "alice-range": "./nodes/alice-range.js",
32 | "alice-color": "./nodes/alice-color.js",
33 | "alice-mode": "./nodes/alice-mode.js",
34 | "alice-sensor": "./nodes/alice-sensor.js",
35 | "alice-event": "./nodes/alice-event.js",
36 | "alice-video": "./nodes/alice-video.js"
37 | }
38 | },
39 | "author": "Efa2000",
40 | "license": "MIT",
41 | "bugs": {
42 | "url": "https://github.com/efa2000/node-red-contrib-alice/issues"
43 | },
44 | "homepage": "https://github.com/efa2000/node-red-contrib-alice#readme",
45 | "dependencies": {
46 | "axios": "^1.4.0",
47 | "mqtt": "^4.3.8"
48 | },
49 | "devDependencies": {
50 | "@types/axios": "^0.14.0",
51 | "@types/mqtt": "^2.5.0",
52 | "@types/node": "^20.11.16",
53 | "@types/node-red": "^1.3.4",
54 | "@typescript-eslint/eslint-plugin": "^6.20.0",
55 | "@typescript-eslint/parser": "^6.20.0",
56 | "eslint": "^8.56.0",
57 | "nodemon": "^3.0.3",
58 | "typescript": "^5.3.3"
59 | },
60 | "nodemonConfig": {
61 | "ignoreRoot" : [".git", "test"],
62 | "restartable": "rs",
63 | "ignore": [
64 | "node_modules/**/node_modules",
65 | "node_modules/**/test",
66 | "*.log"
67 | ],
68 | "verbose": true,
69 | "delay": "1000",
70 | "events": {
71 | "restart": "echo ------ restarted due to: $FILENAME ------"
72 | },
73 | "watch": [
74 | "src/",
75 | "node_modules/node-red-*"
76 | ],
77 | "ext": "js json htm html css"
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/nodes/alice-get.html:
--------------------------------------------------------------------------------
1 |
73 |
74 |
92 |
--------------------------------------------------------------------------------
/src/alice-get.html:
--------------------------------------------------------------------------------
1 |
73 |
74 |
92 |
--------------------------------------------------------------------------------
/nodes/alice-video.html:
--------------------------------------------------------------------------------
1 |
36 |
37 |
59 |
60 |
91 |
--------------------------------------------------------------------------------
/nodes/alice-sensor.js:
--------------------------------------------------------------------------------
1 | module.exports = function(RED) {
2 | // ************** ON/OFF *******************
3 | function AliceSensor(config){
4 | RED.nodes.createNode(this,config);
5 | const device = RED.nodes.getNode(config.device);
6 | device.setMaxListeners(device.getMaxListeners() + 1); // увеличиваем лимит для event
7 | const id =JSON.parse(JSON.stringify(this.id));
8 | const name = config.name;
9 | const stype = config.stype;
10 | const reportable = true;
11 | const retrievable = true;
12 | const unit = config.unit;
13 | const instance = config.instance;
14 | let initState = false;
15 | // this.value;
16 | let curentState= {
17 | type:stype,
18 | state:{
19 | instance: instance,
20 | value: 0
21 | }
22 | };
23 |
24 | this.status({fill:"red",shape:"dot",text:"offline"});
25 |
26 | this.init = ()=>{
27 | this.debug("Starting sensor initilization ...");
28 | let sensor = {
29 | type: stype,
30 | reportable: reportable,
31 | retrievable: retrievable,
32 | parameters: {
33 | instance: instance,
34 | unit: unit
35 | }
36 | };
37 |
38 | device.setSensor(id,sensor)
39 | .then(res=>{
40 | this.debug("Sensor initilization - success!");
41 | this.status({fill:"green",shape:"dot",text:"online"});
42 | initState = true;
43 | })
44 | .catch(err=>{
45 | this.error("Error on create sensor: " +err.message);
46 | this.status({fill:"red",shape:"dot",text:"error"});
47 | });
48 | };
49 |
50 | // Проверяем сам девайс уже инициирован
51 | if (device.initState) this.init();
52 |
53 | device.on("online",()=>{
54 | this.init();
55 | });
56 |
57 | device.on("offline",()=>{
58 | this.status({fill:"red",shape:"dot",text:"offline"});
59 | });
60 |
61 | this.on('input', (msg, send, done)=>{
62 | if (typeof msg.payload != 'number'){
63 | this.error("Wrong type! msg.payload must be number.");
64 | if (done) {done();}
65 | return;
66 | };
67 | if (unit == 'unit.temperature.celsius' || unit == 'unit.ampere'){
68 | msg.payload = +msg.payload.toFixed(1);
69 | }else {
70 | msg.payload = +msg.payload.toFixed(0);
71 | };
72 | if (curentState.state.value == msg.payload){
73 | this.debug("Value not changed. Cancel update");
74 | if (done) {done();}
75 | return;
76 | };
77 | curentState.state.value = msg.payload;
78 | device.updateSensorState(id,curentState)
79 | .then(ref=>{
80 | this.status({fill:"green",shape:"dot",text: msg.payload});
81 | if (done) {done();}
82 | })
83 | .catch(err=>{
84 | this.error("Error on update sensor state: " +err.message);
85 | this.status({fill:"red",shape:"dot",text:"Error"});
86 | if (done) {done();}
87 | })
88 | });
89 |
90 | this.on('close', function(removed, done) {
91 | if (removed) {
92 | device.delSensor(id)
93 | .then(res=>{
94 | done()
95 | })
96 | .catch(err=>{
97 | this.error("Error on delete property: " +err.message);
98 | done();
99 | })
100 | }else{
101 | device.setMaxListeners(device.getMaxListeners() - 1);
102 | done();
103 | }
104 | });
105 | }
106 | RED.nodes.registerType("Sensor",AliceSensor);
107 | };
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # NodeRed Home (node-red-contrib-alice)
2 |
3 | **NodeRed Home** (node-red-contrib-alice) - это сервис позволит, в несколько простых шагов, подключить любые ваши устройства заведенные в Node-RED к умному дому от Яндекса и управлять ими с помощью голосового помощника Алиса.
4 |
5 | [](https://nodered.org)
6 | [](https://nodejs.org/en/)
7 | 
8 | [](https://www.npmjs.com/package/node-red-contrib-alice)
9 | [](https://packagequality.com/#?package=node-red-contrib-alice)
10 | 
11 | 
12 | 
13 |
14 | #### Обсудить и получить поддержку от сообщества и автора можно в Телеграм канале [https://t.me/nodered_home_chat](https://t.me/nodered_home_chat)
15 |
16 | ## Инструкция (RUS)
17 | ### Использование
18 | #### Как настроить навык:
19 | 1. Установите и настройте Node-Red
20 | 2. Из интерфейса Node-Red добавьте модуль node-red-contrib-alice или с использованием npm
21 | ```
22 | npm install node-red-contrib-alice
23 | ```
24 | 3. Добавьте в свою схему устройства и умения Алисы и зарегистрируйтесь на вкладке настройки
25 | 4. Настройте их связь с вашими устройствами
26 | 5. В приложении Яндекс добавьте навык NodeRed Home
27 | 6. Заведенные устройства появятся автоматически
28 |
29 | ### Концепция
30 | Кождое устройство может иметь неограниченное число умений (функционала)
31 | К примеру, лампочка может иметь умение включения/выклюяения, но так же дополнительное умение установки цвета и яркости
32 | Умения устройства можно объеденять в любом порядке
33 | Более подробно о умениях и устройствах можно почитать в документации Yandex [Документация Яндекса](https://yandex.ru/dev/dialogs/alice/doc/smart-home/concepts/capability-types-docpage/)
34 |
35 | ### Особенности
36 | Для того, что бы устройство ответило Алисе, что комманда выполнена успешно, на вход должно прийти соответсвующее значение.
37 | Если ваше устройство отвечает дольше или совсем не возвращает подтверждение просто добавьте оставьте галочку Response включенной
38 |
39 | ### Тарифы
40 | до 5-ти зарегистрированных на шлюзе устройств - бесплатно
41 | 5-ть и более зарегистрированных на шлюзе устройств - 199 руб./мес.
42 |
43 | ## Instruction (ENG - Google Translate)
44 | The module allows you to use Node-Red together with the Yandex.Alice voice assistant service (voice control of smart home devices)
45 |
46 | ### Use
47 | #### How to set up a skill:
48 | 1. Install and configure Node-Red
49 | 2. From the Node-Red interface add the node-red-contrib-alice module or using npm
50 | ```
51 | npm install node-red-contrib-alice
52 | ```
53 | 3. Add Alice’s devices and capability to your circuit and register on the settings tab
54 | 4. Configure their connection with your devices
55 | 5. In the Yandex application, add the NodeRed Home skill
56 | 6. Started devices will appear automatically
57 |
58 | ### Concept
59 | Each device can have an unlimited number of capability (functionality)
60 | For example, a light bulb may have the capability to turn on / off, but also the additional capability to set the color and brightness
61 | Device capabilites can be combined in any order
62 | You can read more about capability and devices in the Yandex documentation [Yandex Documentation] (https://yandex.ru/dev/dialogs/alice/doc/smart-home/concepts/capability-types-docpage/)
63 |
--------------------------------------------------------------------------------
/nodes/alice-event.js:
--------------------------------------------------------------------------------
1 | module.exports = function(RED) {
2 | // ************** ON/OFF *******************
3 | function AliceEvent(config){
4 | RED.nodes.createNode(this,config);
5 | const device = RED.nodes.getNode(config.device);
6 | device.setMaxListeners(device.getMaxListeners() + 1); // увеличиваем лимит для event
7 | const id =JSON.parse(JSON.stringify(this.id));
8 | const name = config.name;
9 | const stype = 'devices.properties.event';
10 | const instance = config.instance;
11 | const reportable = true;
12 | const retrievable = false;
13 | const events = config.events;
14 | let initState = false;
15 | let curentState = {
16 | type:stype,
17 | state:{
18 | instance: instance,
19 | value: ''
20 | }
21 | };
22 | this.status({fill:"red",shape:"dot",text:"offline"});
23 |
24 | const init = ()=>{
25 | this.debug("Starting sensor initilization ...");
26 | let objEvents=[]
27 | events.forEach(v => {
28 | objEvents.push({value:v})
29 | });
30 | let sensor = {
31 | type: stype,
32 | reportable: reportable,
33 | retrievable: retrievable,
34 | parameters: {
35 | instance: instance,
36 | events: objEvents
37 | }
38 | };
39 |
40 | device.setSensor(id,sensor)
41 | .then(res=>{
42 | this.debug("Sensor initilization - success!");
43 | this.status({fill:"green",shape:"dot",text:"online"});
44 | initState = true;
45 | })
46 | .catch(err=>{
47 | this.error("Error on create sensor: " +err.message);
48 | this.status({fill:"red",shape:"dot",text:"error"});
49 | });
50 | };
51 |
52 | // Проверяем сам девайс уже инициирован
53 | if (device.initState) init();
54 |
55 | device.on("online",()=>{
56 | if (!initState){
57 | init();
58 | }else{
59 | this.status({fill:"green",shape:"dot",text: curentState.state.value});
60 | };
61 | });
62 |
63 | device.on("offline",()=>{
64 | this.status({fill:"red",shape:"dot",text:"offline"});
65 | });
66 |
67 | this.on('input', (msg, send, done)=>{
68 | if (!events.includes(msg.payload)){
69 | this.error("Wrong type! msg.payload must be from the list of allowed events.");
70 | if (done) {done();}
71 | return;
72 | };
73 | if (curentState.state.value==msg.payload){
74 | this.debug("Value not changed. Cancel update");
75 | if (done) {done();}
76 | return;
77 | }else{
78 | curentState.state.value = msg.payload;
79 | };
80 | // для кнопок обнуляем значение через 1 сек
81 | if (instance=='button'){
82 | setTimeout(() => {
83 | curentState.state.value = null;
84 | this.status({fill:"green",shape:"dot",text:""});
85 | }, 1000);
86 | };
87 | device.updateSensorState(id,curentState)
88 | .then(ref=>{
89 | this.status({fill:"green",shape:"dot",text: curentState.state.value});
90 | if (done) {done();}
91 | })
92 | .catch(err=>{
93 | this.error("Error on update sensor state: " +err.message);
94 | this.status({fill:"red",shape:"dot",text:"Error"});
95 | if (done) {done();}
96 | })
97 | });
98 |
99 | this.on('close', function(removed, done) {
100 | if (removed) {
101 | device.delSensor(id)
102 | .then(res=>{
103 | done()
104 | })
105 | .catch(err=>{
106 | this.error("Error on delete property: " +err.message);
107 | done();
108 | })
109 | }else{
110 | done();
111 | }
112 | });
113 | }
114 | RED.nodes.registerType("Event",AliceEvent);
115 | };
--------------------------------------------------------------------------------
/nodes/alice-onoff.html:
--------------------------------------------------------------------------------
1 |
32 |
33 |
52 |
53 |
94 |
--------------------------------------------------------------------------------
/nodes/alice-togle.html:
--------------------------------------------------------------------------------
1 |
28 |
29 |
54 |
55 |
97 |
--------------------------------------------------------------------------------
/nodes/alice-togle.js:
--------------------------------------------------------------------------------
1 | module.exports = function(RED) {
2 | // ************** Toggle *******************
3 | function AliceToggle(config){
4 | RED.nodes.createNode(this,config);
5 | this.device = RED.nodes.getNode(config.device);
6 | this.device.setMaxListeners(this.device.getMaxListeners() + 1); // увеличиваем лимит для event
7 | this.name = config.name;
8 | this.ctype = 'devices.capabilities.toggle';
9 | this.instance = config.instance;
10 | this.response = config.response;
11 | this.initState = false;
12 | this.value = false;
13 |
14 | if (config.response === undefined){
15 | this.response = true;
16 | }
17 |
18 | this.status({fill:"red",shape:"dot",text:"offline"});
19 |
20 | this.init = ()=>{
21 | this.debug("Starting capability initilization ...");
22 | let capab = {
23 | type: this.ctype,
24 | retrievable: true,
25 | reportable: true,
26 | parameters: {
27 | instance: this.instance,
28 | }
29 | };
30 | this.device.setCapability(this.id,capab)
31 | .then(res=>{
32 | this.initState = true;
33 | // this.value = capab.state.value;
34 | this.debug("Capability initilization - success!");
35 | this.status({fill:"green",shape:"dot",text:"online"});
36 | })
37 | .catch(err=>{
38 | this.error("Error on create capability: " + err.message);
39 | this.status({fill:"red",shape:"dot",text:"error"});
40 | });
41 | };
42 |
43 | // Проверяем сам девайс уже инициирован
44 | if (this.device.initState) this.init();
45 |
46 | this.device.on("online",()=>{
47 | this.init();
48 | });
49 |
50 | this.device.on("offline",()=>{
51 | this.status({fill:"red",shape:"dot",text:"offline"});
52 | });
53 |
54 | this.device.on(this.id,(val)=>{
55 | this.debug("Received a new value from Yandex...");
56 | this.send({
57 | payload: val
58 | });
59 | let state= {
60 | type:this.ctype,
61 | state:{
62 | instance: this.instance,
63 | value: val
64 | }
65 | };
66 | if (this.response){
67 | this.debug("Automatic confirmation is true, sending confirmation to Yandex ...");
68 | this.device.updateCapabState(this.id,state)
69 | .then (res=>{
70 | this.value = val;
71 | this.status({fill:"green",shape:"dot",text:val});
72 | })
73 | .catch(err=>{
74 | this.error("Error on update capability state: " + err.message);
75 | this.status({fill:"red",shape:"dot",text:"Error"});
76 | })
77 | };
78 | })
79 |
80 | this.on('input', (msg, send, done)=>{
81 | if (typeof msg.payload != 'boolean'){
82 | this.error("Wrong type! msg.payload must be boolean.");
83 | if (done) {done();}
84 | return;
85 | };
86 | if (msg.payload === this.value){
87 | this.debug("Value not changed. Cancel update");
88 | if (done) {done();}
89 | return;
90 | };
91 | let state= {
92 | type:this.ctype,
93 | state:{
94 | instance: this.instance,
95 | value: msg.payload
96 | }
97 | };
98 | this.device.updateCapabState(this.id,state)
99 | .then(ref=>{
100 | this.value = msg.payload;
101 | this.status({fill:"green",shape:"dot",text:msg.payload.toString()});
102 | if (done) {done();}
103 | })
104 | .catch(err=>{
105 | this.error("Error on update capability state: " + err.message);
106 | this.status({fill:"red",shape:"dot",text:"Error"});
107 | if (done) {done();}
108 | })
109 | });
110 |
111 | this.on('close', (removed, done)=>{
112 | if (removed) {
113 | this.device.delCapability(this.id)
114 | .then(res=>{
115 | done()
116 | })
117 | .catch(err=>{
118 | this.error("Error on delete capability: " + err.message);
119 | done();
120 | })
121 | }else{
122 | done();
123 | }
124 | });
125 | }
126 | RED.nodes.registerType("Toggle",AliceToggle);
127 | };
--------------------------------------------------------------------------------
/nodes/alice-onoff.js:
--------------------------------------------------------------------------------
1 | module.exports = function(RED) {
2 | // ************** ON/OFF *******************
3 | function AliceOnOff(config){
4 | RED.nodes.createNode(this,config);
5 | const device = RED.nodes.getNode(config.device);
6 | device.setMaxListeners(device.getMaxListeners() + 1); // увеличиваем лимит для event
7 | const id =JSON.parse(JSON.stringify(this.id));
8 | const ctype = 'devices.capabilities.on_off';
9 | const instance = 'on';
10 | let response = config.response;
11 | let split = config.split;
12 | let initState = false;
13 | let curentState = {
14 | type:ctype,
15 | state:{
16 | instance: instance,
17 | value: false
18 | }
19 | };
20 |
21 | if (config.response === undefined){
22 | response = true;
23 | };
24 | if (config.split === undefined){
25 | split = false;
26 | };
27 |
28 | this.status({fill:"red",shape:"dot",text:"offline"});
29 |
30 | this.init = ()=>{
31 | this.debug("Starting capability initilization ...");
32 | let capab = {
33 | type: ctype,
34 | retrievable: true,
35 | reportable: true,
36 | parameters: {
37 | instance: instance,
38 | split: split
39 | }
40 | };
41 |
42 | device.setCapability(id,capab)
43 | .then(res=>{
44 | this.debug("Capability initilization - success!");
45 | initState = true;
46 | this.status({fill:"green",shape:"dot",text:"online"});
47 | })
48 | .catch(err=>{
49 | this.error("Error on create capability: " + err.message);
50 | this.status({fill:"red",shape:"dot",text:"error"});
51 | });
52 | device.updateCapabState(id,curentState)
53 | .then (res=>{
54 | this.status({fill:"green",shape:"dot",text:"online"});
55 | })
56 | .catch(err=>{
57 | this.error("Error on update capability state: " + err.message);
58 | this.status({fill:"red",shape:"dot",text:"Error"});
59 | });
60 | };
61 |
62 | // Проверяем сам девайс уже инициирован
63 | if (device.initState) this.init();
64 |
65 | device.on("online",()=>{
66 | this.init();
67 | });
68 |
69 | device.on("offline",()=>{
70 | this.status({fill:"red",shape:"dot",text:"offline"});
71 | });
72 |
73 | device.on(id,(val)=>{
74 | this.send({
75 | payload: val
76 | });
77 | if (response){
78 | curentState.state.value = val;
79 | device.updateCapabState(id,curentState)
80 | .then (res=>{
81 | this.status({fill:"green",shape:"dot",text:val.toString()});
82 | })
83 | .catch(err=>{
84 | this.error("Error on update capability state: " + err.message);
85 | this.status({fill:"red",shape:"dot",text:"Error"});
86 | })
87 | };
88 | })
89 |
90 | this.on('input', (msg, send, done)=>{
91 | if (typeof msg.payload != 'boolean'){
92 | this.error("Wrong type! msg.payload must be boolean.");
93 | if (done) {done();}
94 | return;
95 | };
96 | if (msg.payload === curentState.state.value){
97 | this.debug("Value not changed. Cancel update");
98 | if (done) {done();}
99 | return;
100 | };
101 | curentState.state.value = msg.payload;
102 | device.updateCapabState(id,curentState)
103 | .then(ref=>{
104 | this.status({fill:"green",shape:"dot",text:msg.payload.toString()});
105 | if (done) {done();}
106 | })
107 | .catch(err=>{
108 | this.error("Error on update capability state: " + err.message);
109 | this.status({fill:"red",shape:"dot",text:"Error"});
110 | if (done) {done();}
111 | })
112 | });
113 |
114 | this.on('close', (removed, done)=>{
115 | device.setMaxListeners(device.getMaxListeners() - 1);
116 | if (removed) {
117 | device.delCapability(id)
118 | .then(res=>{
119 | done()
120 | })
121 | .catch(err=>{
122 | this.error("Error on delete capability: " + err.message);
123 | done();
124 | })
125 | };
126 | done();
127 | return;
128 | });
129 | }
130 | RED.nodes.registerType("On_Off",AliceOnOff);
131 | };
--------------------------------------------------------------------------------
/nodes/alice-video.js:
--------------------------------------------------------------------------------
1 | module.exports = function(RED) {
2 | // ************** VIDEO *******************
3 | function AliceVideo(config){
4 | RED.nodes.createNode(this,config);
5 | const device = RED.nodes.getNode(config.device);
6 | device.setMaxListeners(device.getMaxListeners() + 1); // увеличиваем лимит для event
7 | const id =this.id;
8 | const name = config.name;
9 | const ctype = 'devices.capabilities.video_stream';
10 | const instance = 'get_stream';
11 | const stream_url = config.stream_url;
12 | const protocol = config.protocol;
13 | const response = true;
14 | const retrievable = false;
15 | const reportable = false;
16 | let initState = false;
17 | let curentState = {
18 | type:ctype,
19 | state:{
20 | instance: instance,
21 | value: {
22 | stream_url: stream_url,
23 | protocol: protocol
24 | }
25 | }
26 | };
27 |
28 | this.status({fill:"red",shape:"dot",text:"offline"});
29 |
30 | this.init = ()=>{
31 | this.debug("Starting capability initilization ...");
32 | let capab = {
33 | type: ctype,
34 | retrievable: retrievable,
35 | reportable: reportable,
36 | parameters: {
37 | instance: instance,
38 | protocols: [protocol]
39 | }
40 | };
41 |
42 | device.setCapability(id,capab)
43 | .then(res=>{
44 | this.debug("Capability initilization - success!");
45 | initState = true;
46 | this.status({fill:"green",shape:"dot",text:"online"});
47 | })
48 | .catch(err=>{
49 | this.error("Error on create capability: " + err.message);
50 | this.status({fill:"red",shape:"dot",text:"Error"});
51 | });
52 | device.updateCapabState(id,curentState)
53 | .then (res=>{
54 | this.status({fill:"green",shape:"dot",text:"online"});
55 | })
56 | .catch(err=>{
57 | this.error("Error on update capability state: " + err.message);
58 | this.status({fill:"red",shape:"dot",text:"Error"});
59 | });
60 | };
61 |
62 | // Проверяем сам девайс уже инициирован
63 | if (device.initState) this.init();
64 |
65 | device.on("online",()=>{
66 | this.init();
67 | });
68 |
69 | device.on("offline",()=>{
70 | this.status({fill:"red",shape:"dot",text:"offline"});
71 | });
72 |
73 | device.on(id,(val)=>{
74 | // this.send({
75 | // payload: val
76 | // });
77 | if (response){
78 | // curentState.state.value = val;
79 | device.updateCapabState(id,curentState)
80 | .then (res=>{
81 | str_url = stream_url.slice(0,25) + "...";
82 | this.status({fill:"green",shape:"dot",text:str_url});
83 | })
84 | .catch(err=>{
85 | this.error("Error on update capability state: " + err.message);
86 | this.status({fill:"red",shape:"dot",text:"Error"});
87 | })
88 | };
89 | })
90 |
91 | // this.on('input', (msg, send, done)=>{
92 | // if (typeof msg.payload != 'boolean'){
93 | // this.error("Wrong type! msg.payload must be boolean.");
94 | // if (done) {done();}
95 | // return;
96 | // };
97 | // if (msg.payload === curentState.state.value){
98 | // this.debug("Value not changed. Cancel update");
99 | // if (done) {done();}
100 | // return;
101 | // };
102 | // curentState.state.value = msg.payload;
103 | // device.updateCapabState(id,curentState)
104 | // .then(ref=>{
105 | // this.status({fill:"green",shape:"dot",text:msg.payload.toString()});
106 | // if (done) {done();}
107 | // })
108 | // .catch(err=>{
109 | // this.error("Error on update capability state: " + err.message);
110 | // this.status({fill:"red",shape:"dot",text:"Error"});
111 | // if (done) {done();}
112 | // })
113 | // });
114 |
115 | this.on('close', (removed, done)=>{
116 | device.setMaxListeners(device.getMaxListeners() - 1);
117 | if (removed) {
118 | device.delCapability(id)
119 | .then(res=>{
120 | done()
121 | })
122 | .catch(err=>{
123 | this.error("Error on delete capability: " + err.message);
124 | done();
125 | })
126 | };
127 | done();
128 | return;
129 | });
130 | }
131 | RED.nodes.registerType("Video",AliceVideo);
132 | };
--------------------------------------------------------------------------------
/nodes/alice-mode.js:
--------------------------------------------------------------------------------
1 | module.exports = function(RED) {
2 | // ************** Modes *******************
3 | function AliceMode(config){
4 | RED.nodes.createNode(this,config);
5 | this.device = RED.nodes.getNode(config.device);
6 | this.name = config.name;
7 | this.ctype = 'devices.capabilities.mode';
8 | this.retrievable = true;
9 | this.random_access = true;
10 | this.response = config.response;
11 | this.instance = config.instance;
12 | this.modes = config.modes;
13 | this.initState = false;
14 | this.value;
15 |
16 | if (config.response === undefined){
17 | this.response = true;
18 | }
19 |
20 | this.init = _=>{
21 | if (this.modes.length<1){
22 | this.status({fill:"red",shape:"dot",text:"error"});
23 | this.error("In the list of supported commands, there must be at least one command");
24 | return;
25 | };
26 | if (!this.instance){
27 | this.status({fill:"red",shape:"dot",text:"error"});
28 | this.error("Mode type not selected");
29 | return;
30 | };
31 | var cfgModes = [];
32 | this.modes.forEach(v=>{
33 | cfgModes.push({value:v});
34 | });
35 | let capab = {
36 | type: this.ctype,
37 | retrievable: this.retrievable,
38 | reportable: true,
39 | parameters: {
40 | instance: this.instance,
41 | modes: cfgModes
42 | }
43 | };
44 | this.device.setCapability(this.id,capab)
45 | .then(res=>{
46 | this.initState = true;
47 | this.status({fill:"green",shape:"dot",text:"online"});
48 | })
49 | .catch(err=>{
50 | this.error("Error on create capability: " + err.message);
51 | this.status({fill:"red",shape:"dot",text:"error"});
52 | });
53 | };
54 |
55 | // Проверяем сам девайс уже инициирован
56 | if (this.device.initState) this.init();
57 |
58 | this.device.on("online",()=>{
59 | this.init();
60 | });
61 |
62 | this.device.on("offline",()=>{
63 | this.status({fill:"red",shape:"dot",text:"offline"});
64 | });
65 |
66 | this.device.on(this.id,(val,fullstate)=>{
67 | let value = val;
68 | this.send({
69 | payload: value
70 | });
71 | let state= {
72 | type:this.ctype,
73 | state:{
74 | instance: this.instance,
75 | value: value
76 | }
77 | };
78 | if (this.response){
79 | this.device.updateCapabState(this.id,state)
80 | .then (res=>{
81 | this.value = value;
82 | this.status({fill:"green",shape:"dot",text:"online"});
83 | })
84 | .catch(err=>{
85 | this.error("Error on update capability state: " + err.message);
86 | this.status({fill:"red",shape:"dot",text:"Error"});
87 | })
88 | };
89 | })
90 |
91 | this.on('input', (msg, send, done)=>{
92 | const value = msg.payload;
93 | if (typeof value != 'string'){
94 | this.error("Wrong type! msg.payload must be String.");
95 | this.status({fill:"red",shape:"dot",text:"Error"});
96 | if (done) {done();}
97 | return;
98 | };
99 | if (this.modes.indexOf(value)<0){
100 | this.error("Error! Unsupported command.");
101 | this.status({fill:"red",shape:"dot",text:"Error"});
102 | if (done) {done();}
103 | return;
104 | };
105 | if (value === this.value){
106 | this.debug("Value not changed. Cancel update");
107 | if (done) {done();}
108 | return;
109 | };
110 | let state= {
111 | type:this.ctype,
112 | state:{
113 | instance: this.instance,
114 | value: value
115 | }
116 | };
117 | this.device.updateCapabState(this.id,state)
118 | .then(ref=>{
119 | this.value = value;
120 | this.status({fill:"green",shape:"dot",text:value});
121 | if (done) {done();}
122 | })
123 | .catch(err=>{
124 | this.error("Error on update capability state: " + err.message);
125 | this.status({fill:"red",shape:"dot",text:"Error"});
126 | if (done) {done();}
127 | });
128 | });
129 |
130 | this.on('close', function(removed, done) {
131 | if (removed) {
132 | this.device.delCapability(this.id)
133 | .then(res=>{
134 | done()
135 | })
136 | .catch(err=>{
137 | this.error("Error on delete capability: " + err.message);
138 | done();
139 | })
140 | }else{
141 | done();
142 | }
143 | });
144 | }
145 | RED.nodes.registerType("Mode",AliceMode);
146 | };
--------------------------------------------------------------------------------
/nodes/alice-range.js:
--------------------------------------------------------------------------------
1 | module.exports = function(RED) {
2 | // ************** Range *******************
3 | function AliceRange(config){
4 | RED.nodes.createNode(this,config);
5 | this.device = RED.nodes.getNode(config.device);
6 | this.device.setMaxListeners(this.device.getMaxListeners() + 1); // увеличиваем лимит для event
7 | this.name = config.name;
8 | this.ctype = 'devices.capabilities.range';
9 | this.retrievable = config.retrievable;
10 | this.instance = config.instance;
11 | this.unit = config.unit;
12 | this.random_access = true;
13 | this.min = parseFloat(config.min);
14 | this.max = parseFloat(config.max);
15 | this.precision = parseFloat(config.precision);
16 | this.response = config.response;
17 | this.initState = false;
18 | this.value = null;
19 |
20 | if (config.response === undefined){
21 | this.response = true;
22 | };
23 | if (typeof this.min != 'number'){this.min = 0};
24 | if (typeof this.max != 'number'){this.max = 100};
25 | if (typeof this.precision != 'number'){this.precision = 1};
26 |
27 | this.status({fill:"red",shape:"dot",text:"offline"});
28 |
29 | this.init = ()=>{
30 | let capab = {
31 | type: this.ctype,
32 | retrievable: this.retrievable,
33 | reportable: true,
34 | parameters: {
35 | instance: this.instance,
36 | unit: this.unit,
37 | random_access: this.random_access,
38 | range: {
39 | min: this.min,
40 | max: this.max,
41 | precision: this.precision
42 | }
43 | }
44 | };
45 | // если unit не пременим к параметру, то нужно удалить
46 | if (this.unit == "unit.number"){
47 | delete capab.parameters.unit;
48 | };
49 |
50 | this.device.setCapability(this.id,capab)
51 | .then(res=>{
52 | this.initState = true;
53 | this.status({fill:"green",shape:"dot",text:"online"});
54 | })
55 | .catch(err=>{
56 | this.error("Error on create capability: " + err.message);
57 | this.status({fill:"red",shape:"dot",text:"error"});
58 | });
59 | };
60 |
61 | // Проверяем сам девайс уже инициирован
62 | if (this.device.initState) this.init();
63 |
64 | this.device.on("online",()=>{
65 | this.init();
66 | });
67 |
68 | this.device.on("offline",()=>{
69 | this.status({fill:"red",shape:"dot",text:"offline"});
70 | });
71 |
72 | this.device.on(this.id,(val, fullstate)=>{
73 | let value = val;
74 | //проверка является ли значение относительным и нужно ли отдавать полное значение
75 | if (fullstate.relative && this.retrievable){
76 | value = this.value + val;
77 | if (val<0 && value0 && value>this.max) value=this.max;
79 | };
80 | this.send({
81 | payload: value
82 | });
83 | let state= {
84 | type:this.ctype,
85 | state:{
86 | instance: this.instance,
87 | value: value
88 | }
89 | };
90 | // если установлено требование немедленно отвечать, отвечаем
91 | if (this.response){
92 | this.device.updateCapabState(this.id,state)
93 | .then (res=>{
94 | this.value = value;
95 | this.status({fill:"green",shape:"dot",text:"online"});
96 | })
97 | .catch(err=>{
98 | this.error("Error on update capability state: " + err.message);
99 | this.status({fill:"red",shape:"dot",text:"Error"});
100 | })
101 | };
102 | })
103 |
104 | this.on('input', (msg, send, done)=>{
105 | const value = msg.payload;
106 | if (typeof value != 'number'){
107 | this.error("Wrong type! msg.payload must be Number.");
108 | if (done) {done();}
109 | return;
110 | }
111 | if (value === this.value){
112 | this.debug("Value not changed. Cancel update");
113 | if (done) {done();}
114 | return;
115 | };
116 | let state= {
117 | type:this.ctype,
118 | state:{
119 | instance: this.instance,
120 | value: value
121 | }
122 | };
123 | this.device.updateCapabState(this.id,state)
124 | .then(ref=>{
125 | this.value = value;
126 | this.status({fill:"green",shape:"dot",text:value});
127 | if (done) {done();}
128 | })
129 | .catch(err=>{
130 | this.error("Error on update capability state: " + err.message);
131 | this.status({fill:"red",shape:"dot",text:"Error"});
132 | if (done) {done();}
133 | })
134 | });
135 |
136 | this.on('close', (removed, done)=>{
137 | this.device.setMaxListeners(this.device.getMaxListeners() - 1); // уменьшаем лимит для event
138 | if (removed) {
139 | this.device.delCapability(this.id)
140 | .then(res=>{
141 | done()
142 | })
143 | .catch(err=>{
144 | this.error("Error on delete capability: " + err.message);
145 | done();
146 | })
147 | }else{
148 | done();
149 | }
150 | });
151 | }
152 | RED.nodes.registerType("Range",AliceRange);
153 | };
--------------------------------------------------------------------------------
/nodes/alice.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var __importDefault = (this && this.__importDefault) || function (mod) {
3 | return (mod && mod.__esModule) ? mod : { "default": mod };
4 | };
5 | const axios_1 = __importDefault(require("axios"));
6 | const mqtt_1 = __importDefault(require("mqtt"));
7 | ;
8 | ;
9 | ;
10 | module.exports = (RED) => {
11 | function AliceService(config) {
12 | RED.nodes.createNode(this, config);
13 | this.debug("Starting Alice service... ID: " + this.id);
14 | const email = this.credentials.email;
15 | const login = this.credentials.id;
16 | const password = this.credentials.password;
17 | const token = this.credentials.token;
18 | const suburl = Buffer.from(email).toString('base64');
19 | RED.httpAdmin.get("/noderedhome/" + suburl + "/clearalldevice", (req, res) => {
20 | const option = {
21 | method: 'POST',
22 | url: 'https://api.nodered-home.ru/gtw/device/clearallconfigs',
23 | headers: {
24 | 'content-type': 'application/json',
25 | 'Authorization': "Bearer " + this.getToken()
26 | },
27 | data: {}
28 | };
29 | axios_1.default.request(option)
30 | .then(result => {
31 | this.trace("All devices configs deleted on gateway successfully");
32 | res.sendStatus(200);
33 | })
34 | .catch(error => {
35 | this.debug("Error when delete All devices configs deleted on gateway: " + error.message);
36 | res.sendStatus(500);
37 | });
38 | });
39 | RED.httpAdmin.get("/noderedhome/" + this.id + "/getfullconfig", (req, res) => {
40 | const option = {
41 | method: 'GET',
42 | url: 'https://api.iot.yandex.net/v1.0/user/info',
43 | headers: {
44 | 'content-type': 'application/json',
45 | 'Authorization': "Bearer " + this.getToken()
46 | }
47 | };
48 | axios_1.default.request(option)
49 | .then(result => {
50 | this.trace("Full Alice SmartHome config successfully retrieved");
51 | res.json(result.data);
52 | })
53 | .catch(error => {
54 | this.debug("Error when retrieve Alice SmartHome config: " + error.message);
55 | res.sendStatus(500);
56 | });
57 | });
58 | this.isOnline = false;
59 | if (!token) {
60 | this.error("Authentication is required!!!");
61 | return;
62 | }
63 | ;
64 | const mqttClient = mqtt_1.default.connect("mqtts://mqtt.cloud.yandex.net", {
65 | port: 8883,
66 | clientId: login,
67 | rejectUnauthorized: false,
68 | username: login,
69 | password: password,
70 | reconnectPeriod: 10000
71 | });
72 | mqttClient.on("message", (topic, payload) => {
73 | const arrTopic = topic.split('/');
74 | const data = JSON.parse(payload);
75 | this.trace("Incoming:" + topic + " timestamp:" + new Date().getTime());
76 | if (payload.length && typeof data === 'object') {
77 | if (arrTopic[3] == 'message') {
78 | this.warn(data.text);
79 | }
80 | else {
81 | this.emit(arrTopic[3], data);
82 | }
83 | ;
84 | }
85 | });
86 | mqttClient.on("connect", () => {
87 | this.debug("Yandex IOT client connected. ");
88 | this.emit('online');
89 | mqttClient.subscribe("$me/device/commands/+", _ => {
90 | this.debug("Yandex IOT client subscribed to the command");
91 | });
92 | });
93 | mqttClient.on("offline", () => {
94 | this.debug("Yandex IOT client offline. ");
95 | this.emit('offline');
96 | });
97 | mqttClient.on("disconnect", () => {
98 | this.debug("Yandex IOT client disconnect.");
99 | this.emit('offline');
100 | });
101 | mqttClient.on("reconnect", () => {
102 | this.debug("Yandex IOT client reconnecting ...");
103 | });
104 | mqttClient.on("error", (err) => {
105 | this.error("Yandex IOT client Error: " + err.message);
106 | this.emit('offline');
107 | });
108 | this.on('offline', () => {
109 | this.isOnline = false;
110 | });
111 | this.on('online', () => {
112 | this.isOnline = true;
113 | });
114 | this.on('close', (done) => {
115 | this.emit('offline');
116 | setTimeout(() => {
117 | mqttClient.end(false, done);
118 | }, 500);
119 | });
120 | this.send2gate = (path, data, retain) => {
121 | this.trace("Outgoing: " + path);
122 | mqttClient.publish(path, data, { qos: 0, retain: retain });
123 | };
124 | this.getToken = () => {
125 | return JSON.parse(token).access_token;
126 | };
127 | }
128 | ;
129 | RED.nodes.registerType("alice-service", AliceService, {
130 | credentials: {
131 | email: { type: "text" },
132 | password: { type: "password" },
133 | token: { type: "password" },
134 | id: { type: "text" }
135 | }
136 | });
137 | };
138 |
--------------------------------------------------------------------------------
/nodes/alice-device.html:
--------------------------------------------------------------------------------
1 |
72 |
73 |
95 |
--------------------------------------------------------------------------------
/nodes/alice-event.html:
--------------------------------------------------------------------------------
1 |
100 |
101 |
116 |
117 |
149 |
--------------------------------------------------------------------------------
/nodes/alice-color.js:
--------------------------------------------------------------------------------
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
49 | }
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
80 | };
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"});
94 | });
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;
161 | };
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 | };
--------------------------------------------------------------------------------
/src/alice.ts:
--------------------------------------------------------------------------------
1 | import {NodeAPI, Node, NodeDef, NodeCredentials, NodeCredential } from "node-red";
2 | import axios from "axios";
3 | import mqtt from "mqtt";
4 |
5 | interface NodeAliceConfig
6 | extends NodeDef {
7 | name:string;
8 | };
9 |
10 | interface NodeAliceCredentials
11 | extends NodeCredential {
12 | email:string;
13 | id:string;
14 | password:string;
15 | token:string;
16 | };
17 |
18 | interface AliceNode
19 | extends Node {
20 | credentials: NodeAliceCredentials;
21 | isOnline:boolean;
22 | getToken():string;
23 | send2gate(topic:string,data:any,retain:boolean):void;
24 | // on(event: 'hello', listener: (name: string) => void): this;
25 | on(event: string, listener: Function): this;
26 | };
27 |
28 | export = (RED: NodeAPI):void =>{
29 | function AliceService(this:AliceNode, config:NodeAliceConfig):void {
30 | RED.nodes.createNode(this,config);
31 | this.debug("Starting Alice service... ID: "+this.id);
32 |
33 | const email = this.credentials.email;
34 | const login = this.credentials.id;
35 | const password = this.credentials.password;
36 | const token = this.credentials.token;
37 |
38 | //вызов для удаления всех устройств
39 | const suburl = Buffer.from(email).toString('base64');
40 | RED.httpAdmin.get("/noderedhome/"+suburl+"/clearalldevice",(req,res)=>{
41 | const option = {
42 | method: 'POST',
43 | url: 'https://api.nodered-home.ru/gtw/device/clearallconfigs',
44 | headers: {
45 | 'content-type': 'application/json',
46 | 'Authorization': "Bearer "+this.getToken()
47 | },
48 | data: {}
49 | };
50 | axios.request(option)
51 | .then(result=>{
52 | this.trace("All devices configs deleted on gateway successfully");
53 | // console.log(result)
54 | res.sendStatus(200);
55 | })
56 | .catch(error=>{
57 | this.debug("Error when delete All devices configs deleted on gateway: "+error.message);
58 | res.sendStatus(500);
59 | });
60 | });
61 |
62 | RED.httpAdmin.get("/noderedhome/"+this.id+"/getfullconfig",(req,res)=>{
63 | const option = {
64 | method: 'GET',
65 | url: 'https://api.iot.yandex.net/v1.0/user/info',
66 | headers: {
67 | 'content-type': 'application/json',
68 | 'Authorization': "Bearer "+this.getToken()
69 | }
70 | };
71 | axios.request(option)
72 | .then(result=>{
73 | this.trace("Full Alice SmartHome config successfully retrieved");
74 | // console.log(result)
75 | res.json(result.data);
76 | })
77 | .catch(error=>{
78 | this.debug("Error when retrieve Alice SmartHome config: "+error.message);
79 | res.sendStatus(500);
80 | });
81 | });
82 |
83 | this.isOnline = false;
84 | //// проверяем а есть ли токен
85 | if (!token){
86 | this.error("Authentication is required!!!");
87 | return;
88 | };
89 | const mqttClient = mqtt.connect("mqtts://mqtt.cloud.yandex.net",{
90 | port: 8883,
91 | clientId: login,
92 | rejectUnauthorized: false,
93 | username: login,
94 | password: password,
95 | reconnectPeriod: 10000
96 | });
97 |
98 | mqttClient.on("message",(topic:string, payload:string)=>{
99 | const arrTopic = topic.split('/');
100 | const data = JSON.parse(payload);
101 | this.trace("Incoming:" + topic +" timestamp:"+new Date().getTime());
102 | if (payload.length && typeof data === 'object'){
103 | if (arrTopic[3]=='message'){
104 | this.warn(data.text);
105 | }else{
106 | this.emit(arrTopic[3],data);
107 | };
108 | }
109 | });
110 | mqttClient.on("connect",()=>{
111 | this.debug("Yandex IOT client connected. ");
112 | this.emit('online');
113 | // Подписываемся на получение комманд
114 | mqttClient.subscribe("$me/device/commands/+",_=>{
115 | this.debug("Yandex IOT client subscribed to the command");
116 | });
117 | });
118 | mqttClient.on("offline",()=>{
119 | this.debug("Yandex IOT client offline. ");
120 | this.emit('offline');
121 | });
122 | mqttClient.on("disconnect",()=>{
123 | this.debug("Yandex IOT client disconnect.");
124 | this.emit('offline');
125 | });
126 | mqttClient.on("reconnect",()=>{
127 | this.debug("Yandex IOT client reconnecting ...");
128 | });
129 | mqttClient.on("error",(err)=>{
130 | this.error("Yandex IOT client Error: "+ err.message);
131 | this.emit('offline');
132 | });
133 |
134 | this.on('offline', ()=>{
135 | this.isOnline = false;
136 | });
137 |
138 | this.on('online', ()=>{
139 | this.isOnline = true;
140 | });
141 |
142 | this.on('close',(done:Object)=>{
143 | this.emit('offline');
144 | setTimeout(()=>{
145 | mqttClient.end(false,done);
146 | },500)
147 | });
148 |
149 | this.send2gate = (path:string,data:any,retain:boolean)=>{
150 | // this.debug(path);
151 | // this.debug(data);
152 | this.trace("Outgoing: "+path);
153 | mqttClient.publish(path, data ,{ qos: 0, retain: retain });
154 | }
155 |
156 |
157 | this.getToken = ()=>{
158 | return JSON.parse(token).access_token;
159 | }
160 | };
161 |
162 | RED.nodes.registerType("alice-service",AliceService,{
163 | credentials: {
164 | email: {type: "text"},
165 | password: {type: "password"},
166 | token: {type: "password"},
167 | id:{type:"text"}
168 | }
169 | });
170 | }
171 |
172 | // const mqtt = require('mqtt');
173 | // const axios = require('axios');
174 |
175 | // module.exports = function(RED) {
176 | // //Sevice node, Alice-Service (credential)
177 | // function AliceService(config) {
178 | // RED.nodes.createNode(this,config);
179 | // this.debug("Starting Alice service...");
180 |
181 | // const email = this.credentials.email;
182 | // const login = this.credentials.id;
183 | // const password = this.credentials.password;
184 | // const token = this.credentials.token;
185 |
186 | // const suburl = Buffer.from(email).toString('base64');
187 | // RED.httpAdmin.get("/noderedhome/"+suburl+"/clearalldevice",(req,res)=>{
188 | // const option = {
189 | // method: 'POST',
190 | // url: 'https://api.nodered-home.ru/gtw/device/clearallconfigs',
191 | // headers: {
192 | // 'content-type': 'application/json',
193 | // 'Authorization': "Bearer "+this.getToken()
194 | // },
195 | // data: {}
196 | // };
197 | // axios.request(option)
198 | // .then(result=>{
199 | // this.trace("All devices configs deleted on gateway successfully");
200 | // // console.log(result)
201 | // res.sendStatus(200);
202 | // })
203 | // .catch(error=>{
204 | // this.debug("Error when delete All devices configs deleted on gateway: "+error.message);
205 | // res.sendStatus(500);
206 | // });
207 | // });
208 |
209 | // this.isOnline = false;
210 | // if (!token){
211 | // this.error("Authentication is required!!!");
212 | // return;
213 | // };
214 | // const mqttClient = mqtt.connect("mqtts://mqtt.cloud.yandex.net",{
215 | // port: 8883,
216 | // clientId: login,
217 | // rejectUnauthorized: false,
218 | // username: login,
219 | // password: password,
220 | // reconnectPeriod: 10000
221 | // });
222 | // mqttClient.on("message",(topic, payload)=>{
223 | // const arrTopic = topic.split('/');
224 | // const data = JSON.parse(payload);
225 | // this.trace("Incoming:" + topic +" timestamp:"+new Date().getTime());
226 | // if (payload.length && typeof data === 'object'){
227 | // if (arrTopic[3]=='message'){
228 | // this.warn(data.text);
229 | // }else{
230 | // this.emit(arrTopic[3],data);
231 | // };
232 | // }
233 | // });
234 | // mqttClient.on("connect",()=>{
235 | // this.debug("Yandex IOT client connected. ");
236 | // this.emit('online');
237 | // // Подписываемся на получение комманд
238 | // mqttClient.subscribe("$me/device/commands/+",_=>{
239 | // this.debug("Yandex IOT client subscribed to the command");
240 | // });
241 | // });
242 | // mqttClient.on("offline",()=>{
243 | // this.debug("Yandex IOT client offline. ");
244 | // this.emit('offline');
245 | // });
246 | // mqttClient.on("disconnect",()=>{
247 | // this.debug("Yandex IOT client disconnect.");
248 | // this.emit('offline');
249 | // });
250 | // mqttClient.on("reconnect",(err)=>{
251 | // this.debug("Yandex IOT client reconnecting ...");
252 | // });
253 | // mqttClient.on("error",(err)=>{
254 | // this.error("Yandex IOT client Error: "+ err.message);
255 | // this.emit('offline');
256 | // });
257 |
258 | // this.on('offline', ()=>{
259 | // this.isOnline = false;
260 | // })
261 |
262 | // this.on('online', ()=>{
263 | // this.isOnline = true;
264 | // })
265 |
266 | // this.on('close',(done)=>{
267 | // this.emit('offline');
268 | // setTimeout(()=>{
269 | // mqttClient.end(false,done);
270 | // },500)
271 | // });
272 |
273 | // this.send2gate= (path,data,retain)=>{
274 | // // this.debug(path);
275 | // // this.debug(data);
276 | // this.trace("Outgoing: "+path);
277 | // mqttClient.publish(path, data ,{ qos: 0, retain: retain });
278 | // }
279 |
280 | // this.getToken = ()=>{
281 | // return JSON.parse(token).access_token;
282 | // }
283 |
284 | // };
285 | // RED.nodes.registerType("alice-service",AliceService,{
286 | // credentials: {
287 | // email: {type: "text"},
288 | // password: {type: "password"},
289 | // token: {type: "password"},
290 | // id:{type:"text"}
291 | // }
292 | // });
293 | // };
294 |
295 |
296 |
297 |
--------------------------------------------------------------------------------
/nodes/alice-device.js:
--------------------------------------------------------------------------------
1 | const axios = require('axios');
2 |
3 | module.exports = function(RED) {
4 | // ***************************** Alice DEVICE ****************************
5 | function AliceDevice(config){
6 | const pjson = require('../package.json');
7 | RED.nodes.createNode(this,config);
8 | const service = RED.nodes.getNode(config.service);
9 | service.setMaxListeners(service.getMaxListeners() + 1); // увеличиваем лимит для event
10 | const name = config.name;
11 | const description = config.description;
12 | const room = config.room;
13 | const dtype = config.dtype;
14 | this.initState = false;
15 | let updating = false;
16 | let needSendEvent = false;
17 | let capabilites = {};
18 | let sensors = {};
19 | let deviceconfig = {
20 | id: this.id,
21 | name: config.name,
22 | description: config.description,
23 | room: config.room,
24 | type: config.dtype,
25 | device_info:{
26 | manufacturer: "NodeRed Home",
27 | model: "virtual device",
28 | sw_version: pjson.version
29 | },
30 | capabilities:[],
31 | properties:[]
32 | };
33 | let states = {
34 | id: this.id,
35 | capabilities: [],
36 | properties: []
37 | };
38 |
39 | if (service.isOnline){
40 | this.emit("online");
41 | this.initState = true;
42 | };
43 | // функция обновления информации об устройстве
44 | this._updateDeviceInfo= _=>{
45 | let now = false;
46 |
47 | if (deviceconfig.capabilities.length==0 && deviceconfig.properties.length==0){
48 | this.debug("DELETE Device config from gateway ...");
49 | /// отправка по http
50 | const option = {
51 | timeout: 5000,
52 | method: 'POST',
53 | url: 'https://api.nodered-home.ru/gtw/device/config',
54 | headers: {
55 | 'content-type': 'application/json',
56 | 'Authorization': "Bearer "+service.getToken()
57 | },
58 | data: {
59 | id: this.id,
60 | config: deviceconfig
61 | }
62 | };
63 | axios.request(option)
64 | .then(res=>{
65 | this.trace("Device config deleted on gateway successfully");
66 | })
67 | .catch(error=>{
68 | this.debug("Error when delete device config on gateway: "+error.message);
69 | });
70 | return;
71 | };
72 |
73 | if (!updating){
74 | updating = true;
75 | setTimeout(() => {
76 | this.debug("Updating Device config ...");
77 | updating = false;
78 | const option = {
79 | timeout: 5000,
80 | method: 'POST',
81 | url: 'https://api.nodered-home.ru/gtw/device/config',
82 | headers: {
83 | 'content-type': 'application/json',
84 | 'Authorization': "Bearer "+service.getToken()
85 | },
86 | data: {
87 | id: this.id,
88 | config: deviceconfig
89 | }
90 | };
91 | axios.request(option)
92 | .then(res=>{
93 | this.trace("Device config updated successfully");
94 | })
95 | .catch(error=>{
96 | this.debug("Error when update device config: "+error.message);
97 | });
98 | }, 1000);
99 | }
100 | };
101 | // функция обновления состояния устройства (умений и сенсоров)
102 | this._updateDeviceState= (event=null)=>{
103 | const option = {
104 | timeout: 5000,
105 | method: 'POST',
106 | url: 'https://api.nodered-home.ru/gtw/device/state',
107 | headers: {
108 | 'content-type': 'application/json',
109 | 'Authorization': "Bearer "+service.getToken()
110 | },
111 | data: {
112 | id: this.id,
113 | event: event,
114 | state: states
115 | }
116 | };
117 | axios.request(option)
118 | .then(res=>{
119 | this.trace("Device state updated successfully");
120 | })
121 | .catch(error=>{
122 | this.debug("Error when update device state: "+error.message);
123 | })
124 | };
125 | // отправка эвентов
126 | this._sendEvent = (event)=>{
127 | let data = JSON.stringify(event);
128 | service.send2gate('$me/device/events/'+this.id, data ,false);
129 | };
130 | // Установка параметров умения
131 | this.setCapability = (capId, capab)=>{
132 | return new Promise((resolve,reject)=>{
133 | let intsance = capab.parameters.instance || '';
134 | let capabIndex = capab.type+"."+intsance;
135 | if (capabilites[capabIndex] && capabilites[capabIndex]!=capId){
136 | reject(new Error("Dublicated capability on same device!"));
137 | return;
138 | };
139 | // проверям было ли такое умение раньше и удалем перед обновлением
140 | if (deviceconfig.capabilities.findIndex(a => a.id === capId)>-1){
141 | this.delCapability(capId);
142 | };
143 | capabilites[capabIndex] = capId; // добавляем новое уменя в локальный список
144 | capab.id = capId;
145 | deviceconfig.capabilities.push(capab);
146 | this._updateDeviceInfo();
147 | resolve(true);
148 | })
149 | };
150 | // Установка параметров сенсора
151 | this.setSensor = (sensId, sensor)=>{
152 | return new Promise((resolve,reject)=>{
153 | let sensorIndex = sensor.type+"."+sensor.parameters.instance;
154 | if (sensors[sensorIndex] && sensors[sensorIndex]!=sensId){
155 | reject(new Error("Dublicated sensor on same device!"));
156 | return;
157 | };
158 | // проверям было ли такое сенсор раньше и удалем перед обновлением
159 | if (deviceconfig.properties.findIndex(a => a.id === sensId)>-1){
160 | this.delSensor(sensId);
161 | };
162 | sensors[sensorIndex] = sensId; // добавляем новый сенсор в локальный список
163 | sensor.id = sensId;
164 | deviceconfig.properties.push(sensor);
165 | this._updateDeviceInfo();
166 | resolve(true);
167 | })
168 | };
169 |
170 | // обновление текущего state умения
171 | this.updateCapabState = (capId,state)=>{
172 | return new Promise((resolve,reject)=>{
173 | state.id = capId;
174 | if (needSendEvent){
175 | this._sendEvent(state);
176 | };
177 | const index = states.capabilities.findIndex(a => a.id === capId);
178 | if (index>-1){
179 | states.capabilities.splice(index, 1);
180 | };
181 | states.capabilities.push(state);
182 | const currentevent = {
183 | id: this.id,
184 | capabilities:[state]
185 | };
186 | this._updateDeviceState(currentevent);
187 | resolve(true);
188 | // reject(new Error("Device not ready"));
189 | })
190 | };
191 | // обновление текущего state сенсора
192 | this.updateSensorState = (sensID,state)=>{
193 | return new Promise((resolve,reject)=>{
194 | state.id = sensID;
195 | const index = states.properties.findIndex(a => a.id === sensID);
196 | if (index>-1){
197 | states.properties.splice(index, 1);
198 | };
199 | states.properties.push(state);
200 | const currentevent = {
201 | id: this.id,
202 | properties:[state]
203 | };
204 | this._updateDeviceState(currentevent);
205 | resolve(true);
206 | // reject(new Error("Device not ready"));
207 | })
208 | };
209 |
210 | // удаление умения
211 | this.delCapability= (capId)=>{
212 | return new Promise((resolve,reject)=>{
213 | // удаляем из конфига
214 | const index = deviceconfig.capabilities.findIndex(a => a.id === capId);
215 | if (index>-1){
216 | deviceconfig.capabilities.splice(index, 1);
217 | };
218 | // удаляем из карты
219 | let capabIndex = Object.keys(capabilites).find(key => capabilites[key] === capId);
220 | delete capabilites[capabIndex];
221 | this._updateDeviceInfo();
222 | // удаляем его текущее состояние
223 | const stateindex = states.capabilities.findIndex(a => a.id === capId);
224 | if (stateindex>-1){
225 | states.capabilities.splice(stateindex, 1);
226 | };
227 | this._updateDeviceState();
228 | resolve(true);
229 | })
230 | };
231 |
232 | // удаление сенсора
233 | this.delSensor= (sensID)=>{
234 | return new Promise((resolve,reject)=>{
235 | // удаляем из конфига
236 | const index = deviceconfig.properties.findIndex(a => a.id === sensID);
237 | if (index>-1){
238 | deviceconfig.properties.splice(index, 1);
239 | }
240 | // удаляем из карты
241 | let sensorIndex = Object.keys(sensors).find(key => sensors[key] === sensID);
242 | delete sensors[sensorIndex];
243 | this._updateDeviceInfo();
244 | // удаляем текущее состояние
245 | const stateindex = states.properties.findIndex(a => a.id === sensID);
246 | if (stateindex>-1){
247 | states.properties.splice(stateindex, 1);
248 | };
249 | this._updateDeviceState();
250 | resolve(true);
251 | })
252 | };
253 |
254 | service.on("online",()=>{
255 | this.debug("Received a signal online from the service");
256 | this.emit("online");
257 | this.initState = true;
258 | });
259 |
260 | service.on("offline",()=>{
261 | this.debug("Received a signal offline from the service");
262 | this.emit("offline");
263 | this.initState = false;
264 | this.status({fill:"red",shape:"dot",text:"offline"});
265 | });
266 |
267 | service.on(this.id,(states)=>{
268 | setTimeout(() => {
269 | needSendEvent = false;
270 | }, 2000);
271 | needSendEvent = true;
272 | states.forEach(cap => {
273 | let capabIndex = cap.type+"."+cap.state.instance;
274 | if (cap.type==="devices.capabilities.color_setting"){
275 | capabIndex = cap.type+".";
276 | };
277 | const capId = capabilites[capabIndex];
278 | this.emit(capId,cap.state.value, cap.state);
279 | });
280 | })
281 |
282 | this.on('close', (removed, done)=>{
283 | this.emit('offline');
284 | if (removed){
285 | deviceconfig.capabilities = [];
286 | deviceconfig.properties = [];
287 | states.capabilities = [];
288 | states.properties = [];
289 | this._updateDeviceState();
290 | this._updateDeviceInfo();
291 | };
292 | setTimeout(()=>{
293 | // this.emit('offline');
294 | done();
295 | },500)
296 | });
297 | };
298 | RED.nodes.registerType("alice-device",AliceDevice);
299 | };
--------------------------------------------------------------------------------
/nodes/alice.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
162 |
163 |
242 |
243 |
--------------------------------------------------------------------------------
/src/alice.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
162 |
163 |
242 |
243 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Visit https://aka.ms/tsconfig to read more about this file */
4 |
5 | /* Projects */
6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
12 |
13 | /* Language and Environment */
14 | "target": "es2018", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
15 | "lib": ["ES6"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
16 | // "jsx": "preserve", /* Specify what JSX code is generated. */
17 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
26 |
27 | /* Modules */
28 | "module": "NodeNext", /* Specify what module code is generated. */
29 | "rootDir": "./src", /* Specify the root folder within your source files. */
30 | // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */
36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
38 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
39 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
40 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
41 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
42 | // "resolveJsonModule": true, /* Enable importing .json files. */
43 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
44 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */
45 |
46 | /* JavaScript Support */
47 | "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
48 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
49 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
50 |
51 | /* Emit */
52 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
53 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */
54 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
55 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */
56 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
57 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
58 | "outDir": "./nodes", /* Specify an output folder for all emitted files. */
59 | "removeComments": true, /* Disable emitting comments. */
60 | // "noEmit": true, /* Disable emitting files from a compilation. */
61 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
62 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
63 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
64 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
65 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
66 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
67 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
68 | // "newLine": "crlf", /* Set the newline character for emitting files. */
69 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
70 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
71 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
72 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
73 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */
74 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
75 |
76 | /* Interop Constraints */
77 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
78 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
79 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
80 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
81 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
82 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
83 |
84 | /* Type Checking */
85 | "strict": true, /* Enable all strict type-checking options. */
86 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
87 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
88 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
89 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
90 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
91 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
92 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
93 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
94 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
95 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
96 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
97 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
98 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
99 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
100 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
101 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
102 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
103 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
104 |
105 | /* Completeness */
106 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
107 | "skipLibCheck": true /* Skip type checking all .d.ts files. */
108 | },
109 | "include": ["./**/*"]
110 | }
111 |
--------------------------------------------------------------------------------
/nodes/alice-color.html:
--------------------------------------------------------------------------------
1 |
101 |
102 |
189 |
190 |
--------------------------------------------------------------------------------
/nodes/alice-sensor.html:
--------------------------------------------------------------------------------
1 |
232 |
233 |
279 |
280 |
308 |
--------------------------------------------------------------------------------
/nodes/alice-range.html:
--------------------------------------------------------------------------------
1 |
179 |
180 |
231 |
232 |
--------------------------------------------------------------------------------
/nodes/alice-mode.html:
--------------------------------------------------------------------------------
1 |
126 |
127 |
251 |
252 |
297 |
--------------------------------------------------------------------------------