├── 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 | [![platform](https://img.shields.io/badge/platform-Node--RED-red?logo=nodered)](https://nodered.org) 6 | [![Min Node Version](https://img.shields.io/node/v/node-red-contrib-alice.svg)](https://nodejs.org/en/) 7 | ![Repo size](https://img.shields.io/github/repo-size/efa2000/node-red-contrib-alice) 8 | [![GitHub version](https://img.shields.io/github/package-json/v/efa2000/node-red-contrib-alice?logo=npm)](https://www.npmjs.com/package/node-red-contrib-alice) 9 | [![Package Quality](https://packagequality.com/shield/node-red-contrib-alice.svg)](https://packagequality.com/#?package=node-red-contrib-alice) 10 | ![GitHub last commit](https://img.shields.io/github/last-commit/efa2000/node-red-contrib-alice/master) 11 | ![NPM Total Downloads](https://img.shields.io/npm/dt/node-red-contrib-alice.svg) 12 | ![NPM Downloads per month](https://img.shields.io/npm/dm/node-red-contrib-alice) 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 | --------------------------------------------------------------------------------